Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions algorithms/backtracking/optimal_account_balancing/README.md
Original file line number Diff line number Diff line change
@@ -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.
91 changes: 91 additions & 0 deletions algorithms/backtracking/optimal_account_balancing/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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()
Loading