diff --git a/DIRECTORY.md b/DIRECTORY.md index 77c530a6..0576d0df 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -27,6 +27,8 @@ * [Test Decode Message](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/decode_message/test_decode_message.py) * Letter Combination * [Test Letter Combination](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/letter_combination/test_letter_combination.py) + * Optimal Account Balancing + * [Test Optimal Account Balancing](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/optimal_account_balancing/test_optimal_account_balancing.py) * Partition String * [Test Partition String](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/partition_string/test_partition_string.py) * Permutations diff --git a/algorithms/backtracking/optimal_account_balancing/README.md b/algorithms/backtracking/optimal_account_balancing/README.md new file mode 100644 index 00000000..33c190da --- /dev/null +++ b/algorithms/backtracking/optimal_account_balancing/README.md @@ -0,0 +1,76 @@ +# Optimal Account Balancing + +Given a list of transactions, where each transaction is represented as transactions[i]=[fromi, toi, amounti], indicating +that the person fromi gave amounti to the person toi. + +Return the minimum number of transactions needed to settle all debts. + +## Constraints + +- 1 ≤ transactions.length ≤ 10 +- transactions[i].length == 3 +- 0 ≤ fromi, toi ≤ 10 +- 1 ≤ amounti ≤ 100 +- fromi ≠ toi + +## Examples + +![Example 1](./images/examples/optimal_account_balancing_example_1.png) +![Example 2](./images/examples/optimal_account_balancing_example_2.png) +![Example 3](./images/examples/optimal_account_balancing_example_3.png) + +## Solution + +This solution calculates the minimum number of transactions required to settle debts among a group of people based on an +initial set of transactions. It begins by computing each person’s net balance, indicating how much they owe or are owed. +Any individuals with a zero net balance are ignored, as they have no outstanding debts or credits. + +Once the net balances are calculated, the algorithm uses a recursive approach to settle the remaining balances. It pairs +individuals with opposite balances (i.e., those who owe with those who are owed) to reduce them to zero with the minimum +number of transactions. The solution explores various pairings and tracks each combination’s cost (number of transactions), +ensuring optimal results. If a particular path doesn’t minimize the transactions, it backtracks to explore alternative +pairings. + +Here’s the step-by-step implementation of the solution: + +- For each transaction, decrease the balance of the person who gave the money and increase the balance of the person who + received it. + +- Ignore people with a zero balance after all transactions, as they neither owe nor are owed. +- Use depth-first search (DFS) to recursively calculate the minimum number of transactions required to settle all balances. + - Base case: If the current person reaches n (number of people with non-zero balances), meaning all balances are zero + or settled, return 0. + - For each next person: + - If the current and next person have opposite sign balance: + - Temporarily add the current person’s balance to the next person’s balance. + - Recursively call DFS with the next person’s index. + - Track the minimum of the existing cost and the new cost calculated by DFS. + - **Backtrack**: Restore the balance to its original state by reversing the temporary addition. + - Return the minimum cost for all possible transaction paths. + +Let’s look at the following illustration to get a better understanding of the solution: + +![Solution 1](./images/solutions/optimal_account_balancing_solution_1.png) +![Solution 2](./images/solutions/optimal_account_balancing_solution_2.png) +![Solution 3](./images/solutions/optimal_account_balancing_solution_3.png) +![Solution 4](./images/solutions/optimal_account_balancing_solution_4.png) +![Solution 5](./images/solutions/optimal_account_balancing_solution_5.png) +![Solution 6](./images/solutions/optimal_account_balancing_solution_6.png) +![Solution 7](./images/solutions/optimal_account_balancing_solution_7.png) +![Solution 8](./images/solutions/optimal_account_balancing_solution_8.png) +![Solution 9](./images/solutions/optimal_account_balancing_solution_9.png) +![Solution 10](./images/solutions/optimal_account_balancing_solution_10.png) +![Solution 11](./images/solutions/optimal_account_balancing_solution_11.png) +![Solution 12](./images/solutions/optimal_account_balancing_solution_12.png) +![Solution 13](./images/solutions/optimal_account_balancing_solution_13.png) + +### Time Complexity + +The time complexity is O((n−1)!), where n is the number of persons. This complexity arises because, in the initial call +to dfs(0), there are n−1 possible choices for the next person, each leading to a recursive call to dfs(1), and so on. +This results in a chain of recursive calls multiplied by the choices at each level, giving a factorial pattern (n−1)!. + +### Space Complexity + +The space complexity is O(n), where n is the number of unique amounts. This is because both balance_map and balance can +hold at most n amounts. diff --git a/algorithms/backtracking/optimal_account_balancing/__init__.py b/algorithms/backtracking/optimal_account_balancing/__init__.py new file mode 100644 index 00000000..afdb1b58 --- /dev/null +++ b/algorithms/backtracking/optimal_account_balancing/__init__.py @@ -0,0 +1,91 @@ +from typing import List, DefaultDict +from collections import defaultdict + + +def min_transfers_dfs(transactions: List[List[int]]) -> int: + if not transactions: + return 0 + + # Net balances where the key is the person and the value is their balance + net_balances: DefaultDict[int, int] = defaultdict(int) + + # Populate net balances + for transaction in transactions: + sender, receiver, amount = transaction + net_balances[sender] -= amount + net_balances[receiver] += amount + + # Remove zero balances + non_zero_balances = [amt for amt in net_balances.values() if amt != 0] + number_of_non_zero_balances = len(non_zero_balances) + + if non_zero_balances == 0: + return 0 + + def dfs(current: int, balances: List[int]) -> int: + # Skip settled accounts, move to the next person with non-zero balaance + while current < number_of_non_zero_balances and balances[current] == 0: + current += 1 + # All accounts settled + if current >= number_of_non_zero_balances: + return 0 + + min_transactions = float("inf") + # Try to settle non_zero_balances[start] by paring with another opposite sign balance + for j in range(current + 1, number_of_non_zero_balances): + # One owes, the other is owed + if balances[current] * balances[j] < 0: + balances[j] += balances[current] + # recurse for remaining transactions after settling this one + min_transactions = min(min_transactions, 1 + dfs(current + 1, balances)) + # backtrack, undo the settlement + non_zero_balances[j] -= non_zero_balances[current] + + return min_transactions + + return dfs(0, non_zero_balances) + + +def min_transfers_backtrack(transactions: List[List[int]]) -> int: + if not transactions: + return 0 + + # Net balances where the key is the person and the value is their balance + net_balances: DefaultDict[int, int] = defaultdict(int) + + # Populate net balances + for transaction in transactions: + sender, receiver, amount = transaction + net_balances[sender] -= amount + net_balances[receiver] += amount + + # Remove zero balances + non_zero_balances = [amt for amt in net_balances.values() if amt != 0] + number_of_non_zero_balances = len(non_zero_balances) + + if non_zero_balances == 0: + return 0 + + def backtrack(start: int) -> int: + # Skip settled accounts + while start < number_of_non_zero_balances and non_zero_balances[start] == 0: + start += 1 + # All accounts settled + if start >= number_of_non_zero_balances: + return 0 + + min_transactions = float("inf") + # Try to settle non_zero_balances[start] by paring with another opposite sign balance + for j in range(start + 1, number_of_non_zero_balances): + # One owes, the other is owed + if non_zero_balances[start] * non_zero_balances[j] < 0: + non_zero_balances[j] += non_zero_balances[start] + # recurse for remaining transactions after settling this one + min_transactions = min(min_transactions, 1 + backtrack(start + 1)) + # backtrack, undo the settlement + non_zero_balances[j] -= non_zero_balances[start] + if non_zero_balances[j] + non_zero_balances[start] == 0: + break + return min_transactions + + return backtrack(0) diff --git a/algorithms/backtracking/optimal_account_balancing/images/examples/optimal_account_balancing_example_1.png b/algorithms/backtracking/optimal_account_balancing/images/examples/optimal_account_balancing_example_1.png new file mode 100644 index 00000000..e9483a9f Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/examples/optimal_account_balancing_example_1.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/examples/optimal_account_balancing_example_2.png b/algorithms/backtracking/optimal_account_balancing/images/examples/optimal_account_balancing_example_2.png new file mode 100644 index 00000000..aee44184 Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/examples/optimal_account_balancing_example_2.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/examples/optimal_account_balancing_example_3.png b/algorithms/backtracking/optimal_account_balancing/images/examples/optimal_account_balancing_example_3.png new file mode 100644 index 00000000..c99ca86b Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/examples/optimal_account_balancing_example_3.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_1.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_1.png new file mode 100644 index 00000000..94da3ee0 Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_1.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_10.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_10.png new file mode 100644 index 00000000..a87e9b7e Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_10.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_11.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_11.png new file mode 100644 index 00000000..4cb0085f Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_11.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_12.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_12.png new file mode 100644 index 00000000..7bf33b6c Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_12.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_13.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_13.png new file mode 100644 index 00000000..f68f7c36 Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_13.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_2.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_2.png new file mode 100644 index 00000000..44db2dac Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_2.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_3.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_3.png new file mode 100644 index 00000000..5870cf03 Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_3.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_4.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_4.png new file mode 100644 index 00000000..a62e8fd1 Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_4.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_5.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_5.png new file mode 100644 index 00000000..c688ec0e Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_5.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_6.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_6.png new file mode 100644 index 00000000..48c2b51f Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_6.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_7.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_7.png new file mode 100644 index 00000000..cb7be4cf Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_7.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_8.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_8.png new file mode 100644 index 00000000..df1f4dc4 Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_8.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_9.png b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_9.png new file mode 100644 index 00000000..7b9c40b2 Binary files /dev/null and b/algorithms/backtracking/optimal_account_balancing/images/solutions/optimal_account_balancing_solution_9.png differ diff --git a/algorithms/backtracking/optimal_account_balancing/test_optimal_account_balancing.py b/algorithms/backtracking/optimal_account_balancing/test_optimal_account_balancing.py new file mode 100644 index 00000000..c501f8fc --- /dev/null +++ b/algorithms/backtracking/optimal_account_balancing/test_optimal_account_balancing.py @@ -0,0 +1,36 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.backtracking.optimal_account_balancing import ( + min_transfers_backtrack, + min_transfers_dfs, +) + +OPTIMAL_ACCOUNT_BALANCING_TEST_CASES = [ + ([[0, 1, 10], [2, 0, 5]], 2), + ([[0, 1, 10], [1, 0, 1], [1, 2, 5], [2, 0, 5]], 1), + ([[0, 1, 40], [1, 0, 30], [1, 4, 10], [4, 5, 15], [0, 3, 10]], 3), + ([[0, 1, 10], [1, 2, 20], [2, 3, 30], [3, 4, 40], [4, 5, 50]], 5), + ([[0, 1, 10], [1, 2, 20], [2, 3, 30]], 3), + ([[0, 1, 20], [0, 1, 20], [2, 0, 30], [3, 4, 30], [4, 5, 5]], 4), + ([[0, 1, 25], [1, 2, 5], [2, 0, 15]], 2), + ([[0, 2, 5], [1, 2, 10], [1, 0, 30], [0, 1, 25]], 1), +] + + +class OptimalAccountBalancingTestCase(unittest.TestCase): + @parameterized.expand(OPTIMAL_ACCOUNT_BALANCING_TEST_CASES) + def test_min_transfers_backtrack( + self, transactions: List[List[int]], expected: int + ): + actual = min_transfers_backtrack(transactions) + self.assertEqual(expected, actual) + + @parameterized.expand(OPTIMAL_ACCOUNT_BALANCING_TEST_CASES) + def test_min_transfers_dfs(self, transactions: List[List[int]], expected: int): + actual = min_transfers_dfs(transactions) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main()