← Daily Logs
Algorithms

Minimum Subset Sum Difference

MediumAug 14, 2026dynamic-programmingknapsacksubset-sumtabulation

The first problem in this run that needs the *whole* last row of the table rather than a single cell. Build subset-sum reachability up to `totalSum`, then read off every sum reachable at or below the halfway mark; the largest of them is the best first half, and the answer is `totalSum - 2 * s1`. The table was always holding this — earlier problems just never looked at more than one square of it.

The problem

Implement `minimumSubsetDiff(nums)`: split an array of non-negative integers into two subsets and return the smallest possible value of `|sum(S1) - sum(S2)|`. Every element must land in exactly one subset.

`[1, 6, 11, 5]` returns `1` — `{1, 5, 6}` sums to `12` and `{11}` to `11`. `[1, 2, 7]` returns `4` — the best split is `{1, 2}` against `{7}`. When a perfect halving exists the answer is `0`, which makes `equalSumPartitionProblem` a yes/no query on this same result.

The approach

The reachable set is the whole point. If a subset sums to `s`, the complement sums to `totalSum - s` by construction, so the difference is `|totalSum - 2s|` — a function of `s` alone. Minimising it means finding the reachable `s` closest to `totalSum / 2`, and since every `s` above the midpoint mirrors one below it, scanning `j` from `0` to `Math.floor(totalSum / 2)` covers every distinct difference without checking anything twice.

That's why the helper returns the whole `memo` instead of a single boolean. Every earlier problem in this series asked one question of the table and read one cell; here the question is 'what's the best reachable sum', which is a scan of the final row. The table has always contained that information — `memo[n][j]` is 'is `j` reachable using all `n` numbers' for every `j` at once — and this is the first problem that spends it.

The scan collects into `possibleValuesOfSum` and then takes the last element, which works because `j` ascends, so the last one pushed is the largest. It can never be empty: `memo[i][0] = true` for every prefix, so `j = 0` is always reachable, and in the degenerate case of an all-zero or empty array the answer comes out `totalSum - 0 = totalSum`, which is right. A running `let best = 0` would do the same job in `O(1)` space instead of building an array only to read its tail.

One naming nit worth fixing before this gets copy-pasted: the local helper is called `countOfSubsetWithGivenSum` but it's the *boolean* subset-sum table — it fills `false`/`true`, uses `||`, and the two comments still say 'initialize with 0' and 'initialize with 1' from whatever it was pasted from. It behaves correctly; it just claims to be the counting variant from the entry above, and `true`/`false` happening to be truthy in `if (memo[nums.length][j])` is what hides the mismatch.

The solution

js
export function minimumSubsetDiff(nums) {
  const totalSum = nums.reduce((acc, curr) => acc + curr, 0);
  const memo = countOfSubsetWithGivenSum(nums, totalSum);
  const possibleValuesOfSum = [];
  for (let j = 0; j <= Math.floor(totalSum / 2); j++) {
    if (memo[nums.length][j]) {
      possibleValuesOfSum.push(j);
    }
  }
  return totalSum - 2 * possibleValuesOfSum[possibleValuesOfSum.length - 1];
}

function countOfSubsetWithGivenSum(nums, target) {
  const memo = Array.from({ length: nums.length + 1 }, () =>
    Array(target + 1).fill(null),
  );

  // initialize the first row with 0
  for (let j = 0; j <= target; j++) {
    memo[0][j] = false;
  }

  // initialize the first column with 1
  for (let i = 0; i <= nums.length; i++) {
    memo[i][0] = true;
  }

  // fill the memo table
  for (let i = 1; i <= nums.length; i++) {
    for (let j = 1; j <= target; j++) {
      if (nums[i - 1] <= j) {
        memo[i][j] = memo[i - 1][j] || memo[i - 1][j - nums[i - 1]];
      } else {
        memo[i][j] = memo[i - 1][j];
      }
    }
  }

  // return the result
  return memo;
}

Time O(n * totalSum)Space O(n * totalSum)

All entries