Coding Interview - Design

Here is some coding interview to practice design

  • Easy: 30 minutes.

  • Medium: 1 hours.

  • Hard: 2 hours.

1. Decrypt Message

Step 1: Clarify Requirements

The messages consist of lowercase Latin letters only, and every word is encrypted separately as follows:

  • Convert every letter to its ASCII value.

  • Modify the ASCII values by adding 1 to the first letter, and for each subsequent letter, add the encrypted ASCII value of the previous letter to its original ASCII value.

  • Subtract 26 from every letter until it is in the range of lowercase letters a-z in ASCII. Convert the values back to letters.

Example: “crime” -> “dnotq”

Step 2: Design Algorithm

  • Notes:

Encrypt idea:

c: 99 -> d: 100

r: 114 + 100 = 224 (- 26 - 26 - 26 - 26) = 110 (n)

i: 105 + 110 = 215 - 26 * 4 = 111 (o)

m: 109 + 111 - 26 * 4 = 116 (t)

e: 101 + 116 - 26 * 4 = 113 (q)

[a - z] = [97 - 122]

Decrypt idea:

d: 100 - 1 = 99 (c)

n: 110 - 100 + 26 * 4 = 114 (r)

o: 111 - 110 + 26 * 4 = 105 (i)

t: 116 - 111 + 26 * 4 = 109 (m)

q: 113 - 116 + 26 * 4 = 101 (e)

Step 3: Design Algorithm

Idea

ascii_value = ord('a')
print(ascii_value)

ascii_number = chr(97)
print(ascii_number)
def decrypt(word: str) -> str:
    res = ""
    prev_char = 1
    for char in word:
        ascii_int = ord(char)
        gap = ascii_int - prev_char

        while gap < 97 or gap > 122:
            if gap < 97:
                gap += 26
            elif gap > 122:
                gap -= 26
            else:
                break
        res += chr(gap)
        prev_char = ord(char)

    return res

# debug your code below
print(decrypt("dnotq"))

2. Most Common Words

2.1. Clarify Requirements

  • Count the frequence of the sentence:
text = 'It was the best of times, it was the worst of times.'

2.2. Example


[
    ('it', 2),
    ('of', 2),
    ('the', 2),
    ('times', 2),
    ('was', 2),
    ('best', 1),
    ('worst', 1)
]
  • Need to sort by alphabet.

  • Need to lower case a string.

2.3. Implement

  • Syntax
from collections import Counter

sentence = sentence.lower()
words = sentence.split()
counter = Counter(words)

# Using method items()
list(counter.items())

# Sort key
sorted_items = sorted(counter.items(), key=lambda x: x[0])
  • Sorted receive: (Item, and Lambda function to sort)

  • Priority sort by frequence first, alphabet later: key = lambda x: (-x[1], x[0])

from typing import List, Tuple
from collections import Counter
import string

def most_common_words(text: str) -> List[Tuple[str, int]]:
    cleanedText = text.translate(str.maketrans('', '', string.punctuation)).lower()
    words = cleanedText.split()

    counter = Counter(words)
    items = counter.items()

    sortItems = sorted(items, key = lambda x: (-x[1], x[0]))
    return list(sortItems)

# debug your code below
print(most_common_words("It was the best of times, it was the worst of times."))

3. Valid Palindrome

3.1. Clarify requirements

  • A man, a plan, a canal, Panama -> True

  • Only count for alphabet

3.2. Idea:

  • Two Pointer legendary question

3.3 Implement:

class Solution:
    def isPalindrome(self, s: str) -> bool:
        left, right = 0, len(s) - 1

        while left < right:
            # Move left to next alphanumeric
            # Reach the left
            while left < right and not s[left].isalnum():
                left += 1
            # Move right to previous alphanumeric
            # Reach the right
            while left < right and not s[right].isalnum():
                right -= 1
            # Compare characters
            if s[left].lower() != s[right].lower():
                return False

            # Need to increase left and right here -> Above is only find the character
            left += 1
            right -= 1

        return True

