In this project, I will try to solve 75 Leetcode questions as listed in this link.

1. Two Sum

Description

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.

Example 1:

Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Output: Because nums[0] + nums[1] == 9, we return [0, 1].

Example 2:

Input: nums = [3,2,4], target = 6
Output: [1,2]
Solution
class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        dic = {}
        for i, num in enumerate(nums):
            if num in dic:
                return [dic[num], i]
            else:
                dic[target - num] = i

121. Best Time to Buy and Sell Stock

Description

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.

Example 1:

Input: prices = [7,1,5,3,6,4]
Output: 5
Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.
Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell.

Example 2:

Input: prices = [7,6,4,3,1]
Output: 0
Explanation: In this case, no transactions are done and the max profit = 0.
Solution

Two pointers starting from both ends (i.e., move left or right pointer based on which side has a better profit)

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        l, r = 0, len(prices)-1
        minL = prices[l]
        maxR = prices[r]

        while l<r: 
            if prices[l] < minL: 
                minL = prices[l]

            if prices[r] > maxR: 
                maxR = prices[r]

            if prices[r-1] - minL > maxR - prices[l+1]: 
                r -= 1 
            else: 
                l += 1 

        return maxR - minL if maxR >= minL else 0

Two pointers starting from right (i.e., find highest price and then find the lowest price before it, fastest solution)

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        max_profit = 0
        max_price = 0
        for i in range(len(prices)-1, -1, -1):
            if prices[i] > max_price:
                max_price = prices[i]
            else:
                max_profit = max(max_profit, max_price - prices[i])
        return max_profit

Two pointers starting from left (i.e., find lowest price and then find the highest price after it)

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        min_price = float('inf')
        max_profit = 0
        for i in range(len(prices)):
            if prices[i] < min_price:
                min_price = prices[i]
            else:
                max_profit = max(max_profit, prices[i] - min_price)
        return max_profit

217. Contains Duplicate

Description

Given an integer array nums, return true if any value appears at least twice in the array, and return false if every element is distinct.

Solution
class Solution:
    def containsDuplicate(self, nums: List[int]) -> bool:
        dic = {}
        for num in nums:
            if num in dic:
                return True
            else:
                dic[num] = 1

238. Product of Array Except Self

Description

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.

Example 1:

Input: nums = [1,2,3,4]
Output: [24,12,8,6]

Example 2:

Input: nums = [-1,1,0,-3,3]
Output: [0,0,9,0,0]
Solution

We will use two arrays to store the product of all elements to the left and right of each element. Then, we will multiply the two arrays to get the final result.

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        left = [1] * len(nums)
        right = [1] * len(nums)
        for i in range(1, len(nums)):
            left[i] = left[i-1] * nums[i-1]
            right[len(nums)-1-i] = right[len(nums)-i] * nums[len(nums)-i]
        return [left[i] * right[i] for i in range(len(nums))]

53. Maximum Subarray

Description

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

A subarray is a contiguous part of an array.

Example 1:

Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

Example 2:

Input: nums = [1]
Output: 1

Example 3:

Input: nums = [5,4,-1,7,8]
Output: 23
Solution

Use Kadane’s algorithm to find the maximum subarray sum. The idea is to keep track of the maximum sum of subarrays ending at each index. If the sum is negative, we reset it to 0. Refer to this video and https://www.geeksforgeeks.org/largest-sum-contiguous-subarray/ article for more details.

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        res = nums[0]
        accsum = nums[0]
        for n in nums[1:]: 
            accsum += n
            
            if accsum <= n:
                accsum = n 
                        
            if accsum >= res: 
                res = accsum 

        return res 

Dynamic programming solution (from greeksforgeeks)

Observation: DP[i] = max(DP[i-1] + nums[i], nums[i])

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        DP = [0] * len(nums)
        DP[0] = nums[0]
        for i in range(1, len(nums)):
            DP[i] = max(DP[i-1] + nums[i], nums[i])
        return max(DP)

152. Maximum Product Subarray

Description

Given an integer array nums, find a contiguous non-empty subarray within the array that has the largest product, and return the product.

It is guaranteed that the answer will fit in a 32-bit integer.

Example 1:

Input: nums = [2,3,-2,4]
Output: 6
Explanation: [2,3] has the largest product 6.

Example 2:

Input: nums = [-2,0,-1]
Output: 0
Explanation: The result cannot be 2, because [-2,-1] is not a subarray.

Example 3:

Input: nums = [-2,3,-4]
Output: 24
Explanation: The result is 24 because (-2)*3*(-4) = 24.
Solution

Observation:

  • It is not the same as the maximum subarray sum. It is because the product of two negative numbers is positive.
  • We need to keep track of the maximum and minimum product so far.
  • cumMax = max(cumMax * n, cumMin * n, n)
  • cumMin = min(cumMax * n, cumMin * n, n)
  • res = max(res, cumMax)
  • We need to reset cumMax and cumMin when we encounter a 0.
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        dp_max = [1] * len(nums)
        dp_min = [1] * len(nums)

        dp_max[0] = nums[0]
        dp_min[0] = nums[0]

        for i in range(1, len(nums)):
            if nums[i] == 0:
                dp_max[i] = 0
                dp_min[i] = 0
                continue 
            
            cur = [dp_max[i-1] * nums[i], dp_min[i-1] * nums[i], nums[i]]
            dp_max[i] = max(cur)
            dp_min[i] = min(cur)
        
        return max(dp_max)     
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        res = max(nums)
        cumMax = 1 
        cumMin = 1 
        
        for n in nums: 
            
            if n == 0: 
                cumMax = 1 
                cumMin = 1 
                continue 
            
            temp = max(cumMax * n, cumMin * n, n)
            cumMin = min(cumMax * n, cumMin * n, n)
            cumMax = temp 
            
            res = max(res, cumMax)
            
        return res

153. Find Minimum in Rotated Sorted Array

Description

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.

You must write an algorithm that runs in O(log n) time.

Example 1:

Input: nums = [3,4,5,1,2]
Output: 1
Explanation: The original array was [1,2,3,4,5] rotated 3 times.

Example 2:

Input: nums = [4,5,6,7,0,1,2]
Output: 0
Explanation: The original array was [0,1,2,4,5,6,7] and it was rotated 4 times.

Example 3:

Input: nums = [11,13,15,17]
Output: 11
Explanation: The original array was [11,13,15,17] and it was rotated 4 times.
Solution

We can use binary search to find the minimum element.

  • if nums[left] < nums[right], then nums[left] is the minimum element.
  • if nums[mid] > nums[right], then the minimum element is in the right half.
  • if nums[mid] < nums[right], then the minimum element is in the left half.
class Solution:
    def findMin(self, nums: List[int]) -> int:
        left, right = 0, len(nums)-1

        if nums[left] < nums[right]:
            return nums[left]
        
        while left < right:
            mid = (left + right) // 2
            if nums[mid] > nums[right]:
                left = mid + 1
            else:
                right = mid
        
        return nums[left]

33. Search in Rotated Sorted Array

Description

There is an integer array nums sorted in ascending order (with distinct values).

Prior to being passed to your function, nums is rotated at an unknown pivot index k (0 <= k < nums.length) such that the resulting array is [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]] (0-indexed). For example, [0,1,2,4,5,6,7] might be rotated at pivot index 3 and become [4,5,6,7,0,1,2].

