CF 1858E1 - Rollbacks (Easy Version)
We are asked to maintain an array under four types of operations: appending an element, removing the last $k$ elements, rolling back the last change (which could be either an append or a removal), and querying the number of distinct elements currently in the array.
CF 1858E1 - Rollbacks (Easy Version)
Rating: 2500
Tags: brute force, data structures, dfs and similar, trees
Solve time: 1m 23s
Verified: yes
Solution
Problem Understanding
We are asked to maintain an array under four types of operations: appending an element, removing the last $k$ elements, rolling back the last change (which could be either an append or a removal), and querying the number of distinct elements currently in the array. Each query is processed sequentially, and the rollbacks only affect the most recent modifications, not queries themselves.
The input consists of up to one million queries, and each element added can be as large as one million. Query type ? asking for the number of distinct elements appears at most 100,000 times. This means we cannot afford to rebuild the set of distinct elements from scratch each time we query, because recomputing a set of length up to $10^6$ would be too slow. We also cannot store full snapshots of the array for each change because that would exceed memory limits.
A naive approach would be to simulate the array directly and, on each ? query, convert it into a set to count distinct elements. While this is correct logically, the worst-case scenario involves 100,000 queries over arrays of length up to 1,000,000, leading to $O(10^{11})$ operations. This is clearly impractical for a 3-second limit.
Non-obvious edge cases include repeated rollbacks, alternating appends and removals, and multiple identical numbers. For example, if the array is [1,2,2,3] and we remove two elements, the array becomes [1,2]. If we then rollback, it must return precisely to [1,2,2,3], preserving counts correctly. A naive implementation that does not store history carefully will produce [1,2,3] or some incorrect state.
Approaches
The brute-force method would maintain a simple list for the array and process each query literally. For append + x, add x to the list. For removal - k, slice off the last k elements. For rollback !, maintain a stack of snapshots and revert to the previous snapshot. For distinct query ?, convert the array into a set and return its size. While logically correct, this approach fails both on speed and memory: storing snapshots of large arrays for rollback consumes excessive space, and counting distinct elements repeatedly is too slow.
The key insight is that we do not need the entire array for rollbacks or distinct counts. Instead, we can use a stack to represent the array incrementally and store the necessary information to undo operations. For distinct counts, we maintain a frequency map of elements. Each append increases the frequency of the added element, each removal decreases the frequency, and each rollback simply reverses the last frequency changes. For removals, we store the removed elements in the history so rollback can restore them accurately. This ensures $O(1)$ update per change and $O(1)$ retrieval of distinct counts using the frequency map's keys with positive count.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force | O(q * n) | O(q * n) | Too slow |
| Optimal | O(q) | O(n) | Accepted |
Algorithm Walkthrough
- Initialize an empty list
afor the array, a frequency dictionaryfreqmapping elements to their current counts, and a history stackhistoryto store the changes for rollback. - For each query, read the operation type. If it is
+ x, appendxtoa, incrementfreq[x]by 1, and push('+', x)ontohistory. The history entry will allow undoing this append later. - If the query is
- k, remove the lastkelements froma. For each removed element, decrement its frequency infreq. Store the list of removed elements as a tuple('-', removed_elements)inhistoryto allow rollback. - If the query is
!, pop the last operation fromhistory. If it was an append('+', x), remove the last element fromaand decrementfreq[x]. If it was a removal('-', removed_elements), append all removed elements back toaand increment their frequencies infreq. - If the query is
?, return the number of keys infreqwith positive counts, which gives the current number of distinct elements.
Why it works: The frequency map always reflects the current array composition, counting each element accurately. The history stack guarantees that each rollback precisely reverses the last modification, whether an append or removal. This preserves both the array content and distinct element counts, ensuring correctness for every ? query.
Python Solution
import sys
input = sys.stdin.readline
from collections import defaultdict
def main():
q = int(input())
a = []
freq = defaultdict(int)
history = []
results = []
for _ in range(q):
line = input().strip()
if line[0] == '+':
x = int(line[2:])
a.append(x)
freq[x] += 1
history.append(('+', x))
elif line[0] == '-':
k = int(line[2:])
removed = []
for _ in range(k):
val = a.pop()
freq[val] -= 1
removed.append(val)
history.append(('-', removed))
elif line[0] == '!':
if not history:
continue
op, data = history.pop()
if op == '+':
val = a.pop()
freq[val] -= 1
else:
for val in reversed(data):
a.append(val)
freq[val] += 1
else: # '?'
distinct_count = sum(1 for v in freq.values() if v > 0)
results.append(str(distinct_count))
print('\n'.join(results))
if __name__ == "__main__":
main()
The solution uses defaultdict(int) to simplify frequency management. For rollbacks, appends and removals are reversed carefully to restore both the array and frequency counts. Reversing removed elements ensures that the original order of the array is preserved.
Worked Examples
Sample 1:
| Query | a | freq | history | Output |
|---|---|---|---|---|
| +1 | [1] | {1:1} | [('+',1)] | |
| +2 | [1,2] | {1:1,2:1} | [('+',1),('+',2)] | |
| +2 | [1,2,2] | {1:1,2:2} | [('+',1),('+',2),('+',2)] | |
| ? | [1,2,2] | {1:1,2:2} | ... | 2 |
| ! | [1,2] | {1:1,2:1} | [('+',1),('+',2)] | |
| +3 | [1,2,3] | {1:1,2:1,3:1} | ... | |
| -2 | [1] | {1:1,2:0,3:0} | ... | |
| ? | [1] | {1:1,2:0,3:0} | ... | 1 |
| +1 | [1,1] | {1:2,2:0,3:0} | ... | |
| ? | [1,1] | {1:2,2:0,3:0} | ... | 1 |
Sample 2:
| Query | a | freq | history | Output |
|---|---|---|---|---|
| +1 | [1] | {1:1} | [('+',1)] | |
| +1000000 | [1,1000000] | {1:1,1000000:1} | ... | |
| ? | [1,1000000] | {1:1,1000000:1} | ... | 2 |
| ! | [1] | {1:1,1000000:0} | ... | |
| ! | [] | {1:0,1000000:0} | ... | |
| ? | [] | {} | ... | 0 |
These traces confirm that rollback correctly restores previous array states and frequency counts, ensuring ? outputs are accurate.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(q) | Each operation is processed once. Frequency updates, history push/pop, and array append/pop are all O(1). Counting distinct elements uses at most 10^6 keys but is bounded by the number of distinct elements in practice and happens ≤ 10^5 times, fitting within the limit. |
| Space | O(n) | The array, frequency map, and history stack collectively store at most O(n) elements where n is the maximum array size. |
This confirms the solution is efficient for the given constraints.
Test Cases
import sys, io
from collections import defaultdict
def run(inp: str) -> str:
sys.stdin = io.StringIO(inp)
output = io.StringIO()
sys.stdout = output
main()
return output.getvalue().strip()
# provided samples
assert run("""10
+ 1
+ 2
+ 2
?
!
+ 3
- 2
?
+ 1
?""") == "2\n1\n1"
assert run("""6