Snapshot Array

  1. Traditional Way - Copying the Whole Array: Normally, if you wanted to keep a record of different versions of an array (or any collection), you might make a complete copy of the array each time you make a change. This is like taking a photograph of the entire room every time you move a single piece of furniture.

  2. Problem with Copying: If the array is very large, or you make many changes (snapshots), copying the whole array each time can take up a lot of memory and become inefficient. Imagine having thousands of photographs of the entire room for every small change.

  3. Optimized Way - Recording Changes: Instead of taking a picture of the whole room, you can write down what changed each time. If you moved a chair, you write down “moved chair.” If you changed the table’s position, you write down “changed table position.” This way, you only record the changes, saving space.

  4. Applying the Concept to Array: In the case of an array, instead of copying the whole array every time, you keep track of what changes in each cell (or position) of the array. For each change (like setting a new value), you record the change along with a snapshot ID (snap_id), which helps you identify when the change occurred.

  5. Recording History for Each Cell: For each position in the array (A[i]), you keep a record of its history. Each record contains a snap_id and the value at that point in time.

  6. Getting a Value from History: When you want to know what a specific position in the array looked like at a certain point in time, you don’t have to look through the entire history. You can use a method called binary search to quickly find the value at that specific time point (snap_id).

  7. Why It’s Better: This approach is like having a detailed notebook for each piece of furniture in the room, where you only write down when that specific piece changes. It’s much more space-efficient than having thousands of photographs of the entire room.

  8. Real-life Analogy: Think of the array as a very long bookshelf, and every snapshot is like a specific day. Instead of writing down the entire state of the bookshelf every day, you only write down which books were added or removed on that specific day. If you want to know how the bookshelf looked on a particular day, you can look through the changes and reconstruct that specific day’s state.

In summary, by recording only the changes instead of copying the entire array, you are making the process more efficient and using less memory. It’s a smart way to keep track of the history without wasting resources.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from bisect import bisect

class SnapshotArray:

    def __init__(self, length: int):
        self.snaps = []
        self.array = [[(0, 0)] for _ in range(length)]
        self.length = length

    def set(self, index: int, val: int) -> None:
        if 0 <= index < self.length:
            self.array[index].append((len(self.snaps), val))

    def snap(self) -> int:
        snap_id = len(self.snaps)
        self.snaps.append(snap_id)
        return snap_id

    def get(self, index: int, snap_id: int) -> int:
        if 0 <= index < self.length:
            snap_values = self.array[index]
            idx = bisect(snap_values, (snap_id + 1, )) - 1
            return snap_values[idx][1]
        return 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class SnapshotArray:

    def __init__(self, length: int):
        self.cur_id = 0
        self.curVals = [0] * length
        self.snapIdArr = [[-1] for _ in range(length)]
        self.arrVal = [[0] for _ in range(length)]
        self.modified = set()

    def set(self, index: int, val: int) -> None:
        if val == self.arrVal[index][-1]:
            if index in self.modified: self.modified.remove(index)
            return
        self.curVals[index] = val
        if index not in self.modified: self.modified.add(index)

    def snap(self) -> int:
        for idx in self.modified:
            self.snapIdArr[idx].append(self.cur_id)
            self.arrVal[idx].append(self.curVals[idx])
        self.modified.clear()
        self.cur_id += 1
        return self.cur_id - 1

    def get(self, index: int, snap_id: int) -> int:
        arr = self.snapIdArr[index]
        l, r = 0, len(arr)
        while l < r:
            m = (l + r) // 2
            if arr[m] <= snap_id:
                l = m + 1
            else: r = m
        return self.arrVal[index][l-1]

Language Agnostic Coding Drills

Drill 1: Basic understanding of class and objects

The very first thing you need to understand is how to define a class and create an object of that class.

Drill 2: Understanding constructors (__init__)

In Python, a constructor is a special method __init__ that is automatically called when an object of a class is created. It initializes the attributes of a class.

Drill 3: Understanding lists and list comprehension

A list is a collection of elements, it can store different types of data. You should understand how to create lists, how to access elements from lists, and how to modify them. List comprehension is a concise way to create lists based on existing lists.

Drill 4: Understanding methods in classes

A method is a function that is associated with an object. In this context, the set, snap, and get methods are defined within the class SnapshotArray. These methods can modify the state of an object and can also access the object’s attributes.

Drill 5: Understanding the concept of a set

A set is an unordered collection of unique elements. Common uses include membership testing, removing duplicates from a sequence, and computing mathematical operations such as intersection, union, difference, and symmetric difference.

Drill 6: Binary search

The get method uses binary search to find the snapshot ID. Binary search is a search algorithm that finds the position of a target value within a sorted array.

Here are the concepts that are used and which you need to understand to be able to understand and implement this code:

  1. Object-Oriented Programming (Classes, Objects, Methods)
  2. Data Structures (Lists, Sets)
  3. Control flow (Conditional statements, Loops)
  4. Arithmetic operations
  5. Binary search algorithm

Each of these topics are vast in themselves and require a good amount of practice to understand and master. The way this code uses these concepts is quite complex, so take your time to understand each one of them and then move to the next one. Once you’ve understood all of them, then start combining them to understand how this entire code works.

Targeted Drills in Python

Sure, let’s break down these concepts into Python code:

Drill 1: Basic understanding of class and objects

1
2
3
4
5
6
7
8
9
class SimpleClass:
    def greet(self):
        return "Hello, World!"

# create an object
obj = SimpleClass()

# use the method greet
print(obj.greet())  # should print: Hello, World!

Drill 2: Understanding constructors (__init__)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class ClassWithConstructor:
    def __init__(self, name):
        self.name = name

    def greet(self):
        return f"Hello, {self.name}!"

# create an object with name
obj = ClassWithConstructor('Alice')

# use the method greet
print(obj.greet())  # should print: Hello, Alice!

Drill 3: Understanding lists and list comprehension

1
2
3
4
5
6
7
8
9
# creating a list
my_list = [1, 2, 3, 4, 5]

# accessing elements
print(my_list[0])  # should print: 1

# list comprehension
squares = [x**2 for x in my_list]
print(squares)  # should print: [1, 4, 9, 16, 25]

Drill 4: Understanding methods in classes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class ClassWithMethods:
    def __init__(self, name):
        self.name = name

    def set_name(self, new_name):
        self.name = new_name

    def greet(self):
        return f"Hello, {self.name}!"

# create an object
obj = ClassWithMethods('Alice')

# use the method greet
print(obj.greet())  # should print: Hello, Alice!

# change name and greet again
obj.set_name('Bob')
print(obj.greet())  # should print: Hello, Bob!

Drill 5: Understanding the concept of a set

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# creating a set
my_set = {1, 2, 3, 4, 5}

# adding elements
my_set.add(6)
print(my_set)  # should print: {1, 2, 3, 4, 5, 6}

# checking if an element is in a set
print(1 in my_set)  # should print: True
print(0 in my_set)  # should print: False

Drill 6: Binary search

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def binary_search(arr, x):
    low = 0
    high = len(arr) - 1
    mid = 0
 
    while low <= high:
        mid = (high + low) // 2
 
        if arr[mid] < x:
            low = mid + 1
        elif arr[mid] > x:
            high = mid - 1
        else:
            return mid
 
    return -1
 
arr = [2, 3, 4, 10, 40]
x = 10
 
result = binary_search(arr, x)
 
if result != -1:
    print(f"Element is present at index {result}")
else:
    print("Element is not present in array")

These drills should give you a good understanding of the basic concepts required to understand the initial code provided.