Given the array nums after the rotation and an integer target, return the index of target if it is in nums, or -1 if it is not in nums.

You must write an algorithm with O(log n) runtime complexity.

Example 1:

Input: nums = [4,5,6,7,0,1,2], target = 0
Output: 4

Example 2:

Input: nums = [4,5,6,7,0,1,2], target = 3
Output: -1
Solution
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)-1

        while left <= right:
            mid = (left + right) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] >= nums[left]:
                if nums[left] <= target < nums[mid]:
                    right = mid -1
                else:
                    left = mid + 1
            else:
                if nums[mid] < target <= nums[right]:
                    left = mid + 1
                else:
                    right = mid - 1
        
        return -1

15. 3Sum

Description

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.

Example 1:

Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]

Example 2:

Input: nums = [0,1,1]
Output: []
Solution
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        if len(nums) == 3:
            return [nums] if sum(nums) == 0 else []
        
        nums.sort()

        n = len(nums)
        res = []

        for i in range(n):
            if i > 0 and nums[i] == nums[i-1]:
                continue
            
            left, right = i+1, n-1

            while left < right:
                sum3 = nums[i] + nums[left] + nums[right]
                if sum3 == 0:
                    res.append([nums[i], nums[left], nums[right]])
                    left += 1
                    right -= 1
                    while left < right and nums[left] == nums[left-1]:
                        left += 1
                    while left < right and nums[right] == nums[right+1]:
                        right -= 1
                elif sum3 < 0:
                    left += 1
                else:
                    right -= 1
        
        return res

11. Container With Most Water

Description

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

Notice that you may not slant the container.

Example 1:

Input: height = [1,8,6,2,5,4,8,3,7]
Output: 49
Explanation: The above vertical lines are represented by array [1,8,6,2,5,4,8,3,7]. In this case, the max area of water (blue section) the container can contain is 49.

Example 2:

Input: height = [1,1]
Output: 1
Solution
class Solution:
    def maxArea(self, height: List[int]) -> int:
        left, right = 0, len(height)-1
        res = 0
        while left < right:
            res = max(res, min(height[left], height[right]) * (right - left))
            if height[left] < height[right]:
                left += 1
            else:
                right -= 1
        return res

70. Climbing Stairs

Description

You are climbing a staircase. It takes n steps to reach the top.

Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

Example 1:

Input: n = 2
Output: 2
Explanation: There are two ways to climb to the top.
1. 1 step + 1 step
2. 2 steps

Example 2:

Input: n = 3
Output: 3
Explanation: There are three ways to climb to the top.
1. 1 step + 1 step + 1 step
2. 1 step + 2 steps
3. 2 steps + 1 step
Solution

Key Observation: DP[i] = DP[i-1] + DP[i-2] which means the number of ways to reach the i-th step is the sum of the number of ways to reach the (i-1)-th step and the number of ways to reach the (i-2)-th step.

class Solution:
    def climbStairs(self, n: int) -> int:
        if n == 1:
            return 1
        a, b = 1, 2
        for _ in range(2, n):
            a, b = b, a+b
        return b
class Solution:
    def climbStairs(self, n: int) -> int:
        if n == 1:
            return 1
        dp = [0] * (n+1)
        dp[0] = 1
        dp[1] = 1
        for i in range(2, n+1):
            dp[i] = dp[i-1] + dp[i-2]
        return dp[n]

322. Coin Change

Description

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.

Example 1:

Input: coins = [1,2,5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1
Solution

Key Observation: DP[i] = min(DP[i-c1], DP[i-c2], ...) + 1 which means the minimum number of coins to make up the amount i is the minimum number of coins to make up the amount i-c1, i-c2, … plus 1.

This problem can be visualized as a tree with the root node as the amount and the children nodes as the amount - coin values. The problem is to find the shortest path from the root to the target amount.

    def coinChange(self, coins: List[int], amount: int) -> int:
        BIG_NUM = amount + 1
        dp = [BIG_NUM] * (amount+1)
        if amount == 0: 
            return 0 
        
        dp[0] = 0
        for i in range(0,amount+1):
            for c in coins:
                if i + c <= amount:
                    dp[i+c] = min(dp[i] + 1,dp[i+c])
        
        if dp[amount] == BIG_NUM:
            return -1 
        else:
            return dp[amount]

300. Longest Increasing Subsequence

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

A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7].

Example 1:

Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.

Example 2:

Input: nums = [0,1,0,3,2,3]
Output: 4
Solution

