Template Summary


Template Summary.

Template of 0-1 Knapsack Problem

0-1 knapsack problem:

  • 2-d dp array:
    • Outer for loop is iterating items, inner for loop is iterating capacities of knapsack. They CAN switch the position.
  • 1-d dp array:
    • Outer for loop is iterating items, inner for loop is iterating capacities of knapsack. They CANNOT switch the position.

Using 2-d dp array:

def test_0x_knapsack(bag_size, weight, value) -> int: 
    rows, cols = len(weight), bag_size + 1
    dp = [[0 for _ in range(cols)] for _ in range(rows)]

    # initialization 
    for i in range(rows): 
          dp[i][0] = 0
    first_item_weight, first_item_value = weight[0], value[0]
    for j in range(1, cols):     
          if first_item_weight <= j: 
                dp[0][j] = first_item_value

    # loop items before iterating capacities
    for i in range(1, len(weight)):
        cur_weight, cur_val = weight[i], value[i]
        for j in range(1, cols):
            if cur_weight > j: # item is heavier than the capacity
                  dp[i][j] = dp[i - 1][j]  
              else:
                    # recurrence relation
                    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cur_weight] + cur_val)
    
      dp[rows-1][cols-1]

Using 1-d dp array (rolling array):

def test_01_knapsack():
    weight = [1, 3, 4]
    value = [15, 20, 30]
    bag_weight = 4

    dp = [0] * (bag_weight + 1)

    # iterating items before iterating capacities
    for i in range(len(weight)):
        for j in range(bag_weight, weight[i] - 1, -1):
            # recurrence relation
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

    print(dp)

Template of 0-x Knapsack Problem

Brief summary of knapsack problems:

  • 0-1 knapsack problem:

    • 2-d dp array:
      • Outer for loop is iterating items, inner for loop is iterating capacities of knapsack. They CAN switch the position.
    • 1-d dp array:
      • Outer for loop is iterating items, inner for loop is iterating capacities of knapsack. They CANNOT switch the position.
  • 0-x knapsack problem:

    • 1-d dp array or 2-d dp array:

      Outer for loop is iterating items, inner for loop is iterating capacities of knapsack. They CAN switch the position.

    • Since we can have multiple copies of the same item, the inner for loop should NOT be in the reverse order any more.

O-x Knapsack code template

# loop items before iterating capacities
def test_complete_pack1():
    weight = [1, 3, 4]
    value = [15, 20, 30]
    bag_weight = 4

    dp = [0]*(bag_weight + 1)

    for i in range(len(weight)):
        for j in range(weight[i], bag_weight + 1):
              # recurrence relation
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
    
    return dp[bag_weight]

# loop capacities before iterating items
def test_complete_pack2():
    weight = [1, 3, 4]
    value = [15, 20, 30]
    bag_weight = 4

    dp = [0]*(bag_weight + 1)

    for j in range(bag_weight + 1):
        for i in range(len(weight)):
            if j >= weight[i]: 
                  # recurrence relation
                  dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
    
    return dp[bag_weight]

Template of Combination and Permutation

Combination(n, d)

def Combination(nums, d, n, s, path, ans):
      if d == n:
          ans.append(path)
        return
      
    for i in range(s, len(nums)):
          path.push(nums[i])
        Combination(nums, d+1, n, i+1, path, ans)
        path.pop()

Permutation(n, d)

def Permutation(nums, d, n, used, path, ans):
      if d == n:
          ans.append(path)
        return
    
    for i in range(0, len(nums)):
          if used[i]: continue
        used[i] = True
        path.push(nums[i])
        Permutation(nums, d+1, n, path, ans)
        path.pop()
        used[i] = False

Template of Recursion

def backtracking(parameters)
    if (stop condition):
        result
        return

    for (choose:elements in current tree level):
        deal with node
        backtracking(path,list) # recursion
        backtrack and retrieve

Template of Union-Find Set

class UnionFind:
    def __init__(self):
        self.father = {}
    
    def find(self,x):
        root = x

        while self.father[root] != None:
            root = self.father[root]

        # optimization: path compression O(log*n)
        while x != root:
            original_father = self.father[x]
            self.father[x] = root
            x = original_father
         
        return root
    
    def merge(self,x,y,val):
        root_x,root_y = self.find(x),self.find(y)
        
        if root_x != root_y:
            self.father[root_x] = root_y

    def is_connected(self,x,y):
        return self.find(x) == self.find(y)
    
    def add(self,x):
        if x not in self.father:
            self.father[x] = None