# Core function
def is_palindrome(s: str) -> bool:
    left, right = 0, len(s) - 1
    while left < right:
        # Core function
        if s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1
    return True

# debug your code below
print(is_palindrome('abcba'))
  • Implementation
def is_palindrome(s: str) -> bool:
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1

        # Core function
        if s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1

    return True

# debug your code below
print(is_palindrome('abcba'))

4. Validate IP Address

4.1. Requirements

  • Clear

4.2. Example

  • Learning convert string sang int.

  • Check number is leading with “0”.

  • Try catch principle.

4.3. Implement

  • Split the queryIP

  • Check list contain 4 items -> IPV4 -> Validate a list of item of IPV4

  • Check list contain 8 items -> IPV6 -> Validate a list of item of IPV6

class Solution:
    def validIPAddress(self, queryIP: str) -> str:
        # Split the queryIP
        # Check list contain 4 items -> IPV4 -> Validate a list of item of IPV4
        # Chekc list contain 8 items -> IPV6 -> Validate a list of item of IPV6

        ipV4List = queryIP.split('.')
        if len(ipV4List) == 4:
            for item in ipV4List:
                try:
                    value = int(item)
                except ValueError:
                    return "Neither"

                if item == "0":
                    continue

                if value < 0 or value > 255 or item.startswith("0"):
                    return "Neither"
            return "IPv4"

        ipV6List = queryIP.split(':')
        if len(ipV6List) == 8:
            for item in ipV6List:
                if len(item) == 0 or len(item) > 4:
                    return "Neither"
                try:
                    value = int(item, 16)
                except ValueError:
                    return "Neither"
                if value < 0 or value > 0xFFFF:
                    return "Neither"
            return "IPv6"

        return "Neither"

5. Sudoku

5.1. Clarify requirements

  • Solving Sudoku

5.2. Idea & Example

  • Solution 1: Go to the empty block -> Try [1 -> 9] -> Check row, col, in square 3 x 3.

  • After that -> BFS first the choice and continue to fill all the column.

5.3. Implement

  • Write backtracking condition first.

  • Write condition check later.

  • Backtracking need to return -> Think backtracking as an asynchonus job, others job below in call in recurstion step.

  • Using backtrack return True -> prevent when find the solution (find điểm dừng) => luôn có điểm dừng cho đệ quy.

