Topics › Dynamic Programming › DP on Subsequences
Partition a set into two subsets with minimum absolute sum difference
Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given an array arr of n non-negative integers. Split it into two groups so that every element belongs to exactly one group (a group may be empty). Let the group sums be S1 and S2. Return the smallest possible value of |S1 - S2|.
Typical constraints: 1 ≤ n ≤ 1000, 0 ≤ arr[i] ≤ 1000, and the total of all elements is at most 10^4. The DP is O(n · total), so it depends on the total being small.
Be careful not to confuse this with LeetCode 2035, which also asks for the minimum difference but forces both halves to contain exactly n elements and allows negative numbers. That variant needs a meet-in-the-middle search; the subset-sum table below does not apply to it.
Example 1
arr = [1, 6, 11, 5]
answer = 1 // {1, 6, 5} = 12 and {11} = 11
Example 2
arr = [1, 2, 3, 4]
answer = 0 // {1, 4} = 5 and {2, 3} = 5
One sum determines the difference
Let total be the sum of all elements. Whatever we put in group 1 with sum S1, group 2 holds everything else, so S2 = total - S1 and
|S1 - S2| = |S1 - (total - S1)| = |total - 2 · S1|
So the problem is really: which values of S1 are achievable as a subset sum? Among those, pick the one closest to total / 2. That is the subset-sum question, but asked for every target at once instead of a single target. The tabulation for subset sum happens to answer it for every target simultaneously, which is why it is the natural tool here.
By symmetry we only need to look at S1 ≤ total / 2: if a subset with sum above half exists, its complement is a subset with sum below half and the same difference.
Approach 1: Recursion
Intuition. Decide each element in turn: it goes into group 1 or it does not. Carry the running sum s of group 1. When every element has been decided, the difference is |total - 2s|. The answer from state (i, s) is the better of the two choices for element i.
g(n, s) = |total - 2 · s|
g(i, s) = min( g(i + 1, s + arr[i]), g(i + 1, s) )
answer = g(0, 0)
This is plain “try every subset”, written so that a cache can be added in the next step.
Algorithm.
- Compute
total. solve(i, s): ifi == n, return|total - 2s|; otherwise return the minimum of takingarr[i]and skipping it.
static int recursive(int[] arr) {
int total = 0;
for (int x : arr) total += x;
return solve(0, 0, arr, total);
}
static int solve(int i, int s, int[] arr, int total) {
if (i == arr.length) return Math.abs(total - 2 * s); // group 2 holds total - s
int take = solve(i + 1, s + arr[i], arr, total);
int skip = solve(i + 1, s, arr, total);
return Math.min(take, skip);
}
Complexity. Time O(2^n): every subset is visited. Space O(n) recursion depth.
Approach 2: Memoization
Intuition. The future of the recursion depends only on (i, s), not on which elements produced s. With arr = [1, 2, 3, 4], taking only 3 or taking 1 and 2 both reach (3, 3). Since s never exceeds total, there are at most n · (total + 1) states, and caching them turns the exponential search into a polynomial one.
All results are non-negative differences, so -1 safely marks “not computed”.
Algorithm.
- Allocate
dp[n][total + 1]filled with-1. - Run the recursion; return the cached value when present, and store every computed value.
static int memoization(int[] arr) {
int total = 0;
for (int x : arr) total += x;
int[][] dp = new int[arr.length][total + 1];
for (int[] row : dp) Arrays.fill(row, -1); // differences are >= 0, so -1 = unknown
return memo(0, 0, arr, total, dp);
}
static int memo(int i, int s, int[] arr, int total, int[][] dp) {
if (i == arr.length) return Math.abs(total - 2 * s);
if (dp[i][s] != -1) return dp[i][s];
int take = memo(i + 1, s + arr[i], arr, total, dp);
int skip = memo(i + 1, s, arr, total, dp);
return dp[i][s] = Math.min(take, skip);
}
Complexity. Time O(n · total). Space O(n · total) for the table plus O(n) stack.
Approach 3: Tabulation
Intuition. Build the boolean subset-sum table dp[i][t]: “some subset of arr[0..i] sums to t”, for every t from 0 to total. The last row dp[n - 1][·] lists every achievable S1. Scan S1 from 0 to total / 2 and keep the smallest total - 2 · S1 among the achievable ones.
Two details are different from the single-target subset sum:
- The table must cover every target up to
total, not just onek, because we do not know in advance whichS1will win. - The base case
dp[0][arr[0]] = truenever needs a bounds check, sincearr[0] ≤ totalalways. Zeros are fine too:dp[0][0]is alreadytrue.
Algorithm.
- Compute
totaland fill the subset-sum table for targets0..total. - For
S1from0tototal / 2, ifdp[n - 1][S1]is true, updatebest = min(best, total - 2 · S1). - Return
best.
static int tabulation(int[] arr) {
int n = arr.length, total = 0;
for (int x : arr) total += x;
boolean[][] dp = new boolean[n][total + 1]; // dp[i][t]: some subset of arr[0..i] sums to t
for (int i = 0; i < n; i++) dp[i][0] = true;
dp[0][arr[0]] = true; // arr[0] <= total always
for (int i = 1; i < n; i++) {
for (int t = 1; t <= total; t++) {
dp[i][t] = dp[i - 1][t] || (arr[i] <= t && dp[i - 1][t - arr[i]]);
}
}
int best = Integer.MAX_VALUE;
for (int s1 = 0; s1 <= total / 2; s1++) { // the smaller group is at most half
if (dp[n - 1][s1]) best = Math.min(best, total - 2 * s1);
}
return best;
}
Complexity. Time O(n · total) to fill the table plus O(total) for the scan. Space O(n · total).
Approach 4: Space optimized
Intuition. Only the last row of the table is ever read, and each row depends only on the one before. A single array reach updated from high index to low gives the same row in place: the cell being read, reach[t - x], is to the left of the cell being written, so it still holds the value from before element x was considered.
A second saving: we only care about S1 ≤ total / 2, so the array can stop at half = total / 2. Sums above half are never needed, because their complements are below half. Finally, scanning S1 downward from half, the first reachable value is the one closest to half and gives the answer immediately.
Algorithm.
reach[0] = true; for every elementx, fortfromhalfdown tox, setreach[t]ifreach[t - x].- For
S1fromhalfdown to0, returntotal - 2 · S1at the first reachableS1.
static int spaceOptimized(int[] arr) {
int total = 0;
for (int x : arr) total += x;
int half = total / 2;
boolean[] reach = new boolean[half + 1]; // only sums up to half can be the smaller group
reach[0] = true;
for (int x : arr) {
for (int t = half; t >= x; t--) {
if (reach[t - x]) reach[t] = true;
}
}
for (int s1 = half; s1 >= 0; s1--) { // the largest reachable s1 <= half wins
if (reach[s1]) return total - 2 * s1;
}
return total; // unreachable: reach[0] is always true
}
Dry run on Example 1 (total = 23, half = 11):
| element | reachable sums up to 11 |
|---|---|
| start | {0} |
| 1 | {0, 1} |
| 6 | {0, 1, 6, 7} |
| 11 | {0, 1, 6, 7, 11} |
| 5 | {0, 1, 5, 6, 7, 11} |
The scan from 11 downward hits S1 = 11 immediately, so the answer is 23 - 22 = 1. The subset with sum 11 is {11} itself.
Complexity. Time O(n · total), space O(total).
Edge cases and pitfalls
- Single element. The other group is empty, so the answer is the element itself:
[7]gives 7. - Zeros. They never change a sum.
[0, 0]gives 0, and the random tests in the Java file include zeros. - One dominant element.
[1, 100]gives 99: the best possible split puts 100 alone. - Scanning the wrong half. Scanning
S1up tototalinstead oftotal / 2is not wrong if you use|total - 2 · S1|, but scanning only to half and writingtotal - 2 · S1withoutabsis cleaner and avoids negative values. - Memo sentinel.
0is a real answer (a perfect split), so it cannot double as “not computed”. The Java file uses-1. - Negative numbers. They break the bounded
0..totalrange of sums. With negatives you would need offset indices or the meet-in-the-middle method.
Related problems
- Subset sum equal to target (DP- 14) builds exactly the table used here, for a single target.
- Partition equal subset sum is the special case that asks whether the answer here is 0.
- Count partitions with given difference counts splits whose difference is a fixed
d. - Target sum rewrites a sign-assignment problem as a split into two groups.