Facebook Pixel

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 gave amount_i dollars to person with ID to_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 10andpersonBisowed10 and person B is owed 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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,BobpaidCharlie10, Bob paid Charlie 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 2fromperson2and2 from person 2 and 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:

  1. Check if their balances sum to zero
  2. If yes, this subset can be settled with (subset_size - 1) transactions
  3. 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 subset
  • i ^ j is the complementary subset (elements in i but not in j)
  • 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 Evaluator

Example 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
  • Subset 010 (person 1 only): Balance = 5

    • Not zero-sum, remains f[2] = inf
  • Subset 011 (persons 0,1): Balance = -7 + 5 = -2

    • Not zero-sum, remains f[3] = inf
  • Subset 100 (person 2 only): Balance = 2

    • Not zero-sum, remains f[4] = inf
  • Subset 101 (persons 0,2): Balance = -7 + 2 = -5

    • Not zero-sum, remains f[5] = inf
  • Subset 110 (persons 1,2): Balance = 5 + 2 = 7

    • Not zero-sum, remains f[6] = inf
  • 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)
    • No split improves the result, so f[7] = 2

Result: The minimum number of transactions is 2.

This makes sense! Person 0 owes $7 total. They can:

  1. Pay $5 to Person 1 (settling Person 1's balance)
  2. 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 using j = (i - 1) & i operation, which generates all non-empty proper subsets
  • For a set with k elements, there are 2^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 equals 3^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 size 2^m to store the minimum transactions for each subset
  • The nums array of size m to store non-zero balances
  • The dictionary g which stores at most n entries where n is the number of unique people in transactions
  • Since we only consider non-zero balances, and m ≤ n, the space complexity is O(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More