Template for Recursive Merge Sort

List1 = [1,3,5], List2 = [2,4], merge in ascending order.

def merge(a,b):
    if not b: return a
    if not a: return b
    if a[0] < b[0]: 
            return a[0] + merge(a[1:], b)
    else:
          return a[0] + merge(a, b[1:])

(1) merge([1,3,5], [2,4]) = [1] + merge([3,5], [2,4]) = [1, 2, 3, 4, 5]

(2) merge([3,5], [2,4]) = [2] + merge([3,5], [4]) = [2, 3, 4, 5]

(3) merge([3,5], [4]) = [3] + merge([5], [4]) = [3, 4, 5]

(4) merge([5], [4]) = [4] + merge([5], []) = [4, 5]

(4) merge([5], []) = [5]

Time complexity:

  • For array: O(n^2). Because getting subarray a[1:] takes O(n).
  • For array: O(n). Because getting sub linked list takes O(1), using .next().

Template of Copying a Linked List

dum is the head of the new linked list and it stays intact. We use double pointer pre and cur to copy a linked list.

def copyLinkedList(self, head: 'Node') -> 'Node':
      cur = head
      dum = pre = Node(0)
      while cur:
            node = Node(cur.val) # copy node at cur
            pre.next = node      # link pre to cur
             cur = cur.next       # move cur to cur.next
            pre = node           # move pre to pre.next
            return dum.next

Template for Addition of Linked Lists

dummy = tail = ListNode(0)
carry = 0

while l1 or l2 or carry: # carry: 0 or 1
      sum = l1.val + l2.val + carry # need to add 0 if lengths differ
    tail.next = ListNode(sum % 10) 
    tail = tail.next
    carry = sum // 10
    l1, l2 = l1.next, l2.next
return dummy.next

Time complexity O(max(n,m)), space complexity O(max(n,m)).

Sometime we need f(m) to determine whether m is the solution. g(m) is used to determine the side of the solution. Usually g(m) is given in the problem. We can consider binary_search works to “find the smallest l such that g(m) is true”.

Time complexity of binary_search is O(log(r-l) * (f(m) + g(m))), space complexity is O(1). Note that the time and space complexity of g(m) depends on the problem, and the space complexity O(1) does not include the space complexity of g(m).

def binary_search(l, r): # [l,r)
    while l < r:
        m = l + (r - l) // 2
        if f(m):
            return m 
        if g(m):
              r = m # new range [l, m)
        else:
            l = m + 1 # new range [m+1, r)
    return l

Application of this Template

1] Find the index of a specified element in a sorted unique array.

def binary_tree(array, val):
    l = 0
    r = len(array)
    while l < r:
        m = l + (r - l) // 2
        if array[m] == val:
            return m
        if array[m] > val:
            l = m
        else:
            r = m + 1
    return -1 # not found

2] Find the lower bound and upper bound of a val in a sorted array.

lower_bound works to find the first index such that A[index] > = val. upper_bound works to find the first index such that A[index] > val.

def lower_bound(array, val):
    l = 0
    r = len(array)
    while l < r:
        m = l + (r - l) // 2
        if A[m] >= val:
            r = m
        else:
            l = m + 1
    return l

def upper_bound(array, val):
    l = 0
    r = len(array)
    while l < r:
        m = l + (r - l) // 2
        if A[m] > val:
            r = m
        else:
            l = m + 1
    return l

Two Ways of Defining Range

Binary Search: given a function g, returns the smallest m in the given range such that g(m) is True. Find a split point m such that for all n (n >= m), conditions are satisfied, then m will become the answer.

  • [l, r)

    In this case, strictly speaking, r should be n + 1, and can overflow if n = 2^31 -1. Sometimes, it’s doable to let r = n, after binary searching if the answer cannot be foun d from 1 to n-1, then n must be the answer so we return n.

    def binary_search(l, r):
        while l < r:
            m = l + (r - l) // 2
            if g(m):
                r = m # range [l, m)
            else:
                l = m + 1 # range [m + 1, r)
        return l
    
  • [l, r]

    def binary_search(l, r):
          while l <= r:
            m = l + (r - l) // 2
            if g(m):
                r = m - 1 # range [l, m - 1]
            else:
                l = m + 1 # range [m + 1, r]
        return l
    

  TOC