import math

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        row, col = len(board), len(board[0])

        def isValid(row_index, col_index, val):
            # Duyệt dọc -> chạy row, Check in col_index
            for i in range(row):
                if board[i][col_index] == val:
                    return False

            # Duyện ngang -> chạy col, Check in row_index
            for j in range(col):
                if board[row_index][j] == val:
                    return False

            # Check in math.sqrt(row) * math.sqrt(col)
            box_start_row = 3 * (row_index // 3)
            box_start_col = 3 * (col_index // 3)
            for i in range(3):
                for j in range(3):
                    if board[box_start_row + i][box_start_col + j] == val:
                        return False

            return True

        def backtrack():
            for row_index in range(row):
                for col_index in range(col):
                    # Find "." and try [1 -> 9]
                    if board[row_index][col_index] == ".":
                        for val in range(1, 10):
                            # Precheck before assign
                            if isValid(row_index, col_index, str(val)):
                                board[row_index][col_index] = str(val)
                                # Asynchronus, find to the end of the table
                                if backtrack():
                                    return True
                                board[row_index][col_index] = "."
                        return False
            return True

        backtrack()

6. N-Queen

6.1. Requirement:

  • Clear requirements

6.2. Example:

  • Try to put N-queens into a table.

6.3. Algorithm:

  • Put 1 queen first in row 0 first. Put another queens later.

  • Step 1: Find ‘.’

  • Step 2: Check valid

  • Step 3: Backtrack

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

        def isValid(row_index, col_index):
            # Check col
            for i in range(n):
                if board[i][col_index] == 'Q':
                    return False

            # Check upper-left diagonal (Only check upper row)
            i, j = row_index - 1, col_index - 1
            while i >= 0 and j >= 0:
                if board[i][j] == 'Q':
                    return False
                i -= 1
                j -= 1

            # Check upper-right diagonal
            i, j = row_index - 1, col_index + 1
            while i >= 0 and j < n:
                if board[i][j] == 'Q':
                    return False
                i -= 1
                j += 1
            return True

        def backtrack(row_index):
            if row_index == n:
                res.append(["".join(r) for r in board])
                return

            for col_index in range(n):
                # Step 1: Find '.'
                # Step 2: Check valid
                # Step 3: Backtrack
                if board[row_index][col_index] == '.':
                    if isValid(row_index, col_index):
                        board[row_index][col_index] = 'Q'
                        backtrack(row_index + 1)
                        board[row_index][col_index] = '.'

        backtrack(0)
        return res

7. Minimum Knight Move (BFS)

7.1. Requirements

  • Find minimum step for knight go from (0,0) -> (x,y)

7.2. Example:

  • Using BFS to find knight move

7.3. Implement

  • Step 1: Visit start, add queue + update visited

  • Step 2: Visit neighbor, add queue + update visited

from collections import deque

class Solution:
    def minStepToReachTarget(self, knightPos, targetPos, n):
        start = (knightPos[0], knightPos[1])
        target = (targetPos[0], targetPos[1])

        if start == target:
            return 0

        # Init
        visited = [[False for _ in range(n)] for _ in range(n)]
        queue = deque()

        # Visit start
        queue.append((knightPos[0], knightPos[1], 0))
        visited[knightPos[0]][knightPos[1]] = True

        # Directions
        directions = [
            (-2, -1), (-1, -2), (1, -2), (2, -1),
            (2, 1), (1, 2), (-1, 2), (-2, 1)
        ]

        def isValid(nx, ny):
            return 0 <= nx < n and 0 <= ny < n and not visited[nx][ny]


        while queue:
            x, y, steps = queue.popleft()

            # Visit neighbors
            for dx, dy in directions:
                nx, ny = x + dx, y + dy

                if isValid(nx, ny):
                    if (nx, ny) == target:
                        return steps + 1
                    # Visit neighbor
                    queue.append(nx, ny, steps + 1)
                    visited[nx][ny] = True

        return -1

8. Island (DFS)

8.1. Requirements

  • Count number of components in matrix

8.2. Example

8.3. Implement

  • Step 1: Find ‘1’ and DFS from it.

  • Step 2: Count number of components

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        row, col = len(grid), len(grid[0])
        visited = [[False for _ in range(col)] for _ in range(row)]
        countNumIslands = 0

        def isValid(row_index, col_index):
            return 0 <= row_index < row and 0 <= col_index < col and not visited[row_index][col_index]


        def dfs(row_index, col_index):
            if not isValid(row_index, col_index) or grid[row_index][col_index] == "0":
                return

            # Visited it
            grid[row_index][col_index] = "0"
            visited[row_index][col_index] = True

            # DFS without backtrack
            dfs(row_index + 1, col_index)
            dfs(row_index - 1, col_index)
            dfs(row_index, col_index + 1)
            dfs(row_index, col_index - 1)


        for i in range(row):
            for j in range(col):
                if grid[i][j] == '1':
                    if isValid(i, j):
                        dfs(i, j)
                        countNumIslands += 1

        return countNumIslands

9. Surrounded Regions (DFS Border)

class Solution:
    def solve(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        row, col = len(board), len(board[0])
        visited = [[False for _ in range(col)] for _ in range(row)]

        def isValid(row_index, col_index):
            return 0 <= row_index < row and 0 <= col_index < col and not visited[row_index][col_index]

        def dfs(row_index, col_index):
            if not isValid(row_index, col_index) or board[row_index][col_index] != "O":
                return

            # Visited it
            board[row_index][col_index] = "#"
            visited[row_index][col_index] = True

            # DFS without backtrack
            dfs(row_index + 1, col_index)
            dfs(row_index - 1, col_index)
            dfs(row_index, col_index + 1)
            dfs(row_index, col_index - 1)

        # DFS in borders
        for i in range(row):
            dfs(i, 0)
            dfs(i, col - 1)
        for j in range(col):
            dfs(0, j)
            dfs(row - 1, j)

        for i in range(row):
            for j in range(col):
                if board[i][j] == 'O':
                    board[i][j] = 'X'
                elif board[i][j] == '#':
                    board[i][j] = 'O'

10. Max CPU Load (Min Heap for End Time)

10.1. Requirements

Find the max CPU load the job overlap in the same time.

10.2. Example:

Input: jobs[] = [{1, 4, 3}, {2, 5, 4}, {7, 9, 6}]
Output: 7

10.3. Implement

  • Step 1: Init min heap

  • Step 2: When come to start -> add (end, load) to queue, update max CPU load => use heap to keep the min end.

  • Step 3: When come to end -> remove all tasks from the queue.

Notes: Heap is sort in real-time.

import heapq

def find_max_cpu_load(jobs):
    sorted_jobs = sorted(jobs, key=lambda x:x[0]) # Sort by start time
    min_heap = [] # store (end, load)

    current_cpu_load = 0
    max_cpu_load = 0

    for job in sorted_jobs:
        start, end, load = job
m
        while min_heap and min_heap[0][0] <= start:
            ended_job = heapq.heappop(min_heap)
            current_cpu_load -= ended_job[1]

        # Push CPU load
        heapq.heappush(min_heap, (end, load))
        current_cpu_load += load

        # Max CPU load
        max_cpu_load = max(current_cpu_load, max_cpu_load)

    return max_cpu_load


jobs = [(1, 4, 3), (2, 5, 4), (7, 9, 6)]
print(find_max_cpu_load(jobs))

11. Task Scheduler (Max Heap)

11.1. Requirements

  • Schedule jobs for CPU with each some must be wait for n time with same category.

11.2. Example

max_heap = [(-5, ‘A’), (-2, ‘B’), (-2, ‘C’)] n = 2

Step 1:

- Slot 1:

  Pop -5 → task A, run it

  New count = -4, push to temp

  cycle += 1

- Slot 2:
  Pop -2 → task B, run it

  New count = -1, push to temp

  cycle += 1

- Slot 3:
  Pop -2 → task C, run it

Current: max_heap = [-4, -1, -1]

11.3. Implement

  • Using max heap to prior complete the long task first.
from collections import Counter
import heapq

class Solution:
    def leastInterval(self, tasks, n):
        tasksCount = Counter(tasks)
        maxHeap = [-count for count in tasksCount.values()]
        heapq.heapify(maxHeap)
        process_time = 0

        while maxHeap:
            coolDownTasks = []
            steps = 0
            # Each cycle
            for _ in range(n + 1):
                if maxHeap:
                    max_workload_jobs = heapq.heappop(maxHeap)
                    if max_workload_jobs + 1 < 0:
                        coolDownTasks.append(max_workload_jobs + 1)
                    steps += 1
                elif coolDownTasks:
                    # idle jobs
                    steps += 1

            for remaining_task in coolDownTasks:
                heapq.heappush(maxHeap, remaining_task)

            process_time += steps

        return process_time

12. Customer - Employee Multitasks

Idea:

  • Step 1: If customer come before time -> add to queue.

  • Step 2: Process all employee by priority > arrival > execute > id.

  • Step 3: If all employee to busy need to update time

import heapq

def serve_customers(customers, num_employees):
    # Step 1: Sort by arrival
    customers.sort(key=lambda c: c['arrival'])

    time = 0
    i = 0
    n = len(customers)
    waiting_customers = []  # (priority, arrival, execute, id)
    busy_employees = []     # (available_time, employee_id)
    result = []
    last_finish_time = 0  # Track the time when last customer finishes

    # All employees available at t = 0
    for eid in range(num_employees):
        heapq.heappush(busy_employees, (0, eid))

    while i < n or waiting_customers:
        # Step 2: Enqueue all customers who have arrived
        while i < n and customers[i]['arrival'] <= time:
            c = customers[i]
            heapq.heappush(waiting_customers, (c['priority'], c['arrival'], c['execute'], c['id']))
            i += 1

        # Step 3: Assign available employees
        while waiting_customers and busy_employees and busy_employees[0][0] <= time:
            _, eid = heapq.heappop(busy_employees)
            priority, arrival, cid, execute = heapq.heappop(waiting_customers)
            result.append(cid)
            end_time = time + execute
            last_finish_time = max(last_finish_time, end_time)
            heapq.heappush(busy_employees, (end_time, eid))  # employee busy until this time

        # Step 4: Advance time
        if waiting_customers and (not busy_employees or busy_employees[0][0] > time):
            time = min(busy_employees[0][0], customers[i]['arrival'] if i < n else float('inf'))
        elif not waiting_customers and i < n:
            time = customers[i]['arrival']
        elif not waiting_customers and not busy_employees:
            break
        else:
            time += 1

    return result, last_finish_time

customers = [
    {"id": 1, "arrival": 0, "execute": 3, "priority": 2},
    {"id": 2, "arrival": 1, "execute": 2, "priority": 1},
    {"id": 3, "arrival": 1, "execute": 1, "priority": 1},
    {"id": 4, "arrival": 2, "execute": 4, "priority": 3}
]

result, total_time = serve_customers(customers, num_employees=2)
print("Order:", result)         # e.g., [3,1,2,4]
print("Total time:", total_time)  # e.g., 7

13. Single-Threaded CPU

import heapq
from typing import List

class Solution:
    def getOrder(self, tasks: List[List[int]]) -> List[int]:
        # Step 1: Add original index and sort tasks by start_time, then execute_time
        tasks = [(task[0], task[1], i) for i, task in enumerate(tasks)]  # (start_time, process_time, index)
        tasks.sort(key=lambda x: (x[0], x[1]))  # Sort by start_time, then process_time

        waiting_tasks = []
        res = []
        i = 0
        n = len(tasks)
        time = 0

        while i < n or waiting_tasks:
            # Push all tasks available at current time into the heap
            while i < n and tasks[i][0] <= time:
                heapq.heappush(waiting_tasks, (tasks[i][1], tasks[i][2]))  # (process_time, index)
                i += 1

            if waiting_tasks:
                process_time, index = heapq.heappop(waiting_tasks)
                time += process_time
                res.append(index)
            else:
                time = tasks[i][0]

        return res

14. Meeting Room 2 (Allocate Meeting or Jobs != Priority Order)

from typing import List
import heapq

# Definition of Interval
class Interval(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end

class Solution:
    def minMeetingRooms(self, intervals: List[Interval]) -> int:
        if not intervals:
            return 0

        intervals.sort(key=lambda x: x.start)

        min_heap = []

        for meeting in intervals:
            if min_heap and min_heap[0] <= meeting.start:
                heapq.heappop(min_heap)

            heapq.heappush(min_heap, meeting.end)

        return len(min_heap)


15. Capacity to Ship Packages Within D Days (Greedy)

15.1. Requirements

  • Maximum of the ship capacity -> load all the pakage in D days.

15.2. Idea & Example (Idea High-level is important, important freely)

  • Ship capacity: [max(arr), sum(arr)]

  • With the capacity -> How to check the valid

  • Idea: Start increasing packages to the ship -> whether is <= days.

15.3. Idea

  • Idea 1: Note, in case the load > capacity, set prev_sum = 0 and continue to load
class Solution:
    def shipWithinDays(self, weights: List[int], days: int) -> int:
        left, right = max(weights), sum(weights)
        for capacity in range(left, right + 1):
            prev_sum = 0
            count_day = 1
            for i in range(len(weights)):
                if prev_sum + weights[i] > capacity:
                    count_day += 1
                    prev_sum = 0
                prev_sum += weights[i]

            if count_day <= days:
                return capacity

        return 0
  • Idea 2
class Solution:
    def shipWithinDays(self, weights: List[int], days: int) -> int:
        left, right = max(weights), sum(weights)

        def canShip(capacity):
            prev_sum = 0
            count_day = 1
            for i in range(len(weights)):
                if prev_sum + weights[i] > capacity:
                    count_day += 1
                    prev_sum = 0
                prev_sum += weights[i]

            return count_day <= days

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

        return left

16. Maximum Profit in Job Scheduling (Knapstack, choose only 1)

Idea:

  • dp[i]: The maximum profit considering the first i jobs.

  • Two choices:

    • Skip the current job → take dp[i - 1]

    • Take the current job → profit becomes dp[idx] + p

from bisect import bisect_right

class Solution:
    def jobScheduling(self, startTime, endTime, profit):
        # Combine all job info
        jobs = sorted(zip(startTime, endTime, profit), key=lambda x: x[1])  # sort by endTime
        n = len(jobs)

        # dp[i]: max profit until taking to first i jobs
        dp = [0] * (n + 1)
        end_times = [job[1] for job in jobs]

        for i in range(1, n + 1):
            s, e, p = jobs[i - 1]

            # Find the last job that ends before this one starts
            idx = bisect_right(end_times, s, lo=0, hi=i-1)  # binary search
            dp[i] = max(dp[i - 1], dp[idx] + p)

        return dp[n]

17. Maximum Earnings From Taxi (Knapstack)

Each ride is an item:

- "Weight" = time interval [start, end]

- "Value" = profit = end - start + tip

You either:

- Take the ride (if it doesn't overlap with previous ones), or

- Skip it
  • Step 1: Duyệt từ (1, n + 1) -> d[i]
  • Step 2: Compare max(dp[i - 1], d[latest_start - 1] + current_profix)
from bisect import bisect_right

class Solution:
    def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:
        rides.sort(key=lambda x: x[1])
        m = len(rides)

        # Extract end times for binary search
        i
        dp = [0] * (m + 1)

        for i in range(1, m + 1):
            start, end, tip = rides[i - 1]
            earnings = end - start + tip

            # Find the last ride that ends <= current ride's start
            idx = bisect_right(end_times, start, lo=0, hi=i - 1)
            # Two choices: take current ride or skip
            dp[i] = max(dp[i - 1], dp[idx] + earnings)

        return dp[m]

18. Coin Change 2 (Unbounding Knapstack)

from typing import List

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0] * (amount + 1)
        dp[0] = 1  # There's 1 way to make amount 0 (pick nothing)

        for coin in coins:
            for i in range(coin, amount + 1):
                dp[i] += dp[i - coin]

        return dp[amount]

19. Work Break

from typing import List

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        word_set = set(wordDict)
        n = len(s)
        dp = [False] * (n + 1)
        dp[0] = True  # Empty string is always valid

        for i in range(1, n + 1):
            for j in range(i):
                if dp[j] and s[j:i] in word_set:
                    dp[i] = True
                    break

        return dp[n]

20. Combination Sum IV (Count permutation -> Auto use Unknapstack Problem)

from typing import List

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0] * (target + 1)
        dp[0] = 1  # There's one way to make 0 — choose nothing

        for total in range(1, target + 1):
            for num in nums:
                if total >= num:
                    dp[total] += dp[total - num]

        return dp[target]

21. Knapstack Problem

weights = [1, 3, 4, 5] values = [1, 4, 5, 7] W = 7 n = 4

def knapsack_01(weights, values, W):
    n = len(weights)
    # dp[i][w]: max value using first i items with capacity w
    dp = [[0] * (W + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        wt = weights[i - 1]
        val = values[i - 1]
        for w in range(W + 1):
            if wt > w:
                dp[i][w] = dp[i - 1][w]  # can't include
            else:
                dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - wt] + val)

    return dp[n][W]

22. Target Sum

  • You select it in 2 subset P(Positive) or N(Negative) -> sum(P)−sum(N)=target

=> sum(P) = (target + totalSum) / 2

from typing import List

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total = sum(nums)
        if (target + total) % 2 != 0 or total < abs(target):
            return 0

        subset_sum = (target + total) // 2

        # Way to get subset_sum
        dp = [0] * (subset_sum + 1)
        dp[0] = 1  # There's 1 way to reach sum 0 — pick nothing

        for num in nums:
            for s in range(subset_sum, num - 1, -1):  # 0/1 Knapsack => go backward
                dp[s] += dp[s - num]

        return dp[subset_sum]

  • Knapstack 0/1:
dp[i][w] = max(
    dp[i-1][w],                        # Don't take current item
    dp[i-1][w - weights[i]] + values[i]  # Take current item
)
for i in range(n):
    for w in range(W, weights[i] - 1, -1):  # Go backward
        dp[w] = max(dp[w], dp[w - weights[i]] + values[i])

  • Example
def canPartition(nums):
    total = sum(nums)
    if total % 2 != 0:
        return False
    target = total // 2

    dp = [False] * (target + 1)
    dp[0] = True

    for num in nums:
        for s in range(target, num - 1, -1):  # Go backward
            dp[s] = dp[s] or dp[s - num]

    return dp[target]

  • Unbounded Knapsack
dp[w] = max(
    dp[w],
    dp[w - weight[i]] + value[i]  # take it again
)
for i in range(n):
    for w in range(weight[i], W + 1):  # Go forward
        dp[w] = max(dp[w], dp[w - weight[i]] + value[i])
  • Example
def change(amount, coins):
    dp = [0] * (amount + 1)
    dp[0] = 1

    for coin in coins:
        for a in range(coin, amount + 1):  # Go forward
            dp[a] += dp[a - coin]

    return dp[amount]

  • Multi-Dimensional Knapsack
dp[w][v] = max(
    dp[w][v],
    dp[w - weight[i]][v - volume[i]] + value[i]
)
for i in range(n):
    for w in range(W, weight[i] - 1, -1):
        for v in range(V, volume[i] - 1, -1):
            dp[w][v] = max(dp[w][v], dp[w - weight[i]][v - volume[i]] + value[i])
  • Example
def findMaxForm(strs, m, n):
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for s in strs:
        zero = s.count('0')
        one = s.count('1')

        for i in range(m, zero - 1, -1):
            for j in range(n, one - 1, -1):
                dp[i][j] = max(dp[i][j], dp[i - zero][j - one] + 1)

    return dp[m][n]

24. Unbounded Knapstack Problem

Input:

  • weights = [2, 3, 4]
  • values = [5, 7, 8]
  • W = 10

Output: 25

def unbounded_knapsack(W, weights, values):
    n = len(weights)
    dp = [0] * (W + 1)

    for w in range(W + 1):
        for i in range(n):
            if weights[i] <= w:
                dp[w] = max(dp[w], dp[w - weights[i]] + values[i])

    return dp[W]

Notes: Cứ profit + subset + coin change -> Auto dynamic programming.


25. Valid Parenthesis

class Solution:
    def isValid(self, s: str) -> bool:
        mapping = {'(': ')', '{': '}', '[': ']'}
        stack = []

        for char in s:
            if char in mapping:
                stack.append(char)
            else:
                if not stack:
                    return False

                top = stack.pop()
                if char != mapping[top]:
                    return False

        return not stack

26. Evaluate Reverse Polish Notation

  • Note: b trước, a sau
class Solution:
    def evalRPN(self, tokens: List[str]) -> int:
        stack = []
        for token in tokens:
            if token in {"+", "-", "*", "/"}:
                # b first, a later
                b = stack.pop()
                a = stack.pop()
                if token == "+":
                    stack.append(a + b)
                elif token == "-":
                    stack.append(a - b)
                elif token == "*":
                    stack.append(a * b)
                else:
                    stack.append(int(a / b))
            else:
                stack.append(int(token))

        return stack.pop() if stack else 0

27. Basic Calculator II

Magic here: ch = ‘’: previous sign = ‘+’ → push 2 → stack = [3, 2], update sign = ‘

class Solution:
    def calculate(self, s: str) -> int:
        stack = []
        num = 0
        sign = '+'  # Initially assume the number is positive
        s = s.replace(" ", "")  # Remove spaces for easier parsing

        for i, ch in enumerate(s):
            if ch.isdigit():
                num = num * 10 + int(ch)
            if not ch.isdigit() or i == len(s) - 1:
                if sign == '+':
                    stack.append(num)
                elif sign == '-':
                    stack.append(-num)
                elif sign == '*':
                    stack.append(stack.pop() * num)
                elif sign == '/':
                    stack.append(int(stack.pop() / num))  # truncate toward zero
                # ch = '*': previous sign = '+' → push 2 → stack = [3, 2], update sign = '*'
                sign = ch
                num = 0

        return sum(stack)

28. Basic Calculator

  • Magic:

    • We think recursively for () in evaluate expression: num = helper(chars) # Evaluate inside parentheses
class Solution:
    def calculate(self, s: str) -> int:
        def helper(chars: list) -> int:
            stack = []
            num = 0
            sign = '+'  # Start with '+' as default operator

            while chars:
                ch = chars.pop(0)

                if ch.isdigit():
                    num = num * 10 + int(ch)

                if ch == '(':
                    num = helper(chars)  # Evaluate inside parentheses

                # If current char is an operator or end of expression
                if (not ch.isdigit() and ch != ' ') or not chars:
                    if sign == '+':
                        stack.append(num)
                    elif sign == '-':
                        stack.append(-num)
                    elif sign == '*':
                        stack.append(stack.pop() * num)
                    elif sign == '/':
                        prev = stack.pop()
                        # Python's int division truncates toward zero
                        stack.append(int(prev / num))
                    sign = ch
                    num = 0

                if ch == ')':
                    break

            return sum(stack)

        return helper(list(s))

29. Next Greater Element

  • Idea: Using stack to reverse data structure
class Solution:
    def nextGreaterElements(self, nums: List[int]) -> List[int]:
        nums2 = nums + nums
        n = len(nums2)

        # Init a stack
        stack = []

        # Result array
        res = [0] * n

        # Loop from the end of the array
        # Use stack for reserved order
        for i in range(n - 1, -1, -1):
            #  Remove all smaller elements in stack
            while stack and stack[-1] <= nums2[i]:
                stack.pop()

            # Update value
            if stack:
                res[i] = stack[-1]
            else:
                res[i] = -1

            stack.append(nums2[i])

        return res[:len(nums)]

28. Generate Phone Numbers

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        digitMapping = {
            "2": "abc",
            "3": "def",
            "4": "ghi",
            "5": "jkl",
            "6": "mno",
            "7": "pqrs",
            "8": "tuv",
            "9": "wxyz"
        }

        res = []

        # Calculate in the backtrack function
        def backtrack(index, path):
            if index == len(digits):
                res.append(path[:])
                return

            keyboardChar = digitMapping[digits[index]]
            for char in keyboardChar:
                # Move the logic to backtrack function too
                backtrack(index + 1, path + char)

        backtrack(0, "")
        return res if digits != "" else []

29. Subset

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []
        n = len(nums)

        def backtrack(index, path):
            if index == n:
                res.append(path[:])
                return

            backtrack(index + 1, path + [nums[index]])
            backtrack(index + 1, path)

        backtrack(0, [])
        return res

30. Combination Sum

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        res = []
        n = len(candidates)
        def backtrack(index, path, remainingSum):
            if remainingSum == 0:
                res.append(path[:])
                return

            if index == n or remainingSum < 0:
                return

            path.append(candidates[index])
            backtrack(index, path, remainingSum - candidates[index])
            path.pop()
            backtrack(index + 1, path, remainingSum)

        backtrack(0, [], target)
        return res

Last Updated On June 26, 2025