Backtracking · 10 of 19
Tug of War
Split kids into two teams of equal size so that the two teams weigh as close to the same as possible.
- Simple way
- time O(2^n)
room O(n) - Clever way
- time O(C(n, n/2))
room O(n)
The problem, simply
You get n numbers. Split them into two groups. If n is even, both groups have n / 2 numbers. If n is odd, one group has n / 2 and the other has one more.
Make the difference between the two group sums as small as possible. Give back that smallest difference (and, if asked, the two groups). Numbers may be negative.
A story to remember it
Picture it
Six kids with their weights. 34 in total, so a perfect split would give each team 17. Team A must get exactly 3 kids.
- looking now
Examples
The walkthrough one
- In
[5, 10, 1, 4, 8, 6]- Out
0
Team A is 5, 4, 8 and team B is 10, 1, 6. Both weigh 17.
The classic
- In
[3, 4, 5, -3, 100, 1, 89, 54, 23, 20]- Out
0
4 + 100 + 1 + 23 + 20 = 148 and 3 + 5 - 3 + 89 + 54 = 148.
An odd count with negatives
- In
[23, 45, -34, 12, 0, 98, -99, 4, 189, -1, 4]- Out
1
One team gets 5 numbers and the other 6. The best split is 45, -34, 12, 98, -1 = 120 against 23, 0, -99, 4, 189, 4 = 121.
Two kids
- In
[1, 100]- Out
99
Each team gets one kid, so there is no choice at all.
Way 1: the simple way
Send every kid to team A or team B in every possible way. Only count the splits where team A has exactly n / 2 kids.
- For kid
i, branch twice: join team A, or join team B. - Carry team A's size and weight.
- When every kid is placed, if team A has
n / 2kids, the difference is|total - 2 * sumA|. Keep the smallest.
class Solution {
private int best;
public int tugOfWar(int[] arr) {
int total = 0;
for (int x : arr) total += x;
best = Integer.MAX_VALUE;
split(arr, 0, 0, 0, total);
return best;
}
private void split(int[] arr, int i, int sizeA, int sumA, int total) {
int n = arr.length;
if (i == n) {
if (sizeA == n / 2) best = Math.min(best, Math.abs(total - 2 * sumA));
return;
}
split(arr, i + 1, sizeA + 1, sumA + arr[i], total); // kid i joins team A
split(arr, i + 1, sizeA, sumA, total); // kid i joins team B
}
}How long it takes: two choices per kid, 2^n splits, and most have the wrong team size. O(2^n). Extra room: the call stack, O(n).
Way 2: the clever way
Only build splits with the right team size, and stop when the split is already perfect.
- Pick team A's members one at a time, always from a later spot than the last pick. Then each team is built once, never in two orders.
- Leave room: if team A still needs
rmore kids, the next pick can be at most at spotn - r. - When team A is full, compute the difference and keep the best team.
- The difference can never be below
total % 2(0 for an even total, 1 for an odd one). If you reach it, stop everything.
Pick team A one kid at a time, and stop at a perfect split
01 / 14
Six kids weigh 5, 10, 1, 4, 8 and 6, 34 in total. Team A gets exactly 3 kids. A perfect split gives each team 17.
- team A
- 0
- team B
- 34
- looking now
- seen
- answer
- rejected
- path
- just added
import java.util.*;
class Solution {
private int best;
private List<Integer> bestTeam;
public int tugOfWar(int[] arr) {
int total = 0;
for (int x : arr) total += x;
best = Integer.MAX_VALUE;
pick(arr, 0, new ArrayList<>(), 0, total);
return best; // bestTeam holds team A's spots
}
private void pick(int[] arr, int start, List<Integer> team, int sumA, int total) {
int n = arr.length, k = n / 2;
if (team.size() == k) {
int diff = Math.abs(total - 2 * sumA);
if (diff < best) {
best = diff;
bestTeam = new ArrayList<>(team);
}
return;
}
int need = k - team.size();
for (int i = start; i <= n - need; i++) { // leave room for the rest
team.add(i);
pick(arr, i + 1, team, sumA + arr[i], total);
team.remove(team.size() - 1);
if (best == (total & 1)) return; // perfect already, stop everything
}
}
}How long it takes: only teams of the right size are built, C(n, n/2) of them, which is about 2^n / sqrt(n). With the running sum each team costs O(1), so O(C(n, n/2)), and the early stop often ends much sooner. Extra room: the team list and the call stack, O(n).
Another way: meet in the middle
For n up to about 40, even C(n, n/2) is too slow. Split the kids into a left half and a right half. List every subset of each half, grouped by how many kids it takes, about 2^(n/2) each. If team A takes i kids from the left, it needs k - i from the right, and the best right sum is the one closest to total / 2 - leftSum. Sort each group of right sums and binary search.
import java.util.*;
class Solution {
public long tugOfWar(int[] arr) {
int n = arr.length, k = n / 2, h = n / 2, m = n - h;
long total = 0;
for (int x : arr) total += x;
List<List<Long>> right = new ArrayList<>(); // right half sums, by team size
for (int j = 0; j <= m; j++) right.add(new ArrayList<>());
for (int mask = 0; mask < (1 << m); mask++) {
long s = 0;
for (int b = 0; b < m; b++) if ((mask >> b & 1) == 1) s += arr[h + b];
right.get(Integer.bitCount(mask)).add(s);
}
for (List<Long> list : right) Collections.sort(list);
long best = Long.MAX_VALUE;
for (int mask = 0; mask < (1 << h); mask++) {
int j = k - Integer.bitCount(mask); // kids still needed from the right
if (j < 0 || j > m) continue;
long s = 0;
for (int b = 0; b < h; b++) if ((mask >> b & 1) == 1) s += arr[b];
List<Long> list = right.get(j);
int lo = 0, hi = list.size(); // first r with 2 * (s + r) >= total
while (lo < hi) {
int mid = (lo + hi) / 2;
if (2 * (s + list.get(mid)) >= total) hi = mid; else lo = mid + 1;
}
for (int p = lo - 1; p <= lo; p++) { // check the neighbors around the middle
if (p >= 0 && p < list.size()) best = Math.min(best, Math.abs(total - 2 * (s + list.get(p))));
}
}
return best;
}
}This runs in O(2^(n/2) * n) time and O(2^(n/2)) room. For 40 kids that is about a million subsets per half, instead of 137 billion teams.
Watch out for
- Negative numbers. Do not prune with "team A is already too heavy", because a later negative number can bring it back down.
- Odd
n. Team A getsn / 2kids and team B gets the rest. Both ways around are covered, because team B is everyone not picked. - Stopping early at difference 0 when the total is odd. The best possible is 1 then. Use
total & 1, which is also safe for negative totals.
What interviewers ask next
- Can you halve the work? When
nis even, swapping the two teams gives the same split. Put kid 0 in team A always, and choose only the othern / 2 - 1members. - What if the numbers are small and positive? Use a table
can[c][s]: can we pickckids that weighs? Fill it like subset sum with an extra count. That isO(n^2 * total). - What if team sizes do not matter? Then it is the minimum subset sum difference problem: fill a subset sum table up to
total / 2and take the biggest reachable sum. - Return the teams too. Keep
bestTeamas in Way 2. Team B is everyone not in it.
Remember this
Grown-up words: picking members in increasing order generates combinations, leaving room and the early stop are pruning, the whole thing is backtracking, and splitting into halves with sorted lists and binary search is meet in the middle.