3711. Maximum Transactions Without Negative Balance 🔒
Problem Description
You are given an integer array transactions, where transactions[i] represents the amount of the i-th transaction:
- A positive value means money is received (the balance increases).
- A negative value means money is sent (the balance decreases).
The account starts with a balance of 0, and the balance must never become negative at any point. Transactions must be processed in the given order, but you are allowed to skip some of them.
Return the maximum number of transactions that can be performed such that the balance never goes below zero.
In other words, you need to pick the largest possible subset of transactions (keeping their original order) so that, when applied one by one starting from a balance of 0, the running balance always stays at 0 or above.
For example, if transactions = [5, -3, -4, 2]:
- Performing all four transactions gives running balances
5, 2, -2, 0— the balance becomes negative after the third transaction, so this is not allowed. - Skipping
-4gives running balances5, 2, 4— the balance never goes negative, so3transactions can be performed.
The answer in this case would be 3.
How We Pick the Algorithm
Why Greedy Algorithms?
This problem maps to Greedy Algorithms through a short path in the full flowchart.
The regret-greedy strategy of always discarding the most negative transaction when the balance dips below zero guarantees the maximum number of transactions.
Open in FlowchartIntuition
The first observation is that positive transactions are always good — receiving money never hurts the balance, so there is no reason to ever skip a positive amount. The only problem comes from negative transactions, which can push the balance below zero.
So the real question becomes: which negative transactions should we skip?
A natural greedy idea emerges: try to take every transaction optimistically. As we process them one by one, keep a running balance s. As long as s stays non-negative, everything is fine. But the moment s drops below zero, we know we have taken too many "bad" transactions — we must undo (skip) at least one of the transactions we've taken so far.
Now, which one should we undo? To fix a negative balance, we want the biggest recovery per removal. Removing a transaction with amount y changes the balance by -y, so removing the smallest (most negative) amount gives the largest boost to the balance. By always discarding the minimum element among the transactions chosen so far, we restore the balance to non-negative while sacrificing the fewest transactions.
This is a classic "regret greedy" pattern: commit to every choice eagerly, and when a constraint is violated, retract the single worst choice made so far. Note that it never matters which specific transaction we drop in terms of ordering — dropping any past transaction just removes its amount from the running sum, and the relative order of the remaining ones is preserved. So only the value matters, and the smallest value is always the best candidate to drop.
To efficiently find and remove the minimum among chosen transactions, we keep them in an ordered structure (a SortedList in Python, or equivalently a min-heap / multiset):
- Add each new transaction
xto the structure and to the balances. - While
s < 0, pop the smallest elementy, dos -= y, and reduce the answer count by1.
Since each transaction is added once and removed at most once, the greedy runs in O(n log n) time, and the final count of kept transactions is guaranteed to be the maximum possible.
Pattern Learn more about Greedy and Heap (Priority Queue) patterns.
Solution Approach
The algorithm follows the Greedy + Ordered Set pattern described in the reference solution. We need a data structure that supports two operations efficiently:
- Insert a new transaction amount.
- Find and remove the smallest amount currently selected.
Python's SortedList (from the sortedcontainers library) handles both in O(log n) time. A min-heap would also work, since we only ever need the minimum.
Step-by-step walkthrough
Initialization
st = SortedList()— the ordered set holding all currently selected transaction amounts.s = 0— the running balance of the selected transactions.ans = len(transactions)— we optimistically assume all transactions can be kept, and decrement this counter each time we are forced to drop one.
Main loop — for each transaction amount x:
-
Take it greedily: add
xto the balance withs += x, and insertxinto the ordered set withst.add(x). -
Repair if broken: if the balance is now negative (
s < 0), we have over-committed. Whiles < 0:- Pop the smallest selected amount:
y = st.pop(0). Sincesis negative, this smallest element is guaranteed to be negative (a set of only non-negative amounts can never sum below zero). - Undo its effect on the balance:
s -= y. Becausey < 0, this strictly increasessby|y|, the largest possible single-removal gain. - One fewer transaction is kept:
ans -= 1.
The loop repeats until
s >= 0, ensuring the balance constraint holds after every step. - Pop the smallest selected amount:
Result: after processing all transactions, ans is the maximum number of transactions that can be performed.
Why one pass suffices
A key invariant: after processing each transaction, s is the sum of the kept transactions and s >= 0. When a future deficit occurs, removing the global minimum of the kept set is always optimal — any other removal recovers less balance, so it can never allow strictly more transactions to survive later. This exchange argument justifies the greedy choice.
Code
class Solution:
def maxTransactions(self, transactions: List[int]) -> int:
st = SortedList()
s = 0
ans = len(transactions)
for x in transactions:
s += x
st.add(x)
while s < 0:
y = st.pop(0) # remove the smallest (most negative) amount
s -= y # balance increases by |y|
ans -= 1 # one fewer transaction kept
return ans
Complexity Analysis
- Time:
O(n log n)— each of thentransactions is inserted into theSortedListonce (O(log n)) and removed at most once (O(log n)withpop(0)on aSortedList). The innerwhileloop's total work across the entire run is bounded by the number of removals, which is at mostn. - Space:
O(n)— the ordered set can hold up tonamounts.
A trace example
For transactions = [3, -5, 4, -2]:
| Step | x | After adding: s, st | Repair | ans |
|---|---|---|---|---|
| 1 | 3 | s = 3, st = [3] | none | 4 |
| 2 | -5 | s = -2, st = [-5, 3] | pop -5 → s = 3, st = [3] | 3 |
| 3 | 4 | s = 7, st = [3, 4] | none | 3 |
| 4 | -2 | s = 5, st = [-2, 3, 4] | none | 3 |
The answer is 3: keep [3, 4, -2] with running balances 3, 7, 5, all non-negative.
Example Walkthrough
Let's trace the algorithm on transactions = [3, -4, 2, -3, 5, -7].
Setup: st = [] (ordered set of kept amounts), s = 0 (running balance), ans = 6 (optimistically assume all six transactions are kept).
Step 1 — x = 3:
Add it: s = 3, st = [3]. Balance is non-negative, no repair needed. ans = 6.
Step 2 — x = -4:
Add it: s = -1, st = [-4, 3]. The balance is negative, so we repair: pop the smallest element y = -4, giving s = -1 - (-4) = 3 and st = [3]. One transaction is dropped: ans = 5. Effectively, we decided to skip the -4.
Step 3 — x = 2:
Add it: s = 5, st = [2, 3]. No repair. ans = 5.
Step 4 — x = -3:
Add it: s = 2, st = [-3, 2, 3]. The balance stays non-negative, so this negative transaction is kept — negatives aren't automatically bad, only the ones the balance can't absorb. ans = 5.
Step 5 — x = 5:
Add it: s = 7, st = [-3, 2, 3, 5]. No repair. ans = 5.
Step 6 — x = -7:
Add it: s = 0 - 7 + 7 = 0... let's compute carefully: s = 7 + (-7) = 0. Hmm — actually 7 - 7 = 0, which is non-negative, so let's make this step interesting by checking the arithmetic precisely: s = 7 + (-7) = 0 ≥ 0. To showcase a second repair, suppose the last value were -8 instead:
Add it: s = 7 + (-8) = -1, st = [-8, -3, 2, 3, 5]. Repair: pop the smallest, y = -8 (note the structure compares -8 vs -3 and correctly picks the more negative one — the biggest recovery per removal). Now s = -1 - (-8) = 7, ans = 4.
Summary table for transactions = [3, -4, 2, -3, 5, -8]:
| Step | x | After adding: s, st | Repair | ans |
|---|---|---|---|---|
| 1 | 3 | s = 3, st = [3] | none | 6 |
| 2 | -4 | s = -1, st = [-4, 3] | pop -4 → s = 3 | 5 |
| 3 | 2 | s = 5, st = [2, 3] | none | 5 |
| 4 | -3 | s = 2, st = [-3, 2, 3] | none | 5 |
| 5 | 5 | s = 7, st = [-3, 2, 3, 5] | none | 5 |
| 6 | -8 | s = -1, st = [-8, -3, 2, 3, 5] | pop -8 → s = 7 | 4 |
Result: ans = 4. The kept transactions are [3, 2, -3, 5], with running balances 3, 5, 2, 7 — never negative.
Verification of optimality: Could we keep 5 transactions? Both -4 and -8 are problematic: keeping -4 after only 3 yields balance -1, and keeping -8 would require the prior balance to reach at least 8, but even keeping everything else (3 + 2 - 3 + 5 = 7) falls short — and dropping -3 to keep -8 gives 3 + 2 + 5 = 10 → 2, which still only totals 4 kept transactions. So 4 is indeed the maximum, matching the greedy's answer.
The walkthrough highlights the two key behaviors of the regret-greedy: negative transactions are kept whenever the balance can absorb them (step 4), and whenever the balance breaks, exactly the most negative committed amount is retracted (steps 2 and 6).
Solution Implementation
1from typing import List
2from sortedcontainers import SortedList
3
4
5class Solution:
6 def maxTransactions(self, transactions: List[int]) -> int:
7 # A sorted container to keep track of all transactions
8 # accepted so far, ordered from smallest to largest.
9 sorted_transactions = SortedList()
10
11 # Running balance after applying the accepted transactions.
12 balance = 0
13
14 # Start by assuming every transaction can be kept.
15 max_count = len(transactions)
16
17 for amount in transactions:
18 # Tentatively accept the current transaction.
19 balance += amount
20 sorted_transactions.add(amount)
21
22 # If the balance turns negative, greedily drop the
23 # smallest (most negative) transaction(s) until the
24 # balance becomes non-negative again. Removing the
25 # smallest value restores the balance the fastest
26 # while sacrificing the fewest transactions.
27 while balance < 0:
28 smallest = sorted_transactions.pop(0)
29 balance -= smallest
30 max_count -= 1
31
32 # The remaining count is the maximum number of
33 # transactions that can be kept with a valid balance.
34 return max_count
35```
36
37**Explanation of the approach:**
38
391. **Greedy strategy** — Process transactions in order, tentatively accepting each one while tracking the running balance.
402. **Conflict resolution** — Whenever the balance dips below zero, the smallest (most negative) accepted transaction is discarded. This restores the balance as quickly as possible while minimizing the number of removals.
413. **Data structure choice** — `SortedList` provides `O(log n)` insertion and efficient access to the minimum element, giving an overall time complexity of `O(n log n)`.
42
43**Alternative perspective:** A min-heap (`heapq`) could replace `SortedList` here, since only the smallest element is ever removed. That would achieve the same `O(n log n)` complexity using only the standard library:
44
45```python3
46import heapq
47from typing import List
48
49
50class Solution:
51 def maxTransactions(self, transactions: List[int]) -> int:
52 min_heap = []
53 balance = 0
54 max_count = len(transactions)
55
56 for amount in transactions:
57 balance += amount
58 heapq.heappush(min_heap, amount)
59
60 # Drop the most negative transaction while balance < 0.
61 while balance < 0:
62 balance -= heapq.heappop(min_heap)
63 max_count -= 1
64
65 return max_count
661class Solution {
2 public int maxTransactions(int[] transactions) {
3 // TreeMap to keep track of all accepted transactions,
4 // mapping transaction value -> its occurrence count.
5 // The keys are sorted in ascending order, so the smallest
6 // (most negative) transaction is always at the front.
7 TreeMap<Integer, Integer> countMap = new TreeMap<>();
8
9 // Start by assuming every transaction can be kept.
10 int maxCount = transactions.length;
11
12 // Running sum (balance) of all currently accepted transactions.
13 long runningSum = 0;
14
15 for (int amount : transactions) {
16 // Tentatively accept the current transaction.
17 runningSum += amount;
18 countMap.merge(amount, 1, Integer::sum);
19
20 // If the balance goes negative, greedily discard the
21 // smallest (most negative) transactions until the
22 // balance becomes non-negative again.
23 while (runningSum < 0) {
24 int smallest = countMap.firstKey();
25
26 // Removing the smallest value increases the balance,
27 // since subtracting a negative number adds to the sum.
28 runningSum -= smallest;
29
30 // One fewer transaction is kept.
31 --maxCount;
32
33 // Decrease its count; remove the entry entirely
34 // when no occurrences remain.
35 if (countMap.merge(smallest, -1, Integer::sum) == 0) {
36 countMap.remove(smallest);
37 }
38 }
39 }
40
41 return maxCount;
42 }
43}
441class Solution {
2public:
3 int maxTransactions(vector<int>& transactions) {
4 // Min-heap-like container that keeps all currently accepted transactions sorted.
5 // A multiset is used because duplicate transaction values are allowed.
6 multiset<int> acceptedTransactions;
7
8 // Initially assume that every transaction can be kept.
9 int maxCount = transactions.size();
10
11 // Running sum of all currently accepted transactions.
12 // Use long long to avoid potential integer overflow.
13 long long runningSum = 0;
14
15 // Process transactions one by one in their original order.
16 for (int transaction : transactions) {
17 // Tentatively accept the current transaction.
18 runningSum += transaction;
19 acceptedTransactions.insert(transaction);
20
21 // If the running sum becomes negative, the current selection is invalid.
22 // Greedily remove the smallest (most negative) transaction(s)
23 // until the running sum is non-negative again.
24 while (runningSum < 0) {
25 // The smallest element is at the beginning of the multiset.
26 auto smallestIt = acceptedTransactions.begin();
27 int smallestValue = *smallestIt;
28
29 // Remove it from the selection and update the running sum.
30 acceptedTransactions.erase(smallestIt);
31 runningSum -= smallestValue;
32
33 // One fewer transaction can be kept.
34 --maxCount;
35 }
36 }
37
38 // Return the maximum number of transactions that can be kept
39 // while the running sum never stays negative.
40 return maxCount;
41 }
42};
431/**
2 * Inserts a value into the min-heap and restores the heap property
3 * by sifting the newly added value up toward the root.
4 */
5function heapPush(heap: number[], value: number): void {
6 heap.push(value);
7 let childIndex = heap.length - 1;
8
9 while (childIndex > 0) {
10 const parentIndex = (childIndex - 1) >> 1;
11
12 // Stop when the parent is already smaller than or equal to the child.
13 if (heap[parentIndex] <= heap[childIndex]) {
14 break;
15 }
16
17 // Swap child with parent and continue sifting up.
18 [heap[parentIndex], heap[childIndex]] = [heap[childIndex], heap[parentIndex]];
19 childIndex = parentIndex;
20 }
21}
22
23/**
24 * Removes and returns the smallest value (the root) from the min-heap,
25 * then restores the heap property by sifting the relocated value down.
26 */
27function heapPop(heap: number[]): number {
28 const smallestValue = heap[0];
29 const lastValue = heap.pop()!;
30
31 if (heap.length > 0) {
32 // Move the last element to the root and sift it down.
33 heap[0] = lastValue;
34 let parentIndex = 0;
35
36 while (true) {
37 const leftIndex = parentIndex * 2 + 1;
38 const rightIndex = leftIndex + 1;
39 let smallestIndex = parentIndex;
40
41 if (leftIndex < heap.length && heap[leftIndex] < heap[smallestIndex]) {
42 smallestIndex = leftIndex;
43 }
44 if (rightIndex < heap.length && heap[rightIndex] < heap[smallestIndex]) {
45 smallestIndex = rightIndex;
46 }
47
48 // Stop when the parent is smaller than both children.
49 if (smallestIndex === parentIndex) {
50 break;
51 }
52
53 // Swap parent with the smaller child and continue sifting down.
54 [heap[parentIndex], heap[smallestIndex]] = [heap[smallestIndex], heap[parentIndex]];
55 parentIndex = smallestIndex;
56 }
57 }
58
59 return smallestValue;
60}
61
62/**
63 * Returns the maximum number of transactions that can be kept
64 * such that the running sum of the kept transactions never stays negative.
65 */
66function maxTransactions(transactions: number[]): number {
67 // Min-heap that keeps all currently accepted transactions,
68 // with the smallest (most negative) value at the root.
69 const acceptedTransactions: number[] = [];
70
71 // Initially assume that every transaction can be kept.
72 let maxCount = transactions.length;
73
74 // Running sum of all currently accepted transactions.
75 let runningSum = 0;
76
77 // Process transactions one by one in their original order.
78 for (const transaction of transactions) {
79 // Tentatively accept the current transaction.
80 runningSum += transaction;
81 heapPush(acceptedTransactions, transaction);
82
83 // If the running sum becomes negative, the current selection is invalid.
84 // Greedily remove the smallest (most negative) transaction(s)
85 // until the running sum is non-negative again.
86 while (runningSum < 0) {
87 const smallestValue = heapPop(acceptedTransactions);
88
89 // Remove it from the selection and update the running sum.
90 runningSum -= smallestValue;
91
92 // One fewer transaction can be kept.
93 --maxCount;
94 }
95 }
96
97 // Maximum number of transactions that can be retained.
98 return maxCount;
99}
100Time and Space Complexity
The time complexity is O(n × log n), where n is the number of transactions. In each iteration of the main loop, the operation st.add(x) inserts an element into the SortedList, which takes O(log n) time. Inside the while loop, st.pop(0) removes the smallest element, which also takes O(log n) time. Since each element is added to and removed from the SortedList at most once across the entire execution, the total number of add and pop operations is bounded by O(n), yielding an overall time complexity of O(n × log n).
The space complexity is O(n), since the SortedList st may store up to n elements in the worst case.
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Skipping the current transaction instead of removing the most negative kept transaction
A very natural — but incorrect — instinct is: "if accepting this transaction makes the balance negative, just skip it and move on." This leads to code like the following:
class Solution:
def maxTransactions(self, transactions: List[int]) -> int:
balance = 0
count = 0
for amount in transactions:
if balance + amount >= 0: # only accept if it keeps balance valid
balance += amount
count += 1
# otherwise skip the current transaction
return count
Why it's wrong: the deficit caused by the current transaction might be better resolved by retroactively dropping an earlier, more negative transaction. Removing the most negative kept amount recovers the maximum possible balance with a single removal, which can allow strictly more future transactions to survive.
Counterexample: transactions = [4, -4, -1, -1, -1]
-
Naive "skip current" approach:
- Take
4→ balance4 - Take
-4→ balance0 -1would make balance-1→ skip-1→ skip,-1→ skip- Result: 2 transactions kept (
[4, -4]).
- Take
-
Correct greedy approach:
- Take
4, take-4→ balance0 - Take
-1→ balance-1→ pop the minimum kept value-4→ balance3 - Take
-1→ balance2, take-1→ balance1 - Result: 4 transactions kept (
[4, -1, -1, -1]).
- Take
The naive approach treats decisions as final, but the optimal strategy requires the ability to revoke an earlier choice. Once a deficit appears, the kept set should be repaired by ejecting its smallest element — which may be the current transaction itself, or one accepted long ago.
Solution: always tentatively accept the transaction, then repair while the balance is negative by removing the global minimum of the kept set (via a min-heap or SortedList):
import heapq
from typing import List
class Solution:
def maxTransactions(self, transactions: List[int]) -> int:
min_heap = []
balance = 0
max_count = len(transactions)
for amount in transactions:
balance += amount
heapq.heappush(min_heap, amount) # accept first, even if it breaks the balance
while balance < 0: # then repair by ejecting the most negative kept value
balance -= heapq.heappop(min_heap)
max_count -= 1
return max_count
Note that the repair loop is safe: whenever balance < 0, the heap's minimum is guaranteed to be negative (a set containing only non-negative values can never sum below zero), so each pop strictly increases the balance and the loop terminates.
Related mistakes to watch for
- Popping the wrong end of a
SortedList:st.pop()with no argument removes the largest element. You must usest.pop(0)to remove the smallest. Popping the largest both fails to fix the deficit quickly and can loop forever or discard valuable positive transactions. - Replacing the
whilewith anif: a single removal may not be enough. For example, after several small negatives accumulate, one new transaction can push the balance deep below zero, requiring multiple removals beforebalance >= 0again. - Negating values for a "max-heap": Python's
heapqis already a min-heap, which is exactly what this problem needs. Reflexively negating values (a common max-heap idiom) silently turns the logic into "remove the largest kept transaction," producing wrong answers.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapThe three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!