Key Observation:

  • When considering the i-th element, we need to find the longest increasing subsequence that ends with nums[i].
  • Straightforward case is when nums[i] is the largest element, then the longest `dp[i] = dp[i-1] + 1
  • The addition of nums[i] to the subsequence causes the change of all previous dp[j] where nums[j] < nums[i].
    def lengthOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        dp = [1] * n 

        for i in range(n-2,-1,-1):
            for j in range(i+1,n,1):
                if nums[i] < nums[j]:
                    dp[i] = max(dp[i], dp[j]+1)
        
        return max(dp)

139. Word Break

Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of one or more dictionary words.

Note that the same word in the dictionary may be reused multiple times in the segmentation.

Example 1:

Input: s = "leetcode", wordDict = ["leet","code"]
Output: true

Example 2:

Input: s = "applepenapple", wordDict = ["apple","pen"]
Output: true

Example 3:

Input: s = "catsandog", wordDict = ["cats","dog","sand","and","cat"]
Output: false
Solution
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
    
    n = len(s)
    dp = [False] * (n + 1)
    dp[-1] = True

    for i in range(n-1, -1, -1): 
        for w in wordDict: 
            # check whether the word w in the head of the sub string s 
            if len(w) <= len(s[i:]):
                if w == s[i:i+len(w)]:
                    dp[i] = any([dp[i+len(w)], dp[i]])
    
    return dp[0]

377. Combination Sum IV

Given an array of distinct integers nums and a target integer target, return the number of possible combinations that add up to target.

The test cases are generated so that the answer can fit in a 32-bit integer.

Example 1:

Input: nums = [1,2,3], target = 4
Output: 7
Explanation:
The possible combination ways are:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3, 1)
(2, 1, 1)
(2, 2, 1)
(3, 1, 1)

Example 2:

Input: nums = [9], target = 3
Output: 0
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0] * (target + 1)
        dp[0] = 1 
         
        for i in range(target+1):
            for num in nums: 
                if i + num <= target:
                    dp[i+num] = dp[i+num] + dp[i]
        
        return dp[-1]

198. House Robber

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.

Example 1:

Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.

Example 2:

Input: nums = [2,7,9,3,1]
Output: 12

Observation:

  • We can either rob the current house or not.
  • If we rob the current house, we cannot rob the previous house.
  • If we do not rob the current house, we can rob the previous house.
  • dp[i] = max(dp[i-1], dp[i-2] + nums[i])
class Solution:
    def rob(self, nums: List[int]) -> int:
        if len(nums) <= 2: 
            return max(nums)

        dp = [0] * len(nums)
        dp[0] = nums[0]
        dp[1] = max(nums[0], nums[1])
        for i in range(2, len(nums)):
            dp[i] = max(dp[i-1], dp[i-2] + nums[i])
        
        return dp[-1]
    def rob(self, nums: List[int]) -> int:
        cur, pre = 0, 0 

        # [pre, cur, n, n+1, ...]
        for n in nums: 
            temp = max(pre + n, cur)
            pre = cur 
            cur = temp 
        
        return cur 

213. House Robber II

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have a security system connected, and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.

Example 1:

Input: nums = [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.
Solution

Observation:

  • We can apply the same solution as the previous problem, and compare between two cases: - rob the first house, not the last house - not rob the first house, rob the last house
class Solution:
    def rob(self, nums: List[int]) -> int:
        
        def _rob(nums):
            dp = [0] * len(nums)
            if len(nums) <= 2:
                return max(nums)
            dp[0] = nums[0]
            dp[1] = max(nums[0], nums[1])
            for i in range(2, len(nums)):
                dp[i] = max(dp[i-1], dp[i-2] + nums[i])
            return dp[-1]
        
        if len(nums)== 1:
            return nums[0]
            
        return max(_rob(nums[:-1]), _rob(nums[1:]))
def rob(self, nums: List[int]) -> int:
    
    def _rob(nums):
        cur, pre = 0, 0 

        # [pre, cur, n, n+1, ...]
        for n in nums:
            temp = max(pre + n, cur)
            pre = cur 
            cur = temp 
        
        return cur 
    
    if len(nums)== 1:
        return nums[0]
        
    return max(_rob(nums[:-1]), _rob(nums[1:]))

62. Unique Paths

There is a robot on an m x n grid. The robot is initially located at the top-left corner (i.e., grid[0][0]). The robot tries to move to the bottom-right corner (i.e., grid[m-1][n-1]). The robot can only move either down or right at any point in time.

Given the two integers m and n, return the number of possible unique paths that the robot can take to reach the bottom-right corner.

Example 1:

Input: m = 3, n = 7
Output: 28
Solution
def uniquePaths(self, m: int, n: int) -> int:
    grid = [[0 for _ in range(n)] for _ in range(m)]
    # set the last row and last col to be 1  
    for i in range(n): 
        grid[m-1][i] = 1 

    for i in range(m): 
        grid[i][n-1] = 1

    for row in range(m-2, -1, -1):
        for col in range(n-2, -1, -1): 
            grid[row][col] = grid[row][col+1] + grid[row+1][col]
    
    return grid[0][0]

55. Jump Game

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.

Example 1:

Input: nums = [2,3,1,1,4]
Output: true
Solution
    def canJump(self, nums: List[int]) -> bool:
        dp = [False] * len(nums)
        dp[-1] = True

        for i in range(len(nums)-2,-1,-1): # start at len(nums)-2, stop at 0 
            # any(dp[i + 0], dp[i + 1], ..., dp[i + nums[i]]) 
            furthest = min(i + nums[i], len(nums)) + 1
            dp[i] = any(dp[i:furthest])

    return dp[0] 

133. Clone Graph

Given a reference of a node in a connected undirected graph, return a deep copy (clone) of the graph.

Each node in the graph contains a value (int) and a list of its neighbors.

class Node {
    public int val;
    public List<Node> neighbors;
}

Test case:

  • An adjacency list is a collection of unordered lists used to represent a finite graph. Each list describes the set of neighbors of a node in the graph.

Example 1:

Input: adjList = [[2,4],[1,3],[2,4],[1,3]]
Output: [[2,4],[1,3],[2,4],[1,3]]
Explanation: There are 4 nodes in the graph.
1st node (val = 1)'s neighbors are 2nd node (val = 2) and 4th node (val = 4).
2nd node (val = 2)'s neighbors are 1st node (val = 1) and 3rd node (val = 3).
3rd node (val = 3)'s neighbors are 2nd node (val = 2) and 4th node (val = 4).
4th node (val = 4)'s neighbors are 1st node (val = 1) and 3rd node (val = 3).

207. Course Schedule

There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai.

Example 1:

Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Solution
def canFinish(numCourses: int, prerequisites: List[List[int]]) -> bool:
    mapPre = {c:[] for c in range(numCourses)}
    for c, p in prerequisites:
        mapPre[c].append(p)
    
    visited = {}

    def dfs(c):
        res = []

        if mapPre[c] == []:
            return True 

        visited[c] = True

        for p in mapPre[c]:
            if p in visited: 
                return False 
            else: 
                res.append(dfs(p))
        
        mapPre[c] = []
        del visited[c]
        return all(res)
    
    res = [dfs(c) for c,p in prerequisites]
    return all(res)

417. Pacific Atlantic Water Flow

There is an m x n rectangular island that borders both the Pacific Ocean and Atlantic Ocean. The Pacific Ocean touches the island’s left and top edges, and the Atlantic Ocean touches the island’s right and bottom edges.

The island is partitioned into a grid of square cells. You are given an m x n integer matrix heights where heights[r][c] represents the height above sea level of the cell at coordinate (r, c).

The island receives a lot of rain, and the rain water can flow to neighboring cells directly north, south, east, and west if the neighboring cell’s height is less than or equal to the current cell’s height. Water can flow from any cell adjacent to an ocean into the ocean.

Return a 2D list of grid coordinates result where result[i] = [ri, ci] denotes that rain water can flow from cell (ri, ci) to both the Pacific and Atlantic oceans.

Example 1:

Input: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]]
Output: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]
Solution
class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        m = len(heights)
        n = len(heights[0])

        po_map = [[0 for _ in range(n)] for _ in range(m)] 
        ao_map = [[0 for _ in range(n)] for _ in range(m)]

        po_map[0] = [1 for _ in range(n)]
        ao_map[-1] = [1 for _ in range(n)]
        
        for i in range(m):
            po_map[i][0] = 1 
            ao_map[i][-1] = 1 
        
        def valid(r, c): 
            if r < 0 or r >= m: 
                return False 
            if c < 0 or c >= n: 
                return False 
            return True 

        def reverse_flow(r,c, flow_map):
            # going reverse flow 
            # if lower --> stop 
            # if higher --> mark as 1, mark as visited 
            # directions [up, down, left, right]
            ds = [[-1,0], [1,0], [0,-1], [0,1]]

            for d in ds:
                r_, c_ = r + d[0], c + d[1]
                
                if not valid(r_, c_): 
                    continue 
                
                if flow_map[r_][c_] == 1:
                    continue

                if heights[r_][c_] >= heights[r][c]: 
                    flow_map[r_][c_] = 1 
                    flow_map = reverse_flow(r_, c_, flow_map)
            
            return flow_map 
        
        for i in range(n):
            po_map = reverse_flow(0, i, po_map)
            ao_map = reverse_flow(m-1,i, ao_map)
        
        for i in range(m):
            po_map = reverse_flow(i, 0, po_map)
            ao_map = reverse_flow(i, n-1, ao_map)
        
        # merge two map 
        res = []
        for r in range(m):
            for c in range(n):
                if po_map[r][c] == 1 and ao_map[r][c] == 1:
                    res.append([r,c])
        
        return res

200. Number of Islands

Given an m x n 2D binary grid grid which represents a map of ‘1’s (land) and ‘0’s (water), return the number of islands.

An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

Example 1:

Input: grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
Output: 1
Solution
  • Use visited to track the visited cells.
  • Use DFS to explore the grid. if the cell is not visited and the cell is “1” and not connected to other islands, then it is a new island.
    def numIslands(self, grid: List[List[str]]) -> int:
        m = len(grid)
        n = len(grid[0])

        visited = [[0 for _ in range(n)] for _ in range(m)]

        def valid(r, c): 
            if r < 0 or r >= m:
                return False
            if c < 0 or c >= n: 
                return False 
            return True 

        def dfs(r,c,ni): 

            # directions [up, down, left, right]
            ds = [[-1,0], [1,0], [0,-1], [0,1]]

            if grid[r][c] == "0":
                return 0 
            
            if visited[r][c] == 1:
                return 0 
            
            visited[r][c] = 1 
            ni += 1

            for d in ds: 
                r_, c_ = r + d[0], c + d[1]

                if not valid(r_, c_): 
                    continue 
                
                if grid[r_][c_] == "0":
                    continue 

                if visited[r_][c_] == 1:
                    continue 

                #  grid[r_][c_] == "1" and not visited 
                # print(r, c, r_, c_, ni, visited)
                ni = dfs(r_, c_, ni)

            return ni 
        
        num_islands = 0 

        for r in range(m): 
            for c in range(n): 

                num_islands += 1 if dfs(r,c,0) > 0 else 0 

        return num_islands 

212. Word Search II

Given an m x n board of characters board and a list of strings words, return all words on the board.

Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.

Example 1:

Input: board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
Output: ["eat","oath"]
Solution
class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:

        m, n = len(board), len(board[0])
        used = [[False for _ in range(n)] for _ in range(m)]

        def findWord(board, word):

            # backtrack
            def backtrack(row, col, i):                
                # boundary 
                if row < 0 or row > m - 1 or col < 0 or col > n - 1:
                    return False 

                if i == len(word) - 1:
                    # last letter 
                    return board[row][col] == word[i] if not used[row][col] else False

                if board[row][col] == word[i] and not used[row][col]:
                    used[row][col] = True
                    a = backtrack(row-1, col, i+1)
                    b = backtrack(row+1, col, i+1)
                    c = backtrack(row, col-1, i+1)
                    d = backtrack(row, col+1, i+1)
                    used[row][col] = False
                    return any([a, b, c, d])
                else:
                    return False 

            # search first word 
            is_found = [False]
            for row in range(m):
                for col in range(n):
                    if board[row][col] == word[0]:
                        is_found.append(backtrack(row, col, 0))

            return any(is_found)
            

        output = [word for word in words if findWord(board, word)] 

        return output

The backtracking solution is not efficient because it is not pruning the invalid paths. We can use Trie to improve the efficiency. The idea is to build a Trie from the words and then use the Trie to search the board.

Strategy to search the board:

  • Scan the board and for each cell, backtrack from the root of the Trie, i.e., backtrack(row, col, root)
  • If the current character is in node.children, we set the node to the current character, and search in four directions for the next character.
class TrieNode:
    def __init__(self):
        self.children = dict()
        self.word = None # to store the word if it is the last letter 

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for l in word:
            if l not in node.children:
                # add l as a node.children 
                node.children[l] = TrieNode()
            node = node.children[l]
        
        node.word = word # last letter 

    def search(self, word):
        node = self.root 
        for l in word: 
            if l not in node.children: 
                return False
            node = node.children[l]
        return True


class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        m, n = len(board), len(board[0])
        result = []

        trie = Trie()
        for word in words:
            trie.insert(word)
        
        def backtrack(row, col, node):
            # boundary 
            if row < 0 or col < 0 or row >= m or col >= n:
                return 
            
            # reach the last character 
            if node.word is not None:
                result.append(node.word)
                node.word = None # delete to avoid duplicate 

            
            if board[row][col] in node.children:
                # mark visited 
                c = board[row][col]
                board[row][col] = '#'

                backtrack(row-1, col, node.children[c])
                backtrack(row+1, col, node.children[c])
                backtrack(row, col-1, node.children[c])
                backtrack(row, col+1, node.children[c])

                board[row][col] = c 

        for row in range(m):
            for col in range(n):
                backtrack(row, col, trie.root)
        
        return result

73. Set Matrix Zeroes

Given an m x n integer matrix matrix, if an element is 0, set its entire row and column to 0’s.

You must do it in place.

Example 1:

Input: matrix = [[1,1,1],[1,0,1],[1,1,1]]
Output: [[1,0,1],[0,0,0],[1,0,1]]
Solution

Key idea is to use the first row and first column as the marker to indicate if the row or column should be set to 0. However, we need to be careful when the first row itself contains a 0.

  • To handle this case, we need a flag rowZERO to indicate if the first row itself contains a 0.
  • We iterate columns from right to left to avoid overwriting the marker.
class Solution:
    def setZeroes(self, matrix: List[List[int]]) -> None:
        """
        Do not return anything, modify matrix in-place instead.
        """
        ROWS = len(matrix)
        COLS = len(matrix[0])
        rowZERO = False 

        for r in range(ROWS):
            for c in range(COLS):
                if matrix[r][c] == 0:
                    if r > 0: 
                        matrix[0][c] = 0
                        matrix[r][0] = 0
                    else:
                        rowZERO = True 
                        matrix[0][c] = 0

        
        for r in range(1,ROWS): 
            for c in range(COLS-1,-1,-1):
                if matrix[0][c] == 0:
                    matrix[r][c] = 0
                if matrix[r][0] == 0:
                    matrix[r][c] = 0
        

        if rowZERO: 
            for c in range(COLS):
                matrix[0][c] = 0

54. Spiral Matrix

Given an m x n matrix, return all elements of the matrix in spiral order.

Example 1:

Input: matrix = [[1,2,3],[4,5,6],[7,8,9]]
Output: [1,2,3,6,9,8,7,4,5]
Solution
class Solution:
    def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
        def move_right(r, c):
            return r, c+1
        
        def move_left(r, c):
            return r, c-1
        
        def move_up(r, c):
            return r-1, c
        
        def move_down(r, c):
            return r+1, c
        
        def move(r, c, direction):
            if direction == 'move_right':
                return move_right(r, c)
            elif direction == 'move_down':
                return move_down(r, c)
            elif direction == 'move_up':
                return move_up(r, c)
            elif direction == 'move_left':
                return move_left(r, c)
            else: 
                raise ValueError
        
        def valid(r, c, left_col, right_col, top_row, bot_row):
            if r < top_row or r > bot_row:
                return False 
            if c < left_col or c > right_col: 
                return False 
            return True
        
        def change_direction(direction, left_col, right_col, top_row, bot_row):
            if direction == 'move_right':
                return 'move_down', left_col, right_col, top_row + 1, bot_row
            elif direction == 'move_down':
                return 'move_left', left_col, right_col - 1, top_row, bot_row
            elif direction == 'move_left':
                return 'move_up', left_col, right_col, top_row, bot_row - 1 
            elif direction == 'move_up':
                return 'move_right', left_col + 1, right_col, top_row, bot_row
            else:
                raise ValueError 
        
        
        def next_move(r, c, direction, left_col, right_col, top_row, bot_row):
            temp_r, temp_c = move(r, c, direction)
            if valid(temp_r, temp_c, left_col, right_col, top_row, bot_row):
                return direction, left_col, right_col, top_row, bot_row
            else:
                return change_direction(direction, left_col, right_col, top_row, bot_row)
                
        output = []
        direction = 'move_right'
        r, c = 0, 0 

        ROWS = len(matrix)
        COLS = len(matrix[0])
        left_col = 0 
        right_col = COLS - 1 
        top_row = 0 
        bot_row = ROWS - 1 

        while len(output) < ROWS * COLS:
            output.append(matrix[r][c])
            direction, left_col, right_col, top_row, bot_row = next_move(r, c, direction, left_col, right_col, top_row, bot_row)
            r, c = move(r, c, direction)
        
        return output 

392. Is Subsequence

Given two strings s and t, return true if s is a subsequence of t, or false otherwise.

A subsequence of a string is a new string that is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (i.e., "ace" is a subsequence of "abcde" while "aec" is not).

Solution
class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        def helper(s, t):
            if len(s) == 0:
                return True 
            if len(t) == 0:
                return False 

            if s[0] == t[0]:
                return helper(s[1:], t[1:])
            else:
                return helper(s, t[1:])
        return helper(s, t)

338. Counting Bits

Given an integer n, return an array ans of length n + 1 such that for each i (0 <= i <= n), ans[i] is the number of 1’s in the binary representation of i.

Solution
  • The largest number of 1’s can be at most log2(n) + 1.
  • When adding 0 to the end of the number, the value of the number is doubled, but the number of 1’s is the same. i.e., 1010 = 2 * 101. dp[i] = dp[i/2]. if i is even
  • When adding 1 to the end of the number, the number of 1’s is the number of 1’s in the previous number plus 1. i.e., 1011 = 2 * 101+ 1. dp[i] = dp[(i-1)/2] + 1. if i is odd
class Solution:
    def countBits(self, n: int) -> List[int]:
        dp = [0] * (n+1)
        dp[0] = 0 
        for i in range(1, n+1):
            if i % 2 == 0:
                dp[i] = dp[i//2]
            else:
                dp[i] = dp[(i-1)//2] + 1
        return dp 

746. Min Cost Climbing Stairs

You are given an integer array cost where cost[i] is the cost of the i-th step on a staircase. Once you pay the cost, you can either climb one or two steps.

You can either start from the step with index 0, or the step with index 1.

Return the minimum cost to reach the top of the floor.

Example 1:

Input: cost = [10,15,20]
Output: 15

It is because there only three steps [start, 10, 15, 20, top], and we can climb one or two steps at a time. Therefore, the best way is from start -> 15 -> top.

Solution

Observation:

  • [start] + cost + [top]: add two dummy nodes at the beginning and end of the array.
  • dp[top] = 0
  • dp[top-1] = cost[-1]
  • dp[top-2] = cost[-2]
  • dp[top-3] = min(dp[top-1] + cost[top-3], dp[top-2] + cost[top-2]). It is because we can either climb one or two steps at a time. So from the current step, we will climb to the one that is cheaper.
class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        n = len(cost)
        dp = [0] * (n+1)
        cost = cost + [0]
        dp[n] = cost[n] # 0 
        dp[n-1] = cost[n-1] # 
        for i in range(n-2,-1,-1):
            dp[i] = min(cost[i] + dp[i+1], cost[i] + dp[i+2])
            
        return min(dp[0], dp[1])

191. Number of 1 Bits

Write a function that takes an unsigned integer and returns the number of 1’s bits it has (also known as the Hamming weight).

Example 1:

Input: n = 11 (00000000000000000000000000001011)
Output: 3
Solution
class Solution:
    def hammingWeight(self, n: int) -> int:
        count = 0
        while n:
            if n % 2 == 1:
                count += 1
            n = n // 2
        return count

268. Missing Number

Given an array nums containing n distinct numbers in the range [0, n], return the only number in the range that is missing from the array.

Example 1:

Input: nums = [3,0,1]
Output: 2
Solution
class Solution:
    def missingNumber(self, nums: List[int]) -> int:
        return sum(range(len(nums)+1)) - sum(nums)

1143. Longest Common Subsequence

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”.

Example 1:

Input: text1 = "abcde", text2 = "ace" 
Output: 3 
Solution

Observation:

  • We need a 2D dp array to store the longest common subsequence. dp[]
  • if text1[i] == text2[j], then dp[i][j] = dp[i-1][j-1] + 1
  • if text1[i] != text2[j], then dp[i][j] = max(dp[i-1][j], dp[i][j-1])
class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        l1 = len(text1) # num columns
        l2 = len(text2) # num rows
        dp = [[0 for _ in range(l1+1)] for _ in range(l2+1)]
        
        for i2 in range(1, l2+1):
            for i1 in range(1, l1+1):
                i1m1 = i1 - 1
                i2m1 = i2 - 1
                if text1[i1m1] == text2[i2m1]:
                    dp[i2][i1] = dp[i2m1][i1m1] + 1
                else:
                    dp[i2][i1] = max(dp[i2m1][i1], dp[i2][i1m1])
        return dp[-1][-1] 

91. Decode Ways

You have intercepted a secret message encoded as a string of numbers. The message is encoded using the following mapping:

'1' -> 'A'
'2' -> 'B'
...
'26' -> 'Z'

However, while decoding the message, you realize that there are many different ways you can decode the message because some codes are contained in other codes (“2” and “5” vs “25”).

For example, the message “11106” can be decoded as “AAJF” with the grouping (1 1 10 6) or “KJF” with the grouping (11 10 6).

Note: there may be strings that are impossible to decode.

Given a string s containing only digits, return the number of ways to decode it. If the entire string cannot be decoded in any valid way, return 0.

Example 1:

Input: s = "12"
Output: 2

Example 2:

Input: s = "226"
Output: 3
Solution
class Solution:
    def numDecodings(self, s: str) -> int:
        dp = [0] * (len(s)+1)
        dp[0] = 1
        dp[1] = 1 if s[0] != '0' else 0
        
        for i in range(2, len(s)+1):
            if s[i-1] != '0':
                dp[i] = dp[i-1]
            if 10 <= int(s[i-2:i]) <= 26:
                dp[i] += dp[i-2]
        return dp[-1]

39. Combination Sum

Given a set of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sum to target.

The same number may be chosen from candidates an unlimited number of times.

Example 1:

Input: candidates = [2,3,6,7], target = 7
Output: [[2,2,3],[7]]
Solution

Observations:

  • Backtracking:
    • dp[7] = [2 + dp[5] if dp[5] is valid, 3 + dp[4] if dp[4] is valid, 6 + dp[1] if dp[1] is valid, 7 + dp[0] if dp[0] is valid]
    • Similarly, dp[5] = [2 + dp[3] if dp[3] is valid, 3 + dp[2] if dp[2] is valid, 6 + dp[-1] if dp[-1] is valid, 7 + dp[-2] if dp[-2] is valid]
  • All dp[i] with i < 0 are invalid.
  • We can build a tree to visualize the process. With the root being the current number t, and children being t - candidates[i]. valid children are the ones that are >= 0.
  • How to form the results?
    • We can use a list to store the current path, i.e., dp[0] = [], dp[1] = [], dp[2] = [[2]], dp[3] = [[3]], dp[4] = [[2, 2]] = [dp[2] + [2]], dp[5] = [[2, 3]] = [dp[3] + [2]], dp[6] = [dp[3] + [3], dp[4] + [2]], etc.
class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        dp = [[]] * (target + 1)
        dp[0] = [] # format of each dp[i] 

        for t in range(1, target+1):
            for c in candidates:
                print(t, c, dp)
                if t - c < 0: 
                    continue
                elif t - c == 0:
                    # add [c] to the current list 
                    if dp[t] == []:
                        dp[t] = [[c]]
                    else:
                        dp[t].append([c])
                else:
                    if dp[t-c] == []:
                        # empty []
                        continue
                    else:
                        # add c to the current list
                        # e.g., dp[2] before [], after [[2]]
                        # e.g., dp[4] before [], after adding dp[2], [[2, 2]]
                        # e.g., dp[6] before [], after adding dp[3]=[[3]] is [[3, 3]], after adding dp[4]=[[2,2]] is [[3,3], [2,2,2]]
                        assert len(dp[t-c]) >= 1, dp[t-c]
                        assert len(dp[t-c][0]) >= 1, dp[t-c]
                        assert type(dp[t-c][0]) is list, dp[t-c]
                        assert type(dp[t-c][0][0]) is int, dp[t-c]

                        new = [l + [c] for l in dp[t-c]]
                        if dp[t] == []:
                            dp[t] = new
                        else:
                            dp[t].extend(new)
                        assert type(dp[t]) is list, dp[t]
                        assert type(dp[t][0]) is list, dp[t]
                        assert type(dp[t][0][0]) is int, dp[t]
        
        return dp[target]

The above solution does not work because it does not consider the duplicate solutions like [2,2,3] and [2,3,2]. To address this issue, we need to sort the candidates and only add a number to the current list if it is greater than the last number in the current list.

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        candidates.sort()  # Sorting helps maintain order
        dp = [[] for _ in range(target + 1)]
        dp[0] = [[]]  # Base case: one way to sum to 0 (empty set)

        for c in candidates:  # Iterate candidates first
            for t in range(c, target + 1):  # Build up DP array
                for comb in dp[t - c]:
                    if not comb or c >= comb[-1]:  # Ensure non-decreasing order
                        dp[t].append(comb + [c])  # Append new valid combination

        return dp[target]

The backtracking solution is as follows:

def combinationSum(candidates, target):
    result = []
    
    def backtrack(start, path, remaining):
        # Base cases
        if remaining == 0:  # Found a valid combination
            result.append(list(path))  # Create a new list to avoid reference issues
            return
        if remaining < 0:  # Invalid path, sum exceeded target
            return

        # Try each candidate from the current starting position
        for i in range(start, len(candidates)):
            # Make choice
            path.append(candidates[i])  
            
            # Explore further with this choice
            # Keep start as i to allow reusing the same number
            backtrack(i, path, remaining - candidates[i])
            
            # Undo choice (backtrack) to try next candidate
            path.pop()

    backtrack(0, [], target)
    return result

Let’s visualize how this works with candidates = [2,3,6,7] and target = 7:

                                   7
                    /      |        |        \
                 2(5)     3(4)     6(1)      7(0)✓
                /  |  \    |  \      |
            2(3) 3(2) 6(-) 3(1) 6(-) 6(-) 
           /  |    |        |
        2(1) 3(0)✓ 6(-)    3(-) 
        /
     2(-) 

Numbers in parentheses show remaining value after each choice
✓ indicates a valid combination found
- indicates an invalid path (remaining < 0)

Key points about this solution:

  1. The start parameter ensures we don’t use numbers before the current position, preventing duplicates like [2,3] and [3,2]

  2. We can reuse numbers by passing i instead of i+1 in the recursive call

  3. The path list tracks our current combination being built

  4. The remaining parameter helps us track how much more we need to sum to reach target

This approach has several advantages:

  • Memory efficient as it only stores valid combinations
  • Naturally avoids duplicates by maintaining order
  • Can handle unlimited reuse of numbers

90. Subsets II

Given a collection of integers that might contain duplicates, return all possible subsets (the power set).

Example 1:

Input: [1,2,2]
Output:
[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
[]
]
Solution

Observations:

  • The subsets can be found by removing one element at a time from the original list and find the subsets of the remaining list.
  • It is a backtracking problem.
  • We can use a set to store the subsets to avoid duplicates. For example, [1,2] and [2,1] are the same.
class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        results = []
        nums.sort()

        def backtrack(start, path):
            results.append(path[:])

            for i in range(start, len(nums)):
                if i > start and nums[i] == nums[i-1]:
                    continue 
                
                path.append(nums[i])
                backtrack(i + 1, path)
                path.pop()
        
        backtrack(0, [])

        return results

Very important point:

  • path is a mutable object (list) that gets modified throughout the recursion
  • path[:] creates a new copy of the list at that point in time
  • When you use results.append(path), all entries in results point to the same list object
  • When you backtrack and modify path, it changes all previous references to that list

Therefore:

  • results.append(path) - all sublists in results will end up being the same (empty after all backtracking is done)
  • results.append(path[:]) - each sublist is an independent copy representing the state of path at that moment

This is why path[:] (or path.copy() or list(path)) is necessary for backtracking problems where you need to store the intermediate states of your path.

46. Permutations

Given a collection of distinct integers, return all possible permutations.

Example 1:

Input: [1,2,3]
Output: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
Solution

Observations:

  • A permutation can be found by adding one number at a time until the length of the permutation is equal to the length of the original list.
  • We need used to track the numbers that have been used.
  • The greedy approach is O(N^N) because we need to try all possible combinations.
  • The backtracking approach is O(N!)
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
    results = []
    nums.sort()
    used = [False] * len(nums)

    def backtrack(path):
        if len(path) == len(nums):
            results.append(list(path))
            return
        
        for i in range(len(nums)):
            if used[i]:
                continue 

            if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
                continue 

            used[i] = True
            path.append(nums[i])
            backtrack(path)
            path.pop()
            used[i] = False
    
    backtrack([])
    return results

257. Binary Tree Paths

Given a binary tree, return all root-to-leaf paths.

Example 1:

Input:

   1
 /   \
2     3
 \
  5

Output: ["1->2->5", "1->3"]
Solution

Observations:

  • A path can be found by adding one node at a time until the node is a leaf node.
  • Need to check if the node is a leaf node.
class Solution:
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        result = []

        def dfs(node, path):
            if not node:
                return 

            path.append(node.val)
            if node.left is None and node.right is None:
                # leaf node
                result.append("->".join(map(str, path)))

            else:
                if node.left is not None:
                    dfs(node.left, path)
                if node.right is not None:
                    dfs(node.right, path)
            path.pop()

        dfs(root, [])

        return result

51. N-Queens

Reference from Geeksforgeeks: https://www.geeksforgeeks.org/n-queen-problem-backtracking-3/

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.

Example 1:

Input: n = 4
Output: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
Solution

Observations:

  • A queen can be placed in a row, column, or diagonal.
  • We need to check if the queen is in the same row, column, or diagonal as any other queen.
    • Same row: row[i] == row[j]
    • Same column: col[i] == col[j]
    • left diagonal: row[i] + col[i] == row[j] + col[j]
    • right diagonal: row[i] - col[i] == row[j] - col[j]
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        results = []

        CELL_EMPTY = '.'
        CELL_QUEEN = 'Q'
        CELL_NA = 'NA'

        board = [[CELL_EMPTY for _ in range(n)] for _ in range(n)]

        def convert_array_to_string_board(board):
            # remove the NA cells
            return ["".join(row).replace(CELL_NA, CELL_EMPTY) for row in board]
        
        def fill_board_with_a_queen(board, row, col):
            # Given a board and a position (row, col), fill the board with a queen - row, col, left diagonal, right diagonal
            assert len(board) == n
            assert len(board[0]) == n

            board[row][col] = CELL_QUEEN
            for i in range(n):
                board[i][col] = CELL_NA
                board[row][i] = CELL_NA
                row_plus_i = min(row + i, n - 1)
                row_minus_i = max(row - i, 0)
                col_plus_i = min(col + i, n - 1)
                col_minus_i = max(col - i, 0)
                # left diagonal, [row_minus_i, col_plus_i] and [row_plus_i, col_minus_i]
                board[row_minus_i][col_plus_i] = CELL_NA
                board[row_plus_i][col_minus_i] = CELL_NA
                # right diagonal, [row_minus_i, col_minus_i] and [row_plus_i, col_plus_i]
                board[row_minus_i][col_minus_i] = CELL_NA
                board[row_plus_i][col_plus_i] = CELL_NA
            
            return board

        def remove_queen_from_board(board, row, col):
            board[row][col] = CELL_EMPTY
            
            for i in range(n):
                board[i][col] = CELL_EMPTY
                board[row][i] = CELL_EMPTY
                row_plus_i = min(row + i, n - 1)
                row_minus_i = max(row - i, 0)
                col_plus_i = min(col + i, n - 1)
                col_minus_i = max(col - i, 0)
                # left diagonal, [row_minus_i, col_plus_i] and [row_plus_i, col_minus_i]
                board[row_minus_i][col_plus_i] = CELL_EMPTY
                board[row_plus_i][col_minus_i] = CELL_EMPTY
                # right diagonal, [row_minus_i, col_minus_i] and [row_plus_i, col_plus_i]
                board[row_minus_i][col_minus_i] = CELL_EMPTY
                board[row_plus_i][col_plus_i] = CELL_EMPTY

            return board
        
        def is_valid_position(board, row, col):
            # check if the position is valid
            return board[row][col] == CELL_EMPTY
        
        def backtrack(board, prev_rows, prev_cols):
            if len(prev_rows) == n:
                results.append(convert_array_to_string_board(board))
                return
            
            for col in range(n):
                if col in prev_cols:
                    continue

                for row in range(n):
                    if row in prev_rows:
                        continue

                    if not is_valid_position(board, row, col):
                        continue

                    prev_rows.append(row)
                    prev_cols.append(col)
                    fill_board_with_a_queen(board, row, col)
                    backtrack(board, prev_rows, prev_cols)
                    remove_queen_from_board(board, row, col)
                    prev_rows.pop()
                    prev_cols.pop()
                
        backtrack(board, [], [])
        return results

Turn out the above solution is not correct. The board has been modified in the process of backtracking.

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        results = []
        board = [["."] * n for _ in range(n)]

        cols = set()
        pos_diagonals = set()
        neg_diagonals = set()

        def backtrack(row):
            if row == n:
                results.append(["".join(row) for row in board])
                return
            
            for col in range(n):
                if col in cols or (row + col) in pos_diagonals or (row - col) in neg_diagonals:
                    continue

                cols.add(col)
                pos_diagonals.add(row + col)
                neg_diagonals.add(row - col)
                board[row][col] = 'Q'

                backtrack(row + 1)

                # remove the queen
                cols.remove(col)
                pos_diagonals.remove(row + col)
                neg_diagonals.remove(row - col)
                board[row][col] = '.'
        
        backtrack(0)
        return results        

17. Letter Combinations of a Phone Number

Given a string containing digits from 2-9 inclusive, return all possible letter combinations that the number could represent. Return the answer in any order.

A mapping of digits to letters (just like on the telephone buttons) is given below. Note that 1 does not map to any letters.

Example 1:

Input: digits = "23"
Output: ["ad","ae","af","bd","be","bf","cd","ce","cf"]
Solution
class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        if len(digits) == 0:
            return []
            
        map_letter = {
            '2': ['a', 'b', 'c'],
            '3': ['d', 'e', 'f'],
            '4': ['g', 'h', 'i'], 
            '5': ['j', 'k', 'l'],
            '6': ['m', 'n', 'o'], 
            '7': ['p', 'q', 'r', 's'],
            '8': ['t', 'u', 'v'], 
            '9': ['w', 'x', 'y', 'z']
        }
        results = []

        def backtrack(start, path):
            if len(path) == len(digits):
                results.append("".join(path))
                return 
            
            for i in range(start, len(digits)):
                d = digits[i]
                for j in range(len(map_letter[d])):
                    path.append(map_letter[d][j])
                    backtrack(i+1, path)
                    path.pop()
        
        backtrack(0, [])
        return results                

77. Combinations

Given two integers n and k, return all possible combinations of k numbers out of the range [1, n].

You may return the answer in any order.

Example 1:

Input: n = 4, k = 2
Output: [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
Solution
class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        results = []
        used = [False] * (n+1)

        def backtrack(start, path):
            if len(path) == k:
                results.append(list(path))
                return 
            
            for i in range(start, n+1):
                if used[i]:
                    continue 
                
                used[i] = True
                path.append(i)
                backtrack(i, path)
                path.pop()
                used[i] = False
        
        backtrack(1, [])
        return results

22. Generate Parentheses

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

Example 1:

Input: n = 3
Output: ["((()))","(()())","(())()","()(())","()()()"]
Solution

Observations:

  • The number of left parentheses must be equal to the number of right parentheses.
  • We can count the number of open and close parentheses. If close > open means invalid path
class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        results = []
        data = ['('] * n + [')'] * n 
        used = [False] * 2 * n

        def count_open(path):
            count = 0 
            for s in path:
                if s == '(':
                    count += 1 
            return count 
        
        def count_close(path):
            count = 0 
            for s in path:
                if s == ')':
                    count += 1
            return count 

        def backtrack(path):
            if len(path) == 2*n and count_open(path) == count_close(path):
                t = "".join(path)
                if t not in results:
                    results.append(t)
                return 
            
            if count_open(path) < count_close(path):
                # invalid 
                return 
            
            for i in range(2*n):
                if used[i]:
                    continue 
                
                used[i] = True
                path.append(data[i])
                backtrack(path)
                path.pop()
                used[i] = False
        
        backtrack([])
        return results

The above solution is not optimal.

  • Checking validity using count_open and count_close is not efficient. We can use two counters to track the number of open and close parentheses.
class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        results = []
        
        def backtrack(path, openP, closeP):
            if openP == closeP == n:
                results.append("".join(path))
                return
            
            if openP < n:
                path.append("(")
                backtrack(path, openP+1, closeP)
                path.pop()

            if closeP < openP:
                path.append(")")
                backtrack(path, openP, closeP+1)
                path.pop()
        
        backtrack([], 0, 0)
        return results 

Given a m x n grid of characters board and a string word, return true if word exists in the grid.

The word can be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once.

Example 1:

Input: board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
Output: true
Solution

Observations:

  • We can use backtracking to solve this problem.
  • We need to scan the board to find the first character of the word.
    • If found, we can mark the cell as visited and
class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        m, n = len(board), len(board[0])

        def backtrack(row, col, idx):
            if idx == len(word):
                return True  # Found the word
            
            if row < 0 or row >= m or col < 0 or col >= n:  # Out of bounds
                return False
            
            if board[row][col] != word[idx]:  # Mismatch
                return False
            
            temp = board[row][col]  # Mark as visited
            board[row][col] = "#"  

            # Explore all 4 directions
            found = (backtrack(row + 1, col, idx + 1) or 
                     backtrack(row - 1, col, idx + 1) or 
                     backtrack(row, col + 1, idx + 1) or 
                     backtrack(row, col - 1, idx + 1))
            
            board[row][col] = temp  # Restore the cell
            return found
        
        # Start backtracking from any matching cell
        for i in range(m):
            for j in range(n):
                if board[i][j] == word[0] and backtrack(i, j, 0):
                    return True

        return False

120. Triangle

Given a triangle array, return the minimum path sum from top to bottom.

For each step, you may move to an adjacent number of the row below. More formally, if you are on index i on the current row, you may move to either index i or index i + 1 on the next row.

Example 1:

Input: triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
Output: 11
Solution

Backtrack solution:

  • Return case: when the row is the last row.
  • Recursive case: for each [row][col], we can move to [row+1][col] or [row+1][col+1].
class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        n = len(triangle)
        min_path_sum = float("inf")

        def backtrack(row, col, path_sum):
            nonlocal min_path_sum
            
            path_sum += triangle[row][col]

            if row == n - 1:
                min_path_sum = min(min_path_sum, path_sum)
                return 
        
            backtrack(row+1, col, path_sum)
            backtrack(row+1, col+1, path_sum)
        
        backtrack(0,0,0)
        return min_path_sum

However, the backtrack solution is not efficient because it explores all possible paths, including paths that have been computed. Complexity is O(2^n).

Dynamic programming solution:

  • Use a 2D array to store the minimum path sum for each cell.
  • The value of each cell is the sum of the value of the cell and the minimum of the values of the two cells directly below it.
class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        n = len(triangle)
        dp = [[float("inf") for _ in range(n)] for _ in range(n)]

        dp[0][0] = triangle[0][0]

        for row in range(1, n):
            for col in range(len(triangle[row])):
                if col == 0:
                    dp[row][col] = dp[row-1][col] + triangle[row][col]
                else:
                    dp[row][col] = min(dp[row-1][col], dp[row-1][col-1]) + triangle[row][col]
        
        return min(dp[-1])

64. Minimum Path Sum

Given a m x n grid filled with non-negative numbers, find a path from top left to bottom right, which minimizes the sum of all numbers along its path.

Example 1:

Input: grid = [[1,3,1],[1,5,1],[4,2,1]]
Output: 7
Solution
class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        m = len(grid)
        n = len(grid[0])
        dp = [[float("inf") for _ in range(n)] for _ in range(m)]

        for row in range(m):
            for col in range(n):
                if row == 0 and col == 0:
                    dp[row][col] = grid[0][0]
                elif row == 0:
                    dp[row][col] = dp[row][col-1] + grid[row][col]
                elif col == 0:
                    dp[row][col] = dp[row-1][col] + grid[row][col]
                else:
                    dp[row][col] = min(dp[row][col-1], dp[row-1][col]) + grid[row][col]
        
        return dp[-1][-1]

63. Unique Paths II

You are given an m x n integer array obstacleGrid. An obstacle and space are marked as 1 or 0 respectively in the grid.

A path that the robot takes cannot include any square that is an obstacle.

Return the number of unique paths that the robot can take to reach the bottom-right corner.

Example 1:

Input: obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
Output: 2
Solution
class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        dp = [[0 for _ in range(n)] for _ in range(m)]

        for row in range(m):
            for col in range(n):
                if row == 0 and col == 0:
                    dp[row][col] = 1 if obstacleGrid[row][col] == 0 else 0
                elif row == 0:
                    dp[row][col] = dp[row][col-1] if obstacleGrid[row][col-1] == 0 else 0 # else 0
                elif col == 0:
                    dp[row][col] = dp[row-1][col] if  obstacleGrid[row-1][col] == 0 else 0 # else 0
                else:
                    dp[row][col] = dp[row][col-1] * (1 - obstacleGrid[row][col-1]) + dp[row-1][col] * (1 - obstacleGrid[row-1][col])
        
        return dp[-1][-1] if obstacleGrid[-1][-1] == 0 else 0 

5. Longest Palindromic Substring

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

Example 1:

Input: s = "babad"
Output: "bab"
Solution

Observations:

  • We can use DP 2D to solve this problem.
  • dp[i][i] = True because a single character is a palindrome.
  • dp[i][j] = True if dp[i+1][j-1] = True and s[i] == s[j]
  • max_length = max(max_length, j-i+1)
  • start = i that has the max_length. We need max_length and start to return the result.
class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        dp = [[False for _ in range(n)] for _ in range(n)]

        start = 0 
        max_length = 0

        if len(s) == 1:
            return s

        for i in range(n):
            dp[i][i] = True 
            start = i 
            max_length = 1

        for length in range(2, n+1):
            for l in range(0, n - length + 1):
                r = l + length - 1
                if s[l] == s[r]:
                    if length == 2 or dp[l+1][r-1]:
                        dp[l][r] = True 
                        if length > max_length:
                            max_length = length 
                            start = l 
        
        return s[start:start+max_length]

Reflection

Backtracking

Backtracking is a recursive algorithm used for solving problems that require exploring all possible solutions and pruning incorrect paths. It is particularly useful for combinatorial problems such as Combination Sum, or finding all possible path from root to leaf in a tree.

  • Technique: Recursive exploration with pruning.
  • Complexity: O(2ⁿ) for subsets, O(N!) for permutations

If a problem involves:

  • Generating all possible solutions → Use backtracking
  • Making choices with constraints → Use backtracking
  • Exploring all paths in a grid, tree, or graph → Use backtracking

Key Concept:

  • Make a choice
  • Recur with that choice
  • Undo the choice (backtrack)
  • In some cases, we need to sort the input array first.
  • In some cases, we need to use a used array to track the used elements, e.g., 77. Combinations
class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        result = []
        nums.sort()

        def backtracking(start, path):
            result.append(list(path))

            for i in range(start, len(nums)):
                if i > start and nums[i] == nums[i-1]:  
                    continue 
                
                path.append(nums[i])
                backtracking(i+1, path)
                path.pop()
        
        backtracking(0, [])
        return result
def FIND_SOLUTIONS(parameters):
    if (valid_solution):
        store_the_solution()
        return

    for choice in all_choices:
        if valid_choice(choice):
            APPLY(choice)
            FIND_SOLUTIONS(parameters)
            BACKTRACK(remove_choice)

Two-pointer

In-place operations

In-place operations modify the array without using extra space for another data structure. This is useful for optimizing space complexity. However, in-place operations require careful handling to avoid overwriting important data.

Key Advantages of In-Place Operations:

  • Saves Space → Avoids creating extra arrays, reducing memory usage from O(n) to O(1).
  • Efficient in Practice → Less memory access and allocation make the program faster.
  • Useful for Large Datasets → Ideal for problems with constraints on space.

Combine with Two-pointer:

left, right = 0, len(nums) - 1

while left < right:
    # do something
    s[left], s[right] = s[right], s[left]
    # update left and right
    left += 1
    right -= 1