9.13

From The Algorithm Design Manual Solution Wiki
Jump to navigation Jump to search

Idea: if the total sum S of the multiset is not divisible by k, no solution exists. Otherwise every subset has to sum to T = S∕k. A depth-first search that always puts the next largest element into the first bucket in which it fits, together with memoisation of already explored states, solves the problem efficiently in practice.

Pseudocode (indices 0 … n-1, numbers sorted descending):

function canPartition(nums, k): S ← sum(nums) if S mod k ≠ 0: return false target ← S / k buckets ← array[k] filled with 0 used ← bitmask 0 memo ← empty set function dfs(idx, buckets, used): if idx = len(nums): return true // all placed if (used) in memo: return false // state seen for i in 0 … k-1: if buckets[i] + nums[idx] ≤ target: buckets[i] += nums[idx] if dfs(idx+1, buckets, used | (1 « idx)): return true buckets[i] -= nums[idx] if buckets[i] = 0: break // symmetry pruning memo.add(used) return false return dfs(0, buckets, used)

Python implementation:
def k_partition(nums, k): nums.sort(reverse=True) s = sum(nums) if s % k: return False target = s // k buckets = [0]*k memo = set() def dfs(idx): if idx == len(nums): return True state = tuple(sorted(buckets)) # small memo key if (idx, state) in memo: return False for i in range(k): if buckets[i] + nums[idx] <= target: buckets[i] += nums[idx] if dfs(idx+1): return True buckets[i] -= nums[idx] if buckets[i] == 0: break memo.add((idx, state)) return False return dfs(0)

Correctness sketch: 1) Feasibility check guarantees a necessary condition. 2) The DFS explores every assignment of items to buckets that respects the capacity T; pruning rules do not remove any viable solution because (a) symmetry pruning only skips permutations of empty buckets, and (b) memoisation merely skips states already fully processed. Hence if a solution exists it will be found.

Complexity: Let n be the number of items. In the worst case DFS enumerates up to kⁿ states. With the two pruning rules the average running time is dramatically lower and handles typical inputs up to 16–20 numbers in milliseconds.

Usage example:
>>> k_partition([4,3,2,3,5,2,1], 4)True (because {5},{1,4},{2,3},{2,3}).


Back to Chapter 9