465. Optimal Account Balancing 🔒
Problem Description
You have a group of people who have been lending money to each other, and now they need to settle all their debts. You're given a list of transactions where each transaction tells you: who paid, who received, and how much money was transferred.
Specifically, you receive an array transactions
where each element transactions[i] = [from_i, to_i, amount_i]
means:
- Person with ID
from_i
gaveamount_i
dollars to person with IDto_i
After all these transactions, some people might owe money while others are owed money. Your task is to find the minimum number of new transactions needed to completely settle all debts so that everyone's balance becomes zero.
For example, if person A owes 10, they can settle with just 1 transaction (A pays B $10). But if there are multiple people with various debts and credits, finding the minimum number of transactions becomes more complex.
The key insight is that people with positive balances (those who are owed money) need to receive payments from people with negative balances (those who owe money). The challenge is to arrange these settlements in the most efficient way possible to minimize the total number of transactions.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: While we have people (which could be nodes) and transactions between them (which could be edges), the core problem is not about graph traversal or graph properties. We're trying to minimize the number of new transactions to settle debts.
Need to solve for kth smallest/largest?
- No: We're not looking for any kth element. We need to find the minimum number of transactions.
Involves Linked Lists?
- No: The problem doesn't involve linked list data structures.
Does the problem have small constraints?
- Yes: After calculating net balances, we only need to consider people with non-zero balances. In practice, this number is often small (typically ≤ 12-15 people), making the problem feasible for exponential time solutions.
Brute force / Backtracking?
- Yes: With small constraints, we can explore different ways to group people and settle debts. The solution uses bit manipulation to explore all possible subsets of people whose debts sum to zero, which is essentially a brute force approach with dynamic programming optimization.
Conclusion: The flowchart correctly leads us to a brute force/backtracking approach. The solution explores all possible ways to group people whose debts cancel out (sum to zero), then finds the minimum number of transactions needed. This is implemented using bitmask DP where each state represents a subset of people, making it an optimized form of backtracking.
Intuition
Let's think about what happens after all the original transactions. Each person has a net balance - they either owe money (negative balance) or are owed money (positive balance). The key insight is that we only care about these final balances, not the original transactions.
For example, if Alice paid Bob 10, and Charlie paid Alice $10, everyone's net balance is zero - no further transactions needed! This tells us we should first calculate everyone's net balance.
Now, here's the crucial observation: when we settle debts, we're essentially trying to group people together such that the sum of their balances equals zero. Within each group, we can settle all debts with (group_size - 1)
transactions. Why? Because in a group of n
people whose balances sum to zero, we can pick one person as a "central bank" and have the other n-1
people settle with them.
For instance, if we have three people with balances [5, -2, -3]
, person 1 can receive 3 from person 3, settling everything with just 2 transactions.
The challenge becomes: how do we optimally partition all people with non-zero balances into such groups? This is where the small constraints become crucial. With typically at most 12-15 people having non-zero balances, we can explore all possible subsets.
We use dynamic programming with bitmasks to avoid recalculating. For each subset of people (represented as a bitmask), we:
- Check if their balances sum to zero
- If yes, this subset can be settled with
(subset_size - 1)
transactions - We also consider splitting this subset into smaller zero-sum subsets to see if that requires fewer total transactions
The minimum across all these possibilities gives us our answer. This approach ensures we find the globally optimal way to group people for debt settlement.
Solution Approach
The implementation follows our intuition of finding optimal groups of people whose balances sum to zero. Let's walk through the code step by step:
Step 1: Calculate Net Balances
g = defaultdict(int)
for f, t, x in transactions:
g[f] -= x # person f loses x amount
g[t] += x # person t gains x amount
We use a dictionary to track each person's net balance. After processing all transactions, positive values mean the person is owed money, negative values mean they owe money.
Step 2: Filter Non-Zero Balances
nums = [x for x in g.values() if x]
m = len(nums)
We only care about people with non-zero balances since those with zero balance are already settled. This significantly reduces our problem size.
Step 3: Initialize DP Array
f = [inf] * (1 << m) f[0] = 0
We create a DP array where f[i]
represents the minimum number of transactions needed to settle debts for the subset of people represented by bitmask i
. The empty set (0) requires 0 transactions.
Step 4: Process Each Subset
for i in range(1, 1 << m):
s = 0
for j, x in enumerate(nums):
if i >> j & 1:
s += x
For each possible subset i
, we first calculate the sum of balances. If bit j
is set in i
, we include nums[j]
in our sum.
Step 5: Handle Zero-Sum Subsets
if s == 0: f[i] = i.bit_count() - 1
If the subset sums to zero, we can settle it internally with (size - 1)
transactions by having everyone settle with one central person.
Step 6: Try Splitting the Subset
j = (i - 1) & i
while j > 0:
f[i] = min(f[i], f[j] + f[i ^ j])
j = (j - 1) & i
This clever bit manipulation iterates through all non-empty subsets j
of i
. For each split:
j
is one subseti ^ j
is the complementary subset (elements ini
but not inj
)- We check if splitting into these two groups gives fewer total transactions
The expression (j - 1) & i
generates the next subset of i
in decreasing order.
Step 7: Return Result
return f[-1]
f[-1]
or f[(1 << m) - 1]
represents the subset containing all people with non-zero balances, which is our answer.
The time complexity is O(3^n)
where n
is the number of people with non-zero balances (due to iterating through all subsets and their subsets), and space complexity is O(2^n)
for the DP array.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the solution works.
Given transactions: [[0,1,10], [1,2,5], [2,0,3]]
- Person 0 gives $10 to Person 1
- Person 1 gives $5 to Person 2
- Person 2 gives $3 to Person 0
Step 1: Calculate Net Balances
- Person 0: -10 (gave) + 3 (received) = -7 (owes $7)
- Person 1: +10 (received) - 5 (gave) = +5 (is owed $5)
- Person 2: +5 (received) - 3 (gave) = +2 (is owed $2)
So nums = [-7, 5, 2]
(3 people with non-zero balances)
Step 2: Initialize DP Array
With 3 people, we have 2³ = 8 possible subsets.
f = [0, inf, inf, inf, inf, inf, inf, inf]
Step 3: Process Each Subset
Let's trace through key subsets (using binary representation):
-
Subset 001 (person 0 only): Balance = -7
- Not zero-sum, remains
f[1] = inf
- Not zero-sum, remains
-
Subset 010 (person 1 only): Balance = 5
- Not zero-sum, remains
f[2] = inf
- Not zero-sum, remains
-
Subset 011 (persons 0,1): Balance = -7 + 5 = -2
- Not zero-sum, remains
f[3] = inf
- Not zero-sum, remains
-
Subset 100 (person 2 only): Balance = 2
- Not zero-sum, remains
f[4] = inf
- Not zero-sum, remains
-
Subset 101 (persons 0,2): Balance = -7 + 2 = -5
- Not zero-sum, remains
f[5] = inf
- Not zero-sum, remains
-
Subset 110 (persons 1,2): Balance = 5 + 2 = 7
- Not zero-sum, remains
f[6] = inf
- Not zero-sum, remains
-
Subset 111 (all three): Balance = -7 + 5 + 2 = 0 ✓
- Zero-sum! Initial value:
f[7] = 3 - 1 = 2
transactions - Now check if splitting helps:
- Split into {person 0} and {persons 1,2}:
f[1] + f[6] = inf + inf = inf
(no help) - Split into {person 1} and {persons 0,2}:
f[2] + f[5] = inf + inf = inf
(no help) - Split into {person 2} and {persons 0,1}:
f[4] + f[3] = inf + inf = inf
(no help) - Split into {persons 0,1} and {person 2}:
f[3] + f[4] = inf + inf = inf
(no help)
- Split into {person 0} and {persons 1,2}:
- No split improves the result, so
f[7] = 2
- Zero-sum! Initial value:
Result: The minimum number of transactions is 2.
This makes sense! Person 0 owes $7 total. They can:
- Pay $5 to Person 1 (settling Person 1's balance)
- Pay $2 to Person 2 (settling Person 2's balance)
Both transactions together settle everyone's debt with just 2 transactions, which matches our algorithm's answer.
Solution Implementation
1class Solution:
2 def minTransfers(self, transactions: List[List[int]]) -> int:
3 # Build a balance map for each person
4 # Negative balance means they owe money, positive means they are owed money
5 balance_map = defaultdict(int)
6 for from_person, to_person, amount in transactions:
7 balance_map[from_person] -= amount # Person giving money
8 balance_map[to_person] += amount # Person receiving money
9
10 # Extract non-zero balances (only people who owe or are owed money)
11 balances = [balance for balance in balance_map.values() if balance != 0]
12 num_people = len(balances)
13
14 # dp[mask] = minimum number of transactions to settle debts for people in mask
15 # mask is a bitmask where bit i represents whether person i is included
16 dp = [float('inf')] * (1 << num_people)
17 dp[0] = 0 # Base case: no people need 0 transactions
18
19 # Iterate through all possible subsets of people
20 for mask in range(1, 1 << num_people):
21 # Calculate the sum of balances for people in current mask
22 balance_sum = 0
23 for person_idx, person_balance in enumerate(balances):
24 if mask >> person_idx & 1: # Check if person_idx is in the mask
25 balance_sum += person_balance
26
27 # If sum is 0, this subset can be settled internally
28 if balance_sum == 0:
29 # A group with zero sum needs (group_size - 1) transactions
30 dp[mask] = mask.bit_count() - 1
31
32 # Try splitting this group into two subgroups
33 # Iterate through all non-empty subsets of current mask
34 submask = (mask - 1) & mask
35 while submask > 0:
36 # Update minimum: current group split into submask and (mask ^ submask)
37 dp[mask] = min(dp[mask], dp[submask] + dp[mask ^ submask])
38 # Move to next submask
39 submask = (submask - 1) & mask
40
41 # Return the minimum transactions needed for all people
42 return dp[-1]
43
1class Solution {
2 public int minTransfers(int[][] transactions) {
3 // Calculate net balance for each person (max 12 people based on constraints)
4 int[] netBalance = new int[12];
5 for (int[] transaction : transactions) {
6 int giver = transaction[0];
7 int receiver = transaction[1];
8 int amount = transaction[2];
9 netBalance[giver] -= amount;
10 netBalance[receiver] += amount;
11 }
12
13 // Collect all non-zero balances (people who owe or are owed money)
14 List<Integer> nonZeroBalances = new ArrayList<>();
15 for (int balance : netBalance) {
16 if (balance != 0) {
17 nonZeroBalances.add(balance);
18 }
19 }
20
21 int numPeople = nonZeroBalances.size();
22
23 // Dynamic programming array where dp[mask] represents minimum transfers
24 // needed to settle debts for the subset of people represented by mask
25 int[] dp = new int[1 << numPeople];
26 Arrays.fill(dp, Integer.MAX_VALUE / 2); // Use large value to avoid overflow
27 dp[0] = 0; // Base case: no people means no transfers needed
28
29 // Iterate through all possible subsets of people
30 for (int mask = 1; mask < (1 << numPeople); mask++) {
31 // Calculate the sum of balances for current subset
32 int sumOfBalances = 0;
33 for (int personIndex = 0; personIndex < numPeople; personIndex++) {
34 if ((mask >> personIndex & 1) == 1) {
35 sumOfBalances += nonZeroBalances.get(personIndex);
36 }
37 }
38
39 // If sum is 0, this subset can be settled internally
40 if (sumOfBalances == 0) {
41 // Maximum transfers needed is (number of people - 1)
42 dp[mask] = Integer.bitCount(mask) - 1;
43
44 // Try partitioning current subset into two groups
45 // and use previously computed results
46 for (int subset = (mask - 1) & mask; subset > 0; subset = (subset - 1) & mask) {
47 int complement = mask ^ subset;
48 dp[mask] = Math.min(dp[mask], dp[subset] + dp[complement]);
49 }
50 }
51 }
52
53 // Return minimum transfers for all people
54 return dp[(1 << numPeople) - 1];
55 }
56}
57
1class Solution {
2public:
3 int minTransfers(vector<vector<int>>& transactions) {
4 // Step 1: Calculate net balance for each person
5 // balance[i] represents how much person i owes (negative) or is owed (positive)
6 int balance[12] = {0};
7 for (auto& transaction : transactions) {
8 int from = transaction[0];
9 int to = transaction[1];
10 int amount = transaction[2];
11
12 balance[from] -= amount; // Person 'from' owes this amount
13 balance[to] += amount; // Person 'to' is owed this amount
14 }
15
16 // Step 2: Extract non-zero balances (only these people need transfers)
17 vector<int> nonZeroBalances;
18 for (int personBalance : balance) {
19 if (personBalance != 0) {
20 nonZeroBalances.push_back(personBalance);
21 }
22 }
23
24 int numPeople = nonZeroBalances.size();
25
26 // Step 3: Dynamic programming with bitmask
27 // dp[mask] = minimum transfers needed to settle debts for people in 'mask'
28 int totalStates = 1 << numPeople; // 2^numPeople possible states
29 int dp[totalStates];
30 memset(dp, 0x3f, sizeof(dp)); // Initialize with large value (infinity)
31 dp[0] = 0; // Base case: no people to settle = 0 transfers
32
33 // Iterate through all possible subsets of people
34 for (int mask = 1; mask < totalStates; ++mask) {
35 // Calculate sum of balances for current subset
36 int sumOfBalances = 0;
37 for (int person = 0; person < numPeople; ++person) {
38 if ((mask >> person) & 1) { // Check if person is in current subset
39 sumOfBalances += nonZeroBalances[person];
40 }
41 }
42
43 // A subset can be settled only if sum of balances is zero
44 if (sumOfBalances == 0) {
45 // Option 1: Settle this subset directly
46 // Number of transfers = number of people - 1
47 int peopleCount = __builtin_popcount(mask);
48 dp[mask] = peopleCount - 1;
49
50 // Option 2: Split into two smaller settled subsets
51 // Enumerate all non-empty proper subsets of current mask
52 for (int subset = (mask - 1) & mask; subset > 0; subset = (subset - 1) & mask) {
53 int complement = mask ^ subset; // The other part of the split
54 dp[mask] = min(dp[mask], dp[subset] + dp[complement]);
55 }
56 }
57 }
58
59 // Return minimum transfers for all people (full mask)
60 return dp[(1 << numPeople) - 1];
61 }
62};
63
1/**
2 * Calculates the minimum number of transactions needed to settle all debts
3 * @param transactions - Array of transactions where each element is [from, to, amount]
4 * @returns Minimum number of transactions to settle all debts
5 */
6function minTransfers(transactions: number[][]): number {
7 // Initialize balance array for each person (assuming max 12 people indexed 0-11)
8 const balances: number[] = new Array(12).fill(0);
9
10 // Calculate net balance for each person
11 for (const [from, to, amount] of transactions) {
12 balances[from] -= amount; // Person 'from' owes this amount
13 balances[to] += amount; // Person 'to' receives this amount
14 }
15
16 // Filter out people with zero balance (they don't need to be involved in settlements)
17 const nonZeroBalances = balances.filter(balance => balance !== 0);
18 const numPeople = nonZeroBalances.length;
19
20 // Dynamic programming array where dp[mask] represents minimum transactions for subset represented by mask
21 const dp: number[] = new Array(1 << numPeople).fill(1 << 29); // Initialize with large value
22 dp[0] = 0; // Empty subset needs 0 transactions
23
24 // Iterate through all possible subsets (represented as bitmasks)
25 for (let mask = 1; mask < (1 << numPeople); ++mask) {
26 // Calculate sum of balances in current subset
27 let subsetSum = 0;
28 for (let person = 0; person < numPeople; ++person) {
29 if (((mask >> person) & 1) === 1) { // Check if person is in current subset
30 subsetSum += nonZeroBalances[person];
31 }
32 }
33
34 // If subset sum is zero, this subset can be settled internally
35 if (subsetSum === 0) {
36 // Base case: settle all people in subset with n-1 transactions (where n is subset size)
37 dp[mask] = bitCount(mask) - 1;
38
39 // Try splitting current subset into two smaller subsets and combine their solutions
40 // Iterate through all non-empty proper subsets of current mask
41 for (let submask = (mask - 1) & mask; submask > 0; submask = (submask - 1) & mask) {
42 // Update minimum transactions: combine solutions of two disjoint subsets
43 dp[mask] = Math.min(dp[mask], dp[submask] + dp[mask ^ submask]);
44 }
45 }
46 }
47
48 // Return minimum transactions for the full set of people with non-zero balances
49 return dp[(1 << numPeople) - 1];
50}
51
52/**
53 * Counts the number of set bits (1s) in the binary representation of a number
54 * Uses Brian Kernighan's algorithm with bit manipulation optimizations
55 * @param i - The number to count bits for
56 * @returns Number of set bits
57 */
58function bitCount(i: number): number {
59 // Count bits in groups of 2
60 i = i - ((i >>> 1) & 0x55555555);
61 // Count bits in groups of 4
62 i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
63 // Count bits in groups of 8
64 i = (i + (i >>> 4)) & 0x0f0f0f0f;
65 // Count bits in groups of 16
66 i = i + (i >>> 8);
67 // Count bits in groups of 32
68 i = i + (i >>> 16);
69 // Return the final count (max 32 bits, so mask with 0x3f)
70 return i & 0x3f;
71}
72
Time and Space Complexity
Time Complexity: O(3^m)
where m
is the number of people with non-zero balance.
The algorithm uses dynamic programming with bitmask to find the minimum number of transactions. For each subset represented by mask i
:
- We iterate through all possible subsets of
i
usingj = (i - 1) & i
operation, which generates all non-empty proper subsets - For a set with
k
elements, there are2^k - 1
non-empty subsets - The total number of operations across all masks is bounded by
∑(i=1 to 2^m) 2^(bit_count(i))
, which equals3^m
- Additionally, for each mask, we compute the sum which takes
O(m)
time - Therefore, the overall time complexity is
O(m * 2^m + 3^m) = O(3^m)
Space Complexity: O(2^m)
where m
is the number of people with non-zero balance.
The space is dominated by:
- The array
f
of size2^m
to store the minimum transactions for each subset - The
nums
array of sizem
to store non-zero balances - The dictionary
g
which stores at mostn
entries wheren
is the number of unique people in transactions - Since we only consider non-zero balances, and
m ≤ n
, the space complexity isO(2^m)
Common Pitfalls
Pitfall 1: Incorrect Balance Calculation Direction
A frequent mistake is reversing the signs when calculating balances. Many developers intuitively think "if A gives money to B, A has positive balance" but this leads to incorrect results.
Wrong approach:
for from_person, to_person, amount in transactions: balance_map[from_person] += amount # WRONG! balance_map[to_person] -= amount # WRONG!
Why it's wrong: This reverses the debt relationship. The person giving money should have a negative balance (they're out money), and the person receiving should have positive balance (they're up money).
Correct approach:
for from_person, to_person, amount in transactions: balance_map[from_person] -= amount # Giver loses money balance_map[to_person] += amount # Receiver gains money
Pitfall 2: Not Filtering Zero Balances
Including people with zero balance in the DP calculation significantly increases complexity without adding value.
Wrong approach:
balances = list(balance_map.values()) # Includes zeros!
Why it's wrong: People with zero balance are already settled and don't need any transactions. Including them increases the state space from 2^n
to 2^m
where m > n
, making the algorithm much slower.
Correct approach:
balances = [balance for balance in balance_map.values() if balance != 0]
Pitfall 3: Incorrect Subset Iteration
The subset enumeration logic using bit manipulation is tricky and easy to get wrong.
Wrong approach:
# Trying to iterate through subsets incorrectly
for submask in range(1, mask):
if submask & mask == submask: # Check if submask is subset of mask
dp[mask] = min(dp[mask], dp[submask] + dp[mask ^ submask])
Why it's wrong: This iterates through many unnecessary values and is inefficient. It checks O(2^n)
values for each mask instead of just the actual subsets.
Correct approach:
submask = (mask - 1) & mask
while submask > 0:
dp[mask] = min(dp[mask], dp[submask] + dp[mask ^ submask])
submask = (submask - 1) & mask
This efficiently iterates only through actual subsets of mask
using the formula (submask - 1) & mask
.
Pitfall 4: Forgetting the Base Case for Zero-Sum Groups
When a subset sums to zero, forgetting to set the base case before trying splits leads to incorrect results.
Wrong approach:
if balance_sum == 0:
# Only try splitting, forget the direct settlement
submask = (mask - 1) & mask
while submask > 0:
dp[mask] = min(dp[mask], dp[submask] + dp[mask ^ submask])
submask = (submask - 1) & mask
Why it's wrong: This misses the fact that a zero-sum group can be settled directly with (size - 1)
transactions, which might be better than any split.
Correct approach:
if balance_sum == 0:
# First set the base case
dp[mask] = mask.bit_count() - 1
# Then try splitting to see if we can do better
submask = (mask - 1) & mask
while submask > 0:
dp[mask] = min(dp[mask], dp[submask] + dp[mask ^ submask])
submask = (submask - 1) & mask
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!