Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

465. Optimal Account Balancing - Leetcode Solution

Problem Description

The Optimal Account Balancing problem asks you to determine the minimum number of transactions required to settle all debts among a group of people, based on a list of transactions. Each transaction is represented as a list [x, y, z], meaning person x gave person y z dollars. The goal is to minimize the number of transactions needed so that every person's net balance becomes zero (everyone is settled up).

  • You may assume there is at least one way to settle all debts.
  • Each transaction must involve two different people.
  • Do not reuse or split transactions; you can only create new ones between people with outstanding balances.
  • The input is a list transactions, where each element is a list [from, to, amount].

Thought Process

At first glance, you might think to simulate each transaction and try to settle debts one by one. However, this quickly becomes inefficient, especially as the number of people grows, since there are many possible ways to pair up debts and credits.

A brute-force approach would be to try every possible sequence of debt settlements, but this is computationally expensive due to the exponential number of possibilities. To optimize, we need to focus on the net balance of each person—how much they owe or are owed after all transactions are considered. Once we know each person's net balance, the problem reduces to finding the minimum number of transactions that can zero out all non-zero balances.

The key insight is that we can ignore the actual transaction history and just work with the net balances. Then, we try to settle debts recursively, pairing people with opposite balances, and use backtracking to explore the minimum number of settlements.

Solution Approach

Let's break down the optimized solution:

  1. Calculate Net Balances:
    • Use a hash map (dictionary) to sum up each person's net balance after all transactions.
    • Only keep people whose net balance is not zero (since zero means they're already settled).
  2. Backtracking Debt Settlement:
    • Store all non-zero balances in a list.
    • Recursively try to settle the first unsettled person's balance with any other person with the opposite sign (debtor with creditor).
    • For each such settlement, adjust the balances and recursively continue to the next unsettled person.
    • Keep track of the minimum number of transactions found during the process.
  3. Pruning for Efficiency:
    • If two balances can cancel each other out exactly, prioritize that path as it may lead to fewer transactions.
    • Skip balances that are already zero during recursion.
  4. Why a Hash Map?
    • Allows O(1) lookup and update of balances per person.
  5. Why Backtracking?
    • We need to explore different ways to pair up debts and credits to find the minimum number of settlements.

Example Walkthrough

Consider the input: transactions = [[0,1,10], [2,0,5]]

  1. Calculate Net Balances:
    • Person 0: -10 (gave 10 to 1), +5 (received 5 from 2) ⇒ net: -5
    • Person 1: +10 (received 10 from 0) ⇒ net: +10
    • Person 2: -5 (gave 5 to 0) ⇒ net: -5

    Balances: [ -5, +10, -5 ]

  2. Backtracking Settlement:
    • Start with balances: [-5, 10, -5]
    • Try to settle first -5 (person 0) with +10 (person 1):
    • After transaction: [0, 5, -5] (person 0 and 1)
    • Now, try to settle next -5 (person 2) with +5 (person 1):
    • After transaction: [0, 0, 0] (all settled)
    • Total transactions: 2

    This is optimal; you can't do it in fewer than 2 transactions. The solution backtracks through all possible pairings to ensure this is the minimum.

Time and Space Complexity

  • Brute-force:
    • Time: O(n!) where n is the number of people with non-zero balances, due to all possible pairings.
    • Space: O(n) for recursion stack and balance storage.
  • Optimized Backtracking:
    • Time: Still O(n!), but much faster in practice due to pruning and skipping zeros.
    • Space: O(n) for the balances array and recursion stack.
  • Why O(n!):
    • At each step, you can pair a debtor with any creditor, leading to factorial growth in possible pairings.

Summary

The Optimal Account Balancing problem is about minimizing the number of transactions needed to settle all debts among a group of people. By focusing on each person's net balance, we can ignore the original transaction order and use backtracking to efficiently explore all possible ways to settle debts. Pruning and skipping zero balances greatly improve performance. The solution is elegant because it reduces a seemingly complex set of transactions to a problem of pairing up positive and negative balances optimally.

Code Implementation

class Solution:
    def minTransfers(self, transactions):
        from collections import defaultdict
        balance = defaultdict(int)
        for frm, to, amt in transactions:
            balance[frm] -= amt
            balance[to] += amt
        debt = [v for v in balance.values() if v != 0]
        
        def dfs(start):
            while start < len(debt) and debt[start] == 0:
                start += 1
            if start == len(debt):
                return 0
            min_txn = float('inf')
            for i in range(start + 1, len(debt)):
                if debt[start] * debt[i] < 0:
                    debt[i] += debt[start]
                    min_txn = min(min_txn, 1 + dfs(start + 1))
                    debt[i] -= debt[start]
            return min_txn if min_txn != float('inf') else 0
        return dfs(0)
      
class Solution {
public:
    int minTransfers(vector<vector<int>>& transactions) {
        unordered_map<int, int> balance;
        for (auto& t : transactions) {
            balance[t[0]] -= t[2];
            balance[t[1]] += t[2];
        }
        vector<int> debt;
        for (auto& p : balance)
            if (p.second != 0)
                debt.push_back(p.second);
        return dfs(debt, 0);
    }
    
    int dfs(vector<int>& debt, int start) {
        while (start < debt.size() && debt[start] == 0) ++start;
        if (start == debt.size()) return 0;
        int min_txn = INT_MAX;
        for (int i = start + 1; i < debt.size(); ++i) {
            if (debt[start] * debt[i] < 0) {
                debt[i] += debt[start];
                min_txn = min(min_txn, 1 + dfs(debt, start + 1));
                debt[i] -= debt[start];
            }
        }
        return min_txn == INT_MAX ? 0 : min_txn;
    }
};
      
class Solution {
    public int minTransfers(int[][] transactions) {
        Map<Integer, Integer> balance = new HashMap<>();
        for (int[] t : transactions) {
            balance.put(t[0], balance.getOrDefault(t[0], 0) - t[2]);
            balance.put(t[1], balance.getOrDefault(t[1], 0) + t[2]);
        }
        List<Integer> debt = new ArrayList<>();
        for (int val : balance.values())
            if (val != 0) debt.add(val);
        return dfs(debt, 0);
    }
    
    private int dfs(List<Integer> debt, int start) {
        while (start < debt.size() && debt.get(start) == 0) start++;
        if (start == debt.size()) return 0;
        int minTxn = Integer.MAX_VALUE;
        for (int i = start + 1; i < debt.size(); i++) {
            if (debt.get(start) * debt.get(i) < 0) {
                debt.set(i, debt.get(i) + debt.get(start));
                minTxn = Math.min(minTxn, 1 + dfs(debt, start + 1));
                debt.set(i, debt.get(i) - debt.get(start));
            }
        }
        return minTxn == Integer.MAX_VALUE ? 0 : minTxn;
    }
}
      
var minTransfers = function(transactions) {
    const balance = {};
    for (const [from, to, amt] of transactions) {
        balance[from] = (balance[from] || 0) - amt;
        balance[to] = (balance[to] || 0) + amt;
    }
    const debt = [];
    for (const val of Object.values(balance)) {
        if (val !== 0) debt.push(val);
    }
    function dfs(start) {
        while (start < debt.length && debt[start] === 0) start++;
        if (start === debt.length) return 0;
        let minTxn = Infinity;
        for (let i = start + 1; i < debt.length; i++) {
            if (debt[start] * debt[i] < 0) {
                debt[i] += debt[start];
                minTxn = Math.min(minTxn, 1 + dfs(start + 1));
                debt[i] -= debt[start];
            }
        }
        return minTxn === Infinity ? 0 : minTxn;
    }
    return dfs(0);
};