this page ⟩ collectionshome

Collection: LC Notes

Table of Content


[top][01]

Arrays

[top][01a01]

Two sum [LC#1]

Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target. You may assume that each input would have exactly one solution, and you may not use the same element twice. You can return the answer in any order.

Sorting and two pointers

def two_sum(nums: List[int], target:Int) -> Tuple[int, int]:
    indexed_nums  = sorted((n, i) for i, n in enumerate(nums))
    left, right = 0, len(indexed_nums) - 1
    
    while left < right:
        current_sum = indexed_nums[left][0] + indexed_nums[right][0]
        
        if current_sum == target:
            return (indexed_nums[left][1], indexed_nums[right][1])
        elif current_sum < target:
            left += 1
        else:
            right -= 1

    return (-1, -1)

Hashset

def two_sum(nums: List[int], target: int) -> Tuple[int, int]:
    num_to_idx = {}
    for i, num in enumerate(nums):
        residue = target - num
        if residue in lut:
            return (i, num_to_idx[residue])
        num_to_idx[num] = i
    return (-1, -1)
[top][01a01a]

Three Sum [LC#15]

Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0. Notice that the solution set must not contain duplicate triplets.

[top][01a03]

Duplicate Number [LC#287]

Given an array of integers nums containing n + 1 integers where each integer is in the range [1, n] inclusive. There is only one repeated number in nums, return this repeated number. You must solve the problem without modifying the array nums and using only constant extra space.

[top][01a06]

Rotate Array [LC#189]

Given an integer array nums, rotate the array to the right by k steps, where k is non-negative.

Intuition

Code

def rotate_array(nums: List[int], k: int) -> None:
    n = len(nums)
    k = k % n # for k > n
    start = 0
    count = 0
    while count < n:
        curr = start
        storage = nums[curr]
        while True:
            curr = (curr + k) % n
            nums[curr], storage = storage, nums[curr]
            count += 1
            if start == curr:
                break
        start += 1

Time complexity

[top][01c]

Sorting

[top][01c01]

Quick Select

Partition Scheme intuition.

#  [ ≤ ][ ≤ ][ > ][ > ][ > ][ > ][ ? ][ hi ]     
#              └─ left        └─ right 

def lomuto_partition(A, lo, hi):
    pivot = A[hi]
    left = lo
    for right in range(lo,  hi): 
        if A[right] <= pivot:
            A[left], A[right] = A[right], A[left]
            left = left + 1
    A[left], A[hi] = A[hi], A[left]
    return left

Quick sort using the partition scheme

def quicksort(A, lo, hi):
    if lo >=hi or lo <0:
        return 
    p = lomuto_partition(A, lo, hi)
    quicksort(A, lo, p - 1)
    quicksort(A, p + 1, hi)
[top][02]

Strings

[top][02b]

Longest Common Prefix

[top][02c]

Group Anagrams

[top][02h01]

Reorganize String [LC#767]

Given a string s, rearrange the characters of s so that any two adjacent characters are not the same. Return any possible rearrangement of s or return "" if not possible.

Intuition

Code

def reorganize_string(self, s: str) -> str:
    character_counts = Counter(s)
    n = len(s)
    if max(character_counts.values()) > (n + 1) // 2:
        return ""

    def interleaved_index_generator(n):
        for i in range(0, n, 2):
            yield i
        for i in range(1, n, 2):
            yield i

    characters = list(s)
    characters.sort(
        key=lambda char: (character_counts[char], char), 
        # break tie when 2 chars have same counts
        reverse=True
    )
    output = characters.copy()
    interleaved_index = interleaved_index_generator(n)
    for character in characters:
        output[next(interleaved_index)] = character
    return "".join(output)

Time complexity

string
[top][14]

Subarray

[top][14a]

Maximum Subarray Sum [LC#53]

Given an integer array nums, find the subarray with the largest sum, and return its sum.

Kadane’s algorithm

def max_subarray_sum(nums: List[int]) -> int:
    curr, ans = 0, nums[0]
    for num in nums:
        curr = max(curr + num, num)
        ans = max(ans, curr)
    return ans
[top][14b]

Best Time to Buy and Sell Stock [LC#121]

You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock. Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

Inutition

Code

def max_profit(prices: List[int]) -> int:
    max_profit = 0
    min_price = prices[0]
    for price in prices:
        min_price = min(min_price, price)
        max_profit = max(max_profit, price - min_price)
    return max_profit

Time Complexity

greedy
[top][14b01]

Best Time to Buy and Sell Stock II [LC#122]

You are given an integer array prices where prices[i] is the price of a given stock on the ith day. On each day, you may decide to buy and/or sell the stock. You can only hold at most one share of the stock at any time. However, you can buy it then immediately sell it on the same day. Find and return the maximum profit you can achieve.

Intuition

Code

def max_profit(self, prices: List[int]) -> int:
    profit = 0
    for i in range(1, len(prices)):
        if prices[i - 1] < prices[i]:
            profit += (prices[i] - prices[i - 1])
    return profit

Time complexity

greedy
[top][14b02]

Best Time to Buy and Sell Stock IV [LC#188]

You are given an integer array prices where prices[i] is the price of a given stock on the ith day, and an integer k. Find the maximum profit you can achieve. You may complete at most k transactions: i.e. you may buy at most k times and sell at most k times. Note: You may not engage in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).

Intuition

Code

def max_profit_with_k_transactions(K: int, prices: List[int]) -> int:
    n = len(prices)
    f = [[0] * (n) for _ in range(K + 1)]
    max_profit = 0
    for k in range(1, K + 1):
        t = f[k - 1][0] - prices[0]
        for i in range(1, n):
            t = max(t, f[k - 1][i] - prices[i])
            f[k][i] = max(f[k][i - 1], prices[i] + t)
            max_profit = max(max_profit, f[k][i])
    return max_profit

Time complexity

[top][14c]

Continuous Subarray Sum [LC#523]

Given an integer array nums and an integer k, return true if nums has a good subarray or false otherwise. A good subarray is a subarray where:

  • Its length is at least two, and
  • The sum of the elements of the subarray is a multiple of k.

Intuition

Solution

def check_subarray_sum(nums: List[int], k: int) -> bool:
    n = len(nums)
    mod_seen = {0: -1}  # for prefixes that exactly sum%k to 0
    prefix_sum = 0
    for i in range(n):
        prefix_sum = prefix_sum + nums[i]
        prefix_mod = prefix_sum % k
        if prefix_mod in mod_seen:
            if i - mod_seen[prefix_mod] > 1:
                return True
        else:
            mod_seen[prefix_mod] = i

        prefix_sum = prefix_mod
    return False

Time Complexity

[top][14d]

Subarray Sum Equals K [LC#560]

Given an array of integers nums and an integer k, return the total number of subarrays whose sum equals to k. A subarray is a contiguous non-empty sequence of elements within an array.

Intuition

Solution

def count_subarray_sum_to_targets(nums: List[int], k: int) -> int:
    prefixes = defaultdict(int)
    prefixes[0] = 1 # empty array is a subarray of sum 0

    current = 0
    counts = 0
    for num in nums:
        current += num
        counts += prefixes[current - k]
        prefixes[current] += 1
    return counts

Time Complexity

[top][14e]

Product of Array Except Self [LC#238]

Given an integer array nums, return an array answer such that answer[i] is equal to the product of all the elements of nums except nums[i]. The product of any prefix or suffix of nums is guaranteed to fit in a 32-bit integer. You must write an algorithm that runs in O(n) time and without using the division operation.

Code

def product_except(nums: List[int]) -> List[int]:
    n = len(nums)
    ans = [1,]
    for i in range(n - 1):
        ans.append(ans[-1] * nums[i])
    post = 1
    for i in range(n - 2, -1, -1):
        post = post * nums[i + 1]
        ans[i] = ans[i] * post
    return ans

Time Complexity

[top][14f]

Maximize Subarray Sum After Removing All Occurrences of One Element [LC#3410]

You are given an integer array nums. You can do the following operation on the array at most once:

  • Choose any integer x such that nums remains non-empty on removing all occurrences of x.
  • Remove all occurrences of x from the array.

Return the maximum subarray sum across all possible resulting arrays.

Intuition

Code

Time complexity

[top][16]

Stack

[top][16a]

Asteroid Collision [LC#735]

We are given an array asteroids of integers representing asteroids in a row. The indices of the asteriod in the array represent their relative position in space. For each asteroid, the absolute value represents its size, and the sign represents its direction (positive meaning right, negative meaning left). Each asteroid moves at the same speed.

Find out the state of the asteroids after all collisions. If two asteroids meet, the smaller one will explode. If both are the same size, both will explode. Two asteroids moving in the same direction will never meet.

Intuition

Code

def asteroid_collision(asteroids: List[int]) -> List[int]:
    stack = []

    for asteroid in asteroids:
        store = True
        while stack and stack[-1] > 0 and asteroid < 0:
            # there is collision
            if abs(stack[-1]) < abs(asteroid):
                stack.pop()
                continue                
            elif abs(stack[-1]) == abs(asteroid):
                stack.pop()
                store = False
                break
            else:
                store = False
                break
        if store:
            stack.append(asteroid)
    return stack

Time complexity

stack
[top][18]

Linked List

[top][18a]

Reverse Linked List [LC#206]

Given the head of a singly linked list, reverse the list, and return the reversed list.

Iterative approach

     ┌────┐    ┌────┐    ┌────┐
 <-- │prev│    │curr│--> │next│-->
     └────┘    └────┘    └────┘
     ┌────┐    ┌────┐    ┌────┐
 <-- │prev│ <--│curr│    │next│-->
     └────┘    └────┘    └────┘
     ┌────┐    ┌────┐    ┌────┐
 <-- │    │ <--│prev│    │next│-->
     └────┘    └────┘    └────┘
     ┌────┐    ┌────┐    ┌────┐
 <-- │    │ <--│prev│    |curr│-->
     └────┘    └────┘    └────┘
def fn(head):
    curr = head
    prev = None
    while curr:
        next_node = curr.next
        curr.next = prev
        prev = curr
        curr = next_node
    return prev

Time Complexity

linked list
[top][18b]

Remove Nth Node From End of List [LC#19]

Given the head of a linked list, remove the nth node from the end of the list and return its head.

Code

def remove_nth_from_end(head: Optional[ListNode], n: int) -> Optional[ListNode]:
    fake_head = ListNode(0, head)
    P = fake_head
    while n>0 and P:
        P = P.next
        n=n-1
    if not P: return head # less than n nodes

    Q = fake_head
    while P.next:
        P = P.next
        Q = Q.next
    Q.next = Q.next.next
    return fake_head.next
linked list
[top][18c]

Merge Two Sorted Lists [LC#21]

You are given the heads of two sorted linked lists list1 and list2. Merge the two lists into one sorted list. The list should be made by splicing together the nodes of the first two lists. Return the head of the merged linked list.

# Definition for singly-linked list.
@dataclass
class ListNode:
    val: int = 0
    next: Optional[ListNode] = None

Iterate and Merge

def merge_two_sorted_lists(list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
      fake_head = ListNode()
      current = fake_head
      
      while list1 and list2:
          if list1.val < list2.val:
              current.next = list1
              list1 = list1.next
          else:
              current.next = list2
              list2 = list2.next
          current = current.next
      current.next = list1 if list1 else list2
      
      return fake_head.next
linked list
[top][18e]

Merge k Sorted Lists [LC#23]

You are given an array of k linked-lists lists, each linked-list is sorted in ascending order. Merge all the linked-lists into one sorted linked-list and return it.

Brute-Force

Priority Queue or Min Heap

def merge_sorted_lists(lists: List[Optional[ListNode]]) -> Optional[ListNode]:
    class HeapNode:
        def __init__(self, node: ListNode):
            self.node = node
        def __lt__(self, other):
            return self.node.val < other.node.val
    
    heap = []
    current = dummy = ListNode(0)
    for lst in lists:
        if lst:
            heapq.heappush(heap, HeapNode(lst))
    
    while heap:
        node = heapq.heappop(heap).node
        current.next = node
        current = current.next
        if node.next:
            heapq.heappush(heap, HeapNode(node.next))

    return dummy.next

Iterative Mergesort on sorted lists

linked list
[top][18f]

Linked List Cycle [LC#141]

Given head, the head of a linked list, determine if the linked list has a cycle in it. Return true if there is a cycle in the linked list. Otherwise, return false.

Floyd’s Turtle and hare algorithm

It relies on the fact that if two pointers are moving at different speeds within a cycle, their distances will reach a max length before being reset to zero at which point they will point to the same element.

Time complexity

Code

def has_cycle(head: Optional[ListNode]) -> bool:
    turtle, hare = ListNode(0, head), ListNode(0, head)
    while turtle and hare:
        if turtle == hare:
            return True
        turtle = turtle.next
        hare = hare.next.next if hare.next else None
    return False
linked list
[top][18g]

Implementing LRU Cache [LC#146]

Design a data structure that follows the constraints of a Least Recently Used (LRU) cache.

Implement the LRUCache class:

  • LRUCache(int capacity) Initialize the LRU cache with positive size capacity.
  • int get(int key) Return the value of the key if the key exists, otherwise return -1.
  • void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key. The functions get and put must each run in $O(1)$ average time complexity.

Intuition

Code

class Node:
    def __init__(self, key, val, nxt=None, prev=None):
        self.key = key
        self.val = val
        self.next = nxt
        self.prev = prev
    
class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.storage = {}
        self.head = Node(-1, -1, None, None)
        self.tail = Node(-2, -2, None, None)
        self.head.next = self.tail
        self.tail.prev = self.head

    def add_at_end(self, node):
        node.prev = self.tail.prev
        node.next = self.tail 
        self.tail.prev.next = node
        self.tail.prev = node

    def remove_node(self, node):
        node.next.prev = node.prev
        node.prev.next = node.next
        return node.key

    def get(self, key: int) -> int:
        if key in self.storage:
            # update freshness
            node = self.storage[key]
            self.remove_node(node)
            self.add_at_end(node)
            return node.val
        return -1

    def put(self, key: int, value: int) -> None:
        if key in self.storage:
            self.storage[key].val = value
            self.get(key)
        else:
            node = Node(key, value, None, None)
            self.storage[key] = node
            self.add_at_end(node)
            if len(self.storage)>self.capacity:
                rm_key = self.remove_node(self.head.next)
                del self.storage[rm_key]     

Time Complexity

linked list
[top][18h]

Remove Nodes From Linked List [LC#2487]

You are given the head of a linked list. Remove every node which has a node with a greater value anywhere to the right side of it. Return the head of the modified linked list.

Intuition

Code

def remove_nodes(self, head: Optional[Node]) -> Optional[Node]:
    stack = []
    node = head
    while node:
        while stack and node and stack[-1].val < node.val:
            mid = stack.pop()
        stack.append(node)
        node = node.next
    
    dummy = node = Node(0, None)
    for v in stack:
        node.next = v
        node = v
    return dummy.next

Time complexity

monotonic stack linked list
[top][20]

Sequences

[top][20a]

Longest Consecutive Sequence [LC#128]

Given an unsorted array of integers nums, return the length of the longest consecutive elements sequence.

Brute Force

Sorting

Hash Table based look up

def longest_consecutive_sequence(nums: List[int]) -> int:
    entries = set(nums)
    best = 0
    for num in nums:
        if num-1 not in entries:
            next_num, seq_len = num+1, 1
            while next_num in entries: 
                next_num += 1
                seq_len += 1
            best = max(best, seq_len)
    return best
[top][20b]

Longest Increasing Subsequence [LC#300]

Given an integer array nums, return the length of the longest strictly increasing subsequence.

Dynamic programming approach

Technique based on patience sorting

[top][20b01]

Russian Doll Envelopes [LC#354]

You are given a 2D array of integers envelopes where envelopes[i] = [wi, hi] represents the width and the height of an envelope. One envelope can fit into another if and only if both the width and height of one envelope are greater than the other envelope’s width and height. Return the maximum number of envelopes you can Russian doll (i.e., put one inside the other). Note: You cannot rotate an envelope.

Intuition

Code


def russian_dolls(envelopes: List[List[int]]) -> int:
    envelopes.sort(key=lambda r: (r[0], -r[1]))
    return self.length_of_lis(h for w, h in envelopes)

def length_of_lis(nums: List[int]) -> int:
    if not nums:
        return 0
    tails = []
    for num in nums:
        pos = bisect.bisect_left(tails, num)
        if pos == len(tails):
            tails.append(num)
        else:
            tails[pos] = num
    return len(tails)

Run time

Time complexity for sorting is $O(n \log n)$ and for LIS it is $O(n \log n)$. Depending on the implementation, we need $O(n)$ extra space.

[top][20c]

Longest Common Subsequence [LC#1143]

Given two strings text1 and text2, return the length of their longest common subsequence. If there is no common subsequence, return 0. A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.

For example, “ace” is a subsequence of “abcde”. A common subsequence of two strings is a subsequence that is common to both strings.

Dynamic programming approach

Dynamic programming approach with space optimisation

[top][20d]

Longest Palindromic Subsequence [LC#516]

Given a string s, find the longest palindromic subsequence’s length in s. A subsequence is a sequence that can be derived from another sequence by deleting some or no elements without changing the order of the remaining elements.

Intuition

If the characters at the both ends of the string sre the same, they must be part of the longest palindrome. We can add 2 to the length and remove those characters. If they are not equal, then the longest palindrome would be in either of the arrays with one of the end characters removed.

Dynamic Programming

def longest_palindrommic_subsequence(s: str) -> int:
    @cache
    def longest(i, j):
        if i == j:
            return 1
        if i > j:
            return 0
        if s[i] == s[j]:
            return 2 + longest(i + 1, j - 1)
        else:
            return max(longest(i + 1, j), longest(i, j - 1))
    return longest(0, len(s) - 1)

Time Complexity

Dynamic Programming with space optimisation

def longest_palindromic_subsequence(s: str) -> int:
    n = len(s)
    curr_dp, prev_dp = [0] * n, [0] * n

    for i in range(n - 1, -1, -1):
        curr_dp[i] = 1
        for j in range(i + 1, n):
            if s[i] == s[j]:
                curr_dp[j] = prev_dp[j - 1] + 2
            else:
                curr_dp[j] = max(prev_dp[j], curr_dp[j - 1])
        prev_dp = curr_dp[:]

    return curr_dp[n - 1]

Time Complexity

[top][20e]

Sum of Good Subsequences [LC#3351]

You are given an integer array nums. A good subsequence is defined as a subsequence of nums where the absolute difference between any two consecutive elements in the subsequence is exactly 1. Return the sum of all possible good subsequences of nums (modulo $10^9 + 7$). Note that a subsequence of size 1 is considered good by definition.

Intuition

Solution

def sum_of_good_subsequences(nums: List[int]) -> int:
    mod = 10 ** 9 + 7
    total = Counter()
    count = Counter()
    for num in nums:
        ending_at = count[num - 1] + count[num + 1] + 1
        total[num] += total[num - 1] + total[num + 1] + num * ending_at
        count[num] += ending_at
    return sum(total.values()) % mod

Time Complexity

sub sequence dp
[top][22]

Palindromes

[top][22a]

Longest Palindromic Substring [LC#5]

Given a string s, return the longest palindromic substring in s.

Intuition

Expand around Center

Code

def longestPalindrome(string: str) -> str:
    def expand_around_center(left: int, right: int, string: str) -> str:
        while left >= 0 and right < len(string) and string[left] == string[right]:
            left = left - 1
            right = right + 1
        return string[left + 1 : right]

    longest = ""
    for i in range(len(s)):
        longest = max(
            longest,
            expand_around_center(i, i, string),
            expand_around_center(i, i + 1, string),
            key=len,
        )
    return longest

Time Complexity

palindromes
[top][22b]

Count Palindromic Substrings [LC#647]

Given a string s, return the number of palindromic substrings in it. A string is a palindrome when it reads the same backward as forward. A substring is a contiguous sequence of characters within the string.

Intuition

Expand around Center

Code

def count_palindromic_substrings(s: str) -> int:
    def expand_around_center(left: int, right: int) -> int:
        count = 0
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left = left - 1
            right = right + 1
            count = count + 1
        return count

    count = 0
    for i in range(len(s)):
        # odd length palindromes
        count += expand_around_center(i, i)

        # even length palindromes
        count += expand_around_center(i, i + 1)
    return count

def count_palindromic_substrings_faster(s: str) -> int:
    p = manacher(s)
    count = 0
    for length in p:
        if length % 2 == 0:
            count += length // 2
        else:
            count += length // 2 + 1
    return count

def manacher(self, s):
    t = "#".join("^{}$".format(s))
    n = len(t)
    p = [0] * n
    center = right = 0

    for i in range(1, n - 1):
        if i < right:
            p[i] = min(right - i, p[2 * center - i])
        while t[i + p[i] + 1] == t[i - p[i] - 1]:
            p[i] += 1

        if i + p[i] > right:
            center, right = i, i + p[i]

    return p[1:-1]

Time Complexity

palindromes
[top][22c]

Manacher Algorithm

All palindromes in $O(1)$

def manacher(s: str):
    t = '#'.join('^{}$'.format(s))
    n = len(t)
    p = [0] * n
    center = right = 0

    for i in range(1, n - 1):
        if i < right:
            p[i] = min(right - i, p[2 * center - i])
            
        while t[i + p[i] + 1] == t[i - p[i] - 1]:
            p[i] += 1

        if i + p[i] > right:
            center, right = i, i + p[i]

    return p[1:-1]
palindromes
[top][22d]

Palindromic Partition

palindromes
[top][23]

Parentheses

[top][23a]

Longest Valid Parentheses [LC32]

Given a string containing just the characters ‘(’ and ‘)’, return the length of the longest valid (well-formed) parentheses substring

Using stack

def longest_valid_parentheses(self, s: str) -> int:
    ans = 0
    stack = [-1]
    for i in range(len(s)):
        if s[i] == "(":
            stack.append(i)
        else:
            stack.pop()
            if stack:
                ans = max(ans, i - stack[-1])
            else:
                stack.append(i)
    return ans

Time Complexity

Using 2 pointers

One of the passes will catch all the longest substring. Check the counter comparison condition and order of the comparisons.

def longest_valid_parentheses(self, s: str) -> int:
    left, right = 0, 0
    ans = 0
    for c in s:
        if c == '(':
            left += 1
        else:
            right += 1
            
        if right == left:
            ans = max(ans, 2*right)
        elif right > left:
            left = right = 0
    
    left = right = 0
    for c in reversed(s):
        if c == '(':
            left += 1
        else:
            right += 1

        if right == left:
            ans = max(ans, 2*right)
        elif left > right:
            left = right = 0
    return ans

Time Complexity

stack
[top][23b]

Valid Parenthesis String [LC#678]

Given a string s containing only three types of characters: '(', ')' and '*', return true if s is valid. The string is valid if it is a valid paranthesised string where any '*' could be treated as '(' or ')' or empty string.

Intuition

Code

def valid_parenthesis(string: str) -> bool:
    n = len(string)
    open_cnt = 0
    close_cnt = 0
    for left, right in zip(range(n), range(n-1, -1, -1)):

        if string[left] in ["(", "*"]:
            open_cnt += 1
        else:
            open_cnt -= 1

        if string[right] in [")", "*"]:
            close_cnt += 1
        else:
            close_cnt -= 1

        if open_cnt < 0 or close_cnt < 0:
            return False
    return True

Time complexity

stack
[top][23c]

Minimum Add to Make Parentheses Valid [LC#921]

You are given a parentheses string s. In one move, you can insert a parenthesis at any position of the string. For example, if s = "()))", you can insert an opening parenthesis to be "(()))" or a closing parenthesis to be "())))". Return the minimum number of moves required to make s valid.

Intuition

Code

def make_valid(s: str) -> int:
    stack = []
    insert_count = 0
    for char in s:
        if char =='(':
            stack.append(char)
        if char == ')':
            if stack:
                stack.pop()
            else:
                insert_count +=1
    insert_count += len(stack)
    return insert_count

def make_valid(self, s: str) -> int:
    insert_count = 0
    open_count = 0
    for char in s:
        if char =='(':
            open_count+=1
        if char == ')':
            if open_count>0:
                open_count-=1
            else:
                insert_count +=1
    insert_count += open_count
    return insert_count

Time complexity

[top][25]

Two Pointers

[top][25a]

Containers with most water [LC#11]

Given $n$ non-negative integers $a_1$, $a_2$, … , $a_n$ , where each represents a point at coordinate $(i, a_i)$. $n$ vertical lines are drawn such that the two endpoints of the line $i$ is at $(i, a_i)$ and $(i, 0)$. Find two lines, which, together with the x-axis forms a container, such that the container contains the most water.

Two pointer approach

def max_water(height: List[int]) -> int:
    left, right = 0, len(height)-1
    max_area = 0
    while left < right:
        max_area = max(
            max_area, 
            (right - left)*(min(height[left], height[right])) 
        )
        # we have to move away from the smaller wall
        # as it is limiting factor of the area
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1
    return max_area
two pointers
[top][27]

Sliding Window

[top][27a]

Longest Substring Without Repeating Characters [LC#3]

Given a string s, find the length of the longest substring without repeating characters.

Sliding Window and 2 pointers

Optimised Sliding window

two pointers sliding window
[top][27b]

String Compression [LC#443]

Given an array of characters chars, compress it using the following algorithm: Begin with an empty string s. For each group of consecutive repeating characters in chars:

  • If the group’s length is 1, append the character to s.
  • Otherwise, append the character followed by the group’s length.

The compressed string s should not be returned separately, but instead, be stored in the input character array chars. Note that group lengths that are 10 or longer will be split into multiple characters in chars. After you are done modifying the input array, return the new length of the array.

Intuition

Code

def string_compress(chars: List[str]) -> int:
    left = 0
    pos = 0
    chars.append(" ") # to proccess last group
    for right in range(len(chars)):
        if chars[right] != chars[left]:
            length = right - left
            if length == 1:
                string = chars[left]
            else:
                string = f"{chars[left]}{length}"
            for c in string:
                chars[pos] = c
                pos += 1
            left = right
    return pos

Time complexity

two pointers sliding window
[top][27c]

Take K of Each Character From Left and Right [LC#2516]

You are given a string s consisting of the characters ‘a’, ‘b’, and ‘c’ and a non-negative integer k. Each minute, you may take either the leftmost character of s, or the rightmost character of s. Return the minimum number of minutes needed for you to take at least k of each character, or return -1 if it is not possible to take k of each character.

Intuition

We are looking for a largest window that can be removed without making count of all characters going below k

Code

def take_characters(s: str, k: int) -> int:
    if k == 0 : return 0 
    count = Counter({"a": 0, "b": 0, "c": 0})
    for char in s:
        count[char] += 1

    if min(count.values()) < k:
        return -1

    max_window = 0
    left = 0
    for right in range(len(s)):
        count[s[right]] -= 1
        while left <= right and min(count.values()) < k:
            count[s[left]] += 1
            left += 1
        max_window = max(max_window, right - left + 1)
    return len(s) - max_window

Time complexity

sliding window
[top][27d]

Partition Labels [LC#763]

You are given a string s. We want to partition the string into as many parts as possible so that each letter appears in at most one part. Note that the partition is done so that after concatenating all the parts in order, the resultant string should be s. Return a list of integers representing the size of these parts.

Intuition

Code

def partition_string(string: str) -> List[int]:
    last_pos = defaultdict(int)
    for i, char in enumerate(string):
        last_pos[char] = i

    left = 0
    right = 0
    lengths = []
    for i, char in enumerate(string):
        right = max(right, last_pos[char])
        if right == i:
            lengths.append(right - left + 1)
            left = i + 1
    return lengths

Time complexity

sliding window
[top][30]

Trees

[top][30a]

Tree Traversals

[top][30a01]

In Order

[top][30a02]

Pre Order

[top][30a03]

Post Order

[top][30a04]

Level Order

[top][30a05]

Construct Binary Tree from Preorder and Inorder Traversal [LC#105]

Given two integer arrays preorder and inorder where preorder is the preorder traversal of a binary tree and inorder is the inorder traversal of the same tree, construct and return the binary tree.

Intuition

The elements in preorder represents the roots where the inorder traversal needs to be bifurcated. Maintaining a index map of inorder to quickly map to position of any element will allow quickly building the tree recursively.

Code

def build_tree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
    val_to_idx = {val: idx for idx, val in enumerate(inorder)}

    def construct(in_left, in_right):
        if in_left > in_right:
            return None

        val = preorder.pop(0)
        in_idx = val_to_idx[val]
        node = TreeNode(val)

        node.left = construct(in_left, in_idx - 1)
        node.right = construct(in_idx + 1, in_right)
        return node
    return construct(0, len(preorder) - 1)

Time Complexity

[top][30b]

Binary Tree Maximum Path Sum [LC#124]

A path in a binary tree is a sequence of nodes where each pair of adjacent nodes in the sequence has an edge connecting them. A node can only appear in the sequence at most once. Note that the path does not need to pass through the root.

The path sum of a path is the sum of the node’s values in the path. Given the root of a binary tree, return the maximum path sum of any non-empty path.

Depth first traversal

def binary_tree_max_path_sum(root: Optional[TreeNode]) -> int:
    ans = float('-inf')
    def ending_with(node):
        # What is the max path sum with path ending at node
        # and the path contains only node's children?

        nonlocal ans
        if not node: return 0

        left_gain = max(ending_with(node.left), 0)
        right_gain = max(ending_with(node.right), 0)

        # max path sum of path passing through node
        current_max = left_gain + node.val + right_gain

        ans = max(ans, current_max)

        node_gain = max(left_gain, right_gain) + node.val
        return node_gain

    ending_with(root)
    return ans
[top][30c]

Lowest Common Ancestor

[top][30c01]

Lowest Common Ancestor of a Binary Tree [LC#236]

Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree. According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”

Code

class Node:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

def lowest_common_ancestor(root: "Node", p: "Node", q: "Node") -> "Node":
    def search(node):
        if node in [p, q, None]:
            return node
        left = search(node.left)
        right = search(node.right)
        if left and right:
            return node
        return left if left else right
    return search(root)

Time complexity

If nodes are not guranteed to be in the tree

def lowest_common_ancestor(root: "Node", p: "Node", q: "Node") -> "Node":
    found = 0
    def search(node):
        nonlocal found
        if node is None: 
            return node

        if node in [p, q]:
            found += 1
            return node

        left = search(node.left)
        right = search(node.right)
        if left and right:
            return node
        return left if left else right

    node = search(root)
    if found >= 2:
        return node
    return None
[top][30c02]

Lowest Common Ancestor of a Binary Search Tree [LC#235]

Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST. According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”

Time complexity

[top][30d]

Segment Trees

The sum of the root vertex at index 1, the sums of its two child vertices at indices 2 and 3.

class SegmentTree:
    def __init__(self, arr: List[int]):
        self.size = len(arr)
        self.tree = [0] * (4 * self.size)  # Allocate space for the segment tree
        self.build(arr, 1, 0, self.size - 1)

    def operator(self, a:int, b:int):
        return a+b

    def build(self, array: List[int], node: int, left: int, right: int):
        if left == right:
            self.tree[node] = array[left]
        else:
            mid = (left + right) // 2
            self.build(array, node * 2, left, mid)
            self.build(array, node * 2 + 1, mid + 1, right)
            self.tree[node] = self.operator(self.tree[node * 2], self.tree[node * 2 + 1])

    def query(self, node: int, left: int, right: int, query_left: int, query_right: int) -> int:
        if query_left > query_right:
            return 0
        if query_left == left and query_right == right:
            return self.tree[node]
        mid = (left + right) // 2
        return (self.sum(node * 2, left, mid, query_left, min(query_right, mid)) +
                self.sum(node * 2 + 1, mid + 1, right, max(query_left, mid + 1), query_right))

    def update(self, node: int, left: int, right: int, position: int, new_value: int):
        if left == right:
            self.tree[node] = new_value
        else:
            mid = (left + right) // 2
            if position <= mid:
                self.update(node * 2, left, mid, position, new_value)
            else:
                self.update(node * 2 + 1, mid + 1, right, position, new_value)
            self.tree[node] = self.operator(self.tree[node * 2], self.tree[node * 2 + 1])

    def range_query(self, query_left: int, query_right: int) -> int:
        return self.query(1, 0, self.size - 1, query_left, query_right)

    def point_update(self, position: int, new_value: int):
        self.update(1, 0, self.size - 1, position, new_value)
[top][30e01]

Check Completeness of a Binary Tree [LC#958]

Given the root of a binary tree, determine if it is a complete binary tree. In a complete binary tree, every level, except possibly the last, is completely filled, and all nodes in the last level are as far left as possible. It can have between 1 and 2h nodes inclusive at the last level h.

Intuition

Code

def is_complete_tree(root: Optional[TreeNode]) -> bool:
    queue = deque([root])
    null_found = False
    while queue:
        node = queue.popleft()
        if node:
            if null_found:
                return False
            else:
                queue.append(node.left)
                queue.append(node.right)
        else:
            null_found = True
    return True

Time complexity

binary tree
[top][32]

Heap

[top][32a]

Find Median from Data Stream [LC#295]

The median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value, and the median is the mean of the two middle values.

  • For example, for arr = [2,3,4], the median is 3.
  • For example, for arr = [2,3], the median is (2 + 3) / 2 = 2.5.

Intuition

Code

class MedianFinder:
    def __init__(self):
        self.low = [] # stores n or n+1 elements
        self.high = [] # stores n elements

    def add(self, num: int) -> None:
        val = heapq.heappushpop(self.low, -num)
        heapq.heappush(self.high, -val)
        if len(self.low) <  len(self.high):
            val = heapq.heappop(self.high)
            heapq.heappush(self.low, -val)

    def find_median(self) -> float:
        if len(self.low) > len(self.high):
            return -self.low[0]
        return (-self.low[0] + self.high[0])/2

Time complexity

heap
[top][35]

BFS

Template

def bfs():
    queue = deque()
    seen = set()
    queue.append(start_node)
    seen.add(start_node)
    while queue:
        node = queue.popleft()
        for neigh in neighbors(node):
            if neigh not in seen:
                visit(neigh)
                queue.append(neigh)
                seen.add(neigh)
[top][35a]

Shortest Path in Binary Matrix [LC#1091]

Given an n x n binary matrix grid, return the length of the shortest clear path in the matrix from top left to bottom right. If there is no clear path, return -1. The matrix is 8 connected and you can only move in cells marked 0.

Intuition

BFS gives the shortest path in binary matrix

Code

def shortest_path_in_binary_matrix(grid: List[List[int]]) -> int:
    n = len(grid[0])

    def neighbours(x, y):
        for dx, dy in [
            (-1, -1), (-1, +0), (-1, +1),
            (+0, -1),           (+0, +1),
            (+1, -1), (+1, +0), (+1, +1),
        ]:
            nx, ny = x + dx, y + dy
            if 0 <= nx < n and 0 <= ny < n:
                yield nx, ny

    if grid[0][0] == 1 or grid[n - 1][n - 1] == 1:
        return -1

    queue = deque()
    seen = set()
    queue.append((0, 0, 1))
    seen.add((0, 0))
    while queue:
        x, y, d = queue.popleft()
        if x == n - 1 and y == n - 1:
            return d
        for nx, ny in neighbours(x, y):
            if grid[nx][ny] == 0 and (nx, ny) not in seen:
                queue.append((nx, ny, d + 1))
                seen.add((nx, ny))
    return -1

Time complexity

bfs
[top][35b]

01 Matrix [LC#542]

Given an m x n binary matrix mat, return the distance of the nearest 0 for each cell. The distance between two cells sharing a common edge is 1.

Intuition

BFS starting at all 0. BFS for binary graph give shortest path.

Code

def neares_zero(matrix: List[List[int]]) -> List[List[int]]:
    m = len(matrix)
    n = len(matrix[0])

    def neighbors(x, y):
        for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
            nx, ny = x + dx, y + dy
            if 0 <= nx < m and 0 <= ny < n:
                yield nx, ny

    queue = deque()
    seen = set()

    for x in range(m):
        for y in range(n):
            if matrix[x][y] == 0:
                queue.append((x, y, 0))
                seen.add((x, y))

    while queue:
        row, col, steps = queue.popleft()
        for nx, ny in neighbors(row, col):
            if (nx, ny) not in seen:
                matrix[nx][ny] = steps + 1
                queue.append((nx, ny, steps + 1))
                seen.add((nx, ny))
    return matrix

Time complexity

bfs
[top][35c]

All Nodes Distance K in Binary Tree [LC#863]

Given the root of a binary tree, the value of a target node target, and an integer k, return an array of the values of all nodes that have a distance k from the target node. You can return the answer in any order.

Intuition

Nodes at distance k can be children of the node or can be be children of one of the ancestors of the node. But without parent pointers searching for children on ancestors is very inefficient. So we create auxillary struture to maintain parent pointers and then perform BFS on the induced graph.

Code

def nodes_at_k_hops(root: TreeNode, target: TreeNode, k: int) -> List[int]:
    parent_node = {}

    def create_parent_node(parent, node):
        if node:
            parent_node[node] = parent
            create_parent_node(node, node.left)
            create_parent_node(node, node.right)

    create_parent_node(None, root)

    # BFS
    visited = set()
    queue = deque([(target, k)])
    answers = []
    while queue:
        node, distance = queue.popleft()
        if not node or node in visited:
            continue

        visited.add(node)

        if distance == 0:
            answers.append(node.val)
            continue

        queue.append((node.left, distance - 1))
        queue.append((node.right, distance - 1))
        queue.append((parent_node[node], distance - 1))
    return answers

Time complexity

Parent pointer creation is $O(n)$ and takes $O(n)$ space. BFS takes simlar space and time.

binary tree bfs
[top][37]

DFS

[top][37c]

Reconstruct Itinerary [LC#332]

You are given a list of airline tickets where tickets[i] = [fromi, toi] represent the departure and the arrival airports of one flight. Reconstruct the itinerary in order and return it. All of the tickets belong to a man who departs from “JFK”, thus, the itinerary must begin with “JFK”. If there are multiple valid itineraries, you should return the itinerary that has the smallest lexical order when read as a single string. For example, the itinerary [“JFK”, “LGA”] has a smaller lexical order than [“JFK”, “LGB”]. You may assume all tickets form at least one valid itinerary. You must use all the tickets once and only once.

Intuition

Code

def reconstruct_iternary(tickets: List[List[str]]) -> List[str]:
    # Eulerian Path , greedy DFS
    graph = defaultdict(list)
    for frm, to in sorted(tickets)[::-1]:
        graph[frm].append(to)

    stack = ["JFK"]
    ans = []
    while stack:
        while graph[stack[-1]]:
            neigh = graph[stack[-1]].pop()
            stack.append(neigh)
        node = stack.pop()
        ans.append(node)        
    return ans[::-1]

Time complexity

eulerian path dfs
[top][40]

Single Source Shortest Path

[top][40a]

Dijkstra algorithm

def dijkstra(graph: List[List[Tuple[int, float]]], source: int) -> List[float]:
    n = len(graph)
    distances: List[float] = [math.inf] * n
    distances[source] = 0.0
    heap: List[Tuple[float, int]] = [(0.0, source)]

    while heap:
        curr_dist, node = heapq.heappop(heap)
        # if curr_dist > distances[node]:
        #     continue
        
        for neighbour, weight in graph[node]:
            dist = curr_dist + weight
            if dist < distances[neighbour]:
                distances[neighbour] = dist
                heapq.heappush(heap, (dist, neighbour))
    
    return distances
dijkstra sssp
[top][40a01]

Cheapest Flights Within K Stops [LC#787]

There are n cities connected by some number of flights. You are given an array flights where flights[i] = [from_i, to_i, price_i] indicates that there is a flight from city from_i to city to_i with cost price_i. You are also given three integers src, dst, and k, return the cheapest price from src to dst with at most k stops. If there is no such route, return -1.

Intuition

Code : Dijkstra’s

def min_distance_within_k_hops(n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
    graph = defaultdict(list)
    for frm, to, price in flights:
        graph[frm].append((to, price))
    dist = {src: math.inf for src in range(n)}

    dist[src] = 0
    heap = [(0, 0, src)]

    while heap:
        stops, distance, node = heapq.heappop(heap)

        for neigh, weight in graph[node]:
            if stops <= k and dist[neigh] > distance + weight:
                dist[neigh] = distance + weight
                heapq.heappush(heap, (stops + 1, dist[neigh], neigh))

    return -1 if dist[dst] == math.inf else dist[dst]

Time complexity : Dijkstra’s

Code : Level wise BFS

def min_distance_within_k_hops(n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
    graph = defaultdict(list)
    for frm, to, price in flights:
        graph[frm].append((to, price))
    dist = {src: math.inf for src in range(n)}
    dist[src] = 0
    queue = deque([(src, 0)])

    steps = 0
    while steps <= k and queue:
        level_size = len(queue)
        for _ in range(level_size):
            node, distance = queue.popleft()
            for neigh, weight in graph[node]:
                if dist[neigh] > distance + weight:
                    dist[neigh] = distance + weight
                    queue.append((neigh, dist[neigh]))
        steps += 1
    return -1 if dist[dst] == math.inf else dist[dst]

Time complexity : Level wise BFS

dijkstra sssp bfs
[top][40b]

Bellman-Ford

def bellman_ford(graph, source):
    # Initialize distances from source to all vertices as INFINITE
    distances = {v: float("inf") for v in graph}
    distances[source] = 0  # Distance from source to itself is always 0

    # Relax all edges |V| - 1 times
    for _ in range(len(graph) - 1):
        for u in graph:
            for v, w in graph[u]:
                if distances[u] != float("inf") and distances[u] + w < distances[v]:
                    distances[v] = distances[u] + w

    # Check for negative-weight cycles
    for u in graph:
        for v, w in graph[u]:
            if distances[u] != float("inf") and distances[u] + w < distances[v]:
                print("Graph contains negative weight cycle")
                return None

    return distances
[top][42]

Minimum Spanning Tree

[top][42a]

Kruskal’s algorithm for Minimum spanning tree

Kruskal’s algorithm is as follows.

@dataclass
class Edge:
    u: int
    v: int
    w: float

def kruskal_mst(num_edges: int, edges: List[Edge]) -> Tuple[List[Edge], float]:
    edges.sort(key=lambda x: x.w)
    dsu = DisjointSetUnion(num_edges)
    mst = []
    total_weight = 0

    for edge in edges:
        if dsu.find(edge.u) != dsu.find(edge.v):
            dsu.union(edge.u, edge.v) 
            mst.append(edge)
            total_weight += edge.w
            
    return mst, total_weight
mst
[top][42a01]

Min Cost to Connect All Points [LC#1584]

You are given an array points representing integer coordinates of some points on a 2D-plane, where points[i] = [xi, yi]. The cost of connecting two points [xi, yi] and [xj, yj] is the manhattan distance between them: |xi - xj| + |yi - yj|. Return the minimum cost to make all points connected. All points are connected if there is exactly one simple path between any two points.

Intuition

Code

def min_cost_connect_points(points: List[List[int]]) -> int:
    class DSU:
        def __init__(self):
            self.parent = {}
            self.rank = {}

        def find(self, node):
            if node not in self.parent:
                self.parent[node] = node
                self.rank[node] = 1

            if self.parent[node] != node:
                self.parent[node] = self.find(self.parent[node])
            return self.parent[node]

        def union(self, x, y):
            x = self.find(x)
            y = self.find(y)
            if x == y:
                return False

            if self.rank[x] < self.rank[y]:
                x, y = y, x

            self.parent[y] = x
            if self.rank[x] == self.rank[y]:
                self.rank[x] += 1

            return True

    def manhattan_distance(p, q):
        return abs(p[0] - q[0]) + abs(p[1] - q[1])

    dsu = DSU()
    edge_list = []
    for i in range(len(points)):
        dsu.find(i)
        for j in range(0, i):
            edge_list.append((i, j, manhattan_distance(points[i], points[j])))

    edge_list.sort(key=lambda r: r[2])
    min_cost = 0
    for i, j, d in edge_list:
        if dsu.find(i) != dsu.find(j):
            min_cost += d
            dsu.union(i, j)

    return min_cost

Time complexity

If n is number of points

mst
[top][42b]

Prim’s Algorithm for Minimum spanning tree

@dataclass
class Edge:
    u: int
    v: int
    w: float

def prims_mst(num_vertices: int, edges: List[Edge]) -> Tuple[List[Edge], float]:
    graph = {u: [] for u in range(num_vertices)}
    for edge in edges:
        graph[edge.u].append((edge.v, edge.w))
        graph[edge.v].append((edge.u, edge.w))

    visited = [False] * num_vertices
    min_heap = [(0.0, 0)]  # (weight, vertex)
    total_cost = 0
    mst_edges = []

    while min_heap:
        weight, u = heapq.heappop(min_heap)
        if visited[u]:
            continue
        visited[u] = True
        total_cost += weight

        if weight > 0:  # Skip the initial vertex
            mst_edges.append(Edge(prev_vertex, u, weight))

        for v, edge_weight in graph[u]:
            if not visited[v]:
                heapq.heappush(min_heap, (edge_weight, v))
                prev_vertex = u  # Track the previous vertex for the edge

    return mst_edges, total_cost, all(visited)

Time Complexity

mst
[top][44]

Topological Sort

Kahn’s Algorithm (BFS-like)

def topological_sort_kahn(graph: Dict[str, List[str]]) -> List[str]:
    in_degree = {u: 0 for u in graph}  # Initialize in-degrees to 0
    for u in graph:
        for v in graph[u]:
            in_degree[v] += 1  # Increment in-degree for each edge

    queue = deque([u for u in in_degree if in_degree[u] == 0])
    topological_order = []

    while queue:
        u = queue.popleft()  # Get a vertex with in-degree 0
        topological_order.append(u)  # Add it to the topological order
        
        for v in graph[u]:
            in_degree[v] -= 1  # Remove the edge u -> v
            if in_degree[v] == 0:  # If in-degree becomes 0, add to queue
                queue.append(v)

    if len(topological_order) != len(graph):
        return []
    return topological_order

Example Graph

graph = {
    'A': ['C'],
    'B': ['C', 'D'],
    'C': ['E'],
    'D': ['E'],
    'E': []
}

DFS like topological sort

def topological_sort_dfs(graph: Dict[str, List[str]]) -> List[str]:
    WHITE, GRAY, BLACK = 0, 1, 2
    color = {node: WHITE for node in graph}
    stack: List[str] = []

    def dfs(node: str) -> bool:
        if color[node] == GRAY:
            return False # Cycle detected
        if color[node] == WHITE:
            color[node] = GRAY
            for neighbor in graph[node]:
                if not dfs(neighbor):
                    # cycle in recursion
                    return False
            color[node] = BLACK
            stack.append(node)
        return True

    for vertex in graph:
        if color[vertex] == WHITE:
            if not dfs(vertex):  # If a cycle is detected
                return []

    return stack[::-1]
topological sort
[top][44a]

Find Eventual Safe States [LC#802]

There is a directed graph of n nodes with each node labeled from 0 to n - 1. The graph is represented by a 0-indexed 2D integer array graph where graph[i] is an integer array of nodes adjacent to node i, meaning there is an edge from node i to each node in graph[i].

A node is a terminal node if there are no outgoing edges. A node is a safe node if every possible path starting from that node leads to a terminal node (or another safe node). Return an array containing all the safe nodes of the graph. The answer should be sorted in ascending order.

Intuition

Terminal nodes are safe nodes. Any node whose all ougoing edge are to safe nodes are also safe. We can iteratively grow the list of safe nodes this way. Essentially, topological sort on the reverse graph.

Code

def eventual_safe_nodes(graph: List[List[int]]) -> List[int]:
    n = len(graph)
    in_degree = [0] * n
    reverse_graph = [[] for _ in range(n)]
    for i in range(n):
        for node in graph[i]:
            reverse_graph[node].append(i)
            in_degree[i] += 1
    safe_nodes = []
    queue = deque([i for i in range(n) if in_degree[i]==0])
    while queue:
        node = queue.popleft()
        safe_nodes.append(node)
        for neigh in reverse_graph[node]:
            in_degree[neigh] -= 1
            if in_degree[neigh] == 0:
                queue.append(neigh)
    return sorted(safe_nodes)

Time complexity

$T(n) = $ $S(n) = $

topological sort
[top][46]

Max Flow Min Cut

[top][50]

Intervals

[top][50a]

Merge Intervals [LC#56]

Given an array of intervals where intervals[i] = [starti, endi], merge all overlapping intervals, and return an array of the non-overlapping intervals that cover all the intervals in the input.

Sort and Itrate approach

def merge_intervals(intervals: List[List[int]]) -> List[List[int]]:
    intervals = sorted(intervals)
    ans = [intervals[0]]
    for start, end in intervals[1:]:
        if start <= ans[-1][1]:
            ans[-1][1] = max(ans[-1][1], end)
        else:
            ans.append([start, end])
    return ans
intervals
[top][50b]

Insert Interval [LC#57]

You are given an array of non-overlapping intervals intervals where intervals[i] = [starti, endi] represent the start and the end of the ith interval and intervals is sorted in ascending order by starti. You are also given an interval newInterval = [start, end] that represents the start and end of another interval.

Insert newInterval into intervals such that intervals is still sorted in ascending order by starti and intervals still does not have any overlapping intervals (merge overlapping intervals if necessary).

Return intervals after the insertion.

Note that you don’t need to modify intervals in-place. You can make a new array and return it.

Intuition

Since the intervals are sorted by start time, when we loop through them in order, we will encounter 3 cases

  1. The interval ends before the new interval
  2. The interval overlaps with the new interval
  3. The interval starts after the new interval

We handle each of the 3 cases separately

Code

@dataclass
class Interval:
    start: float
    end: float
    
def insert_interval(intervals: List[Interval], new: Interval) -> List[Interval]:
    ans = []
    for curr in intervals:
        if curr.end < new.start: # ends before
            ans.append(curr)
        elif curr.start > new.end: # begins after
            if new:
                ans.append(new)
                new = None
            ans.append(curr)
        else: # overlap
            new = Interval(
                min(curr.start, new.start), max(curr.end, new.end)
            )
    if new: # handle when intervals are empty
        ans.append(new)
    return ans

Time Complexity

intervals
[top][50c]

Min meeting rooms [LC#253]

Meeting Rooms II : Given an array of meeting time intervals intervals where intervals[i] = [starti, endi], return the minimum number of conference rooms required.

Intuition

Code

def min_meeting_rooms(intervals: List[List[int]]) -> int:
    intervals = sorted(intervals)
    heap = [intervals[0][1]]  # first end time
    meeting_rooms = 1
    for start_time, end_time in intervals[1:]:
        if start_time >= heap[0]:
            heapq.heappop(heap)
        heapq.heappush(heap, end_time)
        meeting_rooms = max(meeting_rooms, len(heap))
    return meeting_rooms

Time Complexity

intervals greedy
[top][50d]

Line Sweep

This is a technique where we sweep an imaginary line across the whole domain, and the line’s intersections with intervals are used to solve problems. They are particularly useful for problems where we have range updates and we need to find the final state of the array.

Say we are given queries = [(left, right, value),] implying all values in [left, right] are to be incremented by value. To quickly find the final value, we can do the following.

def apply_range_updates(queries, n):
    arr = [0]*n
    for left, right, value in queries:
        arr[left] += value
        arr[right] -= value
    prev = 0
    for i in range(n):
        prev+=arr[i]
        arr[i] = prev
    return arr

The function apply_range_updates takes the list of queries and the size of the array n, and returns the final state of the array after applying all the range updates.

line sweep
[top][50d01]

Zero Array Transformation I [LC#3355]

You are given an integer array nums of length n and a 2D array queries, where queries[i] = [li, ri]. For each queries[i]:

  • Select a subset of indices within the range [li, ri] in nums.
  • Decrement the values at the selected indices by 1.

A Zero Array is an array where all elements are equal to 0. Return true if it is possible to transform nums into a Zero Array after processing all the queries sequentially, otherwise return false.

Intuition

The key idea is to find the maximum possible decrements that can be applied to each element in the array. If the maximum possible decrements for any element is less than the initial value of that element, then it is not possible to transform the array into a Zero Array. We can use a line sweep approach to efficiently compute the maximum possible decrements for each element.

Code

def is_zero_array(nums: List[int], queries: List[List[int]]) -> bool:
    n = len(nums)
    arr = [0] * n
    for left, right in queries:
        arr[left] += 1
        if right + 1 < n:
            arr[right + 1] -= 1

    prev = 0
    for i in range(n):
        prev += arr[i]
        if prev < nums[i]:
            return False
    return True

TimeComplexity

line sweep
[top][50d02]

Zero Array Transformation II [LC#3356]

You are given an integer array nums of length n and a 2D array queries where queries[i] = [l_i, r_i, val_i]. Each queries[i] represents the following action on nums:

  • Decrement the value at each index in the range [l_i, r_i] in nums by at most val_i.
  • The amount by which each value is decremented can be chosen independently for each index. A Zero Array is an array with all its elements equal to 0. Return the minimum possible non-negative value of k, such that after processing the first k queries in sequence, nums becomes a Zero Array. If no such k exists, return -1.

Intuition

Line sweep and binary search on min queries required.

Code

def minZeroArray(nums: List[int], queries: List[List[int]]) -> int:
    n, m = len(nums), len(queries)

    def apply_query(T) -> bool:
        arr = [0] * n
        for left, right, val in queries[:T]:
            arr[left] += val
            if right + 1 < n:
                arr[right + 1] -= val
        prev = 0
        for i in range(n):
            prev += arr[i]
            if prev < nums[i]:
                return False
        return True

    L, R = 0, m
    if not apply_query(m):
        return -1

    while L < R:
        mid = L + (R - L) // 2
        if apply_query(mid):
            R = mid
        else:
            L = mid + 1
    return R

TimeComplexity

If $n$ is the size of array and $m$ is number of queries, then

line sweep binary search
[top][50d03]

Smallest Range Covering Elements from K Lists [LC#632]

You have k lists of sorted integers in non-decreasing order. Find the smallest range that includes at least one number from each of the k lists. We define the range [a, b] is smaller than range [c, d] if b - a < d - c or a < c if b - a == d - c.

Intution

Code

def smallestRange(nums: List[List[int]]) -> List[int]:
    heap = [(arr[0], i, 0) for i, arr in enumerate(nums)]
    heapq.heapify(heap)
    right = max(arr[0] for arr in nums)
    ans = (-math.inf, math.inf)
    
    while heap:
        left, row, col = heapq.heappop(heap)
        if right - left < ans[1] - ans[0]:
            ans = left, right

        if col + 1 == len(nums[row]):
            return ans

        next_point = nums[row][col + 1]
        right = max(right, next_point)
        heapq.heappush(heap, (next_point, row, col + 1))

Time complexity

$n$ max size of lists and $m$ is number of lists

line sweep intervals
[top][50e]

Minimum Number of Arrows to Burst Balloons [LC#452]

There are some spherical balloons taped onto a flat wall that represents the XY-plane. The balloons are represented as a 2D integer array points where points[i] = [xstart, xend] denotes a balloon whose horizontal diameter stretches between xstart and xend. You do not know the exact y-coordinates of the balloons.

Arrows can be shot up directly vertically (in the positive y-direction) from different points along the x-axis. A balloon with xstart and xend is burst by an arrow shot at x if xstart <= x <= xend. There is no limit to the number of arrows that can be shot. A shot arrow keeps traveling up infinitely, bursting any balloons in its path.

Given the array points, return the minimum number of arrows that must be shot to burst all balloons.

Intuition: Greedy Approach

Code

def min_arrows(balloons: List[List[int]]) -> int:
    if not balloons:
        return 0
    balloons.sort(key = lambda r: r[1])
    end_location = balloons[0][1]
    num_arrows = 1
    for balloon in balloons:
        if balloon[0] > end_location:
            end_location = balloon[1]
            num_arrows +=1
    return num_arrows

Time complexity

Time complexity is domniated by sorting. $T(n) = O(n \log n) + O(n)$ , $S(n) = O(1)$

intervals greedy
[top][60]

Monotonic Stack

A monotonic decreasing stack is a data structure based on a stack. While traversing an array, it maintains a sorted list of elements encountered so far that are strictly greater than or equal to the current element being processed. It upholds this property by continuously removing elements from the top of the stack that violate the requirement (i.e., elements that are less than the current element).

stack = []
for num in nums:
    while stack and stack[-1] <= num:
        stack.pop()
    stack.append(num)
    # stack[-1] > stack[-2] > ... > stack[0]

Although a monotonic stack can be used to solve problems that require finding previous smaller (or larger) elements from the stack, a more interesting use is to exploit the interim states of the monotonic stack while it updates itself to maintain the property of monotonicity. Ex.

stack = []
arr.append(-math.inf) # to ensure all elements are processed
for idx range(len(arr)):
    while stack and arr[stack[-1]] <= arr[idx]:
        mid = stack.pop()
        left = -1 if not stack else stack[-1]
        right = idx
        # do something with arr[left] > a[mid] < a[right]
    stack.append(idx)

References

monotonic stack
[top][60a]

Largest Rectangle in Histogram [ LC# 84]

Given an array of integers heights representing the histogram’s bar height where the width of each bar is 1, return the area of the largest rectangle in the histogram.

Brute Force

area[i][j] = min_heights[i:j] * ( j-i+1 )
min_heights[i:j] = min(min_heights[i-1:j-1], heights[i], heights[j])
min_heights[i:i] = heights[i]

Intuition: Monotonic Stack

           ┌─┐         ┌─┐    
       ┌─┐ │ │       ┌─┤ │    
   ┌─┐ │ ├─┤ │     ┌─┤ │ │    
   │ ├─┤ │ │ │   ┌─┤ │ │ │    
   │ │ │ │ │ │...│h│ │ │ ├─┐   
 ┌─┤ │ │ │ │ │   │ │ │ │ │ │   
 └─┴─┴─┴─┴─┴─┘   └─┴─┴─┴─┴─┘   
   └─────────────────────┘                
┌─┴──┐          ┌─┴─┐  ┌──┴──┐
 left            mid    right

Code

def largest_rectangle_in_historgam(heights: List[int]) -> int:
    stack = [-1] # monotonic stack : non decreasing order
    max_area = 0
    
    heights.append(0)
    for i in range(len(heights)):
        while stack and heights[stack[-1]] > heights[i]:
            mid = stack.pop()
            left = stack[-1] + 1
            right = i
            max_area = max(max_area,  heights[mid] * (right - left))
        stack.append(i)
    return max_area

Time complexity

monotonic stack
[top][60b]

Sum of Subarray Minimums [LC#907]

Given an array of integers arr, find the sum of min(b), where b ranges over every (contiguous) subarray of arr. Since the answer may be large, return the answer modulo $10^9 + 7$.

Intuition

If arr[k] is minimum element in [i, j], then arr[k] appears in (k - i) * (j - k) subarrays.

Monotonic Stack

def sum_subarrray_mins(arr: List[int]) -> int:
    stack = []
    sum_of_minimums = 0
    arr.append(-math.inf) # so that everything gets popped out in the end
    for i in range(len(arr)):
        while stack and arr[stack[-1]] >= arr[i]:
            mid = stack.pop()
            left = -1 if not stack else stack[-1]
            right = i

            count = (mid - left) * (right - mid)
            sum_of_minimums += (count * arr[mid])
        stack.append(i)
    return sum_of_minimums

Time Complexity

monotonic stack
[top][60c]

Daily Temperatures [LC#739]

Given an array of integers temperatures represents the daily temperatures, return an array answer such that answer[i] is the number of days you have to wait after the ith day to get a warmer temperature. If there is no future day for which this is possible, keep answer[i] == 0 instead.

Intuition

We have to fine the next largest in the array for every element.

Code

def next_warmer_day(temperatures: List[int]) -> List[int]:
    n = len(temperatures)
    stack = []
    answers = [0] * n
    for i in range(n - 1, -1, -1):
        while stack and temperatures[stack[-1]] <= temperatures[i]:
            stack.pop()
        if stack:
            answers[i] = stack[-1] - i
        stack.append(i)
    return answers

OR

def next_warmer_day(temperatures: List[int]) -> List[int]:
    n = len(temperatures)
    stack = []
    answers = [0] * n
    for curr_day in range(n):
        while stack and temperatures[stack[-1]] < temperatures[curr_day]:
            prev_day = stack.pop()
            answers[prev_day] = curr_day - prev_day
        stack.append(curr_day)
    return answers

Note the different comparison signs between 2 implementation

Time Complexity

monotonic stack
[top][65]

Tries

[top][65a]

Word break [LC#139]

Given a string and a vocabulary, return true if the string can be segmented into a space-separated sequence of one or more words from the vocabulary. Note that the same word in the vocabulary may be reused multiple times in the segmentation.

Intuition

Code

def word_break(string: str, vocabulary: List[str]) -> bool:
    n = len(string)
    TRIE = {}
    EOW = "$"
    for word in vocabulary:
        if len(word) > n:
            continue
        node = TRIE
        for char in word:
            if char not in node:
                node[char] = {}
            node = node[char]
        node[EOW] = ""

    reachable = [False] * (n + 1)
    reachable[0] = True

    for i in range(n + 1):
        if reachable[i]:
            node = TRIE
            j = i
            while j < n:
                if string[j] not in node:
                    break
                node = node[string[j]]
                if EOW in node:
                    reachable[j + 1] = True
                j += 1

    return reachable[n]

Time complexity

n = len(string), m = len(vocabulary), k = max(len(word) in vocabulary)

trie dp
[top][70]

Permutations

[top][70a]

Next Permutation [LC#31]

Given an array of integers nums, find the next permutation of nums.

The next permutation of an array of integers is the next lexicographically greater permutation of its integer. More formally, if all the permutations of the array are sorted in one container according to their lexicographical order, then the next permutation of that array is the permutation that follows it in the sorted container. If such arrangement is not possible, the array must be rearranged as the lowest possible order (i.e., sorted in ascending order).

Intuition

Code

    def next_permutation(self, nums: List[int]) -> List[int]:
        def swap(i, j):
            nums[i], nums[j] = nums[j], nums[i]

        def reverse(start, end):
            nums[start : end + 1] = nums[start : end + 1 : -1]

        n = len(nums)
        # Step 1
        i = n - 2
        while i >= 0 and nums[i] >= nums[i + 1]:
            i -= 1
            
        if i >= 0:
            # Step 2
            j = n - 1
            while nums[i] >= nums[j]:
                j -= 1
            swap(i, j)
        
        # Step 3
        reverse(i + 1, n - 1)
        return nums

Time complexity

permutation
[top][70b]

Permutation Sequence [LC#60]

The set [1, 2, 3, ..., n] contains a total of n! unique permutations. Given n and k, return the kth permutation sequence.

Intuition

Code

def kth_permutation(n: int, k: int) -> str:
    factorials = [1]
    digits = ["1"]
    for i in range(1, n):
        factorials.append(factorials[-1] * i)
        digits.append(str(i + 1))

    output = []
    k -= 1
    for i in range(n - 1, -1, -1):
        # index = k // factorials[i]
        # k = k - idx * factorials[i]
        index, k = divmod(k, factorials[i])
        output.append(digits[index])
        del digits[index]
    return "".join(output)

Time complexity

permutation
[top][72]

Sets

[top][72a]

Subsets II [LC#90]

Given an integer array nums that may contain duplicates, return all possible subsets (the power set). The solution set must not contain duplicate subsets. Return the solution in any order.

Intuition

def subsets_of_multiset(nums: List[int]) -> List[List[int]]:
    powerset = [[],]
    freq = {}
    for num in nums:
        freq[num] = freq.get(num, 0) + 1

    for num, num_freq in freq.items():
        curr = []
        for subset in ans:
            for ff in range(1, num_freq + 1):
                new_subset = subset + [num] * ff
                curr.append(new_subset)
        powerset.extend(curr)
    return powerset
[top][72c]

DSU with Rollbacks

class DSU_rollback:
    def __init__(self):
        self.parent = {}
        self.rank = {}
        self.history = []

    def find(self, v):
        if v not in self.parent:
            self.parent[v] = v
            self.rank[v] = 0
      
        if v == self.parent[v]:
            return v
        # self.parent[v] = self.find(self.parent[v])
        # We don't perform path compression
    
        return self.find(self.parent[v])

    def union(self, v, u):
        v = self.find(v)
        u = self.find(u)
        if v == u:
            return False
        if self.rank[v] > self.rank[u]:
            v, u = u, v
        
        self.history.append((v, self.rank[v], u, self.rank[u]))
        
        self.parent[v] = u
        if self.rank[v] == self.rank[u]:
            self.rank[u] += 1
        return True

    def rollback(self):
        if not self.history:
            return
        v, rank_v, u, rank_u = self.history.pop()
        self.parent[v] = v
        self.rank[v] = rank_v
        self.parent[u] = u
        self.rank[u] = rank_u
[top][73]

DSU Union-Find

class DSU:
    def __init__(self):
        self.parent = {}
        self.rank = {}

    def find(self, x):
        if x not in self.parent:
            # new node
            self.parent[x] = x
            self.rank[x] = 0
            
        if self.parent[x] != x:
            # path compression
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        root_x = self.find(x)
        root_y = self.find(y)
        
        if root_x == root_y:
            return False

        if self.rank[root_x] < self.rank[root_y]:
            root_x, root_y = root_y, root_x 

        self.parent[root_y] = root_x

        if self.rank[root_x] == self.rank[root_y]:
            self.rank[root_x] += 1
        return True
[top][73a]

Number of Connected Components in an Undirected Graph [LC#323]

You have a graph of n nodes. You are given an integer n and an array edges where edges[i] = [ai, bi] indicates that there is an edge between ai and bi in the graph. Return the number of connected components in the graph.

Intuition

Code

def count_components(self, n: int, edges: List[List[int]]) -> int:
    parent = {i: i for i in range(n)}

    def find(a):
        if parent[a] != a:
            parent[a] = find(parent[a])
        return parent[a]

    def union(a, b):
        a = find(a)
        b = find(b)
        parent[b] = a

    for a, b in edges:
        union(a, b)

    for a, b in edges:
        find(a)
        find(b)

    return len(set(v for k, v in union_find.items()))

Time complexity

dsu
[top][73b]

The Earliest Moment When Everyone Become Friends [LC#1101]

There are n people in a social group labeled from 0 to n - 1. You are given an array logs where logs[i] = [timestampi, xi, yi] indicates that xi and yi will be friends at the time timestamp i.

Friendship is symmetric. That means if a is friends with b, then b is friends with a. Also, person a is acquainted with a person b if a is friends with b, or a is a friend of someone acquainted with b.

Return the earliest time for which every person became acquainted with every other person. If there is no such earliest time, return -1.

Intuition

Since the friendship is symetric, wheever two people become friends, we can merge their acquaintance sets togethere. When there is only 1 such sets, everyone is aquainted. For finding earliest time, we simply go through the logs in chronological order.

Code

def earliestAcq(self, logs: List[List[int]], n: int) -> int:
    dsu = DSU()
    for i in range(n):
        dsu.find(i)
    for timestamp, x, y in sorted(logs):
        if dsu.union(x, y):
            if dsu.components == 1:
                return timestamp
    return -1

Time Complexity

[top][73c]

Making A Large Island [LC#827]

You are given an n x n binary matrix grid. You are allowed to change at most one 0 to be 1. Return the size of the largest island in grid after applying this operation. An island is a 4-directionally connected group of 1s.

Intuition

Code

def largest_island(grid: List[List[int]]) -> int:
    parent = {}
    size = {}

    def find(x):
        if x not in parent:
            parent[x], size[x] = x, 1

        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    def union(x, y):
        x = find(x)
        y = find(y)
        if x != y:
            if size[x] < size[y]:
                x, y = y, x
            parent[y], size[x] = x, size[x] + size[y]
        return

    m, n = len(grid), len(grid[0])
    for x in range(m):
        for y in range(n):
            if grid[x][y] == 1:
                find((x, y))
                if x > 0 and grid[x - 1][y] == 1:
                    union((x, y), (x - 1, y))

                if y > 0 and grid[x][y - 1] == 1:
                    union((x, y), (x, y - 1))

    max_size = max(size.values(), default=0)

    for x in range(m):
        for y in range(n):
            if grid[x][y] == 0:
                unique_parents = set()
                if x > 0 and grid[x - 1][y] == 1:
                    unique_parents.add(find((x - 1, y)))
                if y > 0 and grid[x][y - 1] == 1:
                    unique_parents.add(find((x, y - 1)))
                if x < m - 1 and grid[x + 1][y] == 1:
                    unique_parents.add(find((x + 1, y)))
                if y < n - 1 and grid[x][y + 1] == 1:
                    unique_parents.add(find((x, y + 1)))

                new_island_size = 1 + sum(size[p] for p in unique_parents)
                max_size = max(max_size, new_island_size)

    return max_size

Time complexity

dsu
[top][75]

Binary Search

Minimize k , s.t. condition(k) is True

  [ f ][ f ][ f ][ t ][ t ][ t ][ t ][ t ]     
                   └── ans 
def binary_search(search_space) -> int:
    left, right = min(search_space), max(search_space)
    while left < right:
        mid = left + (right - left) // 2
        if condition(mid):
            right = mid
        else:
            left = mid + 1
    return left
[top][75a]

Binary Search in Array

[top][75a01]

Binary search in a sorted array

  [      <      ][ = ][      >      ]                            
def binary_search_array(arr: List[int], target: int) -> int:
    left, right = 0, len(arr) - 1
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] >= target:
            right = mid
        else:
            left = mid + 1
    return left if arr[left] == target else -1
binary search
[top][75a02]

Find First and Last Position of Element in Sorted Array [LC#34]

Given an array of integers nums sorted in non-decreasing order, find the starting and ending position of a given target value. If target is not found in the array, return [-1, -1]. You must write an algorithm with O(log n) runtime complexity.

Code

def searchRange(nums: List[int], target: int) -> Tuple[int, int]:
    def binary_search(value):
        left, right = 0, len(nums) - 1
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] >= value:
                right = mid
            else:
                left = mid + 1
        return left

    nums.append(math.inf) # to handle edge cases

    pos_beg = binary_search(target)
    if nums[pos_beg] != target:
        return (-1, -1)
    pos_end = binary_search(target + 1)
    return (pos_beg, pos_end - 1)

Time Complexity

$T(n) = O(\log n)$

binary search
[top][75a03]

Find Minimum in Rotated Sorted Array [LC#153]

Suppose an array of length n sorted in ascending order is rotated between 1 and n times. For example, the array nums = [0,1,2,4,5,6,7] might become:

  • [4,5,6,7,0,1,2] if it was rotated 4 times.
  • [0,1,2,4,5,6,7] if it was rotated 7 times.

Notice that rotating an array [a[0], a[1], a[2], ..., a[n-1]] 1 time results in the array [a[n-1], a[0], a[1], a[2], ..., a[n-2]]. Given the sorted rotated array nums of unique elements, return the minimum element of this array.

Intuition

minimum number is present in the smallest index k such that nums[k] < nums[-1]

Code

def findMin(nums: List[int]) -> int:
    left, right = 0, len(nums) - 1
    while left < right:
        mid = left + (right - left) // 2
        if nums[mid] <= nums[-1]:
            right = mid
        else:
            left = mid + 1
    return nums[left] 
binary search
[top][75a04]

Find Peak Element [LC#162]

A peak element is an element that is strictly greater than its neighbors. Given a 0-indexed integer array nums, find a peak element, and return its index. If the array contains multiple peaks, return the index to any of the peaks. You may imagine that nums[-1] = nums[n] = $-\infty$. In other words, an element is always considered to be strictly greater than a neighbor that is outside the array. You must write an algorithm that runs in O(log n) time.

Intuition

Code

def find_peak_element(nums: List[int]) -> int:
    lo, hi = 0, len(nums)-1
    nums.append(-math.inf)
    while lo < hi:
        mid = lo + (hi - lo)//2
        if nums[mid] > nums[mid+1]:
            hi = mid
        else:
            lo = mid+1
    return lo

Time complexity

[top][75b]

Minimum Limit of Balls in a Bag [LC#1760]

You are given an integer array nums where the ith bag contains nums[i] balls. You are also given an integer max_operations. You can perform the following operation at most max_operations times:

  • Take any bag of balls and divide it into two new bags with a positive number of balls. For example, a bag of 5 balls can become two new bags of 1 and 4 balls, or two new bags of 2 and 3 balls.

Your penalty is the maximum number of balls in a bag. You want to minimize your penalty after the operations. Return the minimum possible penalty after performing the operations.

Intuition

Binary search on penalty.

Code

def minimum_size(nums: List[int], max_operations: int) -> int:
    def valid(th):
        count = 0
        for num in nums:
            operations = math.ceil(num / th) - 1
            count += operations
            if count > max_operations: return False
        return True
    
    left, right = 1, max(nums)

    while left < right:
        mid = left + (right - left) // 2
        if valid(mid):
            right = mid
        else:
            left = mid + 1
    return left

Time Complexity

The search take log(range) steps. The range is of size $2^b$ if $b$ is max number of bits representing the numbers in the list nums. Hence number of search steps is upperbounded by $\log (2^b) = b$. Each step take $O(n)$ to evaluate the validity condition. So time complexity $T(n) = O(b \ast n)$. If we consider $b$ as a constant then time complexity is $O(n)$

binary search
[top][75d]

Split Array Largest Sum [LC#412]

Given an integer array nums and an integer k, split nums into k non-empty subarrays such that the largest sum of any subarray is minimized. Return the minimized largest sum of the split. A subarray is a contiguous part of the array.

Intuition

Binary search on the range of possible values.

Code

def splita_array_largest_sum(nums: List[int], k: int) -> int:
    left, right = max(nums), sum(nums)

    def valid(th):
        num_partitions = 0
        sum_partition = 0
        for num in nums:
            if sum_partition + num <= th:
                sum_partition += num
            else:
                sum_partition = num
                num_partitions += 1
            if num_partitions >= k:
                return False
        return True

    while left < right:
        mid = left + (right - left) // 2
        if valid(mid):
            right = mid
        else:
            left = mid + 1
    return right

Time Complexity

The search take log(range) steps. The range is of size $2^b$ if $b$ is max number of bits representing the numbers in the list nums. Hence number of search steps is upperbounded by $\log (2^b) = b$. Each step take $O(n)$ to evaluate the validity condition. So time complexity $T(n) = O(b \ast n)$. If we consider $b$ as a constant then time complexity is $O(n)$

binary search
[top][75e]

Swim in Rising Water [LC#778]

You are given an n x n integer matrix grid where each value grid[i][j] represents the elevation at that point (i, j). The rain starts to fall. At time t, the depth of the water everywhere is t. You can swim from a square to another 4-directionally adjacent square if and only if the elevation of both squares individually are at most t. You can swim infinite distances in zero time. Of course, you must stay within the boundaries of the grid during your swim.

Return the least time until you can reach the bottom right square (n - 1, n - 1) if you start at the top left square (0, 0).

Intuition

Code

def swim_in_water(grid: List[List[int]]) -> int:
    m, n = len(grid), len(grid[0])

    def neighbours(x, y):
        for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
            nx, ny = x + dx, y + dy
            if (0 <= nx < m) and (0 <= ny < n):
                yield (nx, ny)

    def bfs_with_limit(limit):
        queue = deque([(0, 0)])
        seen = {(0, 0)}
        while queue:
            row, col = queue.popleft()
            if (row, col) == (m - 1, n - 1):
                return True
            for nx, ny in neighbours(row, col):
                if (nx, ny) not in seen and grid[nx][ny] <= limit:
                    queue.append((nx, ny))
                    seen.add((nx, ny))
        return False

    lo, hi = grid[0][0], max(max(grid[i]) for i in range(m))

    while lo < hi:
        mid = lo + (hi - lo) // 2
        if bfs_with_limit(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

Time complexity

binary search bfs
[top][80]

Dynamic Programming

[top][80a]

Target Sum [LC#494]

You are given an integer array nums and an integer target. You want to build an expression out of nums by adding one of the symbols + and - before each integer in nums and then concatenate all the integers. For example, if nums = [2, 1], you can add a + before 2 and a - before 1 and concatenate them to build the expression "+2-1". Return the number of different expressions that you can build, which evaluates to target.

Intuition

Code

def count_expressions(nums: List[int], target: int) -> int:
    n = len(nums)

    @cache
    def memo(target, i):
        if i == 0:
            if target == 0:
                return 1
            else:
                return 0

        for_plus = +nums[i - 1] + target
        for_minus = -nums[i - 1] + target

        return memo(for_plus, i - 1) + memo(for_minus, i - 1)

    return memo(target, n)

Time complexity

dp
[top][80b]

Edit Distance [LC#72]

Given two strings word1 and word2, return the minimum number of operations required to convert word1 to word2. You have the following three operations permitted on a word:

  • Insert a character
  • Delete a character
  • Replace a character

Intuition

Let cost[i][j] is the minimum edit distance (or the minimum number of operations) required to transform the first i characters of word1 into the first j characters of word2.

Dynamic Programming

def edit_distance(word1: str, word2: str) -> int:
    # Define the costs for operations
    costs = {'insert': 1, 'delete': 1, 'substitute': 1}
    m, n = len(word1), len(word2)
    
    cost = [[0] * (n + 1) for _ in range(m + 1)]
    
    # Initialize the first row and column
    for i in range(m + 1):
        # Cost of deleting all characters from word1 to form word2
        cost[i][0] = i  * costs['delete']
    for j in range(n + 1):
        # Cost of inserting all characters to word1 to form word2
        cost[0][j] = j  * costs['insert']

    # Fill the cost matrix
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if word1[i - 1] == word2[j - 1]:
                cost[i][j] = cost[i - 1][j - 1]  # No operation needed
            else:
                cost[i][j] = min(
                    cost[i - 1][j] + costs['insert'],      # Insert
                    cost[i][j - 1] + costs['delete'],      # Delete
                    cost[i - 1][j - 1] + costs['substitute']  # Substitute
                )
    return cost[m][n]
string dp
[top][80c]

Interleaving String [LC#97]

Given strings u, v, and t, find whether t can formed by an interleaving of u and v.

Intuition

Code

from functools import cache

def isInterleave(u: str, v: str, t: str) -> bool:
    m, n, k = len(u), len(v), len(t)
    if m + n != t: return False
    
    @cache
    def memo(i, j, k):
        if k == 0:
            return i == 0 and j == 0

        if i > 0 and u[i - 1] == t[k - 1] and memo(i - 1, j, k - 1):
            return True
        if j > 0 and v[j - 1] == t[k - 1] and memo(i, j - 1, k - 1):
            return True

        return False

    return memo(m, n, k)

Time complexity

string dp
[top][80d]

Distinct Subsequences [LC#115]

Given two strings s and t, return the number of distinct subsequences of s which equals t.

Intuition

Code

def count_distinct_subsequences(s: str, t: str) -> int:
    m = len(s) 
    n = len(t)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    dp[0][0] = 1

    for i in range(1, m + 1):
        dp[i][0] = 1
        for j in range(1, n + 1):
            dp[i][j] = dp[i - 1][j]
            if s[i - 1] == t[j - 1]:
                dp[i][j] += dp[i - 1][j - 1]
    return dp[m][n]

def count_distinct_subsequences_space_optimised(s: str, t: str) -> int:
    m = len(s)  
    n = len(t)  
    dp = {0: [0] * (n + 1), 1:[0] * (n + 1)}

    dp[0][0] = 1

    for i in range(1, m + 1):
        dp[i%2][0] = 1
        for j in range(1, n + 1):
            dp[i%2][j] = dp[(i - 1)%2][j]
            if s[i - 1] == t[j - 1]:
                dp[i%2][j] += dp[(i - 1)%2][j - 1]
    return dp[m%2][n]

Time complexity

string sub sequence dp
[top][84]

Greedy Approaches

[top][84a01]

Gas Station [LC#134]

There are n gas stations along a circular route, where the amount of gas at the station i is gas[i]. You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from the station i station to (i + 1). You begin the journey with an empty tank at one of the gas stations.

Given two integer arrays gas and cost, return the starting gas station’s index if you can travel around the circuit once in the clockwise direction, otherwise return -1. If there exists a solution, it is guaranteed to be unique.

Intuition: Greedy Approach

Code

def gas_station_valid_starting_point(gas: List[int], cost: List[int]) -> int:
    if sum(gas) < sum(cost):
        return -1
    tank = 0
    starting_index = 0
    for i in range(len(gas)):
        tank += gas[i] - cost[i]
        if tank < 0:
            tank = 0
            starting_index = i + 1
    return starting_index

Time complexity

greedy
[top][84a02]

Max Chunks To Make Sorted [LC#769]

You are given an integer array arr of length n that represents a permutation of the integers in the range [0, n - 1]. We split arr into some number of chunks (i.e., partitions), and individually sort each chunk. After concatenating them, the result should equal the sorted array. Return the largest number of chunks we can make to sort the array.

Intuition

Code

def maximum_chunks(arr: List[int]) -> int:
    right_limit = 0
    count = 0
    for i, num in enumerate(arr):
        right_limit = max(right_limit, num)
        if i >= right_limit:
            count +=1           
    return count

Time complexity

greedy
[top][84a03]

Jump Game [LC#55]

You are given an integer array nums. You are initially positioned at the array’s first index, and each element in the array represents your maximum jump length at that position. Return true if you can reach the last index, or false otherwise.

Intuition

There is a dp approach. But time complexity is $O(n^2)$ with linear storage. The greedy approach.

Code

def jump_game(nums: List[int]) -> bool:
    n = len(nums)
    left_limit = n - 1
    for pos in range(n - 1, -1, -1):
        if pos + nums[pos] >= left_limit:
            left_limit = pos
    return left_limit == 0

Time complexity

greedy
[top][84a03a]

Jump Game II [LC#45]

You are given a 0-indexed array of integers nums of length n. You are initially positioned at nums[0]. Each element nums[i] represents the maximum length of a forward jump from index i. Return the minimum number of jumps to reach the last position.

Intuition

Code

def min_jump(nums: List[int]) -> int:
    n = len(nums)
    if n == 1: return 0
    left, right, jumps = 0, 0, 0
    while right < n - 1:
        max_reach = max(nums[i] + i for i in range(left, right + 1))
        left = right 
        right = max_reach
        jumps += 1
    return jumps

Time complexity

greedy
[top][84a03b]

Jump Game III [LC#1306]

Given an array of non-negative integers arr, you are initially positioned at start index of the array. When you are at index i, you can jump to i + arr[i] or i - arr[i], check if you can reach any index with value 0. Notice that you can not jump outside of the array at any time.

Intuition

Code

def jump(arr: List[int], start: int) -> bool:
    seen = set([start])
    queue = deque([start])
    n = len(arr)
    while queue:
        node = queue.popleft()
        if arr[node]==0: return True

        for idx in [node+arr[node],  node-arr[node]]:
            if idx not in seen and 0 <= idx < n:
                queue.append(idx)
                seen.add(idx)
    return False

Time complexity

bfs
[top][84a04]

Maximum Swap [LC#670]

You are given an integer num. You can swap two digits at most once to get the maximum valued number. Return the maximum valued number you can get.

Intuition

The possible pairs which can be swapped to increase the value of the numbers are such that larger number follows smaller number. The optimum pair among those are the ones where the larger number is most to the left.

Code

def maximum_swap(num: int) -> int:
    digits = list(str(num))
    n = len(digits)
    max_pos = n - 1
    left, right = n, n
    for i in range(n - 1, -1, -1):
        if digits[i] > digits[max_pos]:
            max_pos = i

        if digits[i] < digits[max_pos]:
            left = i
            right = max_pos

    if left == n or right == n:
        return num
    digits[left], digits[right] = digits[right], digits[left]
    return int("".join(digits))

Time complexity

[top][85]

Backtracking

Code template

ans = []
def backtrack(state):
    if is_valid(state):
        ans.append(state)
        # return

    for candidate in get_candidates(state):
        new_state = state + candidate
        backtrack(new_state)
[top][85a]

N-Queens Problem

The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other. Given an integer n, return all distinct solutions to the n-queens puzzle. You may return the answer in any order.

Each solution contains a distinct board configuration of the n-queens’ placement, where ‘Q’ and ‘.’ both indicate a queen and an empty space, respectively.

Intuition

All indices of same diagonal share the same value for (x-y). Similarly, all indices of anti diagonal share the same value for (x+y).

Backtracking

def n_queens(n: int) -> List[List[str]]:
    ans = []
    state = [["."] * n for _ in range(n)]
    m_row = defaultdict(int)
    m_col = defaultdict(int)
    m_diag = defaultdict(int)
    m_antidiag = defaultdict(int)

    def mark(x, y, V=1):
        m_row[x] += V
        m_col[y] += V
        m_diag[x - y] += V
        m_antidiag[x + y] += V

    def get_mark(x, y):
        return m_row[x] + m_col[y] + m_diag[x - y] + m_antidiag[x + y]


    def backtrack(row):
        if row == n:
            ans.append(["".join(c) for c in state])
            return

        for col in range(n):
            if get_mark(row, col) == 0:
                # keep queen
                mark(row, col, 1)
                state[row][col] = "Q"
                backtrack(row + 1)

                # remove queen
                state[row][col] = "."
                mark(row, col, -1)

    backtrack(0)
    return ans
[top][85b]

Sudoku Solver [LC#37]

Write a program to solve a Sudoku puzzle by filling the empty cells. A sudoku solution must satisfy all of the following rules:

  1. Each of the digits 1-9 must occur exactly once in each row.
  2. Each of the digits 1-9 must occur exactly once in each column.
  3. Each of the digits 1-9 must occur exactly once in each of the 9 3x3 sub-boxes of the grid. The ‘.’ character indicates empty cells.

Intuition

Backtracking

Code

def solve_sudoku(board: List[List[str]]) -> None:
    positions = []
    for i in range(9):
        for j in range(9):
            if board[i][j] == ".":
                positions.append((i, j))

    def get_possible_numbers(x, y):
        possible_numbers = set(["1", "2", "3", "4", "5", "6", "7", "8", "9"])
        cross = set()
        for i in range(9):
            cross.add(board[x][i])
            cross.add(board[i][y])

        for i in range((x // 3) * 3, (x // 3 + 1) * 3):
            for j in range((y // 3) * 3, (y // 3 + 1) * 3):
                cross.add(board[i][j])

        return possible_numbers.difference(cross)

    def backtrack(i):
        if i >= len(positions):
            return True

        x, y = positions[i]
        possible_numbers = get_possible_numbers(x, y)

        if not possible_numbers:
            return False

        for num in possible_numbers:
            board[x][y] = num
            if backtrack(i + 1):
                return True

        board[x][y] = "."
        return False

    backtrack(0)
[top][87]

Knapsack

[top][87a]

Bounded Knapsack

Bounded knapsack is when we have $n$ items $[(w_i, v_i, n_i)]$ representing weight, value and copies available of each item. This is tribvially reducible to 0/1 knapsack if we simply assume that there are $n_i$ number of distinct items with same weight and value for each of the original item. The time complexity would then be $O(sum(n_i)\times sum(w_i n_i))$.

def knapsack_01(weights: List[int], values: List[int], capacity: int) -> int:
    max_value = [0] * (capacity + 1)

    for weight, value in zip(weights, values):
        for slack in range(capacity, weight - 1, -1):
            max_value[slack] = max(
                max_value[slack], 
                max_value[slack - weight] + value
            )

    return max_value[capacity]

There is a slightly better way to do the reduction. create logaritmic number of copies of items with 1,2,4,.. times weight, value of each item and solve as a 0/1 knapsack problem. Any number of selection of copies of original problem can be represented by a specific selection of new items. Time complexity is $O(sum(w_i) \times n \times \log (\max n_i))$. Ref

[top][87b]

Unbounded Knapsack

Unbounded knpasack is when we have infinite number of each items.

def unbounded_knapsack(weights: List[int], values: List[int], capacity: int) -> int:
    max_value = [0] * (capacity + 1)
    for i in range(1, capacity + 1):
        for j in range(len(weights)):
            if weights[j] <= i:
                max_value[i] = max(
                    max_value[i], 
                    max_value[i - weights[j]] + values[j]
                )
    return max_value[capacity]
[top][87c]

Fractional Knapsack

[top][87d]

Reducing subset sum to 0/1 knapsack

Intuition

The Subset Sum problem can be reduced to the 0/1 Knapsack problem by treating the elements of the set as both the weights and values of the items. Given a target sum, we can construct a 0/1 Knapsack with target sum as the capacity and if the the maximum value of the knapsack is equal to the target sum, the selected items form the required subset.

def subset_sum(nums: List[int], target: int) -> bool:
    max_value = knapsack_01(nums, nums, target)
    return max_value == target

def knapsack_01(weights: List[int], values: List[int], capacity: int) -> int:
    max_value = [0] * (capacity + 1)

    for weight, value in zip(weights, values):
        for slack in range(capacity, weight - 1, -1):
            max_value[slack] = max(max_value[slack], max_value[slack - weight] + value)

    return max_value[capacity]
[top][87e]

Coin Change

[top][87e01]

Coin Change [LC#322]

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money. Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1. You may assume that you have an infinite number of each kind of coin.

Intuition

Code

def coin_change(coins: List[int], amount: int) -> int:
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    
    for coin in coins:
        for x in range(coin, amount + 1):
            dp[x] = min(dp[x], dp[x - coin] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1 

Time complexity

knapsack
[top][87e02]

Coin Change II [LC#518]

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money. Return the number of combinations that make up that amount. If that amount of money cannot be made up by any combination of the coins, return 0. You may assume that you have an infinite number of each kind of coin.

Intuition

Code

def coin_change(amount: int, coins: List[int]) -> int:
    dp = [0]*(amount +1)
    dp[0] = 1
    for coin in coins:
        for amt in range(coin, amount + 1):
            dp[amt] += dp[amt - coin]
    return dp[amount]

Time complexity

knapsack
[top][90]

Range Queries

[top][90a]

Fenwick Trees or Binary Indexed Tree

References

          0· 1· 2· 3· 4· 5· 6· 7· 8· 9·10·11·12·13·14·15
    A = [  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  ]

T[ 0] = [ ✚|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  ]
T[ 1] = [ ✚| ✚|  |  |  |  |  |  |  |  |  |  |  |  |  |  ]
T[ 2] = [  |  | ✚|  |  |  |  |  |  |  |  |  |  |  |  |  ]
T[ 3] = [ ✚| ✚| ✚| ✚|  |  |  |  |  |  |  |  |  |  |  |  ]
T[ 4] = [  |  |  |  | ✚|  |  |  |  |  |  |  |  |  |  |  ]
T[ 5] = [  |  |  |  | ✚| ✚|  |  |  |  |  |  |  |  |  |  ]
T[ 6] = [  |  |  |  |  |  | ✚|  |  |  |  |  |  |  |  |  ]
T[ 7] = [ ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚|  |  |  |  |  |  |  |  ]
T[ 8] = [  |  |  |  |  |  |  |  | ✚|  |  |  |  |  |  |  ]
T[ 9] = [  |  |  |  |  |  |  |  | ✚| ✚|  |  |  |  |  |  ]
T[10] = [  |  |  |  |  |  |  |  |  |  | ✚|  |  |  |  |  ]
T[11] = [  |  |  |  |  |  |  |  | ✚| ✚| ✚| ✚|  |  |  |  ]
T[12] = [  |  |  |  |  |  |  |  |  |  |  |  | ✚|  |  |  ]
T[13] = [  |  |  |  |  |  |  |  |  |  |  |  | ✚| ✚|  |  ]
T[14] = [  |  |  |  |  |  |  |  |  |  |  |  |  |  | ✚|  ]
T[15] = [ ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚| ✚]
OP = lambda a, b: a+b
OP_INV = lambda a, b: a - b
E = lambda : 0

class FenwickTree:
    def __init__(self, A: List[int]):
        n = len(A)
        self.T = A.copy()
        for i in range(n):
            parent = i | (i + 1)
            if parent < n:
                self.T[parent] += self.T[i]

    def add(self, i, delta):
        n = len(self.T)
        while i < n:
            self.T[i] = OP(self.T[i], delta)
            i = i | (i + 1)

    def prefix_sum(self, i): # sum [0, i]
        res = E()
        while i >= 0:
            res = OP(result, self.T[i])
            i = (i & (i + 1)) - 1
        return result

    def range_sum(self, l, r):  # sum [l, r)
        return OP_INV(self.prefix_sum(r - 1) ,  self.prefix_sum(l - 1))

    def __getitem__(self, idx: int) -> int:
        return self.sum_range(idx, idx)

    def append(self, val):
        n = len(self.T)
        self.T.append(E())
        self.add(n, val)
fenwick trees prefix sum rmq
[top][90b]

Segment Trees

┌───────────────────────────────────────────────────────────────┐
│                               1:                              │     
│                             -----                             │
╔═══════════════════════════════╗───────────────────────────────┤
║               2:              ║               3:              │     
║             [3,11)            ║             -----             │
╟───────────────┬───────────────╫───────────────╔═══════════════╗
║       4:      │       5:      ║       6:      ║       7:      ║
║     [3,7)     │     [7,11)    ║     -----     ║     [1,3)     ║
╟───────┬───────┼───────┬───────╠═══════╦═══════╣───────┬───────╢
║   8:  │   9:  │  10:  │  11:  ║  12:  ║  13:  ║  14:  │  15:  ║
║ [3,5) │ [5,7) │ [7,9) │ [9,11)║[11,13)║   0   ║   1   │   2   ║
╟───┬───┼───┬───┼───┬───┼───┬───╫───┬───╠═══╤═══╩═══╤═══╪═══╤═══╝
║16:│17:│18:│19:│20:│21:│22:│23:║24:│25:║   │   │   │   │   │   |
║ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │10 ║11 │12 ║   │   │   │   │   │   │
╚═══╧═══╧═══╧═══╧═══╧═══╧═══╧═══╩═══╧═══╝───┴───┴───┴───┴───┴───┘
┌─────────┐
│Tree idx:│
│  range  │
└─────────┘

References

OP = lambda a, b: min(a, b)
E = lambda : math.inf

class SegTree:
    def __init__(self, A: List[int]) -> None:
        n = len(A)
        self.T: List[int] = [E()] * (2 * n)
        for i in range(n):
            self.T[n + i] = A[i]

        for i in range(n - 1, 0, -1):
            self.T[i] = OP(self.T[2 * i] , self.T[2 * i + 1])

        self.n: int = n

    def __getitem__(self, i: int) -> int:
        return self.T[self.n + i]
    
    def point_update(self, i: int, value: int) -> None:
        i += self.n
        self.T[i] = value
        while i > 1:
            i //= 2
            self.T[i] = OP(self.T[2 * i] , self.T[2 * i + 1])

    def range_query(self, l: int, r: int) -> int: # [l, r)
        res_l, res_r = E(), E()
        l += self.n
        r += self.n

        while l < r:
            if l % 2 == 1:
                res_l = OP(res_l, self.T[l])
                l += 1
            if r % 2 == 1:
                r -= 1
                res_r = OP(self.T[r], res_r)
            l //= 2
            r //= 2
        return OP(res_l, res_r)
segment trees rmq
[top][90c01]

Range Frequency Queries [LC#2080]

Design a data structure to find the frequency of a given value in a given subarray. The frequency of a value in a subarray is the number of occurrences of that value in the subarray.

Implement the RangeFreqQuery class: RangeFreqQuery(int[] arr) Constructs an instance of the class with the given 0-indexed integer array arr. int query(int left, int right, int value) Returns the frequency of value in the subarray arr[left…right]. A subarray is a contiguous sequence of elements within an array. arr[left…right] denotes the subarray that contains the elements of nums between indices left and right (inclusive).

Intuition

Code

class RangeFreqQuery:
    def __init__(self, arr: List[int]):
        self.index = defaultdict(list)
        for idx, num in enumerate(arr):
            self.index[num].append(idx)

    def query(self, left: int, right: int, value: int) -> int:
        low = bisect.bisect_left(self.index[value], left)
        high = bisect.bisect_right(self.index[value], right)
        return high - low

Time complexity

[top][99]

KMP [LC#xxx]

Question Description

REF: https://www.youtube.com/watch?v=EL4ZbRF587g

Intuition

Code

class KMP:
    def __init__(self, pattern):
        # longest prefix suffix (LPS) array
        m = len(pattern)
        lps = [0]*m
        length = 0
        i = 1
        while i < m:
            if pattern[length] == pattern[i]:
                length += 1
                lps[i] = length
                i += 1
            else:
                if length != 0:
                    length = lps[length - 1] 
                else:
                    lps[i] = 0
                    i += 1

        self.lps = lps
        self.pattern = pattern
        
    def match(self, text):
        n = len(text)
        m = len(self.pattern)
        matches = []
        i = j = 0
        while i < n:
            if self.pattern[j] == text[i]:
                i += 1
                j += 1
            if j == m:
                matches.append(i - j)
                j = self.lps[j - 1]
            elif i < len(text) and self.pattern[j] != text[i]:
                if j != 0:
                    j = self.lps[j - 1]
                else:
                    i += 1
        return matches

Time complexity

[top][99a01]

Rotate Image [LC#48]

You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise). You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and do the rotation.

Intuition

90 degree Rotation clockwise = tranpose + reflect

Code

def rotate(matrix: List[List[int]]) -> None:
    n = len(matrix)

    # tranpose
    for i in range(n):
        for j in range(i+1, n):
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

    # reflect
    for i in range(n):
        for j in range(n//2):
            matrix[i][j], matrix[i][n-j-1] = matrix[i][n-j-1], matrix[i][j]

Time complexity

[top][99a02]

Counting problems

Count the Number of Fair Pairs [LC#2563]

Given an array two limits [lower, upper], return the number of pairs in the array whose sum is withihg the limits (includive)

Intuition

Code

def pairs_within_bounds(self, nums: List[int], lower: int, upper: int) -> int:
    nums = sorted(nums)
    U = self.count_lower_sorted(nums, upper)
    L = self.count_lower_sorted(nums, lower - 1)
    return U - L

def pairs_below_bounds(self, nums: List[int], th: int ):
    left, right = 0, len(nums) - 1
    counts = 0
    while left < right:
        s = nums[left] + nums[right]
        if s <= th:
            counts += right - left
            left += 1
        else:
            right -= 1
    return counts

Time complexity