class SnapshotArray:
def __init__(self, length: int):
self.snaps = 0
self.data = [{} for _ in range(length)]
def set(self, index: int, val: int) -> None:
self.data[index][self.snaps] = val
def snap(self) -> int:
self.snaps += 1
return self.snaps - 1
def get(self, index: int, snap_id: int) -> int:
d = self.data[index]
# Find the latest snap_id <= requested snap_id
while snap_id not in d and snap_id >= 0:
snap_id -= 1
return d.get(snap_id, 0)
class SnapshotArray {
public:
vector<vector<pair<int, int>>> arr;
int snap_id = 0;
SnapshotArray(int length) {
arr.resize(length);
for (int i = 0; i < length; ++i)
arr[i].push_back({0, 0});
}
void set(int index, int val) {
if (arr[index].back().first == snap_id)
arr[index].back().second = val;
else
arr[index].push_back({snap_id, val});
}
int snap() {
return snap_id++;
}
int get(int index, int snap_id) {
auto& v = arr[index];
int l = 0, r = v.size() - 1;
while (l < r) {
int m = (l + r + 1) / 2;
if (v[m].first <= snap_id) l = m;
else r = m - 1;
}
return v[l].second;
}
};
class SnapshotArray {
private List<TreeMap<Integer, Integer>> data;
private int snapId;
public SnapshotArray(int length) {
data = new ArrayList<>();
for (int i = 0; i < length; ++i) {
TreeMap<Integer, Integer> map = new TreeMap<>();
map.put(0, 0);
data.add(map);
}
snapId = 0;
}
public void set(int index, int val) {
data.get(index).put(snapId, val);
}
public int snap() {
return snapId++;
}
public int get(int index, int snap_id) {
Map.Entry<Integer, Integer> entry = data.get(index).floorEntry(snap_id);
return entry == null ? 0 : entry.getValue();
}
}
var SnapshotArray = function(length) {
this.snaps = 0;
this.data = Array.from({length}, () => new Map([[0, 0]]));
};
SnapshotArray.prototype.set = function(index, val) {
this.data[index].set(this.snaps, val);
};
SnapshotArray.prototype.snap = function() {
return this.snaps++;
};
SnapshotArray.prototype.get = function(index, snap_id) {
let m = this.data[index];
while (!m.has(snap_id) && snap_id >= 0) snap_id--;
return m.get(snap_id) ?? 0;
};
The Snapshot Array problem asks you to design a data structure that works like an array but supports versioning. Specifically, you need to:
length
with all values set to 0
.set(index, val)
method to set the value at index
to val
.snap()
method that takes a snapshot of the array, returning the snap_id
(starting from 0
and increasing by 1
each time).get(index, snap_id)
method to get the value at index
at the time when snap_id
was taken.Key constraints:
set
, snap
, and get
each.snap_id
is unique and refers to a single snapshot.get
queries refer to a valid snap_id
previously returned by snap()
.
At first glance, you might think to store a copy of the entire array every time snap()
is called, and then retrieve the value for get()
from the corresponding snapshot. However, this brute-force approach would use too much memory, especially if only a few values change between snapshots.
To optimize, we should notice that most of the array stays the same between snapshots. So, instead of copying the whole array, we can store only the changes (also called "deltas") for each index, tracking when each value was changed.
This leads to the idea of, for each array index, keeping a history of its value changes along with the snap_id
when each change occurred. When we need to get()
a value for a particular snap_id
, we can look up the most recent value at or before that snap_id
.
We solve the problem by using a data structure that, for each array index, keeps track of the values set along with the corresponding snap_id
when the value was set. Here is how we design the solution:
snap_id
to value.snap_id = 0
is 0
.set(index, val)
is called, we record the value val
at the current snap_id
for that index.set
calls are made before a snap()
, we only need to keep the latest value for the current snap_id
.snap()
is called, increment the global snap_id
and return the previous value as the snapshot identifier.get(index, snap_id)
is called, we look up the value at the largest snap_id
less than or equal to the requested snap_id
for that index.snap_id
, return 0
(the default).
This structure ensures that we only store the changes, making both time and space usage efficient. For fast retrieval, we use binary search or data structures like TreeMap or Map, which can efficiently find the value for a given snap_id
.
Let's walk through an example:
SnapshotArray(3)
→ array is [0, 0, 0]
set(0, 5)
→ array becomes [5, 0, 0]
(at snap_id 0, index 0 is 5)snap()
→ returns 0
set(0, 6)
→ array becomes [6, 0, 0]
(at snap_id 1, index 0 is 6)get(0, 0)
→ returns 5
(for snap_id 0, index 0 was set to 5)Here's how the internal structure evolves:
set(0, 5)
: index 0 history = {0: 5}snap()
: snap_id becomes 1set(0, 6)
: index 0 history = {0: 5, 1: 6}get(0, 0)
: find the value at snap_id 0 for index 0, which is 5Brute-force approach:
O(1)
for set
and snap
; O(1)
for get
if we store complete arrays per snapshot.
O(N \times S)
, where N
is the array length and S
is number of snapshots. This can be huge if both are large.
set
: O(1)
snap
: O(1)
get
: O(\log K)
for binary search among K
changes at an indexO(M)
, where M
is the total number of set
calls (since we only store changes, not the full array each time).This makes the approach efficient enough for the largest constraints in the problem.
The Snapshot Array problem is solved efficiently by storing only the values that change at each index, along with the snap_id
at which the change occurred. This allows us to reconstruct the state of the array at any snapshot without storing redundant data. By using maps or lists with binary search, we can quickly find the correct value for any get
query. The elegance of this solution lies in leveraging the sparsity of changes and efficient data lookups, resulting in optimal time and space complexity for large-scale inputs.