Skip to content

Commit ffb76fd

Browse files
authored
Merge pull request #163 from BrianLusina/feat/algorithms-backtracking
feat(algorithms, backtracking): optimal account balancing
2 parents 95377e8 + 93ced3c commit ffb76fd

20 files changed

+205
-0
lines changed

DIRECTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
* [Test Decode Message](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/decode_message/test_decode_message.py)
2828
* Letter Combination
2929
* [Test Letter Combination](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/letter_combination/test_letter_combination.py)
30+
* Optimal Account Balancing
31+
* [Test Optimal Account Balancing](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/optimal_account_balancing/test_optimal_account_balancing.py)
3032
* Partition String
3133
* [Test Partition String](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/backtracking/partition_string/test_partition_string.py)
3234
* Permutations
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Optimal Account Balancing
2+
3+
Given a list of transactions, where each transaction is represented as transactions[i]=[fromi, toi, amounti], indicating
4+
that the person fromi gave amounti to the person toi.
5+
6+
Return the minimum number of transactions needed to settle all debts.
7+
8+
## Constraints
9+
10+
- 1 ≤ transactions.length ≤ 10
11+
- transactions[i].length == 3
12+
- 0 ≤ fromi, toi ≤ 10
13+
- 1 ≤ amounti ≤ 100
14+
- fromi ≠ toi
15+
16+
## Examples
17+
18+
![Example 1](./images/examples/optimal_account_balancing_example_1.png)
19+
![Example 2](./images/examples/optimal_account_balancing_example_2.png)
20+
![Example 3](./images/examples/optimal_account_balancing_example_3.png)
21+
22+
## Solution
23+
24+
This solution calculates the minimum number of transactions required to settle debts among a group of people based on an
25+
initial set of transactions. It begins by computing each person’s net balance, indicating how much they owe or are owed.
26+
Any individuals with a zero net balance are ignored, as they have no outstanding debts or credits.
27+
28+
Once the net balances are calculated, the algorithm uses a recursive approach to settle the remaining balances. It pairs
29+
individuals with opposite balances (i.e., those who owe with those who are owed) to reduce them to zero with the minimum
30+
number of transactions. The solution explores various pairings and tracks each combination’s cost (number of transactions),
31+
ensuring optimal results. If a particular path doesn’t minimize the transactions, it backtracks to explore alternative
32+
pairings.
33+
34+
Here’s the step-by-step implementation of the solution:
35+
36+
- For each transaction, decrease the balance of the person who gave the money and increase the balance of the person who
37+
received it.
38+
39+
- Ignore people with a zero balance after all transactions, as they neither owe nor are owed.
40+
- Use depth-first search (DFS) to recursively calculate the minimum number of transactions required to settle all balances.
41+
- Base case: If the current person reaches n (number of people with non-zero balances), meaning all balances are zero
42+
or settled, return 0.
43+
- For each next person:
44+
- If the current and next person have opposite sign balance:
45+
- Temporarily add the current person’s balance to the next person’s balance.
46+
- Recursively call DFS with the next person’s index.
47+
- Track the minimum of the existing cost and the new cost calculated by DFS.
48+
- **Backtrack**: Restore the balance to its original state by reversing the temporary addition.
49+
- Return the minimum cost for all possible transaction paths.
50+
51+
Let’s look at the following illustration to get a better understanding of the solution:
52+
53+
![Solution 1](./images/solutions/optimal_account_balancing_solution_1.png)
54+
![Solution 2](./images/solutions/optimal_account_balancing_solution_2.png)
55+
![Solution 3](./images/solutions/optimal_account_balancing_solution_3.png)
56+
![Solution 4](./images/solutions/optimal_account_balancing_solution_4.png)
57+
![Solution 5](./images/solutions/optimal_account_balancing_solution_5.png)
58+
![Solution 6](./images/solutions/optimal_account_balancing_solution_6.png)
59+
![Solution 7](./images/solutions/optimal_account_balancing_solution_7.png)
60+
![Solution 8](./images/solutions/optimal_account_balancing_solution_8.png)
61+
![Solution 9](./images/solutions/optimal_account_balancing_solution_9.png)
62+
![Solution 10](./images/solutions/optimal_account_balancing_solution_10.png)
63+
![Solution 11](./images/solutions/optimal_account_balancing_solution_11.png)
64+
![Solution 12](./images/solutions/optimal_account_balancing_solution_12.png)
65+
![Solution 13](./images/solutions/optimal_account_balancing_solution_13.png)
66+
67+
### Time Complexity
68+
69+
The time complexity is O((n−1)!), where n is the number of persons. This complexity arises because, in the initial call
70+
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.
71+
This results in a chain of recursive calls multiplied by the choices at each level, giving a factorial pattern (n−1)!.
72+
73+
### Space Complexity
74+
75+
The space complexity is O(n), where n is the number of unique amounts. This is because both balance_map and balance can
76+
hold at most n amounts.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from typing import List, DefaultDict
2+
from collections import defaultdict
3+
4+
5+
def min_transfers_dfs(transactions: List[List[int]]) -> int:
6+
if not transactions:
7+
return 0
8+
9+
# Net balances where the key is the person and the value is their balance
10+
net_balances: DefaultDict[int, int] = defaultdict(int)
11+
12+
# Populate net balances
13+
for transaction in transactions:
14+
sender, receiver, amount = transaction
15+
net_balances[sender] -= amount
16+
net_balances[receiver] += amount
17+
18+
# Remove zero balances
19+
non_zero_balances = [amt for amt in net_balances.values() if amt != 0]
20+
number_of_non_zero_balances = len(non_zero_balances)
21+
22+
if non_zero_balances == 0:
23+
return 0
24+
25+
def dfs(current: int, balances: List[int]) -> int:
26+
# Skip settled accounts, move to the next person with non-zero balaance
27+
while current < number_of_non_zero_balances and balances[current] == 0:
28+
current += 1
29+
# All accounts settled
30+
if current >= number_of_non_zero_balances:
31+
return 0
32+
33+
min_transactions = float("inf")
34+
# Try to settle non_zero_balances[start] by paring with another opposite sign balance
35+
for j in range(current + 1, number_of_non_zero_balances):
36+
# One owes, the other is owed
37+
if balances[current] * balances[j] < 0:
38+
balances[j] += balances[current]
39+
# recurse for remaining transactions after settling this one
40+
min_transactions = min(min_transactions, 1 + dfs(current + 1, balances))
41+
# backtrack, undo the settlement
42+
non_zero_balances[j] -= non_zero_balances[current]
43+
44+
return min_transactions
45+
46+
return dfs(0, non_zero_balances)
47+
48+
49+
def min_transfers_backtrack(transactions: List[List[int]]) -> int:
50+
if not transactions:
51+
return 0
52+
53+
# Net balances where the key is the person and the value is their balance
54+
net_balances: DefaultDict[int, int] = defaultdict(int)
55+
56+
# Populate net balances
57+
for transaction in transactions:
58+
sender, receiver, amount = transaction
59+
net_balances[sender] -= amount
60+
net_balances[receiver] += amount
61+
62+
# Remove zero balances
63+
non_zero_balances = [amt for amt in net_balances.values() if amt != 0]
64+
number_of_non_zero_balances = len(non_zero_balances)
65+
66+
if non_zero_balances == 0:
67+
return 0
68+
69+
def backtrack(start: int) -> int:
70+
# Skip settled accounts
71+
while start < number_of_non_zero_balances and non_zero_balances[start] == 0:
72+
start += 1
73+
# All accounts settled
74+
if start >= number_of_non_zero_balances:
75+
return 0
76+
77+
min_transactions = float("inf")
78+
# Try to settle non_zero_balances[start] by paring with another opposite sign balance
79+
for j in range(start + 1, number_of_non_zero_balances):
80+
# One owes, the other is owed
81+
if non_zero_balances[start] * non_zero_balances[j] < 0:
82+
non_zero_balances[j] += non_zero_balances[start]
83+
# recurse for remaining transactions after settling this one
84+
min_transactions = min(min_transactions, 1 + backtrack(start + 1))
85+
# backtrack, undo the settlement
86+
non_zero_balances[j] -= non_zero_balances[start]
87+
if non_zero_balances[j] + non_zero_balances[start] == 0:
88+
break
89+
return min_transactions
90+
91+
return backtrack(0)
118 KB
Loading
111 KB
Loading
142 KB
Loading
54.5 KB
Loading
90.9 KB
Loading
98.3 KB
Loading
102 KB
Loading

0 commit comments

Comments
 (0)