NeetCode_150_GPT
NeetCode_150_GPT
To create a single template for solving array and hashing problems, we can focus on the
following common steps:
1. Input Handling: Define how to read input, whether it’s from function arguments or
standard input.
3. Logic Implementation: Implement the specific logic needed to solve the problem, which
often involves iterating through the array and updating the data structures accordingly.
Template
python
def template_problem_solver(input_data):
# Step 1: Input handling
# For example, assume input_data is a list
data = input_data
Problem Implementations
1. Contains Duplicate
def contains_duplicate(nums):
seen = set()
for num in nums:
if num in seen:
return True
seen.add(num)
return False
2. Valid Anagram
python
3. Two Sum
python
4. Group Anagrams
def group_anagrams(strs):
anagrams = {}
for s in strs:
key = ''.join(sorted(s))
if key in anagrams:
anagrams[key].append(s)
else:
anagrams[key] = [s]
return list(anagrams.values())
python
python
def encode(strs):
encoded = ""
for s in strs:
encoded += str(len(s)) + "#" + s
return encoded
def decode(s):
decoded = []
i = 0
while i < len(s):
j = s.find('#', i)
length = int(s[i:j])
decoded.append(s[j+1:j+1+length])
python
def product_except_self(nums):
n = len(nums)
output = [1] * n
left_product = 1
for i in range(n):
output[i] = left_product
left_product *= nums[i]
right_product = 1
for i in range(n-1, -1, -1):
output[i] *= right_product
right_product *= nums[i]
return output
8. Valid Sudoku
python
def is_valid_sudoku(board):
rows, cols, boxes = {}, {}, {}
for i in range(9):
for j in range(9):
num = board[i][j]
if num != '.':
if (i, num) in rows or (num, j) in cols or (i // 3, j // 3, num) in
boxes:
return False
rows[(i, num)] = True
cols[(num, j)] = True
boxes[(i // 3, j // 3, num)] = True
return True
def longest_consecutive(nums):
num_set = set(nums)
longest = 0
for num in num_set:
if num - 1 not in num_set: # Check for the start of a sequence
current_num = num
current_streak = 1
while current_num + 1 in num_set:
current_num += 1
current_streak += 1
longest = max(longest, current_streak)
return longest
Summary
This template provides a structure that can be modified for different problems, while the
individual implementations demonstrate how to apply hashing and array manipulation
techniques to solve each specific problem effectively. You can easily adapt this approach to
other similar problems by focusing on the steps outlined in the template.
Certainly! Let's create a more structured template that highlights the key concepts and
strategies for solving problems related to arrays and hashing. After defining the template,
we'll apply it to solve each of the listed problems step by step.
Template Explanation
1. Function Definition: Define a function that takes the required inputs.
2. Data Structure Initialization: Identify the data structures needed, such as hashmaps,
sets, or lists, depending on the problem.
3. Iterate Through Input: Use loops to process the input data, updating the data
structures as necessary.
4. Logic Implementation: Implement the core logic of the solution, often using conditions,
calculations, or modifications of the data structures.
5. Result Compilation: Collect the results in an appropriate format (e.g., list, dictionary) to
return.
1. Contains Duplicate
python
def contains_duplicate(nums):
# Step 2: Data Structure Initialization
seen = set()
# Example Usage
print(contains_duplicate([1, 2, 3, 4])) # Output: False
print(contains_duplicate([1, 2, 3, 1])) # Output: True
2. Valid Anagram
python
# Example Usage
print(is_anagram("anagram", "nagaram")) # Output: True
print(is_anagram("rat", "car")) # Output: False
3. Two Sum
python
# Example Usage
print(two_sum([2, 7, 11, 15], 9)) # Output: [0, 1]
4. Group Anagrams
python
def group_anagrams(strs):
# Step 2: Data Structure Initialization
anagrams = {}
# Example Usage
print(group_anagrams(["eat", "tea", "tan", "ate", "nat", "bat"])) # Output:
[['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]
python
# Example Usage
print(top_k_frequent([1, 1, 1, 2, 2, 3], 2)) # Output: [1, 2]
python
def encode(strs):
# Step 2: Data Structure Initialization
encoded = ""
def decode(s):
# Step 2: Data Structure Initialization
decoded = []
i = 0
# Example Usage
encoded = encode(["hello", "world"])
print(encoded) # Output: "5#hello5#world"
print(decode(encoded)) # Output: ["hello", "world"]
python
def product_except_self(nums):
# Step 2: Data Structure Initialization
n = len(nums)
output = [1] * n
# Example Usage
print(product_except_self([1, 2, 3, 4])) # Output: [24, 12, 8, 6]
8. Valid Sudoku
python
def is_valid_sudoku(board):
# Step 2: Data Structure Initialization
rows, cols, boxes = {}, {}, {}
# Example Usage
board = [
["5", "3", ".", ".", "7", ".", ".", ".", "."],
["6", ".", ".", "1", "9", "5", ".", ".", "."],
[".", "9", "8", ".", ".", ".", ".", "6", "."],
python
def longest_consecutive(nums):
# Step 2: Data Structure Initialization
num_set = set(nums) # Use a set for O(1) lookups
longest = 0
return longest
# Example Usage
print(longest_consecutive([100, 4, 200, 1, 3, 2])) # Output: 4 (1, 2, 3, 4)
Summary
This approach gives you a unified template for solving problems related to arrays and
hashing in Python. Each specific implementation adapts the template while applying unique
Explanation
The Two Pointers technique is a widely used approach in algorithm design, particularly
useful for solving problems involving sorted arrays or lists. This technique involves using two
pointers to traverse the data structure from different ends, which helps in optimizing space
and time complexity.
Key Advantages:
Efficiency: Many problems that can be solved using this technique can be done in linear
time O(n) instead of quadratic time O(n2 ), which is typical with nested loops.
Simplicity: It often simplifies the logic and reduces the need for additional data
structures.
When dealing with sorted arrays (or when you can sort them).
When the problem involves finding pairs or triplets that satisfy a specific condition (e.g.,
sums).
When you need to compare elements from both ends of an array or list.
When the problem requires finding the maximum or minimum values based on
conditions.
Template
1. Initialization: Set two pointers, usually one at the beginning ( left ) and one at the end
( right ) of the data structure.
2. Iterate with Conditions: Use a loop to traverse the array while the two pointers do not
cross each other. Implement the core logic of the problem using conditions that
compare the values at the two pointers.
3. Adjust Pointers: Depending on the logic, move either the left pointer, the right
pointer, or both.
1. Valid Palindrome
python
def is_palindrome(s):
# Step 1: Initialization
left, right = 0, len(s) - 1
python
# Example Usage
print(two_sum_sorted([2, 7, 11, 15], 9)) # Output: [1, 2]
3. 3Sum
python
def three_sum(nums):
# Step 1: Initialization
nums.sort() # Sort the input for two-pointer technique
results = []
# Example Usage
print(three_sum([-1, 0, 1, 2, -1, -4])) # Output: [[-1, -1, 2], [-1, 0, 1]]
python
def max_area(height):
# Step 1: Initialization
left, right = 0, len(height) - 1
max_area = 0
# Example Usage
print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7])) # Output: 49
python
def trap(height):
# Step 1: Initialization
left, right = 0, len(height) - 1
left_max, right_max = height[left], height[right]
water_trapped = 0
# Example Usage
print(trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1])) # Output: 6
Summary
Explanation
The Sliding Window technique is a highly efficient approach used for solving problems
related to subarrays or substrings. It involves maintaining a window that can expand or
contract based on certain conditions to track elements within a defined range. This
technique is particularly useful for optimizing problems that would otherwise require nested
loops, reducing the time complexity from O(n2 ) to O(n).
Key Advantages:
Efficiency: By using a single pass through the data structure, it minimizes the need for
multiple iterations.
Flexibility: It can be adapted to various problems, such as finding the maximum sum of
a subarray or determining the longest substring with certain properties.
Template
1. Initialization: Define pointers to represent the start ( start ) and end ( end ) of the
window and any additional variables needed for calculations (e.g., count, max sum).
2. Expand the Window: Increment the end pointer to expand the window and include
more elements.
4. Update Results: During the expansion or contraction of the window, update any
required results (e.g., maximum length, minimum size).
5. Return Statement: After processing all elements, return the final result.
Identifying minimum window substrings that contain all characters from another string.
python
def max_profit(prices):
# Step 1: Initialization
min_price = float('inf')
max_profit = 0
# Example Usage
print(max_profit([7, 1, 5, 3, 6, 4])) # Output: 5
python
def length_of_longest_substring(s):
# Step 1: Initialization
char_index_map = {}
start = 0
max_length = 0
# Example Usage
print(length_of_longest_substring("abcabcbb")) # Output: 3 ("abc")
python
# Example Usage
print(character_replacement("AABABBA", 1)) # Output: 4 ("AABA" or "ABBA")
4. Permutation In String
python
# Step 1: Initialization
s1_count = Counter(s1)
s2_count = Counter()
# Example Usage
print(check_inclusion("ab", "eidbaooo")) # Output: True
python
# Step 1: Initialization
t_count = Counter(t)
current_count = {}
start = 0
min_length = float('inf')
min_start = 0
formed = 0
required = len(t_count)
current_count[s[start]] -= 1
if s[start] in t_count and current_count[s[start]] < t_count[s[start]]:
formed -= 1
# Example Usage
print(min_window("ADOBECODEBANC", "ABC")) # Output: "BANC"
python
# Remove indices whose values are less than the current number
while deq and nums[deq[-1]] < nums[i]:
deq.pop()
deq.append(i)
# Example Usage
print(max_sliding_window([1, 3, -1, -3, 5, 3, 6, 7], 3)) # Output: [3, 3, 5, 5, 6,
7]
Stack Template
Explanation
The Stack data structure is a linear data structure that follows the Last In First Out (LIFO)
principle. It is particularly useful for problems that require backtracking or maintaining an
order of operations. Stacks can be implemented using lists or linked lists and provide two
primary operations: push (to add an item) and pop (to remove the most recently added
item).
Key Advantages:
Backtracking: Stacks are great for solving problems where you need to revert to a
previous state, such as evaluating expressions or generating combinations.
Order Management: They help maintain the order of operations and can be used to
check for valid sequences (like parentheses).
Memory Efficiency: They can often solve problems in-place without requiring additional
space.
Template
1. Initialization: Create an empty stack to hold elements as needed.
Use pop to remove elements from the stack based on specific conditions (like
matching parentheses).
4. Final Result: Return the desired output after processing all elements.
1. Valid Parentheses
python
def is_valid(s):
# Step 1: Initialization
stack = []
parentheses_map = {')': '(', '}': '{', ']': '['}
# Step 2: Processing
for char in s:
if char in parentheses_map.values(): # Opening parentheses
stack.append(char)
elif char in parentheses_map.keys(): # Closing parentheses
if not stack or stack.pop() != parentheses_map[char]:
return False
# Example Usage
print(is_valid("()")) # Output: True
print(is_valid("()[]{}")) # Output: True
print(is_valid("(]")) # Output: False
2. Min Stack
python
class MinStack:
def __init__(self):
# Step 1: Initialization
self.stack = []
self.min_stack = []
# Example Usage
min_stack = MinStack()
python
def eval_rpn(tokens):
# Step 1: Initialization
stack = []
operators = {"+", "-", "*", "/"}
# Step 2: Processing
for token in tokens:
if token in operators:
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(a - b)
elif token == '*':
stack.append(a * b)
elif token == '/':
stack.append(int(a / b)) # Use int() to mimic floor division
else:
stack.append(int(token))
# Example Usage
print(eval_rpn(["2", "1", "+", "3", "*"])) # Output: 9
print(eval_rpn(["4", "13", "5", "/", "+"])) # Output: 6
4. Generate Parentheses
python
# Step 1: Initialization
result = []
backtrack("", 0, 0)
# Example Usage
print(generate_parentheses(3)) # Output: ["((()))", "(()())", "(())()", "()(())", "
()()()"]
5. Daily Temperatures
python
def daily_temperatures(temperatures):
# Step 1: Initialization
stack = []
result = [0] * len(temperatures)
# Step 2: Processing
for i, temp in enumerate(temperatures):
while stack and temperatures[stack[-1]] < temp:
index = stack.pop()
result[index] = i - index
stack.append(i)
6. Car Fleet
python
# Step 2: Processing
for pos, spd in cars:
time_to_target = (target - pos) / spd
if not stack or time_to_target > stack[-1]:
stack.append(time_to_target)
# Example Usage
print(car_fleet(12, [10, 8, 0, 5, 3], [2, 4, 1, 1, 3])) # Output: 3
python
def largest_rectangle_area(heights):
# Step 1: Initialization
stack = []
max_area = 0
heights.append(0) # Sentinel to pop all remaining bars
# Step 2: Processing
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
# Example Usage
print(largest_rectangle_area([2, 1, 5, 6, 2, 3])) # Output: 10
Summary
The Stack technique is essential for efficiently solving a variety of problems that require
maintaining order or backtracking through processes. By following the stack template, we
can tackle problems such as "Valid Parentheses," "Min Stack," "Evaluate Reverse Polish
Notation," "Generate Parentheses," "Daily Temperatures," "Car Fleet," and "Largest Rectangle
In Histogram" in a structured and efficient manner.
Explanation
Key Advantages:
Efficiency: Reduces the search space exponentially by eliminating half of the remaining
elements at each step.
Applicability: Useful for finding elements in sorted arrays, locating boundaries, and
optimizing complex conditions (like "Koko Eating Bananas").
3. Adjust Boundaries:
If the middle satisfies the condition but requires further optimization, adjust left
or right .
If it doesn’t satisfy, eliminate half of the search space by moving left or right
accordingly.
Optimizing boundary-based conditions (like minimizing the speed at which Koko eats
bananas).
1. Binary Search
python
# Example Usage
print(binary_search([1, 2, 3, 4, 5, 6], 4)) # Output: 3
2. Search a 2D Matrix
python
# Example Usage
print(search_matrix([[1, 3, 5], [7, 10, 12], [14, 16, 20]], 10)) # Output: True
python
def can_finish(speed):
hours = 0
for pile in piles:
hours += (pile + speed - 1) // speed
# Example Usage
print(min_eating_speed([3, 6, 7, 11], 8)) # Output: 4
python
def find_min(nums):
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[right]: # Minimum is to the right
left = mid + 1
else:
right = mid
return nums[left]
# Example Usage
print(find_min([3, 4, 5, 1, 2])) # Output: 1
python
# Example Usage
print(search([4,5,6,7,0,1,2], 0)) # Output: 4
python
class TimeMap:
def __init__(self):
self.store = defaultdict(list)
# Example Usage
time_map = TimeMap()
time_map.set("foo", "bar", 1)
python
# Example Usage
print(find_median_sorted_arrays([1, 3], [2])) # Output: 2.0
print(find_median_sorted_arrays([1, 2], [3, 4])) # Output: 2.5
Summary
The Binary Search technique is essential for optimizing searches and finding target values or
boundaries efficiently in sorted data. By following the binary search template, we can
address a range of problems, such as "Binary Search," "Search a 2D Matrix," "Koko Eating
Explanation
The Linked List is a fundamental data structure where elements (nodes) are connected in a
sequence, with each node containing a reference to the next node in the list. Linked lists are
particularly useful when the operations involve frequent insertions and deletions, especially
at the start or end of the list. In many problems, linked lists offer an efficient solution due to
their dynamic size and structure, which eliminates the need to shift elements (as in arrays).
2. Two-Pointer Approach: Often used to find elements in the list from the end (like in
"Remove Nth Node from End").
3. Reverse Traversal: Useful for reversing the list or segments of the list.
4. Cycle Detection (Fast and Slow Pointers): Used to detect cycles in the list.
5. Recursion: Commonly used for problems involving reversing lists, merging sorted lists,
and adding numbers (similar to recursion on trees).
Merging: Combining sorted lists efficiently (as in "Merge Two Sorted Lists" and "Merge K
Sorted Lists").
Position-Based Removal/Insertion: For example, removing the nth node from the end,
which is easier with the two-pointer technique.
Cycle Detection: Detecting cycles in lists with the fast-slow pointer technique.
Use two pointers if locating nodes relative to the end of the list.
3. Special Operations:
Detecting cycles.
python
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverse_list(head):
prev, current = None, head
# Example Usage
head = ListNode(1, ListNode(2, ListNode(3, ListNode(4))))
reversed_head = reverse_list(head)
python
tail.next = l1 or l2
return dummy.next
# Example Usage
l1 = ListNode(1, ListNode(3, ListNode(5)))
l2 = ListNode(2, ListNode(4, ListNode(6)))
merged_head = merge_two_lists(l1, l2)
3. Reorder List
python
def reorder_list(head):
if not head:
return
# Example Usage
head = ListNode(1, ListNode(2, ListNode(3, ListNode(4))))
reorder_list(head)
python
for _ in range(n):
fast = fast.next
while fast:
slow, fast = slow.next, fast.next
slow.next = slow.next.next
return dummy.next
# Example Usage
python
class Node:
def __init__(self, val=0, next=None, random=None):
self.val = val
self.next = next
self.random = random
def copy_random_list(head):
if not head:
return None
python
def has_cycle(head):
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
# Example Usage
head = ListNode(1, ListNode(2, ListNode(3, ListNode(4))))
head.next.next.next.next = head.next # Creates a cycle
print(has_cycle(head)) # Output: True
7. LRU Cache
python
class LRUCache:
def __init__(self, capacity: int):
self.cache = OrderedDict()
self.capacity = capacity
# Example Usage
lru = LRUCache(2)
lru.put(1, 1)
lru.put(2, 2)
print(lru.get(1)) # Output: 1
lru.put(3, 3)
print(lru.get(2)) # Output: -1
Summary
The Linked List Template provides a structured way to handle problems that involve pointer
manipulation, traversals, and node reordering. By applying specific techniques like dummy
nodes, two-pointers, and fast-slow pointers, we can efficiently solve various linked list
problems. Problems like "Reverse Linked List," "Merge Two Sorted Lists," "Remove Nth Node
From End of List," "Linked List Cycle," and "LRU Cache" benefit greatly from this approach,
each demonstrating the template’s flexibility in handling a wide range of linked list-related
challenges.
2. Depth-First Search (DFS): DFS uses recursion or a stack to visit nodes down to the leaves
before backtracking. Ideal for calculating depth, checking tree properties, or locating
specific nodes.
5. Binary Search Properties: In a Binary Search Tree (BST), nodes on the left subtree are
smaller, and nodes on the right subtree are larger, enabling efficient search, min/max,
and LCA problems.
Property Checking: Problems that check for a property across the tree (e.g., balanced
tree, same tree).
BST Operations: Problems on Binary Search Trees where nodes have specific ordering
(e.g., finding kth smallest element).
Tree Template
1. Base Condition: Start with checking if the node is None .
4. Final Output: The result of the aggregated values or the specific result derived from
traversing the tree.
python
def invert_tree(root):
if not root:
return None
root.left, root.right = invert_tree(root.right), invert_tree(root.left)
return root
# Example Usage
root = TreeNode(1, TreeNode(2), TreeNode(3))
inverted_root = invert_tree(root)
python
def max_depth(root):
if not root:
return 0
left_depth = max_depth(root.left)
right_depth = max_depth(root.right)
return max(left_depth, right_depth) + 1
# Example Usage
root = TreeNode(1, TreeNode(2), TreeNode(3))
depth = max_depth(root)
python
def diameter_of_binary_tree(root):
diameter = 0
def dfs(node):
nonlocal diameter
if not node:
return 0
dfs(root)
return diameter
# Example Usage
root = TreeNode(1, TreeNode(2, TreeNode(4)), TreeNode(3))
diameter = diameter_of_binary_tree(root)
python
def is_balanced(root):
def check(node):
if not node:
return 0
left = check(node.left)
right = check(node.right)
if left == -1 or right == -1 or abs(left - right) > 1:
return -1
return max(left, right) + 1
return check(root) != -1
# Example Usage
root = TreeNode(1, TreeNode(2, TreeNode(3)), TreeNode(4))
balanced = is_balanced(root)
python
# Example Usage
root = TreeNode(6, TreeNode(2, TreeNode(0), TreeNode(4)), TreeNode(8, TreeNode(7),
TreeNode(9)))
p, q = root.left, root.right # Nodes with values 2 and 8
ancestor = lowest_common_ancestor(root, p, q)
python
def level_order(root):
levels = []
if not root:
return levels
queue = deque([root])
while queue:
level = []
for _ in range(len(queue)):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
levels.append(level)
return levels
# Example Usage
root = TreeNode(1, TreeNode(2), TreeNode(3))
levels = level_order(root)
python
# Example Usage
root = TreeNode(3, TreeNode(1, None, TreeNode(2)), TreeNode(4))
kth_element = kth_smallest(root, 1)
python
class Codec:
def serialize(self, root):
def dfs(node):
if not node:
vals.append('#')
else:
vals.append(str(node.val))
dfs(node.left)
dfs(node.right)
vals = []
dfs(root)
return ' '.join(vals)
vals = iter(data.split())
# Example Usage
codec = Codec()
root = TreeNode(1, TreeNode(2), TreeNode(3))
serialized = codec.serialize(root)
deserialized_root = codec.deserialize(serialized)
Summary
The Tree Template provides a systematic approach for solving problems on binary trees,
such as traversals, finding depths, checking properties, and transforming the tree structure.
By leveraging recursive traversal functions, BFS, and DFS, we can efficiently solve problems
like inverting a binary tree, finding the maximum depth, checking if a tree is balanced, and
finding the kth smallest element in a BST. This template’s versatility helps to break down
complex tree-based problems into manageable, structured steps, making it easier to reason
about and solve each problem effectively.
Useful for node comparisons (like in "Same Tree"), path calculations, and
reconstructing trees.
Especially helpful for problems needing level-by-level access (e.g., "Binary Tree Right
Side View").
A queue can be used to handle level-specific requirements like only showing the
rightmost node per level.
1. Check Base Condition: If root is None , return immediately or set a default result.
1. Same Tree
python
# Example Usage
root1 = TreeNode(1, TreeNode(2), TreeNode(3))
root2 = TreeNode(1, TreeNode(2), TreeNode(3))
result = is_same_tree(root1, root2)
python
if not root:
return False
if is_same(root, sub_root):
return True
return is_subtree(root.left, sub_root) or is_subtree(root.right, sub_root)
# Example Usage
root = TreeNode(3, TreeNode(4, TreeNode(1), TreeNode(2)), TreeNode(5))
sub_root = TreeNode(4, TreeNode(1), TreeNode(2))
result = is_subtree(root, sub_root)
python
def right_side_view(root):
if not root:
return []
view = []
queue = deque([root])
while queue:
level_length = len(queue)
for i in range(level_length):
node = queue.popleft()
if i == level_length - 1:
view.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return view
# Example Usage
python
def good_nodes(root):
def dfs(node, max_val):
if not node:
return 0
total = 1 if node.val >= max_val else 0
max_val = max(max_val, node.val)
total += dfs(node.left, max_val)
total += dfs(node.right, max_val)
return total
# Example Usage
root = TreeNode(3, TreeNode(1, TreeNode(3)), TreeNode(4, TreeNode(1), TreeNode(5)))
result = good_nodes(root)
python
def is_valid_bst(root):
def dfs(node, lower=float('-inf'), upper=float('inf')):
if not node:
return True
if not (lower < node.val < upper):
return False
return dfs(node.left, lower, node.val) and dfs(node.right, node.val, upper)
return dfs(root)
# Example Usage
root = TreeNode(2, TreeNode(1), TreeNode(3))
result = is_valid_bst(root)
# Example Usage
preorder = [3, 9, 20, 15, 7]
inorder = [9, 3, 15, 20, 7]
tree_root = build_tree(preorder, inorder)
python
def max_path_sum(root):
max_sum = float('-inf')
def dfs(node):
nonlocal max_sum
if not node:
return 0
left_gain = max(dfs(node.left), 0)
right_gain = max(dfs(node.right), 0)
max_sum = max(max_sum, node.val + left_gain + right_gain)
return node.val + max(left_gain, right_gain)
dfs(root)
return max_sum
# Example Usage
root = TreeNode(-10, TreeNode(9), TreeNode(20, TreeNode(15), TreeNode(7)))
result = max_path_sum(root)
1. Efficient Min/Max Access: Heaps allow O(log n) insertion and removal times, making
them ideal for keeping track of the largest or smallest elements as data changes.
2. Dynamic Element Ordering: For problems that continuously update and retrieve
elements based on their ordering (like tracking a median or kth largest), heaps allow
dynamic updates while maintaining the necessary order.
3. Space Optimization: When only the k largest or smallest elements are needed, we can
maintain a heap of fixed size k, saving memory compared to storing the entire dataset.
Template Outline
Min-Heap: Good for tracking the largest k elements (use a min-heap of size k, where
the root is the smallest among the k largest).
Max-Heap: Suitable for finding the smallest k elements (can invert values to use a
max-heap as a min-heap).
For continuous input, initialize an empty heap and maintain it with the required
elements.
For a fixed set of data, initialize the heap with the first k elements.
If maintaining only the top k elements, pop the root if the size exceeds k.
python
import heapq
for num in nums[k:]: # Step 3: Maintain the min-heap with only k largest
elements
if num > min_heap[0]:
heapq.heappop(min_heap)
heapq.heappush(min_heap, num)
return min_heap[0] # Step 4: The root of the heap is the k-th largest
# Example Usage
nums = [3, 2, 1, 5, 6, 4]
k = 2
result = find_kth_largest(nums, k)
python
import heapq
def last_stone_weight(stones):
while len(max_heap) > 1: # Step 3: Keep smashing the two heaviest stones
first = -heapq.heappop(max_heap)
second = -heapq.heappop(max_heap)
if first != second:
heapq.heappush(max_heap, -(first - second))
# Example Usage
stones = [2, 7, 4, 1, 8, 1]
result = last_stone_weight(stones)
python
import heapq
import math
for x, y in points:
dist = -(x**2 + y**2) # Step 2: Calculate negative distance for max-heap
if len(max_heap) < k:
heapq.heappush(max_heap, (dist, (x, y)))
else:
heapq.heappushpop(max_heap, (dist, (x, y))) # Step 3: Keep top k
closest
return [point for (_, point) in max_heap] # Step 4: Return k closest points
# Example Usage
points = [[1,3],[-2,2],[5,8],[0,1]]
k = 2
result = k_closest(points, k)
4. Task Scheduler
import heapq
from collections import Counter, deque
# Example Usage
tasks = ["A", "A", "A", "B", "B", "B"]
n = 2
result = least_interval(tasks, n)
python
import heapq
class MedianFinder:
def __init__(self):
self.small = [] # Max-heap (inverted min-heap)
self.large = [] # Min-heap
def find_median(self):
if len(self.small) > len(self.large):
return -self.small[0]
return (-self.small[0] + self.large[0]) / 2
# Example Usage
median_finder = MedianFinder()
median_finder.add_num(1)
median_finder.add_num(2)
print(median_finder.find_median()) # Output: 1.5
median_finder.add_num(3)
print(median_finder.find_median()) # Output: 2
Summary
This template demonstrates that by structuring problems around a min-heap or max-heap
and maintaining only the relevant elements in the heap, we can efficiently solve a wide
variety of problems. The heap-based approach is versatile for dynamically ordered data,
especially when quick access to maximum or minimum elements is essential.
2. Recursive Exploration with Pruning: Each recursive call explores one path, and invalid
paths are abandoned as soon as they are identified, saving computational resources.
3. Incremental Solution Building: Elements are added to the solution one by one, and if an
element doesn’t lead to a valid solution, it is removed (backtracking).
Template Outline
Create a helper function to handle recursion. This function should accept arguments
representing the current state of the solution and the point in the search space to
be explored.
Specify the base condition to indicate when a solution is complete (e.g., a subset is
formed, all elements are used).
For each recursive step, add an element to the current solution, make the recursive
call, then remove the element to backtrack.
Store the results generated by each valid path in a results list, which can be returned
or processed at the end.
1. Subsets
python
def subsets(nums):
result = []
backtrack(0, [])
return result
# Example Usage
nums = [1, 2, 3]
print(subsets(nums))
2. Combination Sum
python
# Example Usage
candidates = [2, 3, 6, 7]
3. Permutations
python
def permute(nums):
result = []
for i in range(len(remaining)):
path.append(remaining[i]) # Choose
backtrack(path, remaining[:i] + remaining[i+1:]) # Recurse with
remaining elements
path.pop() # Un-choose (backtrack)
backtrack([], nums)
return result
# Example Usage
nums = [1, 2, 3]
print(permute(nums))
4. N Queens
python
def solve_n_queens(n):
result = []
backtrack([-1] * n, 0)
return result
# Example Usage
n = 4
print(solve_n_queens(n))
5. Palindrome Partitioning
python
def partition(s):
result = []
def is_palindrome(substring):
return substring == substring[::-1]
backtrack(0, [])
return result
# Example Usage
Summary
This backtracking template allows for systematically exploring the search space of potential
solutions by incrementally building and backtracking paths based on constraints. It’s a
versatile approach that can be applied to a wide range of problems, including generating all
subsets, finding unique combinations, placing queens on a chessboard, and partitioning
palindromic substrings. The key to efficiently solving these problems lies in recursive
exploration with well-defined choices, conditions for pruning, and tracking paths that meet
the required criteria.
Alright! Let’s continue applying the backtracking template to solve additional problems in the
list. Here’s a breakdown of each problem, the approach used, and the code solution.
6. Combination Sum II
Combination Sum II is similar to Combination Sum, but each number can only be used once,
and the candidate list contains duplicates, so we need to avoid duplicate combinations.
python
# Example Usage
candidates = [10, 1, 2, 7, 6, 1, 5]
target = 8
print(combination_sum2(candidates, target))
7. Word Search
In Word Search, we try to find a word in a grid by moving horizontally or vertically. This
requires a DFS search with backtracking to try each cell and track visited positions.
python
# Mark as visited
temp = board[r][c]
board[r][c] = '#'
for r in range(rows):
for c in range(cols):
if board[r][c] == word[0] and backtrack(r, c, 0): # Start DFS from
matching character
return True
return False
# Example Usage
board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]]
word = "ABCCED"
print(exist(board, word))
python
def letter_combinations(digits):
if not digits:
return []
phone_map = {
"2": "abc", "3": "def", "4": "ghi", "5": "jkl",
"6": "mno", "7": "pqrs", "8": "tuv", "9": "wxyz"
}
result = []
backtrack(0, [])
return result
# Example Usage
digits = "23"
print(letter_combinations(digits))
9. N-Queens
The classic N-Queens problem where we place N queens on an N×N chessboard so that no
two queens threaten each other. We utilize the backtracking template to place queens row by
row.
python
def solve_n_queens(n):
result = []
board = [["."] * n for _ in range(n)]
def backtrack(row):
if row == n: # All queens placed
solution = ["".join(row) for row in board]
result.append(solution)
return
backtrack(0)
return result
# Example Usage
n = 4
print(solve_n_queens(n))
Backtracking steps that "undo" the last decision if it does not yield a complete solution.
To tackle problems related to Tries (Prefix Trees), let's build a template that will work
effectively for each of these scenarios. Tries are particularly useful for problems involving
prefix-based search and efficient storage of words or sequences.
Tries are structured to allow quick prefix-based operations, such as checking for the
presence of words, prefixes, or even building and traversing trees in character-specific
1. Nodes with children: Each node represents a character and points to potential child
nodes (next characters in words).
2. End markers: Mark the end of a valid word so we know if a path represents a complete
word.
1. Insert: Traverse the trie, creating nodes for each character of the word. Mark the last
node as the end of the word.
2. Search: Traverse the trie to see if a word exists by following character nodes. Ensure the
final node is marked as a word’s end.
3. Starts With: Similar to search but doesn’t require marking the end of the word, just that
all characters match in sequence.
python
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
python
# Initialize Trie
trie = Trie()
trie.insert("apple")
print(trie.search("apple")) # Returns True
print(trie.search("app")) # Returns False
print(trie.starts_with("app")) # Returns True
trie.insert("app")
print(trie.search("app")) # Returns True
python
class WordDictionary:
def __init__(self):
self.root = TrieNode()
char = word[i]
if char == '.': # Wildcard case
return any(dfs(child, i + 1) for child in node.children.values())
elif char in node.children:
return dfs(node.children[char], i + 1)
return False
return dfs(self.root, 0)
# Example usage
word_dict = WordDictionary()
word_dict.add_word("bad")
word_dict.add_word("dad")
word_dict.add_word("mad")
print(word_dict.search("pad")) # False
print(word_dict.search("bad")) # True
print(word_dict.search(".ad")) # True
print(word_dict.search("b..")) # True
python
class Solution:
def find_words(self, board, words):
# Step 1: Build the Trie with words
trie = Trie()
for word in words:
trie.insert(word)
char = board[r][c]
if char not in node.children:
return
return list(result)
# Example usage
board = [
["o", "a", "a", "n"],
["e", "t", "a", "e"],
["i", "h", "k", "r"],
["i", "f", "l", "v"]
]
words = ["oath", "pea", "eat", "rain"]
solution = Solution()
print(solution.find_words(board, words)) # ["oath", "eat"]
Summary of Applications
Implement Trie: Store and check for prefixes or exact word matches.
Add and Search Words Data Structure: Trie with added complexity for handling
wildcard matching, utilizing DFS.
Word Search II: Combines DFS grid traversal with Trie search to find words in a 2D grid,
leveraging Trie to reduce search space by validating prefixes.
By following this Trie template, we manage words and prefixes efficiently, add powerful
search functionalities, and apply it across a variety of applications requiring prefix or word-
based retrieval.
Graph problems often involve understanding and traversing relationships between nodes,
whether in the form of grid-based connections (like islands or rooms) or directed/undirected
edges (like courses or paths). Problems involving graph traversal, connectivity, and
component identification can typically be solved using two key techniques: Breadth-First
Search (BFS) and Depth-First Search (DFS).
DFS explores as deep as possible along a branch before backtracking. It is useful for
problems that require exploring all possible paths or marking connected components.
BFS explores all neighbors at the present depth before moving deeper. It is particularly
helpful for shortest-path or minimum-step problems.
python
# DFS Template
def dfs(node, visited, graph):
visited.add(node)
# Process node if needed
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor, visited, graph)
# BFS Template
def bfs(start, graph):
visited = set([start])
# Union-Find Template
class UnionFind:
def __init__(self, size):
self.parent = list(range(size))
self.rank = [0] * size
def numIslands(grid):
def dfs(r, c):
if r < 0 or r >= len(grid) or c < 0 or c >= len(grid[0]) or grid[r][c]
== '0':
return
grid[r][c] = '0' # Mark as visited
for dr, dc in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
dfs(r + dr, c + dc)
count = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == '1':
dfs(r, c)
count += 1
return count
Solution: Similar to Number of Islands, but we also keep track of the size of each
island.
python
def maxAreaOfIsland(grid):
def dfs(r, c):
if r < 0 or r >= len(grid) or c < 0 or c >= len(grid[0]) or grid[r][c]
== 0:
return 0
grid[r][c] = 0 # Mark as visited
return 1 + sum(dfs(r + dr, c + dc) for dr, dc in [(0, 1), (1, 0), (0,
-1), (-1, 0)])
max_area = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == 1:
3. Clone Graph
Problem: Clone an undirected graph where each node points to its neighbors.
Solution: Use DFS or BFS with a mapping to clone nodes and connections.
python
def cloneGraph(node):
if not node:
return None
dfs(node)
return clones[node]
python
visited = set()
cycle = set()
def dfs(course):
if course in cycle: # Detected cycle
return False
cycle.add(course)
for neighbor in graph[course]:
if not dfs(neighbor):
return False
cycle.remove(course)
visited.add(course)
return True
Problem: Find cells from which water can flow to both oceans.
Solution: Perform BFS/DFS from each ocean's boundary to find reachable cells.
python
def pacificAtlantic(matrix):
if not matrix: return []
for c in range(cols):
dfs(0, c, pacific_reachable, matrix[0][c])
dfs(rows - 1, c, atlantic_reachable, matrix[rows - 1][c])
for r in range(rows):
dfs(r, 0, pacific_reachable, matrix[r][0])
dfs(r, cols - 1, atlantic_reachable, matrix[r][cols - 1])
Summary
The graph template provides a solid foundation for addressing the following types of
problems:
Grid-based problems: Use DFS or BFS to explore connected regions (e.g., islands,
rooms).
Cycle detection in directed graphs: Use DFS with cycle tracking to detect prerequisites
or dependency issues.
This template can be customized for each specific graph-based problem, allowing for
efficient and systematic exploration of connected components, paths, and cycles.
Let's tackle each of the given problems individually by applying the graph traversal and
manipulation techniques previously described. These include methods like DFS, BFS, and
Union-Find, as well as handling graphs in the form of matrices or adjacency lists.
1. Number of Islands
Count the number of islands in a grid.
python
def numIslands(grid):
def dfs(r, c):
if r < 0 or r >= len(grid) or c < 0 or c >= len(grid[0]) or grid[r][c] ==
'0':
return
grid[r][c] = '0' # Mark as visited
for dr, dc in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
dfs(r + dr, c + dc)
python
def maxAreaOfIsland(grid):
def dfs(r, c):
if r < 0 or r >= len(grid) or c < 0 or c >= len(grid[0]) or grid[r][c] == 0:
return 0
grid[r][c] = 0 # Mark as visited
return 1 + sum(dfs(r + dr, c + dc) for dr, dc in [(0, 1), (1, 0), (0, -1),
(-1, 0)])
max_area = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == 1:
max_area = max(max_area, dfs(r, c))
return max_area
3. Clone Graph
Clone an undirected graph where each node has pointers to its neighbors.
python
def dfs(n):
for neighbor in n.neighbors:
if neighbor not in clones:
clones[neighbor] = Node(neighbor.val)
dfs(neighbor)
clones[n].neighbors.append(clones[neighbor])
dfs(node)
return clones[node]
python
def wallsAndGates(rooms):
queue = deque()
for r in range(len(rooms)):
for c in range(len(rooms[0])):
if rooms[r][c] == 0: # Gate
queue.append((r, c))
while queue:
r, c = queue.popleft()
for dr, dc in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
nr, nc = r + dr, c + dc
if 0 <= nr < len(rooms) and 0 <= nc < len(rooms[0]) and rooms[nr][nc] ==
2147483647:
5. Rotting Oranges
Determine the time taken for all fresh oranges to rot, given a grid where each cell is an
empty space, fresh orange, or rotten orange.
python
def orangesRotting(grid):
queue = deque()
fresh = 0
rows, cols = len(grid), len(grid[0])
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2: # Rotten orange
queue.append((r, c))
elif grid[r][c] == 1: # Fresh orange
fresh += 1
if fresh == 0:
return 0 # No fresh oranges to rot
minutes = 0
while queue and fresh > 0:
minutes += 1
for _ in range(len(queue)):
r, c = queue.popleft()
for dr, dc in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 2
fresh -= 1
queue.append((nr, nc))
python
def pacificAtlantic(matrix):
if not matrix: return []
for c in range(cols):
dfs(0, c, pacific_reachable, matrix[0][c])
dfs(rows - 1, c, atlantic_reachable, matrix[rows - 1][c])
for r in range(rows):
dfs(r, 0, pacific_reachable, matrix[r][0])
dfs(r, cols - 1, atlantic_reachable, matrix[r][cols - 1])
7. Course Schedule
python
visited = set()
cycle = set()
def dfs(course):
if course in cycle:
return False
if course in visited:
return True
cycle.add(course)
for neighbor in graph[course]:
if not dfs(neighbor):
return False
cycle.remove(course)
visited.add(course)
return True
8. Course Schedule II
Return the order in which courses should be taken to finish all courses.
python
while queue:
course = queue.popleft()
order.append(course)
for neighbor in graph[course]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
9. Word Ladder
Find the length of the shortest transformation sequence from the beginWord to endWord .
python
These solutions cover common graph traversal, shortest path finding, and cycle detection
problems frequently encountered in graphs. Each approach optimally fits the problem
requirements based on the graph characteristics (e.g., grid-based or adjacency list
representation).
Advanced graph problems on LeetCode require techniques beyond basic traversal, often
involving shortest-path algorithms (Dijkstra, Bellman-Ford), Minimum Spanning Tree (Prim,
Kruskal), Topological Sort, and Union-Find. These techniques help tackle problems with
constraints on path length, cost, or specific requirements around order and dependencies.
4. Implementation:
If using a priority queue (for Dijkstra or Prim’s), initialize it with starting nodes.
Keep track of visited nodes and conditions on path length, cost, or specific stops.
1. Reconstruct Itinerary
Use DFS with backtracking to reconstruct the itinerary by visiting each airport
lexicographically.
python
def findItinerary(tickets):
graph = defaultdict(list)
for start, end in sorted(tickets):
graph[start].append(end)
itinerary = []
def dfs(airport):
while graph[airport]:
dfs(graph[airport].pop(0))
itinerary.append(airport)
dfs("JFK")
return itinerary[::-1]
Use Prim's algorithm for the Minimum Spanning Tree of the points.
python
import heapq
def minCostConnectPoints(points):
n = len(points)
return result
Use Dijkstra’s algorithm to find the shortest time to reach all nodes from a start node.
python
import heapq
from collections import defaultdict
Use Dijkstra’s algorithm on a grid to find the minimum time required to reach the bottom-
right corner.
python
import heapq
def swimInWater(grid):
n = len(grid)
min_heap = [(grid[0][0], 0, 0)]
visited = set()
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
answer = 0
while min_heap:
elevation, r, c = heapq.heappop(min_heap)
answer = max(answer, elevation)
if (r, c) == (n - 1, n - 1):
return answer
for dr, dc in directions:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n and (nr, nc) not in visited:
visited.add((nr, nc))
heapq.heappush(min_heap, (grid[nr][nc], nr, nc))
return -1
Use Topological Sort to determine the correct order of characters in an alien dictionary.
python
def alienOrder(words):
graph = defaultdict(set)
in_degree = {c: 0 for word in words for c in word}
while queue:
c = queue.popleft()
order.append(c)
for neighbor in graph[c]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
python
import heapq
from collections import defaultdict
while min_heap:
cost, u, stops = heapq.heappop(min_heap)
if u == dst:
return cost
if stops > 0:
for v, w in graph[u]:
heapq.heappush(min_heap, (cost + w, v, stops - 1))
return -1
Min Cost to Connect All Points: Prim’s MST to find the minimum cost to connect all
points.
Network Delay Time: Dijkstra’s for finding the shortest paths from a source node to
every other node.
Swim In Rising Water: Grid-based shortest path using Dijkstra’s for reaching the target
with a minimal maximum cost.
This template and methods are widely applicable to problems involving pathfinding,
minimum spanning trees, topological ordering, and cost-optimization within constraints,
fitting a broad range of advanced graph problems.
Dynamic programming (DP) is a powerful approach for solving problems that exhibit
overlapping subproblems and optimal substructure. In 1D DP problems, the solution often
involves breaking the problem down into a sequence of decisions or subproblems, with each
subproblem depending on the results of the previous ones.
1. Optimal Substructure: The optimal solution to the problem can be constructed from
optimal solutions to its subproblems.
2. Overlapping Subproblems: The problem involves solving the same smaller problems
multiple times.
Examples include finding the maximum/minimum values for subarrays, counting specific
sequences, or determining possible paths in a linear structure (like arrays or lists).
2. Define the Recurrence Relation: This is the formula that determines how to transition
between states. For example, if dp[i] is based on previous states dp[i-1] , dp[i-2] ,
etc., then define the relationship accordingly.
3. Initialize the Base Cases: These are starting values of the DP array. They are essential to
kick-start the recurrence relation.
5. Return the Final Result: Usually, the answer to the problem will be in the last position of
the DP array or derived from it.
1. Climbing Stairs
Each step can be reached from either one step before or two steps before. Hence, dp[i] =
dp[i-1] + dp[i-2] .
python
def climbStairs(n):
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1], dp[2] = 1, 2
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
You can either start from step i-1 or i-2 , so dp[i] = cost[i] + min(dp[i-1], dp[i-2]) .
python
def minCostClimbingStairs(cost):
n = len(cost)
dp = [0] * n
dp[0], dp[1] = cost[0], cost[1]
for i in range(2, n):
3. House Robber
Each house can either be robbed or skipped, resulting in dp[i] = max(dp[i-1], dp[i-2] +
nums[i]) .
python
def rob(nums):
if not nums:
return 0
n = len(nums)
if n == 1:
return nums[0]
dp = [0] * n
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[-1]
4. House Robber II
Since it’s a circular arrangement, split the array into two cases (excluding either the first or
the last house) and apply the House Robber logic.
python
def rob(nums):
def simple_rob(nums):
n = len(nums)
if n == 1:
return nums[0]
dp = [0] * n
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
Expand around the center for each character; check if it forms a palindrome and keep track
of the longest.
python
def longestPalindrome(s):
def expandAroundCenter(s, left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return s[left + 1:right]
res = ""
for i in range(len(s)):
res = max(res, expandAroundCenter(s, i, i), expandAroundCenter(s, i, i + 1),
key=len)
return res
6. Palindromic Substrings
python
def countSubstrings(s):
n = len(s)
dp = [[False] * n for _ in range(n)]
count = 0
for i in range(n - 1, -1, -1):
7. Decode Ways
dp[i] = dp[i-1] + dp[i-2] if the substring can be split into two valid numbers.
python
def numDecodings(s):
if not s or s[0] == "0":
return 0
dp = [0] * (len(s) + 1)
dp[0], dp[1] = 1, 1
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]
8. Coin Change
python
Use dp[i] to represent the length of the longest increasing subsequence ending at i .
python
def lengthOfLIS(nums):
dp = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
python
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 i in range(target, num - 1, -1):
dp[i] = dp[i] or dp[i - num]
return dp[target]
Let’s continue solving the remaining 1D dynamic programming problems by applying the
same template-based approach. I'll walk through each problem using the DP template:
Define the State: Let dp[i] represent whether the substring s[0:i] can be
segmented into words in the dictionary.
Recurrence Relation: For each position i , check if any substring s[j:i] (where j <
i ) exists in the dictionary and if dp[j] is True . Then, set dp[i] to True .
Base Case: dp[0] = True because an empty substring can always be segmented.
python
return dp[len(s)]
python
def maxProduct(nums):
if not nums:
return 0
return result
Define the State: dp[i] represents the length of the longest increasing subsequence
ending at i .
Recurrence Relation: For each j < i , if nums[j] < nums[i] , update dp[i] =
max(dp[i], dp[j] + 1) .
python
dp = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
Recurrence Relation: For each num , if dp[j - num] is True , set dp[j] to True .
python
def canPartition(nums):
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
return dp[target]
1. Identify the State: Defined what dp[i] represents (e.g., max product, sum achievable,
subsequence length).
2. Set Recurrence Relation: Formulated the relationship between dp[i] and previous
states to build the solution.
3. Initialize Base Cases: Defined the base values to start the solution process.
4. Iterate and Solve: Populated the DP array by iterating and applying the recurrence
relation.
This template can solve a wide range of 1D DP problems, providing a structured way to
approach similar challenges.
To solve 2D dynamic programming problems, we’ll use a structured template that leverages
the multi-dimensional nature of the problem. Let’s walk through a 2D DP template and apply
it to each problem:
2D DP Template
1. Define the State:
Establish dp[i][j] as the state that represents the desired outcome at indices i
and j in the problem.
Define base cases where dp[0][j] or dp[i][0] can be initialized directly without
recurrence, often based on problem constraints (like single-element subproblems).
Loop through the entire DP table, filling each entry based on the recurrence relation
until reaching the final solution at dp[m][n] .
Usually, dp[m][n] or another specific cell represents the answer. This can also
involve finding a maximum or minimum within the DP table.
1. Unique Paths
State: dp[i][j] represents the number of unique paths to reach cell (i, j) .
Base Cases: dp[0][j] = 1 and dp[i][0] = 1 since there is only one way to reach any
cell in the first row or column.
python
State: dp[i][j] represents the length of the LCS of text1[:i] and text2[:j] .
State: dp[i][j] with j as 0 (holding) or 1 (not holding) represents max profit at day
i.
python
def maxProfit(prices):
if not prices:
return 0
n = len(prices)
dp = [[0] * 2 for _ in range(n)]
dp[0][0] = -prices[0]
dp[0][1] = 0
for i in range(1, n):
dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][1] + prices[i] if i > 1 else prices[i])
return dp[-1][1]
4. Coin Change II
python
5. Target Sum
State: dp[i][s] represents the number of ways to get sum s with first i elements.
python
Let's go through each remaining problem from your list in the context of 2D dynamic
programming:
6. Interleaving String
Problem: Given two strings s1 and s2 , and a target string s3 , determine if s3 can be
formed by interleaving s1 and s2 .
Recurrence:
Base Cases:
python
State: dp[i][j] represents the length of the longest increasing path starting from cell
(i, j) .
Recurrence: For each cell (i, j) , if matrix[i][j] < matrix[x][y] for neighbors (x,
y) , update dp[i][j] = max(dp[i][j], 1 + dp[x][y]) .
python
def longestIncreasingPath(matrix):
if not matrix or not matrix[0]:
return 0
rows, cols = len(matrix), len(matrix[0])
dp = [[-1] * cols for _ in range(rows)]
State: dp[i][j] represents the number of distinct subsequences of s[:i] that equal
t[:j] .
Recurrence:
python
9. Edit Distance
Problem: Find the minimum number of operations to convert word1 to word2 .
Recurrence:
Base Cases:
python
State: dp[i][j] represents the maximum coins obtained by bursting all balloons in the
subarray nums[i:j] .
Recurrence:
python
def maxCoins(nums):
nums = [1] + nums + [1]
n = len(nums)
dp = [[0] * n for _ in range(n)]
Recurrence:
Base Cases:
python
Each problem requires an understanding of how to set up and maintain the state transition,
recurrence relations, and base cases. Let me know if you want more clarification on any
specific problem!
Let's tackle these greedy problems by first creating a Greedy Template that can be applied to
most of these problems.
Greedy algorithms make a sequence of choices, always choosing the most optimal
option available at each step.
To identify the greedy choice, examine the problem requirements and see if a locally
optimal choice at each step can lead to a globally optimal solution.
Understand the goal of the problem (e.g., maximize, minimize, or achieve a certain
pattern).
Note any specific constraints that might influence which greedy choices are feasible.
Start by solving for a small subset of the problem to validate your greedy approach.
Track how each choice impacts the solution and verify that it does not lead to
suboptimal results in future steps.
Test for possible edge cases, such as empty arrays, minimum or maximum allowed
values, and identical values that might affect greedy choice effectiveness.
1. Maximum Subarray
Goal: Find the contiguous subarray with the maximum sum.
Greedy Choice: Start from the first element, and for each position, choose to either
extend the current subarray or start a new subarray.
Algorithm:
Keep a running sum ( current_sum ) and reset it to the current element if it becomes
negative.
python
def maxSubArray(nums):
max_sum = nums[0]
current_sum = nums[0]
for num in nums[1:]:
current_sum = max(num, current_sum + num)
max_sum = max(max_sum, current_sum)
return max_sum
2. Jump Game
Goal: Determine if you can reach the last index.
Greedy Choice: Track the farthest index you can reach as you traverse the array.
Algorithm:
python
3. Jump Game II
Goal: Find the minimum number of jumps to reach the last index.
Algorithm:
Use jumps , end , and farthest to track the minimum jumps and update them
when reaching the current end .
python
def jump(nums):
jumps, end, farthest = 0, 0, 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == end:
jumps += 1
end = farthest
return jumps
4. Gas Station
Goal: Determine if you can complete a circuit around gas stations.
Greedy Choice: Calculate if the total gas is greater than or equal to the total cost. If not,
return -1 .
Track the surplus gas as you traverse the stations and reset when it goes negative.
python
5. Hand of Straights
Goal: Check if the cards can be grouped into hands of groupSize with consecutive
values.
Greedy Choice: Use the smallest available card as the starting point for each group.
Algorithm:
Use a min-heap or dictionary to track and arrange the cards into groups.
python
Greedy Choice: Accumulate the maximum of each component that matches the target.
Algorithm:
Only consider triplets that do not exceed the target triplet values.
python
7. Partition Labels
Goal: Partition the string so that each character appears in only one part.
Greedy Choice: Find the farthest occurrence of each character in the string.
Algorithm:
Track the end index of the current partition, add the partition size when reaching
end , and reset.
python
Greedy Choice: Track the minimum and maximum open parentheses as you traverse.
Algorithm:
python
def checkValidString(s):
min_open, max_open = 0, 0
for char in s:
if char == '(':
min_open += 1
max_open += 1
elif char == ')':
min_open = max(min_open - 1, 0)
max_open -= 1
else:
min_open = max(min_open - 1, 0)
max_open += 1
if max_open < 0:
return False
return min_open == 0
Interval problems often involve merging, partitioning, or analyzing the overlapping sections
of intervals in order to simplify, combine, or make optimal choices. A systematic approach
with sorting and merging can help handle most interval problems efficiently. Let’s create a
generalized template for interval problems, followed by applications for each problem
listed.
Key Steps:
1. Sort Intervals:
Often, interval problems are simplified by sorting intervals by their starting points
(or sometimes by ending points).
Define conditions for when intervals overlap and when they don’t.
When two intervals overlap, decide whether to merge them or discard one based on
problem requirements.
For problems needing real-time interval adjustments or queries, use structures like
min-heaps or segment trees.
Handle cases with no intervals, single interval, intervals that fully overlap, or
intervals that barely overlap.
1. Insert Interval
Goal: Insert a new interval into a list of sorted intervals and merge where necessary.
Approach:
Split into three cases: intervals before the new interval, overlapping intervals, and
intervals after the new interval.
python
2. Merge Intervals
Goal: Merge all overlapping intervals in a list.
Sort the intervals, then merge each interval with the previous one if they overlap.
python
def merge(intervals):
intervals.sort(key=lambda x: x[0])
merged = []
for interval in intervals:
if not merged or merged[-1][1] < interval[0]:
merged.append(interval)
else:
merged[-1][1] = max(merged[-1][1], interval[1])
return merged
3. Non-Overlapping Intervals
Goal: Find the minimum number of intervals to remove to make the rest non-
overlapping.
Approach:
Sort intervals by ending time and greedily keep track of non-overlapping intervals.
python
def eraseOverlapIntervals(intervals):
intervals.sort(key=lambda x: x[1])
count, end = 0, float('-inf')
for interval in intervals:
if interval[0] >= end:
end = interval[1]
else:
count += 1
return count
Approach:
Sort by start times and check for overlaps by comparing consecutive intervals.
python
def canAttendMeetings(intervals):
intervals.sort(key=lambda x: x[0])
for i in range(1, len(intervals)):
if intervals[i][0] < intervals[i - 1][1]:
return False
return True
5. Meeting Rooms II
Goal: Find the minimum number of meeting rooms required for all meetings.
Approach:
Use a min-heap to track end times of ongoing meetings. Allocate a room when
needed and release one when a meeting ends.
python
import heapq
def minMeetingRooms(intervals):
if not intervals:
return 0
intervals.sort(key=lambda x: x[0])
heap = []
heapq.heappush(heap, intervals[0][1])
for interval in intervals[1:]:
if heap[0] <= interval[0]:
heapq.heappop(heap)
Approach:
Sort intervals and queries, then use a min-heap to track relevant intervals for each
query.
python
import heapq
for q, qi in queries_with_indices:
while i < len(intervals) and intervals[i][0] <= q:
l, r = intervals[i]
heapq.heappush(heap, (r - l + 1, r))
i += 1
while heap and heap[0][1] < q:
heapq.heappop(heap)
if heap:
result[qi] = heap[0][0]
return result
Key Steps:
Divide large calculations into manageable parts. For instance, with matrix
manipulation, handle rows and columns independently.
For number manipulations, consider each digit separately or use appropriate data
structures.
Use loops to apply transformations to each element (e.g., rotating a matrix cell by
cell).
Handle edge cases such as empty inputs, zero values, or negative numbers.
1. Rotate Image
Goal: Rotate a square matrix by 90 degrees clockwise.
Approach:
python
def rotate(matrix):
n = len(matrix)
for i in range(n):
for j in range(i, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
for row in matrix:
row.reverse()
2. Spiral Matrix
Goal: Return all elements of an m x n matrix in spiral order.
Approach:
Use boundary pointers and adjust them after processing each row or column.
python
def spiralOrder(matrix):
result = []
left, right, top, bottom = 0, len(matrix[0]) - 1, 0, len(matrix) - 1
while left <= right and top <= bottom:
for i in range(left, right + 1):
result.append(matrix[top][i])
top += 1
Approach:
Use the first row/column as markers to track zero rows and columns.
python
def setZeroes(matrix):
rows, cols = len(matrix), len(matrix[0])
row_zero = any(matrix[0][j] == 0 for j in range(cols))
col_zero = any(matrix[i][0] == 0 for i in range(rows))
if row_zero:
for j in range(cols):
4. Happy Number
Goal: Determine if a number eventually reaches 1 when repeatedly replacing the
number with the sum of the squares of its digits.
Approach:
python
def isHappy(n):
seen = set()
while n != 1 and n not in seen:
seen.add(n)
n = sum(int(digit)**2 for digit in str(n))
return n == 1
5. Plus One
Goal: Increment the number represented by an array of digits by one.
Approach:
python
def plusOne(digits):
for i in reversed(range(len(digits))):
if digits[i] < 9:
digits[i] += 1
6. Pow(x, n)
Goal: Calculate x raised to the power n .
Approach:
python
7. Multiply Strings
Goal: Multiply two non-negative integers represented as strings.
Approach:
python
8. Detect Squares
Goal: Add points and count squares that can be formed.
Approach:
Use a dictionary to store points and check valid squares based on distance.
python
class DetectSquares:
def __init__(self):
self.points_count = defaultdict(int)
Bit manipulation problems often involve using bitwise operators to achieve solutions with
optimized memory and time usage. These problems focus on leveraging properties of bits to
solve tasks that might be complex with standard arithmetic or loops. Below is a template for
approaching bit manipulation problems, followed by solutions for each of the listed
problems.
Key Steps:
Recognize if bitwise operations like AND ( & ), OR ( | ), XOR ( ^ ), NOT ( ~ ), shift left
( << ), or shift right ( >> ) can simplify the problem.
For tasks involving counting bits or setting bits in a number, use loops to isolate
individual bits by shifting and masking.
Bit shifts ( << , >> ) are useful for calculations that involve powers of two.
Check for overflow issues, especially in languages with fixed integer sizes.
1. Single Number
Goal: Find the single number in an array where every other number appears twice.
Approach:
python
def singleNumber(nums):
result = 0
for num in nums:
result ^= num
return result
2. Number of 1 Bits
Goal: Count the number of 1 s in the binary representation of an integer.
Approach:
Use bitwise AND with 1 and shift right to check each bit.
python
def hammingWeight(n):
count = 0
while n:
count += n & 1
n >>= 1
return count
Approach:
python
def countBits(n):
bits = [0] * (n + 1)
for i in range(1, n + 1):
bits[i] = bits[i >> 1] + (i & 1)
return bits
4. Reverse Bits
Goal: Reverse the bits of a 32-bit unsigned integer.
Approach:
Shift bits from the original number and add to the reversed number with left shift.
python
def reverseBits(n):
result = 0
for _ in range(32):
result = (result << 1) | (n & 1)
n >>= 1
return result
5. Missing Number
Goal: Find the missing number in a sequence from 0 to n .
XOR all indices and elements; the missing number will be left as duplicates cancel.
python
def missingNumber(nums):
result = len(nums)
for i, num in enumerate(nums):
result ^= i ^ num
return result
Approach:
Use XOR for addition without carry and AND for the carry bits. Repeat until no carry
remains.
python
7. Reverse Integer
Goal: Reverse the digits of an integer.
Approach:
def reverse(x):
result, sign = 0, 1
if x < 0:
x, sign = -x, -1
while x:
result = result * 10 + x % 10
x //= 10
result *= sign
return result if -2**31 <= result <= 2**31 - 1 else 0
If you have additional questions or need more examples, feel free to ask!