Q101. Explain Big O notation with examples
# Big O Notation and Time Complexity
# 1. O(1) - Constant Time
def get_first_element(arr):
return arr[0] # Always takes same time
def hash_lookup(dictionary, key):
return dictionary[key] # Hash table lookup
# 2. O(log n) - Logarithmic Time
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# Example: [1, 3, 5, 7, 9, 11, 13, 15]
# Step 1: Check middle (9) - not target
# Step 2: Check half - keeps halving
# log₂(n) steps
# 3. O(n) - Linear Time
def find_max(arr):
max_val = arr[0]
for num in arr: # Check each element once
if num > max_val:
max_val = num
return max_val
def linear_search(arr, target):
for i, val in enumerate(arr):
if val == target:
return i
return -1
# 4. O(n log n) - Linearithmic Time
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # log n divisions
right = merge_sort(arr[mid:])
return merge(left, right) # n merges
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# 5. O(n²) - Quadratic Time
def bubble_sort(arr):
n = len(arr)
for i in range(n): # n iterations
for j in range(n - i - 1): # n iterations
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
return arr
def find_duplicates_naive(arr):
duplicates = []
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] == arr[j] and arr[i] not in duplicates:
duplicates.append(arr[i])
return duplicates
# 6. O(2ⁿ) - Exponential Time
def fibonacci_recursive(n):
if n <= 1:
return n
return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
# Each call branches into 2 calls
# 7. O(n!) - Factorial Time
def generate_permutations(arr):
if len(arr) <= 1:
return [arr]
perms = []
for i in range(len(arr)):
rest = arr[:i] + arr[i+1:]
for perm in generate_permutations(rest):
perms.append([arr[i]] + perm)
return perms
# 8. Space Complexity Examples
# O(1) Space
def sum_array(arr):
total = 0 # Single variable
for num in arr:
total += num
return total
# O(n) Space
def create_copy(arr):
return arr.copy() # New array of size n
# O(n²) Space
def create_matrix(n):
return [[0] * n for _ in range(n)] # n x n matrix
# 9. Complexity Comparison
import time
def compare_algorithms():
sizes = [100, 1000, 10000]
for n in sizes:
arr = list(range(n, 0, -1))
# O(n log n)
start = time.time()
sorted(arr)
fast_time = time.time() - start
# O(n²)
start = time.time()
bubble_sort(arr.copy())
slow_time = time.time() - start
print(f"n={n}: O(n log n)={fast_time:.4f}s, O(n²)={slow_time:.4f}s")
# 10. Best, Average, Worst Cases
def quicksort(arr):
# Best/Average: O(n log n)
# Worst: O(n²) - when pivot is always smallest/largest
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quicksort(left) + middle + quicksort(right)
# 11. Amortized Analysis
class DynamicArray:
# append() is O(1) amortized
# Occasionally O(n) when resizing
# Average over many operations: O(1)
def __init__(self):
self.size = 0
self.capacity = 1
self.arr = [None] * self.capacity
def append(self, item):
if self.size == self.capacity:
self._resize() # O(n) occasionally
self.arr[self.size] = item
self.size += 1
def _resize(self):
self.capacity *= 2
new_arr = [None] * self.capacity
for i in range(self.size):
new_arr[i] = self.arr[i]
self.arr = new_arr
# 12. Complexity Cheat Sheet
complexity_guide = '''
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)
Constant Log Linear Linearithmic Quadratic Exponential Factorial
Common Algorithms:
- Hash table lookup: O(1)
- Binary search: O(log n)
- Linear search: O(n)
- Merge/Quick sort: O(n log n)
- Bubble/Selection sort: O(n²)
- Fibonacci (recursive): O(2ⁿ)
- Permutations: O(n!)
'''
# 13. Optimization Example
# Bad: O(n²)
def has_duplicates_slow(arr):
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] == arr[j]:
return True
return False
# Good: O(n)
def has_duplicates_fast(arr):
seen = set()
for num in arr:
if num in seen:
return True
seen.add(num)
return FalsePythonAnswer: Big O describes algorithm efficiency: O(1) constant, O(log n) logarithmic, O(n) linear, O(n log n) linearithmic, O(n²) quadratic, O(2ⁿ) exponential; focus on worst-case time/space complexity.
Q102. Explain arrays/lists operations and common algorithms
# Arrays and Lists in Python
# 1. List Basics
numbers = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True, [1, 2]]
# Access: O(1)
print(numbers[0]) # First element
print(numbers[-1]) # Last element
# Append: O(1) amortized
numbers.append(6)
# Insert: O(n)
numbers.insert(0, 0) # Insert at beginning
# Delete: O(n)
numbers.pop(0) # Remove first
del numbers[2] # Remove by index
numbers.remove(3) # Remove by value
# 2. List Slicing
arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(arr[2:5]) # [2, 3, 4]
print(arr[:3]) # [0, 1, 2]
print(arr[5:]) # [5, 6, 7, 8, 9]
print(arr[::2]) # [0, 2, 4, 6, 8] - every 2nd
print(arr[::-1]) # Reverse
# 3. Two Pointer Technique
def reverse_array(arr):
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
return arr
# 4. Sliding Window
def max_sum_subarray(arr, k):
'''Maximum sum of k consecutive elements'''
if len(arr) < k:
return None
# Initial window
window_sum = sum(arr[:k])
max_sum = window_sum
# Slide window
for i in range(k, len(arr)):
window_sum += arr[i] - arr[i - k]
max_sum = max(max_sum, window_sum)
return max_sum
# Example: [1, 4, 2, 10, 23, 3, 1, 0, 20], k=4
# Windows: [1,4,2,10]=17, [4,2,10,23]=39, [2,10,23,3]=38...
# 5. Array Rotation
def rotate_array(arr, k):
'''Rotate array right by k positions'''
k = k % len(arr) # Handle k > len(arr)
return arr[-k:] + arr[:-k]
# Alternative: Reverse method
def rotate_reverse(arr, k):
k = k % len(arr)
def reverse(start, end):
while start < end:
arr[start], arr[end] = arr[end], arr[start]
start += 1
end -= 1
reverse(0, len(arr) - 1) # Reverse all
reverse(0, k - 1) # Reverse first k
reverse(k, len(arr) - 1) # Reverse rest
return arr
# 6. Merge Sorted Arrays
def merge_sorted_arrays(arr1, arr2):
result = []
i = j = 0
while i < len(arr1) and j < len(arr2):
if arr1[i] <= arr2[j]:
result.append(arr1[i])
i += 1
else:
result.append(arr2[j])
j += 1
result.extend(arr1[i:])
result.extend(arr2[j:])
return result
# 7. Remove Duplicates (In-place)
def remove_duplicates(arr):
'''Remove duplicates from sorted array in-place'''
if not arr:
return 0
write_idx = 1
for i in range(1, len(arr)):
if arr[i] != arr[i - 1]:
arr[write_idx] = arr[i]
write_idx += 1
return write_idx # New length
# 8. Find Missing Number
def find_missing(arr, n):
'''Array contains 0 to n-1 with one missing'''
# Method 1: Sum formula
expected_sum = n * (n - 1) // 2
actual_sum = sum(arr)
return expected_sum - actual_sum
# Method 2: XOR
def find_missing_xor(arr, n):
xor_all = 0
xor_arr = 0
for i in range(n):
xor_all ^= i
for num in arr:
xor_arr ^= num
return xor_all ^ xor_arr
# 9. Kadane's Algorithm (Max Subarray Sum)
def max_subarray_sum(arr):
max_ending_here = max_so_far = arr[0]
for num in arr[1:]:
max_ending_here = max(num, max_ending_here + num)
max_so_far = max(max_so_far, max_ending_here)
return max_so_far
# Example: [-2, 1, -3, 4, -1, 2, 1, -5, 4]
# Max subarray: [4, -1, 2, 1] = 6
# 10. Dutch National Flag (3-way partition)
def sort_colors(arr):
'''Sort array of 0s, 1s, 2s in-place'''
low = mid = 0
high = len(arr) - 1
while mid <= high:
if arr[mid] == 0:
arr[low], arr[mid] = arr[mid], arr[low]
low += 1
mid += 1
elif arr[mid] == 1:
mid += 1
else: # arr[mid] == 2
arr[mid], arr[high] = arr[high], arr[mid]
high -= 1
return arr
# 11. Array vs array module
import array
# Python list (dynamic, any type)
py_list = [1, 2, 3, 4, 5]
# array module (fixed type, more efficient)
int_array = array.array('i', [1, 2, 3, 4, 5])
# NumPy (numerical operations)
import numpy as np
np_array = np.array([1, 2, 3, 4, 5])
result = np_array * 2 # Vectorized operation
# 12. Common Patterns
def common_array_patterns():
arr = [1, 2, 3, 4, 5]
# Prefix sum
prefix = [0] * (len(arr) + 1)
for i in range(len(arr)):
prefix[i + 1] = prefix[i] + arr[i]
# Range sum query: O(1)
def range_sum(left, right):
return prefix[right + 1] - prefix[left]
# Frequency count
from collections import Counter
freq = Counter(arr)
# Two sum
def two_sum(target):
seen = {}
for i, num in enumerate(arr):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return NonePythonAnswer: Lists support O(1) access/append, O(n) insert/delete; common patterns include two pointers, sliding window, Kadane’s algorithm, array rotation, merging, and in-place operations.
Q103. Implement and explain linked list operations
# Linked Lists
# 1. Node Definition
class Node:
def __init__(self, data):
self.data = data
self.next = None
# 2. Singly Linked List
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
'''Add node at end - O(n)'''
new_node = Node(data)
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def prepend(self, data):
'''Add node at beginning - O(1)'''
new_node = Node(data)
new_node.next = self.head
self.head = new_node
def delete(self, data):
'''Delete first occurrence - O(n)'''
if not self.head:
return
if self.head.data == data:
self.head = self.head.next
return
current = self.head
while current.next:
if current.next.data == data:
current.next = current.next.next
return
current = current.next
def search(self, data):
'''Search for data - O(n)'''
current = self.head
while current:
if current.data == data:
return True
current = current.next
return False
def display(self):
'''Print all nodes'''
elements = []
current = self.head
while current:
elements.append(str(current.data))
current = current.next
print(" -> ".join(elements))
# 3. Doubly Linked List
class DNode:
def __init__(self, data):
self.data = data
self.next = None
self.prev = None
class DoublyLinkedList:
def __init__(self):
self.head = None
def append(self, data):
new_node = DNode(data)
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
new_node.prev = current
# 4. Reverse Linked List
def reverse_list(head):
'''Iterative reversal - O(n)'''
prev = None
current = head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
return prev
# Recursive reversal
def reverse_recursive(head):
if not head or not head.next:
return head
new_head = reverse_recursive(head.next)
head.next.next = head
head.next = None
return new_head
# 5. Detect Cycle (Floyd's Algorithm)
def has_cycle(head):
'''Detect cycle using slow/fast pointers'''
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
# Find cycle start
def detect_cycle_start(head):
slow = fast = head
# Detect cycle
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
# Find start
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
return None
# 6. Find Middle Node
def find_middle(head):
'''Slow/fast pointer technique'''
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
# 7. Merge Two Sorted Lists
def merge_sorted_lists(l1, l2):
dummy = Node(0)
current = dummy
while l1 and l2:
if l1.data <= l2.data:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
current.next = l1 if l1 else l2
return dummy.next
# 8. Remove Nth Node from End
def remove_nth_from_end(head, n):
'''Use two pointers n apart'''
dummy = Node(0)
dummy.next = head
first = second = dummy
# Move first n+1 steps ahead
for _ in range(n + 1):
first = first.next
# Move both until first reaches end
while first:
first = first.next
second = second.next
# Remove node
second.next = second.next.next
return dummy.next
# 9. Palindrome Check
def is_palindrome(head):
'''Check if linked list is palindrome'''
# Find middle
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# Reverse second half
prev = None
while slow:
next_node = slow.next
slow.next = prev
prev = slow
slow = next_node
# Compare halves
left, right = head, prev
while right:
if left.data != right.data:
return False
left = left.next
right = right.next
return True
# 10. Intersection of Two Lists
def find_intersection(headA, headB):
'''Find node where two lists intersect'''
if not headA or not headB:
return None
a, b = headA, headB
while a != b:
a = a.next if a else headB
b = b.next if b else headA
return a
# 11. LRU Cache with Linked List
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = {}
self.head = DNode(0)
self.tail = DNode(0)
self.head.next = self.tail
self.tail.prev = self.head
def get(self, key):
if key in self.cache:
node = self.cache[key]
self._remove(node)
self._add(node)
return node.data
return -1
def put(self, key, value):
if key in self.cache:
self._remove(self.cache[key])
node = DNode(value)
node.key = key
self._add(node)
self.cache[key] = node
if len(self.cache) > self.capacity:
lru = self.head.next
self._remove(lru)
del self.cache[lru.key]
def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _add(self, node):
prev = self.tail.prev
prev.next = node
node.prev = prev
node.next = self.tail
self.tail.prev = nodePythonAnswer: Linked lists support O(1) prepend, O(n) append/search/delete; common patterns include cycle detection (Floyd’s), reverse, merge sorted lists, find middle, palindrome check, and LRU cache.
Q104. Implement stack and solve stack-based problems
# Stacks (LIFO - Last In First Out)
# 1. Stack using List
class Stack:
def __init__(self):
self.items = []
def push(self, item):
'''Add item to top - O(1)'''
self.items.append(item)
def pop(self):
'''Remove and return top item - O(1)'''
if self.is_empty():
raise IndexError("Pop from empty stack")
return self.items.pop()
def peek(self):
'''Return top item without removing - O(1)'''
if self.is_empty():
raise IndexError("Peek from empty stack")
return self.items[-1]
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
# 2. Balanced Parentheses
def is_balanced(expression):
'''Check if parentheses are balanced'''
stack = []
matching = {'(': ')', '[': ']', '{': '}'}
for char in expression:
if char in matching:
stack.append(char)
elif char in matching.values():
if not stack or matching[stack.pop()] != char:
return False
return len(stack) == 0
# Examples:
# "([]{})" -> True
# "([)]" -> False
# "(((" -> False
# 3. Reverse String using Stack
def reverse_string(s):
stack = Stack()
for char in s:
stack.push(char)
reversed_str = ""
while not stack.is_empty():
reversed_str += stack.pop()
return reversed_str
# 4. Evaluate Postfix Expression
def evaluate_postfix(expression):
'''Evaluate RPN (Reverse Polish Notation)'''
stack = []
for token in expression.split():
if token in '+-*/':
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(a / b)
else:
stack.append(float(token))
return stack[0]
# Example: "3 4 + 2 *" = (3 + 4) * 2 = 14
# 5. Infix to Postfix Conversion
def infix_to_postfix(expression):
precedence = {'+': 1, '-': 1, '*': 2, '/': 2, '^': 3}
stack = []
output = []
for char in expression:
if char.isalnum():
output.append(char)
elif char == '(':
stack.append(char)
elif char == ')':
while stack and stack[-1] != '(':
output.append(stack.pop())
stack.pop() # Remove '('
else: # Operator
while (stack and stack[-1] != '(' and
precedence.get(stack[-1], 0) >= precedence.get(char, 0)):
output.append(stack.pop())
stack.append(char)
while stack:
output.append(stack.pop())
return ''.join(output)
# 6. Min Stack
class MinStack:
'''Stack with O(1) minimum retrieval'''
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val):
self.stack.append(val)
if not self.min_stack or val <= self.min_stack[-1]:
self.min_stack.append(val)
def pop(self):
if self.stack.pop() == self.min_stack[-1]:
self.min_stack.pop()
def top(self):
return self.stack[-1]
def get_min(self):
return self.min_stack[-1]
# 7. Next Greater Element
def next_greater_element(arr):
'''Find next greater element for each element'''
result = [-1] * len(arr)
stack = []
for i in range(len(arr) - 1, -1, -1):
while stack and stack[-1] <= arr[i]:
stack.pop()
if stack:
result[i] = stack[-1]
stack.append(arr[i])
return result
# Example: [4, 5, 2, 25]
# Result: [5, 25, 25, -1]
# 8. Stock Span Problem
def calculate_span(prices):
'''Calculate span of stock prices'''
stack = []
span = [0] * len(prices)
for i in range(len(prices)):
while stack and prices[stack[-1]] <= prices[i]:
stack.pop()
span[i] = i + 1 if not stack else i - stack[-1]
stack.append(i)
return span
# 9. Largest Rectangle in Histogram
def largest_rectangle_area(heights):
'''Find largest rectangle in histogram'''
stack = []
max_area = 0
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)
while stack:
height = heights[stack.pop()]
width = len(heights) if not stack else len(heights) - stack[-1] - 1
max_area = max(max_area, height * width)
return max_area
# 10. Stack using Queues
from collections import deque
class StackUsingQueues:
def __init__(self):
self.q = deque()
def push(self, x):
self.q.append(x)
# Rotate to make new element front
for _ in range(len(self.q) - 1):
self.q.append(self.q.popleft())
def pop(self):
return self.q.popleft()
def top(self):
return self.q[0]
# 11. Browser History
class BrowserHistory:
def __init__(self):
self.back_stack = []
self.forward_stack = []
self.current = None
def visit(self, url):
if self.current:
self.back_stack.append(self.current)
self.current = url
self.forward_stack.clear()
def back(self):
if self.back_stack:
self.forward_stack.append(self.current)
self.current = self.back_stack.pop()
return self.current
def forward(self):
if self.forward_stack:
self.back_stack.append(self.current)
self.current = self.forward_stack.pop()
return self.currentPythonAnswer: Stacks (LIFO) support O(1) push/pop; common uses include balanced parentheses, expression evaluation, next greater element, min stack, histogram problems, and browser history.
Q105. Implement queue and solve queue-based problems
# Queues (FIFO - First In First Out)
# 1. Queue using List (inefficient)
class SimpleQueue:
def __init__(self):
self.items = []
def enqueue(self, item):
'''Add to rear - O(1)'''
self.items.append(item)
def dequeue(self):
'''Remove from front - O(n) inefficient!'''
if self.is_empty():
raise IndexError("Dequeue from empty queue")
return self.items.pop(0)
def is_empty(self):
return len(self.items) == 0
# 2. Queue using collections.deque (efficient)
from collections import deque
class Queue:
def __init__(self):
self.items = deque()
def enqueue(self, item):
'''Add to rear - O(1)'''
self.items.append(item)
def dequeue(self):
'''Remove from front - O(1)'''
if self.is_empty():
raise IndexError("Dequeue from empty queue")
return self.items.popleft()
def front(self):
'''Peek front - O(1)'''
if self.is_empty():
raise IndexError("Queue is empty")
return self.items[0]
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
# 3. Circular Queue
class CircularQueue:
def __init__(self, k):
self.size = k
self.queue = [None] * k
self.front = -1
self.rear = -1
def enqueue(self, value):
if self.is_full():
return False
if self.is_empty():
self.front = 0
self.rear = (self.rear + 1) % self.size
self.queue[self.rear] = value
return True
def dequeue(self):
if self.is_empty():
return False
if self.front == self.rear:
self.front = self.rear = -1
else:
self.front = (self.front + 1) % self.size
return True
def is_empty(self):
return self.front == -1
def is_full(self):
return (self.rear + 1) % self.size == self.front
# 4. Priority Queue (using heapq)
import heapq
class PriorityQueue:
def __init__(self):
self.heap = []
self.counter = 0
def push(self, item, priority):
'''Lower priority number = higher priority'''
heapq.heappush(self.heap, (priority, self.counter, item))
self.counter += 1
def pop(self):
if self.is_empty():
raise IndexError("Pop from empty priority queue")
return heapq.heappop(self.heap)[2]
def is_empty(self):
return len(self.heap) == 0
# 5. Queue using Two Stacks
class QueueUsingStacks:
def __init__(self):
self.stack1 = []
self.stack2 = []
def enqueue(self, x):
'''O(1)'''
self.stack1.append(x)
def dequeue(self):
'''Amortized O(1)'''
if not self.stack2:
while self.stack1:
self.stack2.append(self.stack1.pop())
if not self.stack2:
raise IndexError("Dequeue from empty queue")
return self.stack2.pop()
# 6. BFS using Queue
def bfs_graph(graph, start):
'''Breadth-First Search'''
visited = set()
queue = deque([start])
result = []
while queue:
node = queue.popleft()
if node not in visited:
visited.add(node)
result.append(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
queue.append(neighbor)
return result
# 7. Level Order Traversal (Binary Tree)
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def level_order_traversal(root):
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
# 8. Sliding Window Maximum
def max_sliding_window(nums, k):
'''Maximum in each window of size k'''
result = []
dq = deque()
for i, num in enumerate(nums):
# Remove elements outside window
while dq and dq[0] < i - k + 1:
dq.popleft()
# Remove smaller elements
while dq and nums[dq[-1]] < num:
dq.pop()
dq.append(i)
if i >= k - 1:
result.append(nums[dq[0]])
return result
# 9. First Non-Repeating Character in Stream
class FirstUnique:
def __init__(self):
self.queue = deque()
self.count = {}
def add(self, char):
self.queue.append(char)
self.count[char] = self.count.get(char, 0) + 1
def get_first_unique(self):
while self.queue:
if self.count[self.queue[0]] == 1:
return self.queue[0]
self.queue.popleft()
return None
# 10. Task Scheduler
def least_interval(tasks, n):
'''Minimum time to complete tasks with cooldown'''
from collections import Counter
count = Counter(tasks)
max_count = max(count.values())
max_tasks = sum(1 for c in count.values() if c == max_count)
intervals = (max_count - 1) * (n + 1) + max_tasks
return max(intervals, len(tasks))
# 11. Deque (Double-Ended Queue)
class Deque:
def __init__(self):
self.items = deque()
def add_front(self, item):
self.items.appendleft(item)
def add_rear(self, item):
self.items.append(item)
def remove_front(self):
return self.items.popleft()
def remove_rear(self):
return self.items.pop()PythonAnswer: Queues (FIFO) use deque for O(1) enqueue/dequeue; common uses include BFS, level-order traversal, sliding window, circular queue, priority queue, and task scheduling.
Q106. Explain hash tables and solve hash-based problems
# Hash Tables (Dictionaries)
# 1. Dictionary Basics
# Average O(1) for get/set/delete
person = {'name': 'Alice', 'age': 30, 'city': 'NYC'}
# Access: O(1)
print(person['name'])
print(person.get('age', 0))
# Insert/Update: O(1)
person['email'] = 'alice@example.com'
person['age'] = 31
# Delete: O(1)
del person['city']
person.pop('email', None)
# 2. Hash Table Implementation
class HashTable:
def __init__(self, size=10):
self.size = size
self.table = [[] for _ in range(size)]
def _hash(self, key):
'''Simple hash function'''
return hash(key) % self.size
def put(self, key, value):
'''Insert or update - O(1) average'''
index = self._hash(key)
for i, (k, v) in enumerate(self.table[index]):
if k == key:
self.table[index][i] = (key, value)
return
self.table[index].append((key, value))
def get(self, key):
'''Retrieve value - O(1) average'''
index = self._hash(key)
for k, v in self.table[index]:
if k == key:
return v
raise KeyError(key)
def delete(self, key):
'''Remove key - O(1) average'''
index = self._hash(key)
for i, (k, v) in enumerate(self.table[index]):
if k == key:
del self.table[index][i]
return
raise KeyError(key)
# 3. Two Sum Problem
def two_sum(nums, target):
'''Find two numbers that sum to target'''
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return None
# Example: [2, 7, 11, 15], target=9
# Result: [0, 1] (2 + 7 = 9)
# 4. First Unique Character
def first_unique_char(s):
'''Find first non-repeating character'''
from collections import Counter
count = Counter(s)
for i, char in enumerate(s):
if count[char] == 1:
return i
return -1
# 5. Group Anagrams
def group_anagrams(words):
'''Group words that are anagrams'''
from collections import defaultdict
groups = defaultdict(list)
for word in words:
key = tuple(sorted(word))
groups[key].append(word)
return list(groups.values())
# Example: ['eat', 'tea', 'tan', 'ate', 'nat', 'bat']
# Result: [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]
# 6. LRU Cache
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return -1
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
self.cache.popitem(last=False)
# 7. Subarray Sum Equals K
def subarray_sum(nums, k):
'''Count subarrays with sum k'''
count = 0
prefix_sum = 0
sum_freq = {0: 1}
for num in nums:
prefix_sum += num
if prefix_sum - k in sum_freq:
count += sum_freq[prefix_sum - k]
sum_freq[prefix_sum] = sum_freq.get(prefix_sum, 0) + 1
return count
# 8. Longest Consecutive Sequence
def longest_consecutive(nums):
'''Find length of longest consecutive sequence'''
num_set = set(nums)
longest = 0
for num in num_set:
if num - 1 not in num_set:
current = num
length = 1
while current + 1 in num_set:
current += 1
length += 1
longest = max(longest, length)
return longest
# Example: [100, 4, 200, 1, 3, 2]
# Result: 4 (sequence: 1, 2, 3, 4)
# 9. Top K Frequent Elements
def top_k_frequent(nums, k):
'''Find k most frequent elements'''
from collections import Counter
import heapq
count = Counter(nums)
return heapq.nlargest(k, count.keys(), key=count.get)
# 10. Valid Anagram
def is_anagram(s, t):
'''Check if two strings are anagrams'''
if len(s) != len(t):
return False
from collections import Counter
return Counter(s) == Counter(t)
# Alternative: sorting
def is_anagram_sort(s, t):
return sorted(s) == sorted(t)
# 11. Contains Duplicate
def contains_duplicate(nums):
'''Check if array has duplicates'''
return len(nums) != len(set(nums))
# 12. Isomorphic Strings
def is_isomorphic(s, t):
'''Check if strings are isomorphic'''
if len(s) != len(t):
return False
s_to_t = {}
t_to_s = {}
for c1, c2 in zip(s, t):
if c1 in s_to_t:
if s_to_t[c1] != c2:
return False
else:
s_to_t[c1] = c2
if c2 in t_to_s:
if t_to_s[c2] != c1:
return False
else:
t_to_s[c2] = c1
return True
# 13. Custom Hash Function
def custom_hash(s, mod=10**9 + 7):
'''Rolling hash for strings'''
hash_value = 0
p = 31
p_power = 1
for char in s:
hash_value = (hash_value + (ord(char) - ord('a') + 1) * p_power) % mod
p_power = (p_power * p) % mod
return hash_value
# 14. Design HashMap
class MyHashMap:
def __init__(self):
self.size = 1000
self.buckets = [[] for _ in range(self.size)]
def put(self, key, value):
bucket = self.buckets[key % self.size]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
bucket.append((key, value))
def get(self, key):
bucket = self.buckets[key % self.size]
for k, v in bucket:
if k == key:
return v
return -1
def remove(self, key):
bucket = self.buckets[key % self.size]
for i, (k, v) in enumerate(bucket):
if k == key:
del bucket[i]
returnPythonAnswer: Hash tables provide O(1) average get/set/delete; common uses include two sum, anagrams, frequency counting, LRU cache, subarray sum, and longest consecutive sequence.
Q107. Implement binary tree operations and algorithms
# Binary Trees
# 1. Tree Node Definition
class TreeNode:
def __init__(self, val=0):
self.val = val
self.left = None
self.right = None
# 2. Tree Traversals
# Inorder (Left, Root, Right) - sorted for BST
def inorder_traversal(root):
result = []
def inorder(node):
if not node:
return
inorder(node.left)
result.append(node.val)
inorder(node.right)
inorder(root)
return result
# Preorder (Root, Left, Right)
def preorder_traversal(root):
result = []
def preorder(node):
if not node:
return
result.append(node.val)
preorder(node.left)
preorder(node.right)
preorder(root)
return result
# Postorder (Left, Right, Root)
def postorder_traversal(root):
result = []
def postorder(node):
if not node:
return
postorder(node.left)
postorder(node.right)
result.append(node.val)
postorder(root)
return result
# 3. Iterative Traversals
def inorder_iterative(root):
result = []
stack = []
current = root
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
result.append(current.val)
current = current.right
return result
# 4. Level Order Traversal (BFS)
from collections import deque
def level_order(root):
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
# 5. Maximum Depth
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
# 6. Validate BST
def is_valid_bst(root):
def validate(node, min_val, max_val):
if not node:
return True
if not (min_val < node.val < max_val):
return False
return (validate(node.left, min_val, node.val) and
validate(node.right, node.val, max_val))
return validate(root, float('-inf'), float('inf'))
# 7. Lowest Common Ancestor
def lowest_common_ancestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowest_common_ancestor(root.left, p, q)
right = lowest_common_ancestor(root.right, p, q)
if left and right:
return root
return left if left else right
# 8. Diameter of Binary Tree
def diameter_of_tree(root):
diameter = 0
def height(node):
nonlocal diameter
if not node:
return 0
left_height = height(node.left)
right_height = height(node.right)
diameter = max(diameter, left_height + right_height)
return max(left_height, right_height) + 1
height(root)
return diameter
# 9. Invert Binary Tree
def invert_tree(root):
if not root:
return None
root.left, root.right = root.right, root.left
invert_tree(root.left)
invert_tree(root.right)
return root
# 10. Path Sum
def has_path_sum(root, target_sum):
if not root:
return False
if not root.left and not root.right:
return root.val == target_sum
return (has_path_sum(root.left, target_sum - root.val) or
has_path_sum(root.right, target_sum - root.val))
# 11. Serialize and Deserialize
def serialize(root):
'''Convert tree to string'''
if not root:
return 'null'
return f"{root.val},{serialize(root.left)},{serialize(root.right)}"
def deserialize(data):
'''Convert string to tree'''
def build():
val = next(values)
if val == 'null':
return None
node = TreeNode(int(val))
node.left = build()
node.right = build()
return node
values = iter(data.split(','))
return build()
# 12. Construct Tree from Traversals
def build_tree(preorder, inorder):
'''Build tree from preorder and inorder'''
if not preorder or not inorder:
return None
root = TreeNode(preorder[0])
mid = inorder.index(preorder[0])
root.left = build_tree(preorder[1:mid+1], inorder[:mid])
root.right = build_tree(preorder[mid+1:], inorder[mid+1:])
return root
# 13. Right Side View
def right_side_view(root):
'''View tree from right side'''
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
for i in range(level_size):
node = queue.popleft()
if i == level_size - 1:
result.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return result
# 14. Count Complete Tree Nodes
def count_nodes(root):
if not root:
return 0
def get_height(node):
height = 0
while node:
height += 1
node = node.left
return height
left_height = get_height(root.left)
right_height = get_height(root.right)
if left_height == right_height:
return (1 << left_height) + count_nodes(root.right)
else:
return (1 << right_height) + count_nodes(root.left)PythonAnswer: Binary trees support traversals (inorder/preorder/postorder/level-order), validation, depth calculation, diameter, path sum, LCA, serialization, and construction from traversals.
Q108. Implement Binary Search Tree operations
# Binary Search Trees (BST)
# 1. BST Node
class BSTNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
# 2. BST Operations
class BST:
def __init__(self):
self.root = None
def insert(self, val):
'''Insert value - O(log n) average, O(n) worst'''
if not self.root:
self.root = BSTNode(val)
return
self._insert_recursive(self.root, val)
def _insert_recursive(self, node, val):
if val < node.val:
if node.left:
self._insert_recursive(node.left, val)
else:
node.left = BSTNode(val)
else:
if node.right:
self._insert_recursive(node.right, val)
else:
node.right = BSTNode(val)
def search(self, val):
'''Search for value - O(log n) average'''
return self._search_recursive(self.root, val)
def _search_recursive(self, node, val):
if not node:
return False
if node.val == val:
return True
elif val < node.val:
return self._search_recursive(node.left, val)
else:
return self._search_recursive(node.right, val)
def delete(self, val):
'''Delete value - O(log n) average'''
self.root = self._delete_recursive(self.root, val)
def _delete_recursive(self, node, val):
if not node:
return None
if val < node.val:
node.left = self._delete_recursive(node.left, val)
elif val > node.val:
node.right = self._delete_recursive(node.right, val)
else:
# Node found
if not node.left:
return node.right
elif not node.right:
return node.left
# Node has two children
min_node = self._find_min(node.right)
node.val = min_node.val
node.right = self._delete_recursive(node.right, min_node.val)
return node
def _find_min(self, node):
while node.left:
node = node.left
return node
def inorder(self):
'''Inorder traversal gives sorted order'''
result = []
self._inorder_recursive(self.root, result)
return result
def _inorder_recursive(self, node, result):
if node:
self._inorder_recursive(node.left, result)
result.append(node.val)
self._inorder_recursive(node.right, result)
# 3. Kth Smallest Element
def kth_smallest(root, k):
'''Find kth smallest element in BST'''
stack = []
current = root
count = 0
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
count += 1
if count == k:
return current.val
current = current.right
return None
# 4. BST from Sorted Array
def sorted_array_to_bst(nums):
'''Create balanced BST from sorted array'''
if not nums:
return None
mid = len(nums) // 2
root = BSTNode(nums[mid])
root.left = sorted_array_to_bst(nums[:mid])
root.right = sorted_array_to_bst(nums[mid+1:])
return root
# 5. Range Sum in BST
def range_sum_bst(root, low, high):
'''Sum of values in range [low, high]'''
if not root:
return 0
total = 0
if low <= root.val <= high:
total += root.val
if root.val > low:
total += range_sum_bst(root.left, low, high)
if root.val < high:
total += range_sum_bst(root.right, low, high)
return total
# 6. BST Iterator
class BSTIterator:
'''In-order iterator for BST'''
def __init__(self, root):
self.stack = []
self._push_left(root)
def _push_left(self, node):
while node:
self.stack.append(node)
node = node.left
def next(self):
node = self.stack.pop()
self._push_left(node.right)
return node.val
def has_next(self):
return len(self.stack) > 0
# 7. Trim BST
def trim_bst(root, low, high):
'''Remove nodes outside range [low, high]'''
if not root:
return None
if root.val < low:
return trim_bst(root.right, low, high)
if root.val > high:
return trim_bst(root.left, low, high)
root.left = trim_bst(root.left, low, high)
root.right = trim_bst(root.right, low, high)
return root
# 8. Closest Value in BST
def closest_value(root, target):
'''Find value closest to target'''
closest = root.val
while root:
if abs(root.val - target) < abs(closest - target):
closest = root.val
root = root.left if target < root.val else root.right
return closest
# 9. Two Sum IV - BST
def find_target(root, k):
'''Check if two nodes sum to k'''
def inorder(node):
if not node:
return []
return inorder(node.left) + [node.val] + inorder(node.right)
nums = inorder(root)
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == k:
return True
elif total < k:
left += 1
else:
right -= 1
return False
# 10. Convert BST to Greater Tree
def convert_bst(root):
'''Each node's value = sum of all greater values + itself'''
total = 0
def reverse_inorder(node):
nonlocal total
if not node:
return
reverse_inorder(node.right)
total += node.val
node.val = total
reverse_inorder(node.left)
reverse_inorder(root)
return rootPythonAnswer: BST provides O(log n) average insert/search/delete, supports ordered traversal, kth smallest, range queries, and can be balanced from sorted arrays.
Q109. Implement heap operations and solve heap problems
# Heaps and Priority Queues
# 1. Min Heap using heapq
import heapq
class MinHeap:
def __init__(self):
self.heap = []
def push(self, val):
'''Insert element - O(log n)'''
heapq.heappush(self.heap, val)
def pop(self):
'''Remove and return minimum - O(log n)'''
if not self.heap:
raise IndexError("Pop from empty heap")
return heapq.heappop(self.heap)
def peek(self):
'''Get minimum without removing - O(1)'''
if not self.heap:
raise IndexError("Peek from empty heap")
return self.heap[0]
def size(self):
return len(self.heap)
# 2. Max Heap (invert values)
class MaxHeap:
def __init__(self):
self.heap = []
def push(self, val):
heapq.heappush(self.heap, -val)
def pop(self):
return -heapq.heappop(self.heap)
def peek(self):
return -self.heap[0]
# 3. Kth Largest Element
def find_kth_largest(nums, k):
'''Find kth largest using min heap'''
heap = []
for num in nums:
heapq.heappush(heap, num)
if len(heap) > k:
heapq.heappop(heap)
return heap[0]
# Alternative: using heapq.nlargest
def find_kth_largest_simple(nums, k):
return heapq.nlargest(k, nums)[-1]
# 4. Merge K Sorted Lists
def merge_k_sorted_lists(lists):
'''Merge k sorted linked lists using heap'''
heap = []
# Add first node from each list
for i, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst.val, i, lst))
dummy = current = Node(0)
while heap:
val, i, node = heapq.heappop(heap)
current.next = node
current = current.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
# 5. Top K Frequent Elements
def top_k_frequent(nums, k):
'''Find k most frequent elements'''
from collections import Counter
count = Counter(nums)
return heapq.nlargest(k, count.keys(), key=count.get)
# 6. K Closest Points to Origin
def k_closest(points, k):
'''Find k closest points to (0, 0)'''
heap = []
for x, y in points:
dist = -(x*x + y*y) # Negative for max heap
if len(heap) < k:
heapq.heappush(heap, (dist, [x, y]))
elif dist > heap[0][0]:
heapq.heapreplace(heap, (dist, [x, y]))
return [point for _, point in heap]
# 7. Median from Data Stream
class MedianFinder:
'''Find median efficiently using two heaps'''
def __init__(self):
self.small = [] # Max heap (left half)
self.large = [] # Min heap (right half)
def add_num(self, num):
# Add to max heap (small)
heapq.heappush(self.small, -num)
# Balance: move largest from small to large
if self.small and self.large and (-self.small[0] > self.large[0]):
val = -heapq.heappop(self.small)
heapq.heappush(self.large, val)
# Maintain size property
if len(self.small) > len(self.large) + 1:
val = -heapq.heappop(self.small)
heapq.heappush(self.large, val)
if len(self.large) > len(self.small):
val = heapq.heappop(self.large)
heapq.heappush(self.small, -val)
def find_median(self):
if len(self.small) > len(self.large):
return -self.small[0]
return (-self.small[0] + self.large[0]) / 2
# 8. Task Scheduler
def least_interval(tasks, n):
'''Minimum intervals to complete tasks with cooldown'''
from collections import Counter
count = Counter(tasks)
max_heap = [-c for c in count.values()]
heapq.heapify(max_heap)
time = 0
while max_heap:
temp = []
for _ in range(n + 1):
if max_heap:
temp.append(heapq.heappop(max_heap))
for item in temp:
if item + 1 < 0:
heapq.heappush(max_heap, item + 1)
time += (n + 1) if max_heap else len(temp)
return time
# 9. Heap Sort
def heap_sort(arr):
'''Sort using heap - O(n log n)'''
heapq.heapify(arr)
return [heapq.heappop(arr) for _ in range(len(arr))]
# 10. Sliding Window Median
def median_sliding_window(nums, k):
'''Median of each sliding window'''
result = []
window = sorted(nums[:k])
for i in range(k, len(nums) + 1):
# Calculate median
if k % 2 == 0:
median = (window[k//2 - 1] + window[k//2]) / 2
else:
median = window[k//2]
result.append(median)
if i < len(nums):
# Remove outgoing element
window.remove(nums[i - k])
# Add incoming element
bisect.insort(window, nums[i])
return result
# 11. Reorganize String
def reorganize_string(s):
'''Rearrange so no two same chars adjacent'''
from collections import Counter
count = Counter(s)
max_heap = [(-freq, char) for char, freq in count.items()]
heapq.heapify(max_heap)
result = []
prev_freq, prev_char = 0, ''
while max_heap:
freq, char = heapq.heappop(max_heap)
result.append(char)
if prev_freq < 0:
heapq.heappush(max_heap, (prev_freq, prev_char))
prev_freq, prev_char = freq + 1, char
result_str = ''.join(result)
return result_str if len(result_str) == len(s) else ''
# 12. Custom Comparator Heap
class Task:
def __init__(self, priority, name):
self.priority = priority
self.name = name
def __lt__(self, other):
return self.priority < other.priority
def priority_queue_example():
heap = []
heapq.heappush(heap, Task(3, "Low"))
heapq.heappush(heap, Task(1, "High"))
heapq.heappush(heap, Task(2, "Medium"))
while heap:
task = heapq.heappop(heap)
print(f"{task.name}: {task.priority}")PythonAnswer: Heaps provide O(log n) insert/delete and O(1) peek; use for priority queues, kth largest, median finding, merge k lists, and scheduling problems.
Q110. Implement graph algorithms (DFS, BFS, cycles, paths)
# Graph Algorithms
# 1. Graph Representations
# Adjacency List (most common)
graph_list = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}
# Adjacency Matrix
graph_matrix = [
[0, 1, 1, 0, 0, 0], # A
[1, 0, 0, 1, 1, 0], # B
[1, 0, 0, 0, 0, 1], # C
[0, 1, 0, 0, 0, 0], # D
[0, 1, 0, 0, 0, 1], # E
[0, 0, 1, 0, 1, 0] # F
]
# 2. Graph Class
class Graph:
def __init__(self):
self.graph = {}
def add_edge(self, u, v, directed=False):
if u not in self.graph:
self.graph[u] = []
self.graph[u].append(v)
if not directed:
if v not in self.graph:
self.graph[v] = []
self.graph[v].append(u)
def get_vertices(self):
return list(self.graph.keys())
def get_edges(self):
edges = []
for u in self.graph:
for v in self.graph[u]:
edges.append((u, v))
return edges
# 3. Depth-First Search (DFS)
def dfs_recursive(graph, start, visited=None):
'''DFS using recursion'''
if visited is None:
visited = set()
visited.add(start)
result = [start]
for neighbor in graph.get(start, []):
if neighbor not in visited:
result.extend(dfs_recursive(graph, neighbor, visited))
return result
def dfs_iterative(graph, start):
'''DFS using stack'''
visited = set()
stack = [start]
result = []
while stack:
node = stack.pop()
if node not in visited:
visited.add(node)
result.append(node)
# Add neighbors in reverse order
for neighbor in reversed(graph.get(node, [])):
if neighbor not in visited:
stack.append(neighbor)
return result
# 4. Breadth-First Search (BFS)
from collections import deque
def bfs(graph, start):
'''BFS using queue'''
visited = set([start])
queue = deque([start])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
return result
# 5. Detect Cycle in Undirected Graph
def has_cycle_undirected(graph):
'''Detect cycle using DFS'''
visited = set()
def dfs(node, parent):
visited.add(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
if dfs(neighbor, node):
return True
elif neighbor != parent:
return True
return False
for node in graph:
if node not in visited:
if dfs(node, None):
return True
return False
# 6. Detect Cycle in Directed Graph
def has_cycle_directed(graph):
'''Detect cycle using colors'''
WHITE, GRAY, BLACK = 0, 1, 2
color = {node: WHITE for node in graph}
def dfs(node):
if color[node] == GRAY:
return True
if color[node] == BLACK:
return False
color[node] = GRAY
for neighbor in graph.get(node, []):
if dfs(neighbor):
return True
color[node] = BLACK
return False
for node in graph:
if color[node] == WHITE:
if dfs(node):
return True
return False
# 7. Topological Sort
def topological_sort(graph):
'''Topological ordering of DAG'''
visited = set()
stack = []
def dfs(node):
visited.add(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
dfs(neighbor)
stack.append(node)
for node in graph:
if node not in visited:
dfs(node)
return stack[::-1]
# 8. Shortest Path (BFS for unweighted)
def shortest_path_bfs(graph, start, end):
'''Find shortest path using BFS'''
if start == end:
return [start]
visited = {start}
queue = deque([(start, [start])])
while queue:
node, path = queue.popleft()
for neighbor in graph.get(node, []):
if neighbor == end:
return path + [neighbor]
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return None
# 9. Number of Connected Components
def count_components(n, edges):
'''Count connected components using Union-Find'''
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(x, y):
root_x, root_y = find(x), find(y)
if root_x != root_y:
parent[root_x] = root_y
return True
return False
components = n
for u, v in edges:
if union(u, v):
components -= 1
return components
# 10. Clone Graph
class GraphNode:
def __init__(self, val=0):
self.val = val
self.neighbors = []
def clone_graph(node):
'''Deep copy of graph'''
if not node:
return None
clones = {}
def dfs(node):
if node in clones:
return clones[node]
clone = GraphNode(node.val)
clones[node] = clone
for neighbor in node.neighbors:
clone.neighbors.append(dfs(neighbor))
return clone
return dfs(node)
# 11. Course Schedule (Cycle Detection)
def can_finish(num_courses, prerequisites):
'''Check if courses can be completed'''
graph = {i: [] for i in range(num_courses)}
for course, prereq in prerequisites:
graph[course].append(prereq)
return not has_cycle_directed(graph)
# 12. All Paths from Source to Target
def all_paths_source_target(graph):
'''Find all paths from 0 to n-1'''
n = len(graph)
result = []
def dfs(node, path):
if node == n - 1:
result.append(path[:])
return
for neighbor in graph[node]:
path.append(neighbor)
dfs(neighbor, path)
path.pop()
dfs(0, [0])
return result
# 13. Is Bipartite
def is_bipartite(graph):
'''Check if graph can be 2-colored'''
color = {}
def dfs(node, c):
color[node] = c
for neighbor in graph.get(node, []):
if neighbor in color:
if color[neighbor] == c:
return False
else:
if not dfs(neighbor, 1 - c):
return False
return True
for node in graph:
if node not in color:
if not dfs(node, 0):
return False
return TruePythonAnswer: Graphs use adjacency lists/matrices; support DFS (O(V+E)), BFS (O(V+E)), cycle detection, topological sort, shortest paths, connected components, and bipartite checking.
Q111. Implement and compare sorting algorithms
# Sorting Algorithms
# 1. Bubble Sort - O(n²)
def bubble_sort(arr):
'''Compare adjacent elements, swap if wrong order'''
n = len(arr)
for i in range(n):
swapped = False
for j in range(n - i - 1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
swapped = True
if not swapped:
break
return arr
# 2. Selection Sort - O(n²)
def selection_sort(arr):
'''Find minimum, place at beginning'''
n = len(arr)
for i in range(n):
min_idx = i
for j in range(i + 1, n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
# 3. Insertion Sort - O(n²)
def insertion_sort(arr):
'''Insert each element in sorted position'''
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
return arr
# 4. Merge Sort - O(n log n)
def merge_sort(arr):
'''Divide and conquer, merge sorted halves'''
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# 5. Quick Sort - O(n log n) average, O(n²) worst
def quick_sort(arr):
'''Pick pivot, partition, recursively sort'''
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
# In-place quick sort
def quick_sort_inplace(arr, low=0, high=None):
if high is None:
high = len(arr) - 1
if low < high:
pi = partition(arr, low, high)
quick_sort_inplace(arr, low, pi - 1)
quick_sort_inplace(arr, pi + 1, high)
return arr
def partition(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
# 6. Heap Sort - O(n log n)
def heap_sort(arr):
'''Build max heap, extract max repeatedly'''
n = len(arr)
# Build max heap
for i in range(n // 2 - 1, -1, -1):
heapify(arr, n, i)
# Extract elements
for i in range(n - 1, 0, -1):
arr[0], arr[i] = arr[i], arr[0]
heapify(arr, i, 0)
return arr
def heapify(arr, n, i):
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[left] > arr[largest]:
largest = left
if right < n and arr[right] > arr[largest]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(arr, n, largest)
# 7. Counting Sort - O(n + k)
def counting_sort(arr):
'''For integers in limited range'''
if not arr:
return arr
max_val = max(arr)
min_val = min(arr)
range_size = max_val - min_val + 1
count = [0] * range_size
output = [0] * len(arr)
# Count occurrences
for num in arr:
count[num - min_val] += 1
# Cumulative count
for i in range(1, len(count)):
count[i] += count[i - 1]
# Build output
for num in reversed(arr):
output[count[num - min_val] - 1] = num
count[num - min_val] -= 1
return output
# 8. Radix Sort - O(d * n)
def radix_sort(arr):
'''Sort by digits from least to most significant'''
max_val = max(arr)
exp = 1
while max_val // exp > 0:
counting_sort_by_digit(arr, exp)
exp *= 10
return arr
def counting_sort_by_digit(arr, exp):
n = len(arr)
output = [0] * n
count = [0] * 10
for num in arr:
index = (num // exp) % 10
count[index] += 1
for i in range(1, 10):
count[i] += count[i - 1]
for num in reversed(arr):
index = (num // exp) % 10
output[count[index] - 1] = num
count[index] -= 1
for i in range(n):
arr[i] = output[i]
# 9. Tim Sort (Python's built-in)
def tim_sort(arr):
'''Hybrid of merge sort and insertion sort'''
return sorted(arr) # Python uses Timsort
# 10. Comparison of Sorting Algorithms
sorting_comparison = '''
Algorithm | Time Best | Time Avg | Time Worst | Space | Stable
---------------|-----------|-----------|------------|-------|-------
Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | Yes
Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | No
Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | Yes
Merge Sort | O(n log n)| O(n log n)| O(n log n) | O(n) | Yes
Quick Sort | O(n log n)| O(n log n)| O(n²) | O(log n)| No
Heap Sort | O(n log n)| O(n log n)| O(n log n) | O(1) | No
Counting Sort | O(n + k) | O(n + k) | O(n + k) | O(k) | Yes
Radix Sort | O(d*n) | O(d*n) | O(d*n) | O(n+k)| Yes
'''
# 11. Custom Sort Key
def custom_sort_examples():
# Sort by length
words = ["apple", "pie", "a", "zoo"]
sorted_words = sorted(words, key=len)
# Sort tuples by second element
pairs = [(1, 5), (3, 2), (2, 8)]
sorted_pairs = sorted(pairs, key=lambda x: x[1])
# Sort strings case-insensitive
names = ["Alice", "bob", "Charlie"]
sorted_names = sorted(names, key=str.lower)
# Reverse sort
nums = [3, 1, 4, 1, 5]
sorted_desc = sorted(nums, reverse=True)
return sorted_words, sorted_pairs, sorted_names, sorted_desc
# 12. Stable vs Unstable Sort
def demonstrate_stability():
'''Stable sort preserves order of equal elements'''
data = [(1, 'a'), (2, 'b'), (1, 'c'), (2, 'd')]
# Stable sort (preserves original order of equal keys)
stable_sorted = sorted(data, key=lambda x: x[0])
# Result: [(1, 'a'), (1, 'c'), (2, 'b'), (2, 'd')]
return stable_sortedPythonAnswer: Sorting algorithms: Bubble/Selection/Insertion O(n²), Merge/Quick/Heap O(n log n), Counting/Radix O(n); choose based on data size, range, stability needs, and space constraints.
Q112. Implement binary search and its variants
# Binary Search and Variants
# 1. Basic Binary Search - O(log n)
def binary_search(arr, target):
'''Search in sorted array'''
left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# Recursive version
def binary_search_recursive(arr, target, left=0, right=None):
if right is None:
right = len(arr) - 1
if left > right:
return -1
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recursive(arr, target, mid + 1, right)
else:
return binary_search_recursive(arr, target, left, mid - 1)
# 2. First and Last Position
def search_range(nums, target):
'''Find first and last occurrence'''
def find_first():
left, right = 0, len(nums) - 1
result = -1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
result = mid
right = mid - 1 # Continue searching left
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
def find_last():
left, right = 0, len(nums) - 1
result = -1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
result = mid
left = mid + 1 # Continue searching right
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return result
return [find_first(), find_last()]
# 3. Search in Rotated Sorted Array
def search_rotated(nums, target):
'''Search in rotated sorted array'''
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# Left half is sorted
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
# Right half is sorted
else:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
# 4. Find Minimum in Rotated Array
def find_min_rotated(nums):
'''Find minimum in rotated sorted array'''
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
return nums[left]
# 5. Peak Element
def find_peak_element(nums):
'''Find any peak element (greater than neighbors)'''
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[mid + 1]:
right = mid
else:
left = mid + 1
return left
# 6. Square Root (Binary Search)
def my_sqrt(x):
'''Find integer square root'''
if x < 2:
return x
left, right = 1, x // 2
while left <= right:
mid = (left + right) // 2
if mid * mid == x:
return mid
elif mid * mid < x:
left = mid + 1
else:
right = mid - 1
return right
# 7. Search 2D Matrix
def search_matrix(matrix, target):
'''Search in row and column sorted matrix'''
if not matrix or not matrix[0]:
return False
m, n = len(matrix), len(matrix[0])
left, right = 0, m * n - 1
while left <= right:
mid = (left + right) // 2
mid_val = matrix[mid // n][mid % n]
if mid_val == target:
return True
elif mid_val < target:
left = mid + 1
else:
right = mid - 1
return False
# 8. Find K Closest Elements
def find_closest_elements(arr, k, x):
'''Find k closest elements to x'''
left, right = 0, len(arr) - k
while left < right:
mid = (left + right) // 2
if x - arr[mid] > arr[mid + k] - x:
left = mid + 1
else:
right = mid
return arr[left:left + k]
# 9. Capacity to Ship Packages
def ship_within_days(weights, days):
'''Minimum ship capacity to ship in given days'''
def can_ship(capacity):
current_weight = 0
days_needed = 1
for weight in weights:
if current_weight + weight > capacity:
days_needed += 1
current_weight = weight
else:
current_weight += weight
return days_needed <= days
left, right = max(weights), sum(weights)
while left < right:
mid = (left + right) // 2
if can_ship(mid):
right = mid
else:
left = mid + 1
return left
# 10. Median of Two Sorted Arrays
def find_median_sorted_arrays(nums1, nums2):
'''Find median of two sorted arrays - O(log(min(m,n)))'''
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m, n = len(nums1), len(nums2)
left, right = 0, m
while left <= right:
partition1 = (left + right) // 2
partition2 = (m + n + 1) // 2 - partition1
max_left1 = float('-inf') if partition1 == 0 else nums1[partition1 - 1]
min_right1 = float('inf') if partition1 == m else nums1[partition1]
max_left2 = float('-inf') if partition2 == 0 else nums2[partition2 - 1]
min_right2 = float('inf') if partition2 == n else nums2[partition2]
if max_left1 <= min_right2 and max_left2 <= min_right1:
if (m + n) % 2 == 0:
return (max(max_left1, max_left2) + min(min_right1, min_right2)) / 2
else:
return max(max_left1, max_left2)
elif max_left1 > min_right2:
right = partition1 - 1
else:
left = partition1 + 1
return 0
# 11. Binary Search Template
def binary_search_template(arr, target):
'''General template for binary search'''
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2 # Avoid overflow
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
# Post-processing:
# - left is insertion point
# - right is last element < target
return -1
# 12. Lower and Upper Bound
import bisect
def lower_bound(arr, target):
'''Find first position >= target'''
return bisect.bisect_left(arr, target)
def upper_bound(arr, target):
'''Find first position > target'''
return bisect.bisect_right(arr, target)PythonAnswer: Binary search runs in O(log n) on sorted data; variants include finding first/last occurrence, searching rotated arrays, finding peaks, 2D matrix search, and capacity problems.
Q113. Implement string pattern matching algorithms
# String Pattern Matching Algorithms
# 1. Naive Pattern Matching - O(nm)
def naive_search(text, pattern):
'''Brute force pattern search'''
n, m = len(text), len(pattern)
positions = []
for i in range(n - m + 1):
j = 0
while j < m and text[i + j] == pattern[j]:
j += 1
if j == m:
positions.append(i)
return positions
# 2. KMP Algorithm - O(n + m)
def kmp_search(text, pattern):
'''Knuth-Morris-Pratt pattern matching'''
def compute_lps(pattern):
'''Longest Proper Prefix which is also Suffix'''
m = len(pattern)
lps = [0] * m
length = 0
i = 1
while i < m:
if pattern[i] == pattern[length]:
length += 1
lps[i] = length
i += 1
else:
if length != 0:
length = lps[length - 1]
else:
lps[i] = 0
i += 1
return lps
n, m = len(text), len(pattern)
lps = compute_lps(pattern)
positions = []
i = j = 0
while i < n:
if text[i] == pattern[j]:
i += 1
j += 1
if j == m:
positions.append(i - j)
j = lps[j - 1]
elif i < n and text[i] != pattern[j]:
if j != 0:
j = lps[j - 1]
else:
i += 1
return positions
# 3. Rabin-Karp Algorithm - O(n + m)
def rabin_karp(text, pattern, prime=101):
'''Rolling hash pattern matching'''
n, m = len(text), len(pattern)
d = 256 # Number of characters
h = pow(d, m - 1, prime)
p_hash = 0 # Pattern hash
t_hash = 0 # Text window hash
positions = []
# Calculate initial hashes
for i in range(m):
p_hash = (d * p_hash + ord(pattern[i])) % prime
t_hash = (d * t_hash + ord(text[i])) % prime
# Slide pattern over text
for i in range(n - m + 1):
if p_hash == t_hash:
# Hash match, verify character by character
if text[i:i+m] == pattern:
positions.append(i)
# Calculate hash for next window
if i < n - m:
t_hash = (d * (t_hash - ord(text[i]) * h) + ord(text[i + m])) % prime
if t_hash < 0:
t_hash += prime
return positions
# 4. Boyer-Moore Algorithm
def boyer_moore_search(text, pattern):
'''Bad character heuristic'''
def bad_char_table(pattern):
table = {}
m = len(pattern)
for i in range(m - 1):
table[pattern[i]] = m - 1 - i
return table
n, m = len(text), len(pattern)
bad_char = bad_char_table(pattern)
positions = []
shift = 0
while shift <= n - m:
j = m - 1
while j >= 0 and pattern[j] == text[shift + j]:
j -= 1
if j < 0:
positions.append(shift)
shift += bad_char.get(text[shift + m], m) if shift + m < n else 1
else:
shift += max(1, bad_char.get(text[shift + j], m))
return positions
# 5. Z Algorithm - O(n)
def z_algorithm(s):
'''Z array: longest substring starting at i matching prefix'''
n = len(s)
z = [0] * n
left = right = 0
for i in range(1, n):
if i > right:
left = right = i
while right < n and s[right] == s[right - left]:
right += 1
z[i] = right - left
right -= 1
else:
k = i - left
if z[k] < right - i + 1:
z[i] = z[k]
else:
left = i
while right < n and s[right] == s[right - left]:
right += 1
z[i] = right - left
right -= 1
return z
# 6. Longest Common Substring
def longest_common_substring(s1, s2):
'''Find longest common substring'''
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
max_len = 0
end_pos = 0
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
if dp[i][j] > max_len:
max_len = dp[i][j]
end_pos = i
return s1[end_pos - max_len:end_pos]
# 7. Longest Palindromic Substring
def longest_palindrome(s):
'''Find longest palindromic substring'''
if not s:
return ""
def expand_around_center(left, right):
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
return right - left - 1
start = end = 0
for i in range(len(s)):
len1 = expand_around_center(i, i) # Odd length
len2 = expand_around_center(i, i + 1) # Even length
max_len = max(len1, len2)
if max_len > end - start:
start = i - (max_len - 1) // 2
end = i + max_len // 2
return s[start:end + 1]
# 8. Manacher's Algorithm - O(n)
def manacher_longest_palindrome(s):
'''Linear time palindrome finding'''
# Transform string
t = '#'.join('^{}$'.format(s))
n = len(t)
p = [0] * n
center = right = 0
for i in range(1, n - 1):
if i < right:
p[i] = min(right - i, p[2 * center - i])
# Expand around center
while t[i + p[i] + 1] == t[i - p[i] - 1]:
p[i] += 1
# Update center and right
if i + p[i] > right:
center, right = i, i + p[i]
# Find longest palindrome
max_len, center_idx = max((n, i) for i, n in enumerate(p))
start = (center_idx - max_len) // 2
return s[start:start + max_len]
# 9. Edit Distance (Levenshtein)
def edit_distance(word1, word2):
'''Minimum operations to convert word1 to word2'''
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = 1 + min(
dp[i - 1][j], # Delete
dp[i][j - 1], # Insert
dp[i - 1][j - 1] # Replace
)
return dp[m][n]
# 10. String Compression
def compress_string(s):
'''Run-length encoding'''
if not s:
return ""
result = []
count = 1
for i in range(1, len(s)):
if s[i] == s[i - 1]:
count += 1
else:
result.append(s[i - 1] + str(count))
count = 1
result.append(s[-1] + str(count))
compressed = ''.join(result)
return compressed if len(compressed) < len(s) else s
# 11. Wildcard Matching
def is_match(s, p):
'''Match string with * and ? wildcards'''
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
# Handle leading asterisks
for j in range(1, n + 1):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 1]
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j - 1] == '*':
dp[i][j] = dp[i - 1][j] or dp[i][j - 1]
elif p[j - 1] == '?' or s[i - 1] == p[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
return dp[m][n]
# 12. Longest Repeating Substring
def longest_repeating_substring(s):
'''Find longest repeating substring'''
n = len(s)
dp = [[0] * (n + 1) for _ in range(n + 1)]
max_len = 0
for i in range(1, n + 1):
for j in range(i + 1, n + 1):
if s[i - 1] == s[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
max_len = max(max_len, dp[i][j])
return max_lenPythonAnswer: String algorithms: Naive O(nm), KMP O(n+m) with LPS array, Rabin-Karp O(n+m) with rolling hash, Boyer-Moore with bad character heuristic, Z algorithm for pattern finding, and dynamic programming for edit distance.
Q114. Implement Trie (prefix tree) data structure
# Trie (Prefix Tree) Data Structure
# 1. Basic Trie Implementation
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
'''Prefix tree for efficient string operations'''
def __init__(self):
self.root = TrieNode()
def insert(self, word):
'''Insert word - O(m) where m is word length'''
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word):
'''Search exact word - O(m)'''
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
def starts_with(self, prefix):
'''Check if any word starts with prefix - O(m)'''
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
def delete(self, word):
'''Delete word from trie'''
def _delete(node, word, index):
if index == len(word):
if not node.is_end_of_word:
return False
node.is_end_of_word = False
return len(node.children) == 0
char = word[index]
if char not in node.children:
return False
should_delete = _delete(node.children[char], word, index + 1)
if should_delete:
del node.children[char]
return len(node.children) == 0 and not node.is_end_of_word
return False
_delete(self.root, word, 0)
def get_all_words(self):
'''Get all words in trie'''
words = []
def dfs(node, current_word):
if node.is_end_of_word:
words.append(current_word)
for char, child in node.children.items():
dfs(child, current_word + char)
dfs(self.root, "")
return words
def autocomplete(self, prefix):
'''Get all words with given prefix'''
node = self.root
# Navigate to prefix
for char in prefix:
if char not in node.children:
return []
node = node.children[char]
# Find all words from this node
words = []
def dfs(n, current):
if n.is_end_of_word:
words.append(prefix + current)
for char, child in n.children.items():
dfs(child, current + char)
dfs(node, "")
return words
# 2. Word Search II (Trie + Backtracking)
class WordSearchII:
'''Find all words from dictionary in board'''
def find_words(self, board, words):
# Build trie
trie = Trie()
for word in words:
trie.insert(word)
result = set()
m, n = len(board), len(board[0])
def backtrack(i, j, node, path):
char = board[i][j]
if char not in node.children:
return
next_node = node.children[char]
path += char
if next_node.is_end_of_word:
result.add(path)
# Mark visited
board[i][j] = '#'
# Explore neighbors
for di, dj in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
ni, nj = i + di, j + dj
if 0 <= ni < m and 0 <= nj < n and board[ni][nj] != '#':
backtrack(ni, nj, next_node, path)
# Restore
board[i][j] = char
for i in range(m):
for j in range(n):
backtrack(i, j, trie.root, "")
return list(result)
# 3. Implement Dictionary with Wildcard
class WordDictionary:
'''Dictionary supporting . wildcard'''
def __init__(self):
self.root = TrieNode()
def add_word(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word):
def dfs(node, i):
if i == len(word):
return node.is_end_of_word
char = word[i]
if char == '.':
for child in node.children.values():
if dfs(child, i + 1):
return True
return False
else:
if char not in node.children:
return False
return dfs(node.children[char], i + 1)
return dfs(self.root, 0)
# 4. Longest Word in Dictionary
def longest_word(words):
'''Find longest word built one character at a time'''
trie = Trie()
for word in words:
trie.insert(word)
def can_build(word):
node = trie.root
for char in word[:-1]:
if char not in node.children:
return False
node = node.children[char]
if not node.is_end_of_word:
return False
return True
result = ""
for word in words:
if can_build(word):
if len(word) > len(result) or (len(word) == len(result) and word < result):
result = word
return result
# 5. Replace Words (Prefix)
def replace_words(dictionary, sentence):
'''Replace words with shortest root'''
trie = Trie()
for root in dictionary:
trie.insert(root)
def find_root(word):
node = trie.root
prefix = ""
for char in word:
if char not in node.children:
return word
node = node.children[char]
prefix += char
if node.is_end_of_word:
return prefix
return word
words = sentence.split()
return ' '.join(find_root(word) for word in words)
# 6. Stream of Characters
class StreamChecker:
'''Check if suffix of stream matches any word'''
def __init__(self, words):
# Build reverse trie
self.trie = TrieNode()
self.stream = []
self.max_len = 0
for word in words:
node = self.trie
self.max_len = max(self.max_len, len(word))
for char in reversed(word):
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def query(self, letter):
self.stream.append(letter)
node = self.trie
for i in range(len(self.stream) - 1, max(-1, len(self.stream) - self.max_len - 1), -1):
char = self.stream[i]
if char not in node.children:
return False
node = node.children[char]
if node.is_end_of_word:
return True
return False
# 7. Trie with Count
class TrieWithCount:
'''Track word frequencies'''
class Node:
def __init__(self):
self.children = {}
self.count = 0
def __init__(self):
self.root = self.Node()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = self.Node()
node = node.children[char]
node.count += 1
def count_words_equal_to(self, word):
node = self.root
for char in word:
if char not in node.children:
return 0
node = node.children[char]
return node.count
def count_words_starting_with(self, prefix):
node = self.root
for char in prefix:
if char not in node.children:
return 0
node = node.children[char]
def count_all(n):
total = n.count
for child in n.children.values():
total += count_all(child)
return total
return count_all(node)PythonAnswer: Trie provides O(m) insert/search/delete for strings of length m; used for autocomplete, spell checking, IP routing, and word games; stores common prefixes efficiently.
Q115. Master advanced algorithm patterns
# Advanced Algorithm Patterns
# 1. Sliding Window Pattern
def sliding_window_examples():
'''Maximum/minimum in sliding window'''
# Maximum sum subarray of size k
def max_sum_subarray(arr, k):
if len(arr) < k:
return -1
window_sum = sum(arr[:k])
max_sum = window_sum
for i in range(k, len(arr)):
window_sum += arr[i] - arr[i - k]
max_sum = max(max_sum, window_sum)
return max_sum
# Longest substring without repeating
def length_of_longest_substring(s):
char_set = set()
left = 0
max_len = 0
for right in range(len(s)):
while s[right] in char_set:
char_set.remove(s[left])
left += 1
char_set.add(s[right])
max_len = max(max_len, right - left + 1)
return max_len
# Minimum window substring
def min_window(s, t):
from collections import Counter
need = Counter(t)
have = {}
required = len(need)
formed = 0
left = 0
min_len = float('inf')
min_window = ""
for right in range(len(s)):
char = s[right]
have[char] = have.get(char, 0) + 1
if char in need and have[char] == need[char]:
formed += 1
while formed == required:
if right - left + 1 < min_len:
min_len = right - left + 1
min_window = s[left:right + 1]
char = s[left]
have[char] -= 1
if char in need and have[char] < need[char]:
formed -= 1
left += 1
return min_window
return max_sum_subarray, length_of_longest_substring, min_window
# 2. Two Pointers Pattern
def two_pointers_examples():
'''Problems solved with two pointers'''
# Container with most water
def max_area(height):
left, right = 0, len(height) - 1
max_water = 0
while left < right:
width = right - left
max_water = max(max_water, min(height[left], height[right]) * width)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_water
# Three sum
def three_sum(nums):
nums.sort()
result = []
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, len(nums) - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total == 0:
result.append([nums[i], nums[left], nums[right]])
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
elif total < 0:
left += 1
else:
right -= 1
return result
# Trapping rain water
def trap(height):
if not height:
return 0
left, right = 0, len(height) - 1
left_max = right_max = 0
water = 0
while left < right:
if height[left] < height[right]:
if height[left] >= left_max:
left_max = height[left]
else:
water += left_max - height[left]
left += 1
else:
if height[right] >= right_max:
right_max = height[right]
else:
water += right_max - height[right]
right -= 1
return water
return max_area, three_sum, trap
# 3. Fast and Slow Pointers
def fast_slow_pointers():
'''Cycle detection and middle finding'''
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Detect cycle
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
# Find cycle start
def detect_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
return None
# Middle of linked list
def middle_node(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
# Happy number
def is_happy(n):
def get_next(num):
total = 0
while num > 0:
digit = num % 10
total += digit ** 2
num //= 10
return total
slow = fast = n
while True:
slow = get_next(slow)
fast = get_next(get_next(fast))
if fast == 1:
return True
if slow == fast:
return False
return has_cycle, detect_cycle, middle_node, is_happy
# 4. Merge Intervals Pattern
def merge_intervals_examples():
'''Interval problems'''
# Merge overlapping intervals
def merge(intervals):
if not intervals:
return []
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for current in intervals[1:]:
if current[0] <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], current[1])
else:
merged.append(current)
return merged
# Insert interval
def insert(intervals, newInterval):
result = []
i = 0
n = len(intervals)
# Add all intervals before newInterval
while i < n and intervals[i][1] < newInterval[0]:
result.append(intervals[i])
i += 1
# Merge overlapping intervals
while i < n and intervals[i][0] <= newInterval[1]:
newInterval[0] = min(newInterval[0], intervals[i][0])
newInterval[1] = max(newInterval[1], intervals[i][1])
i += 1
result.append(newInterval)
# Add remaining intervals
while i < n:
result.append(intervals[i])
i += 1
return result
# Meeting rooms II
def min_meeting_rooms(intervals):
import heapq
if not intervals:
return 0
intervals.sort(key=lambda x: x[0])
heap = []
heapq.heappush(heap, intervals[0][1])
for i in range(1, len(intervals)):
if intervals[i][0] >= heap[0]:
heapq.heappop(heap)
heapq.heappush(heap, intervals[i][1])
return len(heap)
return merge, insert, min_meeting_rooms
# 5. Cyclic Sort Pattern
def cyclic_sort_pattern():
'''For arrays with numbers in range [1, n]'''
# Find missing number
def find_missing(nums):
i = 0
n = len(nums)
while i < n:
j = nums[i]
if j < n and nums[i] != nums[j]:
nums[i], nums[j] = nums[j], nums[i]
else:
i += 1
for i in range(n):
if nums[i] != i:
return i
return n
# Find all duplicates
def find_duplicates(nums):
duplicates = []
for num in nums:
index = abs(num) - 1
if nums[index] < 0:
duplicates.append(abs(num))
else:
nums[index] = -nums[index]
return duplicates
# First missing positive
def first_missing_positive(nums):
n = len(nums)
# Mark numbers outside [1, n]
for i in range(n):
if nums[i] <= 0 or nums[i] > n:
nums[i] = n + 1
# Mark presence
for i in range(n):
num = abs(nums[i])
if num <= n:
nums[num - 1] = -abs(nums[num - 1])
# Find first positive
for i in range(n):
if nums[i] > 0:
return i + 1
return n + 1
return find_missing, find_duplicates, first_missing_positive
# 6. Top K Elements Pattern
def top_k_pattern():
'''Using heaps for top/bottom K'''
import heapq
# Kth largest element
def find_kth_largest(nums, k):
return heapq.nlargest(k, nums)[-1]
# K closest points
def k_closest(points, k):
return heapq.nsmallest(k, points, key=lambda p: p[0]**2 + p[1]**2)
# Top K frequent
def top_k_frequent(nums, k):
from collections import Counter
count = Counter(nums)
return heapq.nlargest(k, count.keys(), key=count.get)
return find_kth_largest, k_closest, top_k_frequent
# 7. Modified Binary Search
def modified_binary_search():
'''Binary search variations'''
# Search in infinite sorted array
def search_infinite(reader, target):
left, right = 0, 1
while reader.get(right) < target:
left = right
right *= 2
while left <= right:
mid = (left + right) // 2
val = reader.get(mid)
if val == target:
return mid
elif val < target:
left = mid + 1
else:
right = mid - 1
return -1
return search_infinitePythonAnswer: Key patterns: sliding window (variable/fixed size), two pointers (opposite/same direction), fast-slow pointers (cycle detection), merge intervals, cyclic sort, top K elements, and modified binary search for optimization.
Q116. Understand dynamic programming fundamentals
# Dynamic Programming - Introduction
# 1. Fibonacci - Classic DP
def fibonacci_approaches(n):
'''Multiple approaches to Fibonacci'''
# Recursive - O(2^n)
def fib_recursive(n):
if n <= 1:
return n
return fib_recursive(n - 1) + fib_recursive(n - 2)
# Memoization (Top-Down) - O(n)
def fib_memo(n, memo=None):
if memo is None:
memo = {}
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
return memo[n]
# Tabulation (Bottom-Up) - O(n)
def fib_tabulation(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
# Space Optimized - O(1) space
def fib_optimized(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for _ in range(2, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current
return prev1
return fib_recursive, fib_memo, fib_tabulation, fib_optimized
# 2. Climbing Stairs
def climb_stairs(n):
'''Ways to climb n stairs (1 or 2 steps at a time)'''
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]
# Space optimized
def climb_stairs_optimized(n):
if n <= 2:
return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current
return prev1
# 3. House Robber
def rob(nums):
'''Maximum money from non-adjacent houses'''
if not nums:
return 0
if len(nums) <= 2:
return max(nums)
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp[-1]
# Space optimized
def rob_optimized(nums):
if not nums:
return 0
prev2 = prev1 = 0
for num in nums:
current = max(prev1, prev2 + num)
prev2 = prev1
prev1 = current
return prev1
# 4. Coin Change
def coin_change(coins, amount):
'''Minimum coins to make amount'''
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if i >= coin:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
# Coin change II - number of combinations
def change(amount, coins):
'''Number of ways to make amount'''
dp = [0] * (amount + 1)
dp[0] = 1
for coin in coins:
for i in range(coin, amount + 1):
dp[i] += dp[i - coin]
return dp[amount]
# 5. Longest Increasing Subsequence
def length_of_lis(nums):
'''Length of longest increasing subsequence'''
if not nums:
return 0
n = len(nums)
dp = [1] * n
for i in range(1, n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
# Optimized with binary search - O(n log n)
def length_of_lis_binary(nums):
import bisect
sub = []
for num in nums:
pos = bisect.bisect_left(sub, num)
if pos == len(sub):
sub.append(num)
else:
sub[pos] = num
return len(sub)
# 6. Longest Common Subsequence
def longest_common_subsequence(text1, text2):
'''LCS of two strings'''
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return dp[m][n]
# 7. 0/1 Knapsack
def knapsack_01(weights, values, capacity):
'''Maximum value with weight limit'''
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(capacity + 1):
if weights[i - 1] <= w:
dp[i][w] = max(
dp[i - 1][w],
dp[i - 1][w - weights[i - 1]] + values[i - 1]
)
else:
dp[i][w] = dp[i - 1][w]
return dp[n][capacity]
# Space optimized
def knapsack_optimized(weights, values, capacity):
dp = [0] * (capacity + 1)
for i in range(len(weights)):
for w in range(capacity, weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[capacity]
# 8. Partition Equal Subset Sum
def can_partition(nums):
'''Can partition into two equal sum subsets'''
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]
# 9. Word Break
def word_break(s, word_dict):
'''Can segment string into dictionary words'''
n = len(s)
dp = [False] * (n + 1)
dp[0] = True
word_set = set(word_dict)
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]
# Word break II - return all sentences
def word_break_ii(s, word_dict):
word_set = set(word_dict)
memo = {}
def backtrack(start):
if start in memo:
return memo[start]
if start == len(s):
return [[]]
result = []
for end in range(start + 1, len(s) + 1):
word = s[start:end]
if word in word_set:
for rest in backtrack(end):
result.append([word] + rest)
memo[start] = result
return result
return [' '.join(words) for words in backtrack(0)]
# 10. Maximum Product Subarray
def max_product(nums):
'''Maximum product contiguous subarray'''
if not nums:
return 0
max_prod = min_prod = result = nums[0]
for num in nums[1:]:
if num < 0:
max_prod, min_prod = min_prod, max_prod
max_prod = max(num, max_prod * num)
min_prod = min(num, min_prod * num)
result = max(result, max_prod)
return resultPythonAnswer: DP solves problems with overlapping subproblems and optimal substructure; use memoization (top-down) or tabulation (bottom-up); space can often be optimized; common patterns: Fibonacci, knapsack, subsequence, partition.
Q117. Implement backtracking algorithms
# Backtracking Algorithms
# 1. Permutations
def permute(nums):
'''Generate all permutations'''
result = []
def backtrack(path):
if len(path) == len(nums):
result.append(path[:])
return
for num in nums:
if num not in path:
path.append(num)
backtrack(path)
path.pop()
backtrack([])
return result
# Permutations II (with duplicates)
def permute_unique(nums):
'''Permutations with duplicates'''
result = []
nums.sort()
used = [False] * len(nums)
def backtrack(path):
if len(path) == len(nums):
result.append(path[:])
return
for i in range(len(nums)):
if used[i]:
continue
# Skip duplicates
if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
continue
path.append(nums[i])
used[i] = True
backtrack(path)
path.pop()
used[i] = False
backtrack([])
return result
# 2. Combinations
def combine(n, k):
'''All combinations of k numbers from 1 to n'''
result = []
def backtrack(start, path):
if len(path) == k:
result.append(path[:])
return
for i in range(start, n + 1):
path.append(i)
backtrack(i + 1, path)
path.pop()
backtrack(1, [])
return result
# Combination sum
def combination_sum(candidates, target):
'''Find all combinations that sum to target (reusable)'''
result = []
def backtrack(start, path, total):
if total == target:
result.append(path[:])
return
if total > target:
return
for i in range(start, len(candidates)):
path.append(candidates[i])
backtrack(i, path, total + candidates[i])
path.pop()
backtrack(0, [], 0)
return result
# 3. Subsets
def subsets(nums):
'''Generate all subsets (power set)'''
result = []
def backtrack(start, path):
result.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
backtrack(0, [])
return result
# Subsets II (with duplicates)
def subsets_with_dup(nums):
result = []
nums.sort()
def backtrack(start, path):
result.append(path[:])
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i - 1]:
continue
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
backtrack(0, [])
return result
# 4. N-Queens
def solve_n_queens(n):
'''Place n queens on n×n board'''
result = []
board = [['.'] * n for _ in range(n)]
def is_valid(row, col):
# Check column
for i in range(row):
if board[i][col] == 'Q':
return False
# Check diagonal
i, j = row - 1, col - 1
while i >= 0 and j >= 0:
if board[i][j] == 'Q':
return False
i -= 1
j -= 1
# Check anti-diagonal
i, j = row - 1, col + 1
while i >= 0 and j < n:
if board[i][j] == 'Q':
return False
i -= 1
j += 1
return True
def backtrack(row):
if row == n:
result.append([''.join(row) for row in board])
return
for col in range(n):
if is_valid(row, col):
board[row][col] = 'Q'
backtrack(row + 1)
board[row][col] = '.'
backtrack(0)
return result
# 5. Sudoku Solver
def solve_sudoku(board):
'''Solve 9×9 Sudoku puzzle'''
def is_valid(row, col, num):
# Check row
if num in board[row]:
return False
# Check column
if num in [board[i][col] for i in range(9)]:
return False
# Check 3×3 box
box_row, box_col = 3 * (row // 3), 3 * (col // 3)
for i in range(box_row, box_row + 3):
for j in range(box_col, box_col + 3):
if board[i][j] == num:
return False
return True
def backtrack():
for row in range(9):
for col in range(9):
if board[row][col] == '.':
for num in '123456789':
if is_valid(row, col, num):
board[row][col] = num
if backtrack():
return True
board[row][col] = '.'
return False
return True
backtrack()
# 6. Letter Combinations of Phone Number
def letter_combinations(digits):
'''Map digits to letters like phone keypad'''
if not digits:
return []
mapping = {
'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
'6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'
}
result = []
def backtrack(index, path):
if index == len(digits):
result.append(path)
return
for letter in mapping[digits[index]]:
backtrack(index + 1, path + letter)
backtrack(0, "")
return result
# 7. Generate Parentheses
def generate_parentheses(n):
'''Generate all valid n pairs of parentheses'''
result = []
def backtrack(path, open_count, close_count):
if len(path) == 2 * n:
result.append(path)
return
if open_count < n:
backtrack(path + '(', open_count + 1, close_count)
if close_count < open_count:
backtrack(path + ')', open_count, close_count + 1)
backtrack("", 0, 0)
return result
# 8. Palindrome Partitioning
def partition(s):
'''Partition string into palindromic substrings'''
result = []
def is_palindrome(string):
return string == string[::-1]
def backtrack(start, path):
if start == len(s):
result.append(path[:])
return
for end in range(start + 1, len(s) + 1):
substring = s[start:end]
if is_palindrome(substring):
path.append(substring)
backtrack(end, path)
path.pop()
backtrack(0, [])
return result
# 9. Word Search
def exist(board, word):
'''Find if word exists in board'''
m, n = len(board), len(board[0])
def backtrack(i, j, k):
if k == len(word):
return True
if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[k]:
return False
temp = board[i][j]
board[i][j] = '#'
found = (backtrack(i + 1, j, k + 1) or
backtrack(i - 1, j, k + 1) or
backtrack(i, j + 1, k + 1) or
backtrack(i, j - 1, k + 1))
board[i][j] = temp
return found
for i in range(m):
for j in range(n):
if backtrack(i, j, 0):
return True
return False
# 10. Restore IP Addresses
def restore_ip_addresses(s):
'''Generate all valid IP addresses'''
result = []
def is_valid(segment):
if not segment or len(segment) > 3:
return False
if segment[0] == '0' and len(segment) > 1:
return False
return 0 <= int(segment) <= 255
def backtrack(start, path):
if len(path) == 4:
if start == len(s):
result.append('.'.join(path))
return
for end in range(start + 1, min(start + 4, len(s) + 1)):
segment = s[start:end]
if is_valid(segment):
path.append(segment)
backtrack(end, path)
path.pop()
backtrack(0, [])
return resultPythonAnswer: Backtracking explores all possible solutions by trying choices, recursing, and undoing (backtracking) when constraint violated; used for permutations, combinations, N-Queens, Sudoku, and constraint satisfaction problems.
Q118. Solve complex DP problems
# More Dynamic Programming Problems
# 1. Edit Distance (Levenshtein Distance)
def min_distance(word1, word2):
'''Minimum operations to convert word1 to word2'''
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = 1 + min(
dp[i - 1][j], # Delete
dp[i][j - 1], # Insert
dp[i - 1][j - 1] # Replace
)
return dp[m][n]
# 2. Regular Expression Matching
def is_match_regex(s, p):
'''Match with . and * wildcards'''
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
# Handle patterns like a*, a*b*, etc.
for j in range(2, n + 1, 2):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 2]
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j - 1] == s[i - 1] or p[j - 1] == '.':
dp[i][j] = dp[i - 1][j - 1]
elif p[j - 1] == '*':
dp[i][j] = dp[i][j - 2] # Zero occurrence
if p[j - 2] == s[i - 1] or p[j - 2] == '.':
dp[i][j] = dp[i][j] or dp[i - 1][j]
return dp[m][n]
# 3. Longest Palindromic Subsequence
def longest_palindrome_subseq(s):
'''Length of longest palindromic subsequence'''
n = len(s)
dp = [[0] * n for _ in range(n)]
for i in range(n - 1, -1, -1):
dp[i][i] = 1
for j in range(i + 1, n):
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return dp[0][n - 1]
# 4. Distinct Subsequences
def num_distinct(s, t):
'''Number of distinct subsequences of t in s'''
m, n = len(s), len(t)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(m + 1):
dp[i][0] = 1
for i in range(1, m + 1):
for j in range(1, n + 1):
if s[i - 1] == t[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
else:
dp[i][j] = dp[i - 1][j]
return dp[m][n]
# 5. Interleaving String
def is_interleave(s1, s2, s3):
'''Check if s3 is interleaving of s1 and s2'''
m, n = len(s1), len(s2)
if m + n != len(s3):
return False
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
for i in range(1, m + 1):
dp[i][0] = dp[i - 1][0] and s1[i - 1] == s3[i - 1]
for j in range(1, n + 1):
dp[0][j] = dp[0][j - 1] and s2[j - 1] == s3[j - 1]
for i in range(1, m + 1):
for j in range(1, n + 1):
dp[i][j] = (
(dp[i - 1][j] and s1[i - 1] == s3[i + j - 1]) or
(dp[i][j - 1] and s2[j - 1] == s3[i + j - 1])
)
return dp[m][n]
# 6. Burst Balloons
def max_coins(nums):
'''Maximum coins from bursting balloons'''
nums = [1] + nums + [1]
n = len(nums)
dp = [[0] * n for _ in range(n)]
for length in range(2, n):
for left in range(n - length):
right = left + length
for i in range(left + 1, right):
dp[left][right] = max(
dp[left][right],
nums[left] * nums[i] * nums[right] +
dp[left][i] + dp[i][right]
)
return dp[0][n - 1]
# 7. Decode Ways
def num_decodings(s):
'''Number of ways to decode string to letters'''
if not s or s[0] == '0':
return 0
n = len(s)
dp = [0] * (n + 1)
dp[0] = dp[1] = 1
for i in range(2, n + 1):
one_digit = int(s[i - 1:i])
two_digits = int(s[i - 2:i])
if 1 <= one_digit <= 9:
dp[i] += dp[i - 1]
if 10 <= two_digits <= 26:
dp[i] += dp[i - 2]
return dp[n]
# 8. Unique Binary Search Trees
def num_trees(n):
'''Number of structurally unique BSTs with n nodes'''
dp = [0] * (n + 1)
dp[0] = dp[1] = 1
for nodes in range(2, n + 1):
for root in range(1, nodes + 1):
left = root - 1
right = nodes - root
dp[nodes] += dp[left] * dp[right]
return dp[n]PythonAnswer: Advanced DP: edit distance uses 3 operations matrix; regex matching handles . and *; palindrome subsequence uses range DP; distinct subsequences counts paths; interleaving validates merge.
Q119. Master DP optimization techniques
# Advanced DP and Optimization
# 1. Matrix Chain Multiplication
def matrix_chain_order(dimensions):
'''Minimum scalar multiplications for matrix chain'''
n = len(dimensions) - 1
dp = [[0] * n for _ in range(n)]
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = float('inf')
for k in range(i, j):
cost = (dp[i][k] + dp[k + 1][j] +
dimensions[i] * dimensions[k + 1] * dimensions[j + 1])
dp[i][j] = min(dp[i][j], cost)
return dp[0][n - 1]
# 2. Egg Drop Problem
def super_egg_drop(eggs, floors):
'''Minimum trials to find critical floor'''
dp = [[0] * (floors + 1) for _ in range(eggs + 1)]
for trial in range(1, floors + 1):
for egg in range(1, eggs + 1):
dp[egg][trial] = dp[egg - 1][trial - 1] + dp[egg][trial - 1] + 1
if dp[egg][trial] >= floors:
return trial
return floors
# 3. Minimum Path Sum
def min_path_sum(grid):
'''Minimum path sum from top-left to bottom-right'''
m, n = len(grid), len(grid[0])
for i in range(1, m):
grid[i][0] += grid[i - 1][0]
for j in range(1, n):
grid[0][j] += grid[0][j - 1]
for i in range(1, m):
for j in range(1, n):
grid[i][j] += min(grid[i - 1][j], grid[i][j - 1])
return grid[m - 1][n - 1]
# 4. Unique Paths
def unique_paths(m, n):
'''Number of paths in m×n grid'''
dp = [[1] * n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[m - 1][n - 1]
# Space optimized
def unique_paths_optimized(m, n):
dp = [1] * n
for _ in range(1, m):
for j in range(1, n):
dp[j] += dp[j - 1]
return dp[n - 1]PythonAnswer: Matrix chain uses interval DP; egg drop uses binary search DP; path problems use grid DP; space optimization reduces O(mn) to O(n) using rolling array.
Q120. Implement greedy algorithms
# Greedy Algorithms
# 1. Activity Selection
def activity_selection(start, finish):
'''Maximum non-overlapping activities'''
activities = sorted(zip(start, finish), key=lambda x: x[1])
count = 1
last_finish = activities[0][1]
for i in range(1, len(activities)):
if activities[i][0] >= last_finish:
count += 1
last_finish = activities[i][1]
return count
# 2. Jump Game
def can_jump(nums):
'''Can reach last index'''
max_reach = 0
for i in range(len(nums)):
if i > max_reach:
return False
max_reach = max(max_reach, i + nums[i])
return True
# Jump game II - minimum jumps
def jump(nums):
'''Minimum jumps to reach end'''
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
return jumps
# 3. Gas Station
def can_complete_circuit(gas, cost):
'''Starting gas station to complete circuit'''
if sum(gas) < sum(cost):
return -1
start = 0
tank = 0
for i in range(len(gas)):
tank += gas[i] - cost[i]
if tank < 0:
start = i + 1
tank = 0
return start
# 4. Task Scheduler
def least_interval(tasks, n):
'''Minimum intervals to execute tasks'''
from collections import Counter
import heapq
freq = Counter(tasks)
max_heap = [-count for count in freq.values()]
heapq.heapify(max_heap)
time = 0
while max_heap:
temp = []
for _ in range(n + 1):
if max_heap:
count = heapq.heappop(max_heap)
if count < -1:
temp.append(count + 1)
time += 1
if not max_heap and not temp:
break
for count in temp:
heapq.heappush(max_heap, count)
return timePythonAnswer: Greedy makes locally optimal choice; works when problem has greedy-choice property and optimal substructure; examples: activity selection, jump game, gas station, task scheduling.
Q121. Implement Union-Find data structure
# Union-Find (Disjoint Set)
class UnionFind:
'''Efficient union-find with path compression'''
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
self.count = n
def find(self, x):
'''Find with path compression'''
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
'''Union by rank'''
root_x = self.find(x)
root_y = self.find(y)
if root_x == root_y:
return False
if self.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
elif self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1
self.count -= 1
return True
def connected(self, x, y):
return self.find(x) == self.find(y)
# Applications
def number_of_provinces(is_connected):
'''Count connected components'''
n = len(is_connected)
uf = UnionFind(n)
for i in range(n):
for j in range(i + 1, n):
if is_connected[i][j]:
uf.union(i, j)
return uf.count
def find_redundant_connection(edges):
'''Find edge that creates cycle'''
uf = UnionFind(len(edges) + 1)
for u, v in edges:
if not uf.union(u, v):
return [u, v]
return []PythonAnswer: Union-Find tracks disjoint sets with near-constant time operations using path compression and union by rank; used for connectivity, cycle detection, Kruskal’s MST.
Q122. Implement Segment Tree
# Segment Tree
class SegmentTree:
'''Range query and update'''
def __init__(self, nums):
self.n = len(nums)
self.tree = [0] * (4 * self.n)
self.build(nums, 0, 0, self.n - 1)
def build(self, nums, node, start, end):
if start == end:
self.tree[node] = nums[start]
else:
mid = (start + end) // 2
left = 2 * node + 1
right = 2 * node + 2
self.build(nums, left, start, mid)
self.build(nums, right, mid + 1, end)
self.tree[node] = self.tree[left] + self.tree[right]
def update(self, index, value, node=0, start=None, end=None):
if start is None:
start, end = 0, self.n - 1
if start == end:
self.tree[node] = value
else:
mid = (start + end) // 2
left = 2 * node + 1
right = 2 * node + 2
if index <= mid:
self.update(index, value, left, start, mid)
else:
self.update(index, value, right, mid + 1, end)
self.tree[node] = self.tree[left] + self.tree[right]
def query(self, l, r, node=0, start=None, end=None):
if start is None:
start, end = 0, self.n - 1
if r < start or l > end:
return 0
if l <= start and end <= r:
return self.tree[node]
mid = (start + end) // 2
left_sum = self.query(l, r, 2 * node + 1, start, mid)
right_sum = self.query(l, r, 2 * node + 2, mid + 1, end)
return left_sum + right_sumPythonAnswer: Segment tree supports range queries and updates in O(log n); built as binary tree with leaf nodes as array elements; each internal node stores aggregate of children.
Q123. Master bit manipulation techniques
# Bit Manipulation
# 1. Basic Operations
def bit_operations():
'''Common bit manipulation operations'''
# Check if bit is set
def is_bit_set(num, i):
return (num & (1 << i)) != 0
# Set bit
def set_bit(num, i):
return num | (1 << i)
# Clear bit
def clear_bit(num, i):
return num & ~(1 << i)
# Toggle bit
def toggle_bit(num, i):
return num ^ (1 << i)
# Count set bits
def count_bits(n):
count = 0
while n:
n &= n - 1 # Clear rightmost set bit
count += 1
return count
# Is power of 2
def is_power_of_two(n):
return n > 0 and (n & (n - 1)) == 0
# Find rightmost set bit
def rightmost_set_bit(n):
return n & -n
return (is_bit_set, set_bit, clear_bit, toggle_bit,
count_bits, is_power_of_two, rightmost_set_bit)
# 2. Single Number Problems
def single_number(nums):
'''Find number appearing once (others twice)'''
result = 0
for num in nums:
result ^= num
return result
def single_number_ii(nums):
'''Find number appearing once (others thrice)'''
ones = twos = 0
for num in nums:
ones = (ones ^ num) & ~twos
twos = (twos ^ num) & ~ones
return ones
def single_number_iii(nums):
'''Find two numbers appearing once'''
xor = 0
for num in nums:
xor ^= num
# Find rightmost set bit
diff_bit = xor & -xor
a = b = 0
for num in nums:
if num & diff_bit:
a ^= num
else:
b ^= num
return [a, b]
# 3. Subsets using Bit Manipulation
def subsets_bits(nums):
'''Generate all subsets using bits'''
n = len(nums)
result = []
for mask in range(1 << n):
subset = []
for i in range(n):
if mask & (1 << i):
subset.append(nums[i])
result.append(subset)
return result
# 4. Reverse Bits
def reverse_bits(n):
'''Reverse 32-bit unsigned integer'''
result = 0
for _ in range(32):
result = (result << 1) | (n & 1)
n >>= 1
return result
# 5. Maximum XOR
def find_maximum_xor(nums):
'''Maximum XOR of two numbers'''
max_xor = 0
mask = 0
for i in range(31, -1, -1):
mask |= (1 << i)
prefixes = {num & mask for num in nums}
temp = max_xor | (1 << i)
for prefix in prefixes:
if temp ^ prefix in prefixes:
max_xor = temp
break
return max_xorPythonAnswer: Bit operations: XOR for single number, bit masking for subsets, & for checking bits, | for setting, ^ for toggling; useful for space optimization and fast operations.
Q124. Implement mathematical algorithms
# Math and Number Theory
# 1. Prime Numbers
def sieve_of_eratosthenes(n):
'''Find all primes up to n'''
is_prime = [True] * (n + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(n**0.5) + 1):
if is_prime[i]:
for j in range(i*i, n + 1, i):
is_prime[j] = False
return [i for i in range(n + 1) if is_prime[i]]
def is_prime(n):
'''Check if number is prime'''
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
for i in range(3, int(n**0.5) + 1, 2):
if n % i == 0:
return False
return True
# 2. GCD and LCM
def gcd(a, b):
'''Greatest Common Divisor'''
while b:
a, b = b, a % b
return a
def lcm(a, b):
'''Least Common Multiple'''
return (a * b) // gcd(a, b)
# 3. Fast Exponentiation
def power(base, exp, mod=None):
'''Fast exponentiation with optional modulo'''
result = 1
base = base % mod if mod else base
while exp > 0:
if exp & 1:
result = (result * base) % mod if mod else result * base
exp >>= 1
base = (base * base) % mod if mod else base * base
return result
# 4. Factorial and Combinations
def factorial(n):
'''Calculate factorial'''
if n <= 1:
return 1
return n * factorial(n - 1)
def combinations(n, k):
'''Calculate C(n, k)'''
if k > n - k:
k = n - k
result = 1
for i in range(k):
result = result * (n - i) // (i + 1)
return result
# 5. Happy Number
def is_happy(n):
'''Determine if happy number'''
def get_next(num):
total = 0
while num:
digit = num % 10
total += digit ** 2
num //= 10
return total
seen = set()
while n != 1 and n not in seen:
seen.add(n)
n = get_next(n)
return n == 1PythonAnswer: Number theory: Sieve of Eratosthenes for primes O(n log log n), Euclidean GCD O(log n), fast exponentiation O(log n), factorial and combinations for combinatorics.
Q125. Apply advanced problem-solving techniques
# Advanced Problem Solving Techniques
# 1. Monotonic Stack
def next_greater_element(nums):
'''Find next greater element for each number'''
result = [-1] * len(nums)
stack = []
for i in range(len(nums)):
while stack and nums[stack[-1]] < nums[i]:
idx = stack.pop()
result[idx] = nums[i]
stack.append(i)
return result
# 2. Prefix Sum
class PrefixSum:
'''Range sum queries in O(1)'''
def __init__(self, nums):
self.prefix = [0]
for num in nums:
self.prefix.append(self.prefix[-1] + num)
def range_sum(self, left, right):
return self.prefix[right + 1] - self.prefix[left]
# 3. Difference Array
class DifferenceArray:
'''Range updates in O(1)'''
def __init__(self, nums):
self.diff = [0] * len(nums)
self.diff[0] = nums[0]
for i in range(1, len(nums)):
self.diff[i] = nums[i] - nums[i - 1]
def range_add(self, left, right, val):
self.diff[left] += val
if right + 1 < len(self.diff):
self.diff[right + 1] -= val
def get_array(self):
result = [0] * len(self.diff)
result[0] = self.diff[0]
for i in range(1, len(self.diff)):
result[i] = result[i - 1] + self.diff[i]
return result
# 4. Reservoir Sampling
import random
def reservoir_sample(stream, k):
'''Sample k items from stream of unknown length'''
reservoir = []
for i, item in enumerate(stream):
if i < k:
reservoir.append(item)
else:
j = random.randint(0, i)
if j < k:
reservoir[j] = item
return reservoir
# 5. Dutch National Flag
def sort_colors(nums):
'''Sort array with 3 colors in-place'''
low = mid = 0
high = len(nums) - 1
while mid <= high:
if nums[mid] == 0:
nums[low], nums[mid] = nums[mid], nums[low]
low += 1
mid += 1
elif nums[mid] == 1:
mid += 1
else:
nums[mid], nums[high] = nums[high], nums[mid]
high -= 1
# 6. Moore's Voting Algorithm
def majority_element(nums):
'''Find majority element (> n/2)'''
candidate = count = 0
for num in nums:
if count == 0:
candidate = num
count += 1 if num == candidate else -1
return candidate
# 7. Rolling Hash
class RollingHash:
'''Efficient string hashing'''
def __init__(self, s, base=26, mod=10**9+7):
self.n = len(s)
self.base = base
self.mod = mod
self.hash = [0] * (self.n + 1)
self.power = [1] * (self.n + 1)
for i in range(self.n):
self.hash[i + 1] = (self.hash[i] * base + ord(s[i])) % mod
self.power[i + 1] = (self.power[i] * base) % mod
def get_hash(self, left, right):
return (self.hash[right + 1] -
self.hash[left] * self.power[right - left + 1]) % self.modPythonAnswer: Techniques: monotonic stack for next greater, prefix sum for range queries, difference array for range updates, reservoir sampling for streams, Moore’s voting for majority element.
Q126. Implement shortest path algorithms
# Shortest Path Algorithms
# 1. Dijkstra's Algorithm - O((V + E) log V)
import heapq
from collections import defaultdict
def dijkstra(graph, start):
'''Single source shortest path for non-negative weights'''
distances = {node: float('inf') for node in graph}
distances[start] = 0
pq = [(0, start)] # (distance, node)
visited = set()
while pq:
current_dist, current = heapq.heappop(pq)
if current in visited:
continue
visited.add(current)
for neighbor, weight in graph[current]:
distance = current_dist + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(pq, (distance, neighbor))
return distances
def dijkstra_with_path(graph, start, end):
'''Return shortest path and distance'''
distances = {node: float('inf') for node in graph}
distances[start] = 0
previous = {}
pq = [(0, start)]
while pq:
current_dist, current = heapq.heappop(pq)
if current == end:
break
if current_dist > distances[current]:
continue
for neighbor, weight in graph[current]:
distance = current_dist + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
previous[neighbor] = current
heapq.heappush(pq, (distance, neighbor))
# Reconstruct path
path = []
current = end
while current in previous:
path.append(current)
current = previous[current]
path.append(start)
path.reverse()
return distances[end], path
# 2. Bellman-Ford Algorithm - O(VE)
def bellman_ford(graph, start, n):
'''Handles negative weights, detects negative cycles'''
distances = [float('inf')] * n
distances[start] = 0
# Relax edges V-1 times
for _ in range(n - 1):
for u in range(n):
for v, weight in graph[u]:
if distances[u] + weight < distances[v]:
distances[v] = distances[u] + weight
# Check for negative cycles
for u in range(n):
for v, weight in graph[u]:
if distances[u] + weight < distances[v]:
return None # Negative cycle detected
return distances
# 3. Floyd-Warshall Algorithm - O(V³)
def floyd_warshall(graph):
'''All pairs shortest path'''
n = len(graph)
dist = [[float('inf')] * n for _ in range(n)]
# Initialize distances
for i in range(n):
dist[i][i] = 0
for u in range(n):
for v, weight in graph[u]:
dist[u][v] = weight
# Dynamic programming
for k in range(n):
for i in range(n):
for j in range(n):
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
return dist
# 4. A* Search Algorithm
def a_star(graph, start, goal, heuristic):
'''Informed search using heuristic'''
open_set = [(0, start)]
came_from = {}
g_score = {node: float('inf') for node in graph}
g_score[start] = 0
f_score = {node: float('inf') for node in graph}
f_score[start] = heuristic(start, goal)
while open_set:
current_f, current = heapq.heappop(open_set)
if current == goal:
path = []
while current in came_from:
path.append(current)
current = came_from[current]
path.append(start)
return path[::-1]
for neighbor, weight in graph[current]:
tentative_g = g_score[current] + weight
if tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + heuristic(neighbor, goal)
heapq.heappush(open_set, (f_score[neighbor], neighbor))
return None
# 5. Network Delay Time
def network_delay_time(times, n, k):
'''Time for all nodes to receive signal'''
graph = defaultdict(list)
for u, v, w in times:
graph[u].append((v, w))
distances = dijkstra(graph, k)
# Get all nodes
all_nodes = set(range(1, n + 1))
max_dist = 0
for node in all_nodes:
if distances.get(node, float('inf')) == float('inf'):
return -1
max_dist = max(max_dist, distances[node])
return max_dist
# 6. Cheapest Flights Within K Stops
def find_cheapest_price(n, flights, src, dst, k):
'''Shortest path with at most k stops'''
graph = defaultdict(list)
for u, v, price in flights:
graph[u].append((v, price))
# (cost, node, stops)
pq = [(0, src, 0)]
visited = {}
while pq:
cost, node, stops = heapq.heappop(pq)
if node == dst:
return cost
if stops > k:
continue
if node in visited and visited[node] <= stops:
continue
visited[node] = stops
for neighbor, price in graph[node]:
heapq.heappush(pq, (cost + price, neighbor, stops + 1))
return -1
# 7. Path With Minimum Effort
def minimum_effort_path(heights):
'''Minimum effort path in grid'''
m, n = len(heights), len(heights[0])
# (effort, row, col)
pq = [(0, 0, 0)]
efforts = [[float('inf')] * n for _ in range(m)]
efforts[0][0] = 0
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
while pq:
effort, row, col = heapq.heappop(pq)
if row == m - 1 and col == n - 1:
return effort
if effort > efforts[row][col]:
continue
for dr, dc in directions:
new_row, new_col = row + dr, col + dc
if 0 <= new_row < m and 0 <= new_col < n:
new_effort = max(effort, abs(heights[new_row][new_col] - heights[row][col]))
if new_effort < efforts[new_row][new_col]:
efforts[new_row][new_col] = new_effort
heapq.heappush(pq, (new_effort, new_row, new_col))
return 0PythonAnswer: Shortest path: Dijkstra O((V+E)log V) for non-negative weights using priority queue; Bellman-Ford O(VE) handles negative weights and detects cycles; Floyd-Warshall O(V³) for all-pairs; A* uses heuristic for optimization.
Q127. Implement minimum spanning tree algorithms
# Minimum Spanning Tree Algorithms
# 1. Kruskal's Algorithm - O(E log E)
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False
if self.rank[px] < self.rank[py]:
self.parent[px] = py
elif self.rank[px] > self.rank[py]:
self.parent[py] = px
else:
self.parent[py] = px
self.rank[px] += 1
return True
def kruskal(n, edges):
'''MST using edge sorting and union-find'''
# Sort edges by weight
edges.sort(key=lambda x: x[2])
uf = UnionFind(n)
mst = []
total_weight = 0
for u, v, weight in edges:
if uf.union(u, v):
mst.append((u, v, weight))
total_weight += weight
if len(mst) == n - 1:
break
return mst, total_weight
# 2. Prim's Algorithm - O(E log V)
def prim(graph, n):
'''MST using greedy approach'''
visited = [False] * n
mst = []
total_weight = 0
# (weight, node, parent)
pq = [(0, 0, -1)]
while pq:
weight, node, parent = heapq.heappop(pq)
if visited[node]:
continue
visited[node] = True
total_weight += weight
if parent != -1:
mst.append((parent, node, weight))
for neighbor, edge_weight in graph[node]:
if not visited[neighbor]:
heapq.heappush(pq, (edge_weight, neighbor, node))
return mst, total_weight
# 3. Min Cost to Connect All Points
def min_cost_connect_points(points):
'''MST in complete graph'''
n = len(points)
def manhattan_distance(p1, p2):
return abs(p1[0] - p2[0]) + abs(p1[1] - p2[1])
visited = [False] * n
min_heap = [(0, 0)] # (cost, point_index)
total_cost = 0
edges_used = 0
while edges_used < n:
cost, current = heapq.heappop(min_heap)
if visited[current]:
continue
visited[current] = True
total_cost += cost
edges_used += 1
for next_point in range(n):
if not visited[next_point]:
distance = manhattan_distance(points[current], points[next_point])
heapq.heappush(min_heap, (distance, next_point))
return total_cost
# 4. Critical Connections (Bridges)
def critical_connections(n, connections):
'''Find bridges in graph using Tarjan's algorithm'''
graph = defaultdict(list)
for u, v in connections:
graph[u].append(v)
graph[v].append(u)
discovery = [-1] * n
low = [-1] * n
time = [0]
bridges = []
def dfs(node, parent):
discovery[node] = low[node] = time[0]
time[0] += 1
for neighbor in graph[node]:
if neighbor == parent:
continue
if discovery[neighbor] == -1:
dfs(neighbor, node)
low[node] = min(low[node], low[neighbor])
if low[neighbor] > discovery[node]:
bridges.append([node, neighbor])
else:
low[node] = min(low[node], discovery[neighbor])
for i in range(n):
if discovery[i] == -1:
dfs(i, -1)
return bridges
# 5. Articulation Points
def find_articulation_points(n, edges):
'''Find cut vertices using Tarjan's algorithm'''
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
discovery = [-1] * n
low = [-1] * n
parent = [-1] * n
time = [0]
articulation_points = set()
def dfs(u):
children = 0
discovery[u] = low[u] = time[0]
time[0] += 1
for v in graph[u]:
if discovery[v] == -1:
children += 1
parent[v] = u
dfs(v)
low[u] = min(low[u], low[v])
# u is articulation point if:
# 1. u is root and has 2+ children
if parent[u] == -1 and children > 1:
articulation_points.add(u)
# 2. u is not root and low[v] >= discovery[u]
if parent[u] != -1 and low[v] >= discovery[u]:
articulation_points.add(u)
elif v != parent[u]:
low[u] = min(low[u], discovery[v])
for i in range(n):
if discovery[i] == -1:
dfs(i)
return list(articulation_points)PythonAnswer: MST: Kruskal’s O(E log E) sorts edges and uses union-find to avoid cycles; Prim’s O(E log V) grows tree from starting vertex using priority queue; both find minimum cost tree connecting all vertices.
Q128. Master advanced tree algorithms
# Advanced Tree Algorithms
# 1. Lowest Common Ancestor
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def lowest_common_ancestor(root, p, q):
'''LCA in binary tree'''
if not root or root == p or root == q:
return root
left = lowest_common_ancestor(root.left, p, q)
right = lowest_common_ancestor(root.right, p, q)
if left and right:
return root
return left if left else right
def lca_bst(root, p, q):
'''LCA in BST - O(h)'''
while root:
if p.val < root.val and q.val < root.val:
root = root.left
elif p.val > root.val and q.val > root.val:
root = root.right
else:
return root
return None
# 2. Binary Lifting for LCA
class BinaryLifting:
'''Preprocess tree for O(log n) LCA queries'''
def __init__(self, n, edges, root=0):
self.n = n
self.LOG = 20
self.graph = defaultdict(list)
for u, v in edges:
self.graph[u].append(v)
self.graph[v].append(u)
self.depth = [0] * n
self.parent = [[-1] * self.LOG for _ in range(n)]
self._dfs(root, -1, 0)
self._preprocess()
def _dfs(self, node, par, d):
self.depth[node] = d
self.parent[node][0] = par
for neighbor in self.graph[node]:
if neighbor != par:
self._dfs(neighbor, node, d + 1)
def _preprocess(self):
for j in range(1, self.LOG):
for i in range(self.n):
if self.parent[i][j - 1] != -1:
self.parent[i][j] = self.parent[self.parent[i][j - 1]][j - 1]
def lca(self, u, v):
if self.depth[u] < self.depth[v]:
u, v = v, u
# Bring u to same level as v
diff = self.depth[u] - self.depth[v]
for i in range(self.LOG):
if (diff >> i) & 1:
u = self.parent[u][i]
if u == v:
return u
# Binary search for LCA
for i in range(self.LOG - 1, -1, -1):
if self.parent[u][i] != self.parent[v][i]:
u = self.parent[u][i]
v = self.parent[v][i]
return self.parent[u][0]
# 3. Tree Diameter
def tree_diameter(n, edges):
'''Longest path in tree'''
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
def bfs(start):
visited = {start}
queue = [(start, 0)]
farthest = (start, 0)
while queue:
node, dist = queue.pop(0)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, dist + 1))
if dist + 1 > farthest[1]:
farthest = (neighbor, dist + 1)
return farthest
# Find farthest node from arbitrary start
farthest_from_0, _ = bfs(0)
# Find farthest node from previous farthest
_, diameter = bfs(farthest_from_0)
return diameter
# 4. Tree Center
def find_tree_center(n, edges):
'''Find center(s) of tree'''
if n <= 2:
return list(range(n))
graph = defaultdict(list)
degree = [0] * n
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
degree[u] += 1
degree[v] += 1
# Start with leaves
leaves = [i for i in range(n) if degree[i] == 1]
remaining = n
while remaining > 2:
remaining -= len(leaves)
new_leaves = []
for leaf in leaves:
for neighbor in graph[leaf]:
degree[neighbor] -= 1
if degree[neighbor] == 1:
new_leaves.append(neighbor)
leaves = new_leaves
return leaves
# 5. Serialize and Deserialize Tree
class Codec:
'''Encode/decode binary tree'''
def serialize(self, root):
def dfs(node):
if not node:
return ['null']
return [str(node.val)] + dfs(node.left) + dfs(node.right)
return ','.join(dfs(root))
def deserialize(self, data):
def dfs(vals):
val = next(vals)
if val == 'null':
return None
node = TreeNode(int(val))
node.left = dfs(vals)
node.right = dfs(vals)
return node
return dfs(iter(data.split(',')))
# 6. Vertical Order Traversal
def vertical_order(root):
'''Traverse tree vertically'''
if not root:
return []
column_table = defaultdict(list)
queue = [(root, 0)] # (node, column)
while queue:
node, col = queue.pop(0)
if node:
column_table[col].append(node.val)
queue.append((node.left, col - 1))
queue.append((node.right, col + 1))
return [column_table[col] for col in sorted(column_table.keys())]
# 7. Morris Traversal (O(1) space)
def morris_inorder(root):
'''Inorder traversal without recursion/stack'''
result = []
current = root
while current:
if not current.left:
result.append(current.val)
current = current.right
else:
# Find predecessor
predecessor = current.left
while predecessor.right and predecessor.right != current:
predecessor = predecessor.right
if not predecessor.right:
predecessor.right = current
current = current.left
else:
predecessor.right = None
result.append(current.val)
current = current.right
return resultPythonAnswer: Advanced tree: LCA using recursion O(n) or binary lifting O(log n); tree diameter via two BFS; tree center by removing leaves; Morris traversal O(1) space using threading; serialize/deserialize for persistence.
Q129. Implement Fenwick Tree (Binary Indexed Tree)
# Fenwick Tree (Binary Indexed Tree)
class FenwickTree:
'''Efficient range sum queries and point updates - O(log n)'''
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def update(self, i, delta):
'''Add delta to element at index i'''
i += 1 # 1-indexed
while i <= self.n:
self.tree[i] += delta
i += i & (-i) # Add last set bit
def query(self, i):
'''Sum of elements from 0 to i'''
i += 1 # 1-indexed
total = 0
while i > 0:
total += self.tree[i]
i -= i & (-i) # Remove last set bit
return total
def range_query(self, left, right):
'''Sum of elements from left to right'''
if left == 0:
return self.query(right)
return self.query(right) - self.query(left - 1)
# Build from array
def build_fenwick(arr):
'''Build Fenwick tree from array'''
n = len(arr)
ft = FenwickTree(n)
for i, val in enumerate(arr):
ft.update(i, val)
return ft
# 2D Fenwick Tree
class FenwickTree2D:
'''2D range sum queries'''
def __init__(self, m, n):
self.m = m
self.n = n
self.tree = [[0] * (n + 1) for _ in range(m + 1)]
def update(self, i, j, delta):
i += 1
while i <= self.m:
j_curr = j + 1
while j_curr <= self.n:
self.tree[i][j_curr] += delta
j_curr += j_curr & (-j_curr)
i += i & (-i)
def query(self, i, j):
i += 1
total = 0
while i > 0:
j_curr = j + 1
while j_curr > 0:
total += self.tree[i][j_curr]
j_curr -= j_curr & (-j_curr)
i -= i & (-i)
return total
def range_query(self, r1, c1, r2, c2):
'''Sum in rectangle from (r1,c1) to (r2,c2)'''
total = self.query(r2, c2)
if r1 > 0:
total -= self.query(r1 - 1, c2)
if c1 > 0:
total -= self.query(r2, c1 - 1)
if r1 > 0 and c1 > 0:
total += self.query(r1 - 1, c1 - 1)
return total
# Count inversions using Fenwick Tree
def count_inversions(arr):
'''Count pairs (i,j) where i<j and arr[i]>arr[j]'''
# Coordinate compression
sorted_arr = sorted(set(arr))
rank = {val: i for i, val in enumerate(sorted_arr)}
ft = FenwickTree(len(sorted_arr))
inversions = 0
for i in range(len(arr) - 1, -1, -1):
r = rank[arr[i]]
inversions += ft.query(r - 1) if r > 0 else 0
ft.update(r, 1)
return inversions
# Range Update and Point Query
class FenwickTreeRangeUpdate:
'''Support range updates and point queries'''
def __init__(self, n):
self.n = n
self.tree = [0] * (n + 1)
def range_update(self, left, right, delta):
'''Add delta to range [left, right]'''
self._update(left, delta)
self._update(right + 1, -delta)
def _update(self, i, delta):
i += 1
while i <= self.n:
self.tree[i] += delta
i += i & (-i)
def point_query(self, i):
'''Get value at index i'''
i += 1
total = 0
while i > 0:
total += self.tree[i]
i -= i & (-i)
return totalPythonAnswer: Fenwick Tree supports range sum queries and point updates in O(log n); uses binary representation for efficient tree traversal; can extend to 2D, range updates, and inversion counting.
Q130. Master advanced Trie applications
# Advanced Trie Applications
# 1. Longest Common Prefix
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
def longest_common_prefix(strs):
'''Find longest common prefix using Trie'''
if not strs:
return ""
# Build trie
root = TrieNode()
for word in strs:
node = root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
# Find LCP
prefix = []
node = root
while len(node.children) == 1 and not node.is_end:
char = list(node.children.keys())[0]
prefix.append(char)
node = node.children[char]
return ''.join(prefix)
# 2. Maximum XOR with Trie
class TrieNodeBit:
def __init__(self):
self.children = {}
def find_maximum_xor_trie(nums):
'''Find maximum XOR using binary trie'''
root = TrieNodeBit()
# Build trie
for num in nums:
node = root
for i in range(31, -1, -1):
bit = (num >> i) & 1
if bit not in node.children:
node.children[bit] = TrieNodeBit()
node = node.children[bit]
max_xor = 0
# Find maximum XOR
for num in nums:
node = root
current_xor = 0
for i in range(31, -1, -1):
bit = (num >> i) & 1
# Try to go opposite direction
toggle = 1 - bit
if toggle in node.children:
current_xor |= (1 << i)
node = node.children[toggle]
else:
node = node.children[bit]
max_xor = max(max_xor, current_xor)
return max_xor
# 3. Word Squares
def word_squares(words):
'''Find all word squares'''
def build_prefix_map(words):
prefix_map = {}
for word in words:
for i in range(len(word) + 1):
prefix = word[:i]
if prefix not in prefix_map:
prefix_map[prefix] = []
prefix_map[prefix].append(word)
return prefix_map
prefix_map = build_prefix_map(words)
n = len(words[0])
result = []
def backtrack(square):
if len(square) == n:
result.append(square[:])
return
prefix_len = len(square)
prefix = ''.join(word[prefix_len] for word in square)
for word in prefix_map.get(prefix, []):
square.append(word)
backtrack(square)
square.pop()
for word in words:
backtrack([word])
return result
# 4. Concatenated Words
def find_all_concatenated_words(words):
'''Find words made of other words'''
word_set = set(words)
memo = {}
def can_form(word, original=True):
if word in memo:
return memo[word]
if not original and word in word_set:
return True
for i in range(1, len(word)):
prefix = word[:i]
if prefix in word_set:
suffix = word[i:]
if can_form(suffix, False):
memo[word] = True
return True
memo[word] = False
return False
result = []
for word in words:
if can_form(word, True):
result.append(word)
return result
# 5. Lexicographical Numbers
def lexical_order(n):
'''Generate numbers 1 to n in lexicographical order'''
result = []
def dfs(current):
if current > n:
return
result.append(current)
for digit in range(10):
next_num = current * 10 + digit
if next_num > n:
break
dfs(next_num)
for i in range(1, 10):
dfs(i)
return result
# 6. Stream of Characters with Trie
class StreamChecker:
'''Check if suffix matches any word'''
def __init__(self, words):
self.trie = TrieNode()
self.stream = []
self.max_len = 0
# Build reverse trie
for word in words:
node = self.trie
self.max_len = max(self.max_len, len(word))
for char in reversed(word):
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
def query(self, letter):
self.stream.append(letter)
node = self.trie
# Check suffix
for i in range(len(self.stream) - 1, max(-1, len(self.stream) - self.max_len - 1), -1):
char = self.stream[i]
if char not in node.children:
return False
node = node.children[char]
if node.is_end:
return True
return FalsePythonAnswer: Advanced Trie: binary trie for XOR problems, prefix map for word squares, reverse trie for suffix matching, DFS for lexicographical order; optimize with memoization and pruning.
Q131. Master advanced DP patterns
# Advanced DP Patterns
# 1. Bitmask DP - Traveling Salesman Problem
def tsp(dist):
'''Minimum cost to visit all cities'''
n = len(dist)
VISITED_ALL = (1 << n) - 1
# dp[mask][pos] = min cost to visit cities in mask, ending at pos
dp = [[float('inf')] * n for _ in range(1 << n)]
dp[1][0] = 0 # Start at city 0
for mask in range(1 << n):
for pos in range(n):
if not (mask & (1 << pos)):
continue
for next_city in range(n):
if mask & (1 << next_city):
continue
next_mask = mask | (1 << next_city)
dp[next_mask][next_city] = min(
dp[next_mask][next_city],
dp[mask][pos] + dist[pos][next_city]
)
# Return to starting city
return min(dp[VISITED_ALL][i] + dist[i][0] for i in range(n))
# 2. Digit DP
def count_numbers_with_unique_digits(n):
'''Count numbers with unique digits from 0 to 10^n - 1'''
if n == 0:
return 1
result = 10 # For n=1: 0-9
unique_digits = 9
available = 9
while n > 1 and available > 0:
unique_digits *= available
result += unique_digits
available -= 1
n -= 1
return result
# 3. DP on Trees
def tree_dp_max_sum(root):
'''Maximum path sum in tree (rob house III)'''
def dfs(node):
if not node:
return (0, 0) # (rob, not_rob)
left_rob, left_not_rob = dfs(node.left)
right_rob, right_not_rob = dfs(node.right)
# Rob current node
rob = node.val + left_not_rob + right_not_rob
# Don't rob current node
not_rob = max(left_rob, left_not_rob) + max(right_rob, right_not_rob)
return (rob, not_rob)
return max(dfs(root))
# 4. Probability DP
def knight_probability(n, k, row, col):
'''Probability knight stays on board after k moves'''
directions = [
(-2, -1), (-2, 1), (-1, -2), (-1, 2),
(1, -2), (1, 2), (2, -1), (2, 1)
]
dp = [[0] * n for _ in range(n)]
dp[row][col] = 1
for _ in range(k):
new_dp = [[0] * n for _ in range(n)]
for r in range(n):
for c in range(n):
for dr, dc in directions:
nr, nc = r + dr, c + dc
if 0 <= nr < n and 0 <= nc < n:
new_dp[nr][nc] += dp[r][c] / 8.0
dp = new_dp
return sum(sum(row) for row in dp)
# 5. State Machine DP - Stock Trading
def max_profit_k_transactions(prices, k):
'''Maximum profit with at most k transactions'''
if not prices or k == 0:
return 0
n = len(prices)
# Optimize for unlimited transactions
if k >= n // 2:
return sum(max(prices[i+1] - prices[i], 0) for i in range(n-1))
# dp[i][j][0] = max profit after i transactions, on day j, not holding stock
# dp[i][j][1] = max profit after i transactions, on day j, holding stock
buy = [-float('inf')] * (k + 1)
sell = [0] * (k + 1)
for price in prices:
for i in range(k, 0, -1):
sell[i] = max(sell[i], buy[i] + price)
buy[i] = max(buy[i], sell[i-1] - price)
return sell[k]
# 6. Game Theory DP
def stone_game_dp(piles):
'''Optimal strategy for stone game'''
n = len(piles)
dp = [[0] * n for _ in range(n)]
# Base case: single pile
for i in range(n):
dp[i][i] = piles[i]
# Fill DP table
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = max(
piles[i] - dp[i+1][j], # Take first pile
piles[j] - dp[i][j-1] # Take last pile
)
return dp[0][n-1] > 0
# 7. Convex Hull Trick
def min_cost_polygon_triangulation(values):
'''Minimum score from polygon triangulation'''
n = len(values)
dp = [[0] * n for _ in range(n)]
for length in range(3, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = float('inf')
for k in range(i + 1, j):
cost = dp[i][k] + dp[k][j] + values[i] * values[k] * values[j]
dp[i][j] = min(dp[i][j], cost)
return dp[0][n-1]PythonAnswer: Advanced DP: bitmask DP for TSP and subset problems; digit DP for counting; tree DP for path problems; probability DP for expected values; state machine for stock trading; game theory for optimal strategies.
Q132. Implement advanced string algorithms
# Advanced String Algorithms
# 1. Suffix Array
def build_suffix_array(s):
'''Build suffix array in O(n log n)'''
n = len(s)
suffixes = [(s[i:], i) for i in range(n)]
suffixes.sort()
return [suffix[1] for suffix in suffixes]
def build_suffix_array_fast(s):
'''Faster O(n log^2 n) implementation'''
n = len(s)
s += '$' # Sentinel
# Initial ranking
order = sorted(range(n + 1), key=lambda i: s[i])
rank = [0] * (n + 1)
for i, suffix in enumerate(order):
rank[suffix] = i
k = 0
while (1 << k) < n:
# Sort by pairs
order = sorted(range(n + 1),
key=lambda i: (rank[i], rank[min(i + (1 << k), n)]))
new_rank = [0] * (n + 1)
for i in range(1, n + 1):
new_rank[order[i]] = new_rank[order[i-1]]
if (rank[order[i]] != rank[order[i-1]] or
rank[min(order[i] + (1 << k), n)] !=
rank[min(order[i-1] + (1 << k), n)]):
new_rank[order[i]] += 1
rank = new_rank
k += 1
return order[1:] # Exclude sentinel
# 2. LCP Array (Longest Common Prefix)
def build_lcp_array(s, suffix_array):
'''Build LCP array using Kasai's algorithm - O(n)'''
n = len(s)
rank = [0] * n
for i, suffix in enumerate(suffix_array):
rank[suffix] = i
lcp = [0] * n
h = 0
for i in range(n):
if rank[i] > 0:
j = suffix_array[rank[i] - 1]
while i + h < n and j + h < n and s[i + h] == s[j + h]:
h += 1
lcp[rank[i]] = h
if h > 0:
h -= 1
return lcp
# 3. Aho-Corasick Algorithm
class AhoCorasick:
'''Multiple pattern matching'''
class Node:
def __init__(self):
self.children = {}
self.fail = None
self.output = []
def __init__(self, patterns):
self.root = self.Node()
self._build_trie(patterns)
self._build_failure_links()
def _build_trie(self, patterns):
for pattern in patterns:
node = self.root
for char in pattern:
if char not in node.children:
node.children[char] = self.Node()
node = node.children[char]
node.output.append(pattern)
def _build_failure_links(self):
from collections import deque
queue = deque()
# Set failure for depth 1
for child in self.root.children.values():
child.fail = self.root
queue.append(child)
# BFS to set failure links
while queue:
node = queue.popleft()
for char, child in node.children.items():
queue.append(child)
fail = node.fail
while fail and char not in fail.children:
fail = fail.fail
child.fail = fail.children[char] if fail else self.root
child.output.extend(child.fail.output)
def search(self, text):
'''Find all pattern occurrences'''
results = []
node = self.root
for i, char in enumerate(text):
while node and char not in node.children:
node = node.fail
node = node.children[char] if node else self.root
for pattern in node.output:
results.append((i - len(pattern) + 1, pattern))
return results
# 4. Suffix Automaton
class SuffixAutomaton:
'''Recognize all substrings efficiently'''
class State:
def __init__(self):
self.length = 0
self.link = None
self.next = {}
def __init__(self, s):
self.last = self.root = self.State()
for char in s:
self._extend(char)
def _extend(self, char):
new_state = self.State()
new_state.length = self.last.length + 1
current = self.last
while current and char not in current.next:
current.next[char] = new_state
current = current.link
if not current:
new_state.link = self.root
else:
next_state = current.next[char]
if current.length + 1 == next_state.length:
new_state.link = next_state
else:
clone = self.State()
clone.length = current.length + 1
clone.next = next_state.next.copy()
clone.link = next_state.link
while current and current.next.get(char) == next_state:
current.next[char] = clone
current = current.link
next_state.link = new_state.link = clone
self.last = new_state
def contains(self, substring):
'''Check if substring exists'''
current = self.root
for char in substring:
if char not in current.next:
return False
current = current.next[char]
return True
# 5. Longest Palindrome Substring (Manacher's)
def manacher(s):
'''Find longest palindrome in O(n)'''
# Transform string
t = '#'.join('^{}$'.format(s))
n = len(t)
p = [0] * n
center = right = 0
for i in range(1, n - 1):
if i < right:
mirror = 2 * center - i
p[i] = min(right - i, p[mirror])
# Expand around i
while t[i + p[i] + 1] == t[i - p[i] - 1]:
p[i] += 1
# Update center and right
if i + p[i] > right:
center, right = i, i + p[i]
# Find longest palindrome
max_len, center_idx = max((n, i) for i, n in enumerate(p))
start = (center_idx - max_len) // 2
return s[start:start + max_len]PythonAnswer: Advanced strings: suffix array O(n log n) for substring problems; LCP array O(n) using Kasai’s; Aho-Corasick for multiple pattern matching; suffix automaton for all substrings; Manacher’s O(n) for palindromes.
Q133. Solve computational geometry problems
# Computational Geometry
# 1. Convex Hull - Graham Scan
def convex_hull(points):
'''Find convex hull using Graham scan - O(n log n)'''
def cross(o, a, b):
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
points = sorted(points)
# Build lower hull
lower = []
for p in points:
while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
lower.pop()
lower.append(p)
# Build upper hull
upper = []
for p in reversed(points):
while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
upper.pop()
upper.append(p)
return lower[:-1] + upper[:-1]
# 2. Line Intersection
def line_intersection(p1, p2, p3, p4):
'''Check if line segments intersect'''
def ccw(a, b, c):
return (c[1] - a[1]) * (b[0] - a[0]) > (b[1] - a[1]) * (c[0] - a[0])
return (ccw(p1, p3, p4) != ccw(p2, p3, p4) and
ccw(p1, p2, p3) != ccw(p1, p2, p4))
def get_intersection_point(p1, p2, p3, p4):
'''Get intersection point of two lines'''
x1, y1 = p1
x2, y2 = p2
x3, y3 = p3
x4, y4 = p4
denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
if abs(denom) < 1e-10:
return None # Parallel lines
t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom
x = x1 + t * (x2 - x1)
y = y1 + t * (y2 - y1)
return (x, y)
# 3. Point in Polygon
def point_in_polygon(point, polygon):
'''Ray casting algorithm'''
x, y = point
n = len(polygon)
inside = False
p1x, p1y = polygon[0]
for i in range(1, n + 1):
p2x, p2y = polygon[i % n]
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
# 4. Closest Pair of Points
def closest_pair(points):
'''Find closest pair in O(n log n)'''
points = sorted(points)
def distance(p1, p2):
return ((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)**0.5
def closest_pair_recursive(px, py):
n = len(px)
if n <= 3:
return min((distance(px[i], px[j]), (px[i], px[j]))
for i in range(n) for j in range(i + 1, n))
mid = n // 2
midpoint = px[mid]
pyl = [p for p in py if p[0] <= midpoint[0]]
pyr = [p for p in py if p[0] > midpoint[0]]
dl, pair_l = closest_pair_recursive(px[:mid], pyl)
dr, pair_r = closest_pair_recursive(px[mid:], pyr)
d, pair = (dl, pair_l) if dl < dr else (dr, pair_r)
# Check strip
strip = [p for p in py if abs(p[0] - midpoint[0]) < d]
for i in range(len(strip)):
for j in range(i + 1, min(i + 7, len(strip))):
dist = distance(strip[i], strip[j])
if dist < d:
d, pair = dist, (strip[i], strip[j])
return d, pair
py = sorted(points, key=lambda p: p[1])
return closest_pair_recursive(points, py)
# 5. Maximum Points on a Line
def max_points_on_line(points):
'''Maximum points on same line'''
if len(points) <= 2:
return len(points)
from collections import defaultdict
from math import gcd
def get_slope(p1, p2):
dx = p2[0] - p1[0]
dy = p2[1] - p1[1]
if dx == 0:
return (0, 1)
if dy == 0:
return (1, 0)
g = gcd(dx, dy)
return (dy // g, dx // g)
max_count = 0
for i, p1 in enumerate(points):
slopes = defaultdict(int)
same = 1
for j, p2 in enumerate(points):
if i == j:
continue
if p1 == p2:
same += 1
else:
slope = get_slope(p1, p2)
slopes[slope] += 1
max_count = max(max_count, same + (max(slopes.values()) if slopes else 0))
return max_count
# 6. Rectangle Overlap
def is_rectangle_overlap(rec1, rec2):
'''Check if two rectangles overlap'''
x1, y1, x2, y2 = rec1
x3, y3, x4, y4 = rec2
return not (x2 <= x3 or x4 <= x1 or y2 <= y3 or y4 <= y1)PythonAnswer: Geometry: Graham scan O(n log n) for convex hull; line intersection using cross product; point in polygon via ray casting; closest pair divide-and-conquer O(n log n); slope calculation with GCD for collinear points.
Q134. Implement advanced cache data structures
# Cache and LRU Implementations
# 1. LRU Cache
from collections import OrderedDict
class LRUCache:
'''Least Recently Used cache - O(1) operations'''
def __init__(self, capacity):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return -1
# Move to end (most recent)
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
# Remove least recently used (first item)
self.cache.popitem(last=False)
# Manual implementation with doubly linked list
class LRUCacheManual:
'''LRU using doubly linked list and hash map'''
class Node:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
def __init__(self, capacity):
self.capacity = capacity
self.cache = {}
# Dummy head and tail
self.head = self.Node()
self.tail = self.Node()
self.head.next = self.tail
self.tail.prev = self.head
def _add_to_head(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def _remove_node(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def get(self, key):
if key not in self.cache:
return -1
node = self.cache[key]
self._remove_node(node)
self._add_to_head(node)
return node.value
def put(self, key, value):
if key in self.cache:
node = self.cache[key]
node.value = value
self._remove_node(node)
self._add_to_head(node)
else:
node = self.Node(key, value)
self.cache[key] = node
self._add_to_head(node)
if len(self.cache) > self.capacity:
# Remove LRU
lru = self.tail.prev
self._remove_node(lru)
del self.cache[lru.key]
# 2. LFU Cache
class LFUCache:
'''Least Frequently Used cache'''
class Node:
def __init__(self, key, value, freq=1):
self.key = key
self.value = value
self.freq = freq
def __init__(self, capacity):
self.capacity = capacity
self.min_freq = 0
self.key_to_node = {}
self.freq_to_keys = defaultdict(OrderedDict)
def _update_freq(self, node):
key, freq = node.key, node.freq
# Remove from current frequency
del self.freq_to_keys[freq][key]
if not self.freq_to_keys[freq]:
del self.freq_to_keys[freq]
if self.min_freq == freq:
self.min_freq += 1
# Add to new frequency
node.freq += 1
self.freq_to_keys[node.freq][key] = node
def get(self, key):
if key not in self.key_to_node:
return -1
node = self.key_to_node[key]
self._update_freq(node)
return node.value
def put(self, key, value):
if self.capacity == 0:
return
if key in self.key_to_node:
node = self.key_to_node[key]
node.value = value
self._update_freq(node)
else:
if len(self.key_to_node) >= self.capacity:
# Remove LFU item
lfu_key, _ = self.freq_to_keys[self.min_freq].popitem(last=False)
del self.key_to_node[lfu_key]
# Add new item
node = self.Node(key, value)
self.key_to_node[key] = node
self.freq_to_keys[1][key] = node
self.min_freq = 1
# 3. Time-based Key-Value Store
class TimeMap:
'''Store values with timestamps'''
def __init__(self):
self.store = defaultdict(list)
def set(self, key, value, timestamp):
self.store[key].append((timestamp, value))
def get(self, key, timestamp):
if key not in self.store:
return ""
values = self.store[key]
# Binary search for largest timestamp <= given timestamp
left, right = 0, len(values) - 1
result = ""
while left <= right:
mid = (left + right) // 2
if values[mid][0] <= timestamp:
result = values[mid][1]
left = mid + 1
else:
right = mid - 1
return resultPythonAnswer: Caching: LRU uses OrderedDict or doubly linked list + hashmap for O(1) get/put; LFU tracks frequency with nested maps; TimeMap uses binary search on sorted timestamps; all optimize for constant time access.
Q135. Design complex data structures for real systems
# Design Advanced Data Structures
# 1. Design Twitter
from collections import defaultdict, deque
import heapq
class Twitter:
'''Design simplified Twitter'''
def __init__(self):
self.tweets = defaultdict(deque) # userId -> tweets
self.following = defaultdict(set) # userId -> followees
self.timestamp = 0
def postTweet(self, userId, tweetId):
self.tweets[userId].appendleft((self.timestamp, tweetId))
self.timestamp += 1
# Keep only recent 10 tweets per user
if len(self.tweets[userId]) > 10:
self.tweets[userId].pop()
def getNewsFeed(self, userId):
# Merge tweets from user and followees
heap = []
# Add user's own tweets
if self.tweets[userId]:
timestamp, tweetId = self.tweets[userId][0]
heapq.heappush(heap, (-timestamp, tweetId, userId, 0))
# Add followees' tweets
for followeeId in self.following[userId]:
if self.tweets[followeeId]:
timestamp, tweetId = self.tweets[followeeId][0]
heapq.heappush(heap, (-timestamp, tweetId, followeeId, 0))
# Get top 10 tweets
result = []
while heap and len(result) < 10:
timestamp, tweetId, uid, index = heapq.heappop(heap)
result.append(tweetId)
# Add next tweet from same user
if index + 1 < len(self.tweets[uid]):
next_timestamp, next_tweetId = self.tweets[uid][index + 1]
heapq.heappush(heap, (-next_timestamp, next_tweetId, uid, index + 1))
return result
def follow(self, followerId, followeeId):
if followerId != followeeId:
self.following[followerId].add(followeeId)
def unfollow(self, followerId, followeeId):
self.following[followerId].discard(followeeId)
# 2. Design Search Autocomplete System
class AutocompleteSystem:
'''Search autocomplete with ranking'''
class TrieNode:
def __init__(self):
self.children = {}
self.sentences = {} # sentence -> frequency
def __init__(self, sentences, times):
self.root = self.TrieNode()
self.current_node = self.root
self.current_input = ""
# Build trie
for sentence, time in zip(sentences, times):
self._insert(sentence, time)
def _insert(self, sentence, freq):
node = self.root
for char in sentence:
if char not in node.children:
node.children[char] = self.TrieNode()
node = node.children[char]
node.sentences[sentence] = node.sentences.get(sentence, 0) + freq
def input(self, c):
if c == '#':
# Save current input
self._insert(self.current_input, 1)
self.current_input = ""
self.current_node = self.root
return []
self.current_input += c
if c not in self.current_node.children:
self.current_node.children[c] = self.TrieNode()
self.current_node = self.current_node.children[c]
# Get top 3 sentences
sentences = sorted(
self.current_node.sentences.items(),
key=lambda x: (-x[1], x[0])
)
return [s for s, _ in sentences[:3]]
# 3. Design In-Memory File System
class FileSystem:
'''In-memory file system with directory structure'''
class Node:
def __init__(self, is_file=False):
self.is_file = is_file
self.content = ""
self.children = {}
def __init__(self):
self.root = self.Node()
def _get_node(self, path):
node = self.root
if path == "/":
return node
parts = path.split("/")[1:]
for part in parts:
if part not in node.children:
node.children[part] = self.Node()
node = node.children[part]
return node
def ls(self, path):
node = self._get_node(path)
if node.is_file:
return [path.split("/")[-1]]
return sorted(node.children.keys())
def mkdir(self, path):
self._get_node(path)
def addContentToFile(self, filePath, content):
node = self._get_node(filePath)
node.is_file = True
node.content += content
def readContentFromFile(self, filePath):
node = self._get_node(filePath)
return node.content
# 4. Design Hit Counter
class HitCounter:
'''Count hits in last 5 minutes'''
def __init__(self):
self.hits = deque()
def hit(self, timestamp):
self.hits.append(timestamp)
def getHits(self, timestamp):
# Remove hits older than 300 seconds
while self.hits and self.hits[0] <= timestamp - 300:
self.hits.popleft()
return len(self.hits)
# Optimized with buckets
class HitCounterOptimized:
'''O(1) space complexity'''
def __init__(self):
self.times = [0] * 300
self.hits = [0] * 300
def hit(self, timestamp):
idx = timestamp % 300
if self.times[idx] != timestamp:
self.times[idx] = timestamp
self.hits[idx] = 1
else:
self.hits[idx] += 1
def getHits(self, timestamp):
total = 0
for i in range(300):
if timestamp - self.times[i] < 300:
total += self.hits[i]
return total
# 5. Design Tic-Tac-Toe
class TicTacToe:
'''Efficient tic-tac-toe checker'''
def __init__(self, n):
self.n = n
self.rows = [0] * n
self.cols = [0] * n
self.diagonal = 0
self.anti_diagonal = 0
def move(self, row, col, player):
to_add = 1 if player == 1 else -1
self.rows[row] += to_add
self.cols[col] += to_add
if row == col:
self.diagonal += to_add
if row + col == self.n - 1:
self.anti_diagonal += to_add
# Check win
if (abs(self.rows[row]) == self.n or
abs(self.cols[col]) == self.n or
abs(self.diagonal) == self.n or
abs(self.anti_diagonal) == self.n):
return player
return 0PythonAnswer: System design: Twitter uses heap for news feed merge; autocomplete uses trie with frequency tracking; file system uses tree structure; hit counter uses circular buffer or deque; tic-tac-toe uses row/col/diagonal counters.
Q136. Solve complex algorithmic problems
# Advanced Problem Solving Techniques
# 1. Expression Evaluation
def calculate(s):
'''Basic calculator with +, -, (), spaces'''
stack = []
num = 0
sign = 1
result = 0
for char in s:
if char.isdigit():
num = num * 10 + int(char)
elif char == '+':
result += sign * num
num = 0
sign = 1
elif char == '-':
result += sign * num
num = 0
sign = -1
elif char == '(':
stack.append(result)
stack.append(sign)
result = 0
sign = 1
elif char == ')':
result += sign * num
num = 0
result *= stack.pop() # sign before (
result += stack.pop() # result before (
result += sign * num
return result
def calculate_ii(s):
'''Calculator with +, -, *, /'''
stack = []
num = 0
operation = '+'
for i, char in enumerate(s):
if char.isdigit():
num = num * 10 + int(char)
if char in '+-*/' or i == len(s) - 1:
if operation == '+':
stack.append(num)
elif operation == '-':
stack.append(-num)
elif operation == '*':
stack.append(stack.pop() * num)
elif operation == '/':
stack.append(int(stack.pop() / num))
if i < len(s) - 1:
operation = char
num = 0
return sum(stack)
# 2. Median Finder (Running Median)
class MedianFinder:
'''Find median in data stream'''
def __init__(self):
self.small = [] # max heap (negative values)
self.large = [] # min heap
def addNum(self, num):
# Add to max heap
heapq.heappush(self.small, -num)
# Balance heaps
if self.small and self.large and (-self.small[0] > self.large[0]):
val = -heapq.heappop(self.small)
heapq.heappush(self.large, val)
# Size balance
if len(self.small) > len(self.large) + 1:
val = -heapq.heappop(self.small)
heapq.heappush(self.large, val)
if len(self.large) > len(self.small):
val = heapq.heappop(self.large)
heapq.heappush(self.small, -val)
def findMedian(self):
if len(self.small) > len(self.large):
return -self.small[0]
return (-self.small[0] + self.large[0]) / 2.0
# 3. Skyline Problem
def get_skyline(buildings):
'''Critical points where height changes'''
events = []
for left, right, height in buildings:
events.append((left, -height, right)) # Start event
events.append((right, 0, 0)) # End event
events.sort()
result = []
heap = [(0, float('inf'))] # (negative height, end position)
i = 0
while i < len(events):
current_x = events[i][0]
# Process all events at same x
while i < len(events) and events[i][0] == current_x:
x, neg_h, end = events[i]
if neg_h: # Start event
heapq.heappush(heap, (neg_h, end))
i += 1
# Remove buildings that have ended
while heap[0][1] <= current_x:
heapq.heappop(heap)
# Check if height changed
max_height = -heap[0][0]
if not result or result[-1][1] != max_height:
result.append([current_x, max_height])
return result
# 4. Reverse Polish Notation
def eval_rpn(tokens):
'''Evaluate RPN expression'''
stack = []
for token in tokens:
if token in '+-*/':
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[0]
# 5. Encode and Decode Strings
class Codec:
'''Encode list of strings to single string'''
def encode(self, strs):
return ''.join(f"{len(s)}#{s}" for s in strs)
def decode(self, s):
result = []
i = 0
while i < len(s):
# Find length
j = i
while s[j] != '#':
j += 1
length = int(s[i:j])
result.append(s[j + 1:j + 1 + length])
i = j + 1 + length
return result
# 6. Longest Increasing Path in Matrix
def longest_increasing_path(matrix):
'''DFS with memoization'''
if not matrix:
return 0
m, n = len(matrix), len(matrix[0])
memo = {}
def dfs(i, j):
if (i, j) in memo:
return memo[(i, j)]
max_path = 1
for di, dj in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
ni, nj = i + di, j + dj
if (0 <= ni < m and 0 <= nj < n and
matrix[ni][nj] > matrix[i][j]):
max_path = max(max_path, 1 + dfs(ni, nj))
memo[(i, j)] = max_path
return max_path
return max(dfs(i, j) for i in range(m) for j in range(n))PythonAnswer: Advanced techniques: expression evaluation uses stack; median finder uses two heaps; skyline uses sweep line + heap; RPN uses stack; encode/decode handles variable length; longest path uses DFS + memoization.
Q137. Implement randomized algorithms
# Randomized Algorithms
# 1. Shuffle Array (Fisher-Yates)
import random
class Solution:
'''Shuffle array with equal probability'''
def __init__(self, nums):
self.original = nums[:]
self.array = nums[:]
def reset(self):
self.array = self.original[:]
return self.array
def shuffle(self):
for i in range(len(self.array) - 1, 0, -1):
j = random.randint(0, i)
self.array[i], self.array[j] = self.array[j], self.array[i]
return self.array
# 2. Random Pick with Weight
class RandomPickWeight:
'''Pick index based on weights'''
def __init__(self, w):
self.prefix_sums = []
total = 0
for weight in w:
total += weight
self.prefix_sums.append(total)
self.total = total
def pick_index(self):
target = random.random() * self.total
# Binary search
left, right = 0, len(self.prefix_sums) - 1
while left < right:
mid = (left + right) // 2
if self.prefix_sums[mid] < target:
left = mid + 1
else:
right = mid
return left
# 3. Random Pick Index
class RandomPickIndex:
'''Pick random index of target value'''
def __init__(self, nums):
self.nums = nums
def pick(self, target):
count = 0
result = -1
for i, num in enumerate(self.nums):
if num == target:
count += 1
# Reservoir sampling
if random.randint(1, count) == 1:
result = i
return result
# 4. Generate Random Point in Circle
class RandomPointInCircle:
'''Uniformly random point in circle'''
def __init__(self, radius, x_center, y_center):
self.radius = radius
self.x_center = x_center
self.y_center = y_center
def rand_point(self):
# Rejection sampling
while True:
x = random.uniform(-self.radius, self.radius)
y = random.uniform(-self.radius, self.radius)
if x**2 + y**2 <= self.radius**2:
return [self.x_center + x, self.y_center + y]
# 5. Linked List Random Node
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class LinkedListRandom:
'''Random node from linked list (unknown length)'''
def __init__(self, head):
self.head = head
def get_random(self):
result = self.head.val
current = self.head.next
count = 2
while current:
if random.randint(1, count) == 1:
result = current.val
current = current.next
count += 1
return resultPythonAnswer: Randomization: Fisher-Yates shuffle O(n); weighted random using prefix sums + binary search; reservoir sampling for streams; rejection sampling for geometric shapes; maintains uniform probability distribution.
Q138. Solve matrix manipulation problems
# Matrix Algorithms
# 1. Rotate Matrix 90 Degrees
def rotate(matrix):
'''Rotate n×n matrix 90 degrees clockwise in-place'''
n = len(matrix)
# Transpose
for i in range(n):
for j in range(i + 1, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
# Reverse each row
for i in range(n):
matrix[i].reverse()
# 2. Spiral Matrix
def spiral_order(matrix):
'''Return elements in spiral order'''
if not matrix:
return []
result = []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
while top <= bottom and left <= right:
# Right
for j in range(left, right + 1):
result.append(matrix[top][j])
top += 1
# Down
for i in range(top, bottom + 1):
result.append(matrix[i][right])
right -= 1
# Left
if top <= bottom:
for j in range(right, left - 1, -1):
result.append(matrix[bottom][j])
bottom -= 1
# Up
if left <= right:
for i in range(bottom, top - 1, -1):
result.append(matrix[i][left])
left += 1
return result
# 3. Set Matrix Zeroes
def set_zeroes(matrix):
'''Set row and column to 0 if element is 0'''
m, n = len(matrix), len(matrix[0])
first_row_zero = any(matrix[0][j] == 0 for j in range(n))
first_col_zero = any(matrix[i][0] == 0 for i in range(m))
# Use first row/col as markers
for i in range(1, m):
for j in range(1, n):
if matrix[i][j] == 0:
matrix[0][j] = 0
matrix[i][0] = 0
# Set zeros based on markers
for i in range(1, m):
for j in range(1, n):
if matrix[0][j] == 0 or matrix[i][0] == 0:
matrix[i][j] = 0
# Handle first row and column
if first_row_zero:
for j in range(n):
matrix[0][j] = 0
if first_col_zero:
for i in range(m):
matrix[i][0] = 0
# 4. Search 2D Matrix II
def search_matrix_ii(matrix, target):
'''Search in row and column sorted matrix'''
if not matrix:
return False
m, n = len(matrix), len(matrix[0])
row, col = 0, n - 1
while row < m and col >= 0:
if matrix[row][col] == target:
return True
elif matrix[row][col] > target:
col -= 1
else:
row += 1
return False
# 5. Valid Sudoku
def is_valid_sudoku(board):
'''Check if Sudoku board is valid'''
rows = [set() for _ in range(9)]
cols = [set() for _ in range(9)]
boxes = [set() for _ in range(9)]
for i in range(9):
for j in range(9):
if board[i][j] == '.':
continue
num = board[i][j]
box_idx = (i // 3) * 3 + j // 3
if num in rows[i] or num in cols[j] or num in boxes[box_idx]:
return False
rows[i].add(num)
cols[j].add(num)
boxes[box_idx].add(num)
return TruePythonAnswer: Matrix operations: rotate via transpose + reverse rows; spiral traversal with boundary pointers; set zeroes using first row/col as markers O(1) space; search sorted matrix from top-right corner O(m+n).
Q139. Master interval problems
# Interval Problems
# 1. Merge Intervals
def merge_intervals(intervals):
'''Merge overlapping intervals'''
if not intervals:
return []
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for start, end in intervals[1:]:
if start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
# 2. Insert Interval
def insert_interval(intervals, new_interval):
'''Insert and merge interval'''
result = []
i = 0
n = len(intervals)
# Add intervals before new_interval
while i < n and intervals[i][1] < new_interval[0]:
result.append(intervals[i])
i += 1
# Merge overlapping intervals
while i < n and intervals[i][0] <= new_interval[1]:
new_interval[0] = min(new_interval[0], intervals[i][0])
new_interval[1] = max(new_interval[1], intervals[i][1])
i += 1
result.append(new_interval)
# Add remaining intervals
while i < n:
result.append(intervals[i])
i += 1
return result
# 3. Employee Free Time
def employee_free_time(schedule):
'''Find common free time for all employees'''
# Flatten all intervals
intervals = []
for employee in schedule:
intervals.extend(employee)
# Sort by start time
intervals.sort(key=lambda x: x.start)
# Find gaps
free_time = []
prev_end = intervals[0].end
for interval in intervals[1:]:
if interval.start > prev_end:
free_time.append([prev_end, interval.start])
prev_end = max(prev_end, interval.end)
return free_time
# 4. Non-overlapping Intervals
def erase_overlap_intervals(intervals):
'''Minimum removals to make non-overlapping'''
if not intervals:
return 0
intervals.sort(key=lambda x: x[1])
end = intervals[0][1]
count = 0
for i in range(1, len(intervals)):
if intervals[i][0] < end:
count += 1
else:
end = intervals[i][1]
return count
# 5. My Calendar (Interval Booking)
class MyCalendar:
'''Book events without double booking'''
def __init__(self):
self.calendar = []
def book(self, start, end):
for s, e in self.calendar:
if not (end <= s or start >= e):
return False
self.calendar.append((start, end))
return TruePythonAnswer: Intervals: merge by sorting + greedy O(n log n); insert requires three phases; employee free time finds gaps; non-overlapping uses activity selection; calendar checks conflicts before booking.
Q140. Solve parentheses problems
# Parentheses Problems
# 1. Valid Parentheses
def is_valid(s):
'''Check if parentheses are valid'''
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
if not stack or stack[-1] != mapping[char]:
return False
stack.pop()
else:
stack.append(char)
return len(stack) == 0
# 2. Generate Parentheses
def generate_parenthesis(n):
'''Generate all valid n pairs of parentheses'''
result = []
def backtrack(current, open_count, close_count):
if len(current) == 2 * n:
result.append(current)
return
if open_count < n:
backtrack(current + '(', open_count + 1, close_count)
if close_count < open_count:
backtrack(current + ')', open_count, close_count + 1)
backtrack('', 0, 0)
return result
# 3. Longest Valid Parentheses
def longest_valid_parentheses(s):
'''Length of longest valid parentheses substring'''
stack = [-1]
max_len = 0
for i, char in enumerate(s):
if char == '(':
stack.append(i)
else:
stack.pop()
if not stack:
stack.append(i)
else:
max_len = max(max_len, i - stack[-1])
return max_len
# 4. Remove Invalid Parentheses
def remove_invalid_parentheses(s):
'''Remove minimum parentheses to make valid'''
def is_valid(s):
count = 0
for char in s:
if char == '(':
count += 1
elif char == ')':
count -= 1
if count < 0:
return False
return count == 0
level = {s}
while True:
valid = [s for s in level if is_valid(s)]
if valid:
return valid
next_level = set()
for string in level:
for i in range(len(string)):
if string[i] in '()':
next_level.add(string[:i] + string[i+1:])
level = next_level
# 5. Score of Parentheses
def score_of_parentheses(s):
'''Calculate score of balanced parentheses'''
stack = [0]
for char in s:
if char == '(':
stack.append(0)
else:
val = stack.pop()
stack[-1] += max(2 * val, 1)
return stack[0]PythonAnswer: Parentheses: validation uses stack with mapping; generation uses backtracking with counters; longest valid uses stack to track indices; removal uses BFS; score calculation uses nested stack values.
Q141. Range query and update problems
# Additional Advanced Topics
# Q141: Range Problems
def range_sum_query_mutable(nums):
'''Range sum with updates using Fenwick Tree'''
# See Q129 for full Fenwick Tree implementation
passPythonAnswer: Range queries: Fenwick Tree O(log n) for point update and range sum; Segment Tree for range updates; coordinate compression for large ranges; difference array for batch updates.
Q142. Palindrome partitioning and problems
: Palindrome Problems
def palindrome_partitioning(s):
'''Partition into all palindromes'''
result = []
def is_palindrome(string):
return string == string[::-1]
def backtrack(start, path):
if start == len(s):
result.append(path[:])
return
for end in range(start + 1, len(s) + 1):
if is_palindrome(s[start:end]):
path.append(s[start:end])
backtrack(end, path)
path.pop()
backtrack(0, [])
return resultPythonAnswer: Palindromes: backtracking for partitioning; DP for minimum cuts; expand around center for detection; Manacher’s O(n) for all palindromic substrings.
Q143. Monotonic queue applications
: Monotonic Queue
from collections import deque
def sliding_window_maximum(nums, k):
'''Maximum in each sliding window'''
dq = deque()
result = []
for i, num in enumerate(nums):
# Remove elements outside window
while dq and dq[0] < i - k + 1:
dq.popleft()
# Remove smaller elements
while dq and nums[dq[-1]] < num:
dq.pop()
dq.append(i)
if i >= k - 1:
result.append(nums[dq[0]])
return resultPythonAnswer: Monotonic queue: sliding window maximum O(n); maintains decreasing/increasing order; each element enters and exits once; useful for range min/max queries.
Q144. Tree construction from traversals
: Tree Construction
def build_tree_preorder_inorder(preorder, inorder):
'''Construct tree from preorder and inorder'''
if not preorder:
return None
root_val = preorder[0]
root = TreeNode(root_val)
mid = inorder.index(root_val)
root.left = build_tree_preorder_inorder(
preorder[1:mid+1],
inorder[:mid]
)
root.right = build_tree_preorder_inorder(
preorder[mid+1:],
inorder[mid+1:]
)
return rootPythonAnswer: Tree construction: preorder + inorder uniquely determines tree; postorder + inorder works; preorder + postorder only works for full binary trees; O(n) with index map.
Q145. Advanced graph problems and topological sort
: Advanced Graph Problems
def alien_dictionary(words):
'''Find alien language character order'''
from collections import defaultdict, deque
graph = defaultdict(set)
in_degree = {char: 0 for word in words for char in word}
# Build graph
for i in range(len(words) - 1):
w1, w2 = words[i], words[i + 1]
min_len = min(len(w1), len(w2))
for j in range(min_len):
if w1[j] != w2[j]:
if w2[j] not in graph[w1[j]]:
graph[w1[j]].add(w2[j])
in_degree[w2[j]] += 1
break
# Topological sort
queue = deque([c for c in in_degree if in_degree[c] == 0])
result = []
while queue:
char = queue.popleft()
result.append(char)
for neighbor in graph[char]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
return ''.join(result) if len(result) == len(in_degree) else ""PythonAnswer: Advanced graphs: alien dictionary uses topological sort; course schedule detects cycles; word ladder is BFS; accounts merge uses union-find.
Q146. Serialize complex data structures
: Serialize Complex Structures
def serialize_nary_tree(root):
'''Serialize N-ary tree'''
if not root:
return ""
def dfs(node):
if not node:
return ""
result = [str(node.val), str(len(node.children))]
for child in node.children:
result.append(dfs(child))
return ','.join(result)
return dfs(root)PythonAnswer: Serialization: N-ary tree stores child count; encode delimiter for strings; use level-order for reconstruction; handle null pointers explicitly.
Q147. Pattern matching and isomorphism
: Pattern Matching
def word_pattern(pattern, s):
'''Check if string follows pattern'''
words = s.split()
if len(pattern) != len(words):
return False
char_to_word = {}
word_to_char = {}
for char, word in zip(pattern, words):
if char in char_to_word:
if char_to_word[char] != word:
return False
else:
char_to_word[char] = word
if word in word_to_char:
if word_to_char[word] != char:
return False
else:
word_to_char[word] = char
return TruePythonAnswer: Pattern matching: use bidirectional mapping; check isomorphism with two hash maps; verify one-to-one correspondence; handle duplicates carefully.
Q148. Sliding window problems
: Window Problems
def min_window_substring(s, t):
'''Minimum window containing all characters of t'''
from collections import Counter
need = Counter(t)
have = {}
required = len(need)
formed = 0
left = 0
min_len = float('inf')
min_window = ""
for right in range(len(s)):
char = s[right]
have[char] = have.get(char, 0) + 1
if char in need and have[char] == need[char]:
formed += 1
while formed == required:
if right - left + 1 < min_len:
min_len = right - left + 1
min_window = s[left:right + 1]
char = s[left]
have[char] -= 1
if char in need and have[char] < need[char]:
formed -= 1
left += 1
return min_windowPythonAnswer: Sliding window: minimum window substring uses hash map + two pointers; expand right to include characters, shrink left to minimize; track required vs formed counts.
Q149. Divide and conquer advanced
: Divide and Conquer
def count_smaller(nums):
'''Count smaller elements to the right'''
def merge_sort(enum):
if len(enum) <= 1:
return enum
mid = len(enum) // 2
left = merge_sort(enum[:mid])
right = merge_sort(enum[mid:])
for i in range(len(right))[::-1]:
if not left or right[i][1] >= left[-1][1]:
left.append(right[i])
else:
while left and right[i][1] < left[-1][1]:
result[left[-1][0]] += len(right) - i
left.pop()
left.append(right[i])
return left
result = [0] * len(nums)
merge_sort(list(enumerate(nums)))
return resultPythonAnswer: Divide and conquer: count smaller uses merge sort with index tracking; maximum subarray uses Kadane’s or D&C; closest pair uses sorted points; median of medians for selection.
Q150. Algorithm selection and best practices
: Summary and Best Practices
best_practices = '''
Algorithm Selection Guide:
1. Sorting: Use built-in sort O(n log n), or counting sort O(n) for integers
2. Searching: Binary search O(log n) for sorted, hash table O(1) average
3. Graph: BFS for shortest path, DFS for connectivity, Dijkstra for weighted
4. Tree: Recursion for traversal, iterative with stack for space optimization
5. DP: Top-down for clarity, bottom-up for efficiency
6. String: KMP for pattern matching, trie for prefix queries
7. Interval: Sort by start/end, sweep line for events
8. Matrix: In-place when possible, consider boundaries carefully
Time Complexity Targets:
- O(1): Hash table operations, array access
- O(log n): Binary search, balanced tree operations
- O(n): Single pass algorithms, hash table building
- O(n log n): Sorting, divide and conquer
- O(n²): Nested loops (avoid when possible)
- O(2ⁿ): Backtracking with pruning
Space Optimization:
- Use variables instead of arrays when possible
- Sliding window instead of storing all subarrays
- In-place modification when allowed
- Two pointers to avoid extra space
Common Pitfalls:
- Off-by-one errors in binary search
- Not handling empty input
- Integer overflow in calculations
- Modifying collection while iterating
- Not considering duplicate values
- Forgetting edge cases
'''PythonAnswer: Best practices: choose right algorithm for complexity; optimize space when possible; handle edge cases; avoid common pitfalls; write clean, readable code; test thoroughly with various inputs including edge cases.
Q151. Master advanced subsequence DP problems
# Advanced Dynamic Programming - Subsequence Problems
# 1. Longest Palindromic Subsequence
def longest_palindrome_subseq(s):
'''Longest palindromic subsequence using DP'''
n = len(s)
dp = [[0] * n for _ in range(n)]
# Every single character is a palindrome
for i in range(n):
dp[i][i] = 1
# Build table bottom-up
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
dp[i][j] = dp[i + 1][j - 1] + 2
else:
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
return dp[0][n - 1]
# 2. Number of Longest Increasing Subsequence
def find_number_of_lis(nums):
'''Count of longest increasing subsequences'''
if not nums:
return 0
n = len(nums)
lengths = [1] * n
counts = [1] * n
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
if lengths[j] + 1 > lengths[i]:
lengths[i] = lengths[j] + 1
counts[i] = counts[j]
elif lengths[j] + 1 == lengths[i]:
counts[i] += counts[j]
max_length = max(lengths)
return sum(c for l, c in zip(lengths, counts) if l == max_length)
# 3. Russian Doll Envelopes
def max_envelopes(envelopes):
'''Maximum nested envelopes (2D LIS)'''
# Sort by width ascending, height descending
envelopes.sort(key=lambda x: (x[0], -x[1]))
# Find LIS on heights
def lis(heights):
import bisect
dp = []
for h in heights:
pos = bisect.bisect_left(dp, h)
if pos == len(dp):
dp.append(h)
else:
dp[pos] = h
return len(dp)
return lis([h for w, h in envelopes])
# 4. Maximum Length of Repeated Subarray
def find_length(nums1, nums2):
'''Longest common contiguous subarray'''
m, n = len(nums1), len(nums2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
max_length = 0
for i in range(1, m + 1):
for j in range(1, n + 1):
if nums1[i - 1] == nums2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
max_length = max(max_length, dp[i][j])
return max_length
# 5. Shortest Common Supersequence
def shortest_common_supersequence(str1, str2):
'''Build shortest string containing both strings'''
m, n = len(str1), len(str2)
# Find LCS
dp = [[""] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if str1[i - 1] == str2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + str1[i - 1]
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], key=len)
# Build supersequence
lcs = dp[m][n]
i = j = k = 0
result = []
for char in lcs:
while i < m and str1[i] != char:
result.append(str1[i])
i += 1
while j < n and str2[j] != char:
result.append(str2[j])
j += 1
result.append(char)
i += 1
j += 1
result.extend(str1[i:])
result.extend(str2[j:])
return ''.join(result)
# 6. Is Subsequence
def is_subsequence(s, t):
'''Check if s is subsequence of t'''
i = 0
for char in t:
if i < len(s) and char == s[i]:
i += 1
return i == len(s)
# Follow-up: Multiple queries
def is_subsequence_multiple(s_list, t):
'''Handle multiple s queries efficiently'''
from collections import defaultdict
import bisect
# Build index map for t
indices = defaultdict(list)
for i, char in enumerate(t):
indices[char].append(i)
def check(s):
prev = -1
for char in s:
if char not in indices:
return False
# Binary search for next occurrence
pos = bisect.bisect_right(indices[char], prev)
if pos == len(indices[char]):
return False
prev = indices[char][pos]
return True
return [check(s) for s in s_list]
# 7. Delete Operation for Two Strings
def min_distance_delete(word1, word2):
'''Minimum deletions to make strings equal'''
m, n = len(word1), len(word2)
# Find LCS
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
lcs_length = dp[m][n]
return (m - lcs_length) + (n - lcs_length)
# 8. Minimum ASCII Delete Sum
def minimum_delete_sum(s1, s2):
'''Minimum ASCII sum of deleted characters'''
m, n = len(s1), len(s2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Initialize first row and column
for i in range(1, m + 1):
dp[i][0] = dp[i - 1][0] + ord(s1[i - 1])
for j in range(1, n + 1):
dp[0][j] = dp[0][j - 1] + ord(s2[j - 1])
# Fill DP table
for i in range(1, m + 1):
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(
dp[i - 1][j] + ord(s1[i - 1]),
dp[i][j - 1] + ord(s2[j - 1])
)
return dp[m][n]PythonAnswer: Subsequence DP: LPS uses range DP with character matching; LIS count tracks both length and count; Russian doll is 2D LIS with sorting; LCS builds solutions; ASCII delete optimizes character costs.
Q152. Apply state compression in DP
# Dynamic Programming with State Compression
# 1. Partition to K Equal Sum Subsets
def can_partition_k_subsets(nums, k):
'''Partition into k subsets with equal sum using bitmask'''
total = sum(nums)
if total % k != 0:
return False
target = total // k
nums.sort(reverse=True)
if nums[0] > target:
return False
n = len(nums)
dp = [-1] * (1 << n)
dp[0] = 0
for mask in range(1 << n):
if dp[mask] == -1:
continue
for i in range(n):
if mask & (1 << i):
continue
new_mask = mask | (1 << i)
if dp[mask] + nums[i] <= target:
if dp[new_mask] == -1:
dp[new_mask] = (dp[mask] + nums[i]) % target
return dp[(1 << n) - 1] == 0
# 2. Shortest Path Visiting All Nodes
def shortest_path_length(graph):
'''Shortest path visiting all nodes using BFS + bitmask'''
from collections import deque
n = len(graph)
target = (1 << n) - 1
# (node, visited_mask, distance)
queue = deque([(i, 1 << i, 0) for i in range(n)])
visited = {(i, 1 << i) for i in range(n)}
while queue:
node, mask, dist = queue.popleft()
if mask == target:
return dist
for neighbor in graph[node]:
new_mask = mask | (1 << neighbor)
if (neighbor, new_mask) not in visited:
visited.add((neighbor, new_mask))
queue.append((neighbor, new_mask, dist + 1))
return -1
# 3. Find the Shortest Superstring
def shortest_superstring(words):
'''Shortest string containing all words'''
n = len(words)
# Overlap[i][j] = overlap when word i comes before word j
overlap = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
if i == j:
continue
for k in range(min(len(words[i]), len(words[j])), 0, -1):
if words[i][-k:] == words[j][:k]:
overlap[i][j] = k
break
# dp[mask][i] = min length ending at word i with mask visited
dp = [[float('inf')] * n for _ in range(1 << n)]
parent = [[-1] * n for _ in range(1 << n)]
# Initialize single words
for i in range(n):
dp[1 << i][i] = len(words[i])
# Fill DP table
for mask in range(1, 1 << n):
for i in range(n):
if not (mask & (1 << i)):
continue
prev_mask = mask ^ (1 << i)
if prev_mask == 0:
continue
for j in range(n):
if not (prev_mask & (1 << j)):
continue
if dp[prev_mask][j] + len(words[i]) - overlap[j][i] < dp[mask][i]:
dp[mask][i] = dp[prev_mask][j] + len(words[i]) - overlap[j][i]
parent[mask][i] = j
# Reconstruct path
target = (1 << n) - 1
last = min(range(n), key=lambda i: dp[target][i])
path = [last]
mask = target
while parent[mask][last] != -1:
prev = parent[mask][last]
path.append(prev)
mask ^= (1 << last)
last = prev
path.reverse()
# Build result
result = words[path[0]]
for i in range(1, len(path)):
prev_word = path[i - 1]
curr_word = path[i]
result += words[curr_word][overlap[prev_word][curr_word]:]
return result
# 4. Minimum Cost to Connect Two Groups
def connect_two_groups(cost):
'''Min cost to connect all nodes in two groups'''
m, n = len(cost), len(cost[0])
# dp[i][mask] = min cost for first i from group1, mask for group2
dp = [[float('inf')] * (1 << n) for _ in range(m + 1)]
dp[0][0] = 0
for i in range(m):
for mask in range(1 << n):
if dp[i][mask] == float('inf'):
continue
# Try connecting node i to each subset of group2
for new_mask in range(1 << n):
if new_mask == 0:
continue
total_cost = 0
for j in range(n):
if new_mask & (1 << j):
total_cost += cost[i][j]
next_mask = mask | new_mask
dp[i + 1][next_mask] = min(
dp[i + 1][next_mask],
dp[i][mask] + total_cost
)
# Connect remaining nodes in group2
result = float('inf')
full_mask = (1 << n) - 1
for mask in range(1 << n):
if dp[m][mask] == float('inf'):
continue
cost_remaining = 0
for j in range(n):
if not (mask & (1 << j)):
# Find minimum cost to connect this node
min_cost = min(cost[i][j] for i in range(m))
cost_remaining += min_cost
result = min(result, dp[m][mask] + cost_remaining)
return resultPythonAnswer: State compression: bitmask represents visited/selected items; reduces O(2^n * n!) to O(2^n * n); TSP, subset partition, path covering all nodes; use integer as state for exponential problems.
Q153. Solve advanced string DP problems
# Advanced String Dynamic Programming
# 1. Scramble String
def is_scramble(s1, s2):
'''Check if s1 is scrambled version of s2'''
if s1 == s2:
return True
if sorted(s1) != sorted(s2):
return False
n = len(s1)
memo = {}
def dp(i1, i2, length):
if (i1, i2, length) in memo:
return memo[(i1, i2, length)]
if length == 1:
return s1[i1] == s2[i2]
# Check if already equal
if s1[i1:i1+length] == s2[i2:i2+length]:
memo[(i1, i2, length)] = True
return True
# Try all split positions
for k in range(1, length):
# No swap
if (dp(i1, i2, k) and dp(i1 + k, i2 + k, length - k)):
memo[(i1, i2, length)] = True
return True
# With swap
if (dp(i1, i2 + length - k, k) and
dp(i1 + k, i2, length - k)):
memo[(i1, i2, length)] = True
return True
memo[(i1, i2, length)] = False
return False
return dp(0, 0, n)
# 2. Distinct Subsequences II
def distinct_subseq_ii(s):
'''Count distinct non-empty subsequences'''
MOD = 10**9 + 7
n = len(s)
# dp[i] = number of distinct subsequences ending at i
dp = [1] # Empty subsequence
last = {}
for i, char in enumerate(s):
# Double previous count
new_count = (2 * dp[-1]) % MOD
# Subtract duplicates if char seen before
if char in last:
new_count = (new_count - dp[last[char]]) % MOD
dp.append(new_count)
last[char] = i
return (dp[-1] - 1) % MOD # Exclude empty subsequence
# 3. Count Different Palindromic Subsequences
def count_palindromic_subsequences(s):
'''Count distinct palindromic subsequences'''
MOD = 10**9 + 7
n = len(s)
# dp[i][j] = count in substring s[i:j+1]
dp = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j]:
left = i + 1
right = j - 1
# Find same characters inside
while left <= right and s[left] != s[i]:
left += 1
while left <= right and s[right] != s[i]:
right -= 1
if left > right:
# No same character inside
dp[i][j] = (2 * dp[i + 1][j - 1] + 2) % MOD
elif left == right:
# One same character inside
dp[i][j] = (2 * dp[i + 1][j - 1] + 1) % MOD
else:
# Two or more same characters
dp[i][j] = (2 * dp[i + 1][j - 1] - dp[left + 1][right - 1]) % MOD
else:
dp[i][j] = (dp[i + 1][j] + dp[i][j - 1] - dp[i + 1][j - 1]) % MOD
return dp[0][n - 1] % MOD
# 4. Longest Chunked Palindrome Decomposition
def longest_decomposition(text):
'''Maximum chunks that form palindrome'''
n = len(text)
def solve(left, right):
if left > right:
return 0
# Try to find matching prefix and suffix
for k in range(1, (right - left) // 2 + 2):
if text[left:left+k] == text[right-k+1:right+1]:
return 2 + solve(left + k, right - k)
# No match found, whole string is one chunk
return 1
return solve(0, n - 1)
# 5. String Compression II
def get_length_of_optimal_compression(s, k):
'''Minimum length after deleting k characters and run-length encoding'''
from functools import lru_cache
@lru_cache(None)
def dp(i, prev, prev_count, k_left):
if k_left < 0:
return float('inf')
if i == len(s):
return 0
# Delete current character
delete = dp(i + 1, prev, prev_count, k_left - 1)
# Keep current character
if s[i] == prev:
# Extend current run
added_length = 1 if prev_count in [1, 9, 99] else 0
keep = added_length + dp(i + 1, prev, prev_count + 1, k_left)
else:
# Start new run
keep = 1 + dp(i + 1, s[i], 1, k_left)
return min(delete, keep)
return dp(0, '', 0, k)PythonAnswer: Advanced string DP: scramble string uses recursive partition; distinct subsequences tracks last occurrence; palindromic subsequences handles duplicates; compression optimizes encoding; memoization critical for efficiency.
Q154. Master DP on grid problems
# Dynamic Programming on Grids
# 1. Dungeon Game
def calculate_minimum_hp(dungeon):
'''Minimum initial health to reach princess'''
m, n = len(dungeon), len(dungeon[0])
# dp[i][j] = min health needed at position (i, j)
dp = [[float('inf')] * n for _ in range(m)]
# Base case: princess room
dp[m - 1][n - 1] = max(1, 1 - dungeon[m - 1][n - 1])
# Last row
for j in range(n - 2, -1, -1):
dp[m - 1][j] = max(1, dp[m - 1][j + 1] - dungeon[m - 1][j])
# Last column
for i in range(m - 2, -1, -1):
dp[i][n - 1] = max(1, dp[i + 1][n - 1] - dungeon[i][n - 1])
# Fill rest of table
for i in range(m - 2, -1, -1):
for j in range(n - 2, -1, -1):
min_health_on_exit = min(dp[i + 1][j], dp[i][j + 1])
dp[i][j] = max(1, min_health_on_exit - dungeon[i][j])
return dp[0][0]
# 2. Cherry Pickup
def cherry_pickup(grid):
'''Maximum cherries collecting going and returning'''
n = len(grid)
# dp[i1][j1][i2] where j2 = i1 + j1 - i2
# Two people walk simultaneously
dp = {}
def solve(i1, j1, i2):
j2 = i1 + j1 - i2
# Out of bounds
if (i1 >= n or j1 >= n or i2 >= n or j2 >= n or
grid[i1][j1] == -1 or grid[i2][j2] == -1):
return float('-inf')
# Reached end
if i1 == n - 1 and j1 == n - 1:
return grid[i1][j1]
if (i1, j1, i2) in dp:
return dp[(i1, j1, i2)]
# Collect cherries
cherries = grid[i1][j1]
if i1 != i2: # Different cells
cherries += grid[i2][j2]
# Try all 4 combinations of moves
result = cherries + max(
solve(i1 + 1, j1, i2 + 1), # Both down
solve(i1 + 1, j1, i2), # P1 down, P2 right
solve(i1, j1 + 1, i2 + 1), # P1 right, P2 down
solve(i1, j1 + 1, i2) # Both right
)
dp[(i1, j1, i2)] = result
return result
return max(0, solve(0, 0, 0))
# 3. Maximal Square
def maximal_square(matrix):
'''Largest square containing only 1s'''
if not matrix:
return 0
m, n = len(matrix), len(matrix[0])
dp = [[0] * n for _ in range(m)]
max_side = 0
for i in range(m):
for j in range(n):
if matrix[i][j] == '1':
if i == 0 or j == 0:
dp[i][j] = 1
else:
dp[i][j] = min(
dp[i - 1][j],
dp[i][j - 1],
dp[i - 1][j - 1]
) + 1
max_side = max(max_side, dp[i][j])
return max_side * max_side
# 4. Count Square Submatrices
def count_squares(matrix):
'''Count all square submatrices with all 1s'''
m, n = len(matrix), len(matrix[0])
dp = [[0] * n for _ in range(m)]
total = 0
for i in range(m):
for j in range(n):
if matrix[i][j] == 1:
if i == 0 or j == 0:
dp[i][j] = 1
else:
dp[i][j] = min(
dp[i - 1][j],
dp[i][j - 1],
dp[i - 1][j - 1]
) + 1
total += dp[i][j]
return total
# 5. Count Submatrices With All Ones
def num_submat(mat):
'''Count submatrices with all ones'''
m, n = len(mat), len(mat[0])
# heights[i][j] = consecutive 1s ending at (i, j)
heights = [[0] * n for _ in range(m)]
for i in range(m):
for j in range(n):
if mat[i][j] == 1:
heights[i][j] = 1 if i == 0 else heights[i - 1][j] + 1
total = 0
# For each row, count rectangles
for i in range(m):
for j in range(n):
min_height = heights[i][j]
for k in range(j, -1, -1):
if heights[i][k] == 0:
break
min_height = min(min_height, heights[i][k])
total += min_height
return total
# 6. Number of Ways to Stay in Same Place
def num_ways(steps, arr_len):
'''Ways to return to index 0 after exactly steps'''
MOD = 10**9 + 7
max_pos = min(steps // 2 + 1, arr_len)
# dp[i][j] = ways to reach position j in i steps
dp = [[0] * max_pos for _ in range(steps + 1)]
dp[0][0] = 1
for i in range(1, steps + 1):
for j in range(max_pos):
# Stay
dp[i][j] = dp[i - 1][j]
# From left
if j > 0:
dp[i][j] = (dp[i][j] + dp[i - 1][j - 1]) % MOD
# From right
if j < max_pos - 1:
dp[i][j] = (dp[i][j] + dp[i - 1][j + 1]) % MOD
return dp[steps][0]PythonAnswer: Grid DP: dungeon uses backward DP from goal; cherry pickup simulates two paths simultaneously; maximal square uses min of three neighbors; count squares accumulates sizes; optimize with rolling array for space.
Q155. Implement network flow algorithms
# Advanced Graph Algorithms - Network Flow and Matching
# 1. Maximum Flow - Ford-Fulkerson with Edmonds-Karp (BFS)
from collections import deque, defaultdict
def max_flow(graph, source, sink):
'''Maximum flow using Edmonds-Karp algorithm'''
def bfs(source, sink, parent):
'''Find augmenting path using BFS'''
visited = {source}
queue = deque([source])
while queue:
u = queue.popleft()
for v in graph[u]:
if v not in visited and graph[u][v] > 0:
visited.add(v)
queue.append(v)
parent[v] = u
if v == sink:
return True
return False
# Create residual graph
residual = defaultdict(lambda: defaultdict(int))
for u in graph:
for v in graph[u]:
residual[u][v] = graph[u][v]
parent = {}
max_flow_value = 0
# Find augmenting paths
while bfs(source, sink, parent):
# Find minimum residual capacity along path
path_flow = float('inf')
s = sink
while s != source:
path_flow = min(path_flow, residual[parent[s]][s])
s = parent[s]
# Update residual capacities
v = sink
while v != source:
u = parent[v]
residual[u][v] -= path_flow
residual[v][u] += path_flow
v = parent[v]
max_flow_value += path_flow
parent = {}
return max_flow_value
# 2. Minimum Cut
def min_cut(graph, source, sink):
'''Find minimum cut in flow network'''
def bfs_reachable(source, residual):
'''Find all nodes reachable from source'''
visited = {source}
queue = deque([source])
while queue:
u = queue.popleft()
for v in residual[u]:
if v not in visited and residual[u][v] > 0:
visited.add(v)
queue.append(v)
return visited
# Run max flow
residual = defaultdict(lambda: defaultdict(int))
for u in graph:
for v in graph[u]:
residual[u][v] = graph[u][v]
# Find max flow (same as above)
parent = {}
def bfs(source, sink):
visited = {source}
queue = deque([source])
parent.clear()
while queue:
u = queue.popleft()
for v in residual[u]:
if v not in visited and residual[u][v] > 0:
visited.add(v)
queue.append(v)
parent[v] = u
if v == sink:
return True
return False
while bfs(source, sink):
path_flow = float('inf')
s = sink
while s != source:
path_flow = min(path_flow, residual[parent[s]][s])
s = parent[s]
v = sink
while v != source:
u = parent[v]
residual[u][v] -= path_flow
residual[v][u] += path_flow
v = parent[v]
# Find min cut edges
reachable = bfs_reachable(source, residual)
min_cut_edges = []
for u in graph:
for v in graph[u]:
if u in reachable and v not in reachable and graph[u][v] > 0:
min_cut_edges.append((u, v))
return min_cut_edges
# 3. Bipartite Matching - Hopcroft-Karp
def max_bipartite_matching(graph, n1, n2):
'''Maximum matching in bipartite graph'''
# graph[u] = list of neighbors for u in left partition
pair_u = {} # pair_u[u] = matched node in right
pair_v = {} # pair_v[v] = matched node in left
dist = {}
def bfs():
'''Build level graph'''
queue = deque()
for u in range(n1):
if u not in pair_u:
dist[u] = 0
queue.append(u)
else:
dist[u] = float('inf')
dist[None] = float('inf')
while queue:
u = queue.popleft()
if dist[u] < dist[None]:
for v in graph.get(u, []):
if dist.get(pair_v.get(v), float('inf')) == float('inf'):
dist[pair_v.get(v)] = dist[u] + 1
queue.append(pair_v.get(v))
return dist[None] != float('inf')
def dfs(u):
'''Find augmenting path'''
if u is not None:
for v in graph.get(u, []):
if dist.get(pair_v.get(v), float('inf')) == dist[u] + 1:
if dfs(pair_v.get(v)):
pair_v[v] = u
pair_u[u] = v
return True
dist[u] = float('inf')
return False
return True
matching = 0
while bfs():
for u in range(n1):
if u not in pair_u:
if dfs(u):
matching += 1
return matching, pair_u
# 4. Dinic's Algorithm
def dinic_max_flow(graph, source, sink, n):
'''Maximum flow using Dinic's algorithm - O(V^2 * E)'''
residual = [[0] * n for _ in range(n)]
for u in range(n):
for v, cap in graph.get(u, []):
residual[u][v] = cap
def bfs():
'''Build level graph'''
level = [-1] * n
level[source] = 0
queue = deque([source])
while queue:
u = queue.popleft()
for v in range(n):
if level[v] < 0 and residual[u][v] > 0:
level[v] = level[u] + 1
queue.append(v)
return level
def dfs(u, sink, flow, level, start):
'''Send flow using DFS'''
if u == sink:
return flow
while start[u] < n:
v = start[u]
if level[v] == level[u] + 1 and residual[u][v] > 0:
min_flow = min(flow, residual[u][v])
pushed = dfs(v, sink, min_flow, level, start)
if pushed > 0:
residual[u][v] -= pushed
residual[v][u] += pushed
return pushed
start[u] += 1
return 0
max_flow_value = 0
while True:
level = bfs()
if level[sink] < 0:
break
start = [0] * n
while True:
flow = dfs(source, sink, float('inf'), level, start)
if flow == 0:
break
max_flow_value += flow
return max_flow_valuePythonAnswer: Network flow: Ford-Fulkerson finds augmenting paths until none exist; Edmonds-Karp uses BFS for O(VE²); Dinic’s uses level graph for O(V²E); min-cut theorem: max flow = min cut; bipartite matching via flow.
Q156. Solve advanced tree algorithm problems
# Advanced Tree Algorithms and Optimization
# 1. Tree Diameter - Two BFS/DFS Approach
def tree_diameter(edges, n):
'''Find longest path in tree using two BFS'''
from collections import deque, defaultdict
# Build adjacency list
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
def bfs(start):
'''BFS to find farthest node and distance'''
visited = {start}
queue = deque([(start, 0)])
farthest = (start, 0)
while queue:
node, dist = queue.popleft()
if dist > farthest[1]:
farthest = (node, dist)
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, dist + 1))
return farthest
# Find one end of diameter
node1, _ = bfs(0)
# Find other end and diameter
node2, diameter = bfs(node1)
return diameter
# 2. Tree Center - Topological Sort Approach
def find_tree_center(edges, n):
'''Find center(s) of tree'''
from collections import defaultdict, deque
if n <= 2:
return list(range(n))
# Build adjacency list and degrees
graph = defaultdict(set)
for u, v in edges:
graph[u].add(v)
graph[v].add(u)
# Find leaves
leaves = deque([i for i in range(n) if len(graph[i]) == 1])
remaining = n
# Remove leaves layer by layer
while remaining > 2:
leaf_count = len(leaves)
remaining -= leaf_count
for _ in range(leaf_count):
leaf = leaves.popleft()
# Remove leaf from its neighbor
neighbor = next(iter(graph[leaf]))
graph[neighbor].remove(leaf)
# If neighbor becomes leaf
if len(graph[neighbor]) == 1:
leaves.append(neighbor)
return list(leaves)
# 3. All Nodes Distance K in Binary Tree
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def distance_k(root, target, k):
'''Find all nodes at distance k from target'''
from collections import defaultdict, deque
# Build graph representation with parent pointers
graph = defaultdict(list)
def build_graph(node, parent=None):
if not node:
return
if parent:
graph[node.val].append(parent.val)
graph[parent.val].append(node.val)
build_graph(node.left, node)
build_graph(node.right, node)
build_graph(root)
# BFS from target
visited = {target.val}
queue = deque([(target.val, 0)])
result = []
while queue:
node, dist = queue.popleft()
if dist == k:
result.append(node)
continue
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, dist + 1))
return result
# 4. Binary Tree Maximum Path Sum
def max_path_sum(root):
'''Maximum path sum in binary tree'''
max_sum = float('-inf')
def dfs(node):
nonlocal max_sum
if not node:
return 0
# Get max path sum from left and right (ignore negative)
left = max(0, dfs(node.left))
right = max(0, dfs(node.right))
# Max path through current node
max_sum = max(max_sum, node.val + left + right)
# Return max path including current node
return node.val + max(left, right)
dfs(root)
return max_sum
# 5. Subtree of Another Tree
def is_subtree(root, subroot):
'''Check if subroot is subtree of root'''
def is_same(s, t):
if not s and not t:
return True
if not s or not t:
return False
return (s.val == t.val and
is_same(s.left, t.left) and
is_same(s.right, t.right))
def dfs(node):
if not node:
return False
if is_same(node, subroot):
return True
return dfs(node.left) or dfs(node.right)
return dfs(root)
# 6. Count Good Nodes in Binary Tree
def good_nodes(root):
'''Count nodes >= all ancestors'''
def dfs(node, max_val):
if not node:
return 0
count = 1 if node.val >= max_val else 0
new_max = max(max_val, node.val)
count += dfs(node.left, new_max)
count += dfs(node.right, new_max)
return count
return dfs(root, float('-inf'))PythonAnswer: Advanced tree algorithms: diameter via two BFS/DFS; center by removing leaves; distance K with graph conversion; max path sum with subtree contribution; pattern: DFS with state tracking for ancestor properties.
Q157. Implement computational geometry algorithms
# Computational Geometry - Advanced Problems
# 1. Convex Hull - Andrew's Monotone Chain
def convex_hull(points):
'''Convex hull using Andrew's algorithm - O(n log n)'''
points = sorted(set(map(tuple, points)))
if len(points) <= 1:
return points
def cross(o, a, b):
'''Cross product to determine turn direction'''
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
# Build lower hull
lower = []
for p in points:
while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0:
lower.pop()
lower.append(p)
# Build upper hull
upper = []
for p in reversed(points):
while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0:
upper.pop()
upper.append(p)
# Remove last point of each half because it's repeated
return lower[:-1] + upper[:-1]
# 2. Closest Pair of Points - Divide and Conquer
def closest_pair(points):
'''Find closest pair using divide and conquer - O(n log n)'''
points = sorted(points)
def distance(p1, p2):
return ((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)**0.5
def brute_force(pts):
'''Brute force for small arrays'''
min_dist = float('inf')
n = len(pts)
for i in range(n):
for j in range(i + 1, n):
min_dist = min(min_dist, distance(pts[i], pts[j]))
return min_dist
def strip_closest(strip, d):
'''Find closest in strip'''
min_dist = d
strip.sort(key=lambda p: p[1])
for i in range(len(strip)):
j = i + 1
while j < len(strip) and (strip[j][1] - strip[i][1]) < min_dist:
min_dist = min(min_dist, distance(strip[i], strip[j]))
j += 1
return min_dist
def closest_util(px, py):
n = len(px)
if n <= 3:
return brute_force(px)
# Divide
mid = n // 2
midpoint = px[mid]
pyl = [p for p in py if p[0] <= midpoint[0]]
pyr = [p for p in py if p[0] > midpoint[0]]
# Conquer
dl = closest_util(px[:mid], pyl)
dr = closest_util(px[mid:], pyr)
d = min(dl, dr)
# Combine - check strip
strip = [p for p in py if abs(p[0] - midpoint[0]) < d]
return min(d, strip_closest(strip, d))
py = sorted(points, key=lambda p: p[1])
return closest_util(points, py)
# 3. Line Segment Intersection
def segments_intersect(p1, q1, p2, q2):
'''Check if two line segments intersect'''
def orientation(p, q, r):
'''Find orientation of ordered triplet (p, q, r)
0: Collinear, 1: Clockwise, 2: Counterclockwise'''
val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])
if val == 0:
return 0
return 1 if val > 0 else 2
def on_segment(p, q, r):
'''Check if point q lies on segment pr'''
return (q[0] <= max(p[0], r[0]) and q[0] >= min(p[0], r[0]) and
q[1] <= max(p[1], r[1]) and q[1] >= min(p[1], r[1]))
o1 = orientation(p1, q1, p2)
o2 = orientation(p1, q1, q2)
o3 = orientation(p2, q2, p1)
o4 = orientation(p2, q2, q1)
# General case
if o1 != o2 and o3 != o4:
return True
# Special cases (collinear)
if o1 == 0 and on_segment(p1, p2, q1):
return True
if o2 == 0 and on_segment(p1, q2, q1):
return True
if o3 == 0 and on_segment(p2, p1, q2):
return True
if o4 == 0 and on_segment(p2, q1, q2):
return True
return False
# 4. Point in Polygon - Ray Casting
def point_in_polygon(point, polygon):
'''Check if point is inside polygon using ray casting'''
x, y = point
n = len(polygon)
inside = False
p1x, p1y = polygon[0]
for i in range(1, n + 1):
p2x, p2y = polygon[i % n]
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
# 5. Area of Polygon - Shoelace Formula
def polygon_area(vertices):
'''Calculate area using shoelace formula'''
n = len(vertices)
area = 0
for i in range(n):
j = (i + 1) % n
area += vertices[i][0] * vertices[j][1]
area -= vertices[j][0] * vertices[i][1]
return abs(area) / 2
# 6. Rotating Calipers - Diameter of Convex Polygon
def diameter_convex_polygon(polygon):
'''Find diameter of convex polygon using rotating calipers'''
def distance_sq(p1, p2):
return (p1[0] - p2[0])**2 + (p1[1] - p2[1])**2
n = len(polygon)
max_dist = 0
# Find rightmost point
k = 1
while distance_sq(polygon[n-1], polygon[k+1]) > distance_sq(polygon[n-1], polygon[k]):
k += 1
j = k
# Rotate calipers
for i in range(n):
while distance_sq(polygon[i], polygon[(j+1)%n]) > distance_sq(polygon[i], polygon[j]):
j = (j + 1) % n
max_dist = max(max_dist, distance_sq(polygon[i], polygon[j]))
max_dist = max(max_dist, distance_sq(polygon[i], polygon[j]))
return max_dist ** 0.5PythonAnswer: Computational geometry: convex hull via Andrew’s monotone chain O(n log n); closest pair with divide-conquer; line intersection via orientation test; point in polygon with ray casting; rotating calipers for antipodal pairs.
Q158. Solve advanced optimization problems
# Advanced Optimization and Approximation Algorithms
# 1. Job Scheduling - Weighted Interval Scheduling
def weighted_interval_scheduling(jobs):
'''Maximum weight non-overlapping jobs'''
# jobs = [(start, end, weight)]
jobs.sort(key=lambda x: x[1]) # Sort by end time
n = len(jobs)
# dp[i] = max weight using jobs 0..i
dp = [0] * n
dp[0] = jobs[0][2]
def binary_search(i):
'''Find latest non-overlapping job'''
lo, hi = 0, i - 1
result = -1
while lo <= hi:
mid = (lo + hi) // 2
if jobs[mid][1] <= jobs[i][0]:
result = mid
lo = mid + 1
else:
hi = mid - 1
return result
for i in range(1, n):
# Include current job
include = jobs[i][2]
prev = binary_search(i)
if prev != -1:
include += dp[prev]
# Exclude current job
exclude = dp[i - 1]
dp[i] = max(include, exclude)
return dp[n - 1]
# 2. Coin Change - Minimum Coins
def coin_change_min(coins, amount):
'''Minimum coins to make amount'''
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if coin <= i:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
# 3. Partition Problem - Subset Sum Equal Partition
def can_partition(nums):
'''Check if array can be partitioned into two equal sum subsets'''
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
# Traverse backward to avoid using same element twice
for i in range(target, num - 1, -1):
dp[i] = dp[i] or dp[i - num]
return dp[target]
# 4. Rod Cutting - Maximum Revenue
def rod_cutting(prices, n):
'''Maximum revenue from cutting rod of length n'''
# prices[i] = price of rod of length i+1
dp = [0] * (n + 1)
for i in range(1, n + 1):
max_val = float('-inf')
for j in range(i):
max_val = max(max_val, prices[j] + dp[i - j - 1])
dp[i] = max_val
return dp[n]
# 5. Box Stacking - Maximum Height
def max_stack_height(boxes):
'''Maximum height of stack with decreasing dimensions'''
# Each box can be rotated - generate all rotations
rotations = []
for l, w, h in boxes:
rotations.append((l, w, h))
rotations.append((w, l, h))
rotations.append((h, l, w))
rotations.append((l, h, w))
rotations.append((w, h, l))
rotations.append((h, w, l))
# Sort by base area (descending)
rotations.sort(key=lambda x: x[0] * x[1], reverse=True)
n = len(rotations)
dp = [box[2] for box in rotations]
for i in range(1, n):
for j in range(i):
if (rotations[j][0] > rotations[i][0] and
rotations[j][1] > rotations[i][1]):
dp[i] = max(dp[i], dp[j] + rotations[i][2])
return max(dp)
# 6. Minimum Path Sum with K Steps
def min_path_sum_k_steps(grid, k):
'''Minimum path sum from top-left to bottom-right in exactly k steps'''
m, n = len(grid), len(grid[0])
# dp[i][j][steps] = min sum to reach (i,j) in steps
dp = [[[float('inf')] * (k + 1) for _ in range(n)] for _ in range(m)]
dp[0][0][0] = grid[0][0]
for steps in range(k):
for i in range(m):
for j in range(n):
if dp[i][j][steps] == float('inf'):
continue
# Move down
if i + 1 < m:
dp[i + 1][j][steps + 1] = min(
dp[i + 1][j][steps + 1],
dp[i][j][steps] + grid[i + 1][j]
)
# Move right
if j + 1 < n:
dp[i][j + 1][steps + 1] = min(
dp[i][j + 1][steps + 1],
dp[i][j][steps] + grid[i][j + 1]
)
return dp[m - 1][n - 1][k] if dp[m - 1][n - 1][k] != float('inf') else -1
# 7. Burst Balloons
def max_coins_burst_balloons(nums):
'''Maximum coins from bursting balloons'''
nums = [1] + nums + [1]
n = len(nums)
# dp[i][j] = max coins bursting balloons between i and j (exclusive)
dp = [[0] * n for _ in range(n)]
for length in range(2, n):
for left in range(n - length):
right = left + length
for i in range(left + 1, right):
# Burst balloon i last in range [left, right]
coins = nums[left] * nums[i] * nums[right]
coins += dp[left][i] + dp[i][right]
dp[left][right] = max(dp[left][right], coins)
return dp[0][n - 1]PythonAnswer: Advanced optimization: weighted interval scheduling with DP + binary search; partition via subset sum; rod cutting maximizes revenue; box stacking with rotation; burst balloons uses range DP; pattern: optimal substructure identification.
Q159. Master greedy algorithm techniques
# Advanced Greedy Algorithms and Proofs
# 1. Activity Selection - Maximum Non-overlapping Activities
def activity_selection(activities):
'''Select maximum number of non-overlapping activities'''
# activities = [(start, end)]
activities.sort(key=lambda x: x[1]) # Sort by end time
selected = [activities[0]]
last_end = activities[0][1]
for start, end in activities[1:]:
if start >= last_end:
selected.append((start, end))
last_end = end
return selected
# 2. Huffman Coding - Optimal Prefix-Free Codes
import heapq
from collections import Counter, defaultdict
class HuffmanNode:
def __init__(self, char, freq):
self.char = char
self.freq = freq
self.left = None
self.right = None
def __lt__(self, other):
return self.freq < other.freq
def huffman_encoding(text):
'''Generate Huffman codes for characters'''
if not text:
return {}, ""
# Count frequencies
freq = Counter(text)
# Build heap
heap = [HuffmanNode(char, f) for char, f in freq.items()]
heapq.heapify(heap)
# Build Huffman tree
while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
merged = HuffmanNode(None, left.freq + right.freq)
merged.left = left
merged.right = right
heapq.heappush(heap, merged)
# Generate codes
root = heap[0]
codes = {}
def generate_codes(node, code):
if node.char is not None:
codes[node.char] = code
return
if node.left:
generate_codes(node.left, code + '0')
if node.right:
generate_codes(node.right, code + '1')
generate_codes(root, '')
# Encode text
encoded = ''.join(codes[char] for char in text)
return codes, encoded
# 3. Fractional Knapsack
def fractional_knapsack(items, capacity):
'''Maximum value with fractional items allowed'''
# items = [(value, weight)]
# Sort by value/weight ratio (descending)
items.sort(key=lambda x: x[0] / x[1], reverse=True)
total_value = 0
remaining = capacity
for value, weight in items:
if weight <= remaining:
# Take whole item
total_value += value
remaining -= weight
else:
# Take fraction
fraction = remaining / weight
total_value += value * fraction
break
return total_value
# 4. Minimum Platforms Required
def min_platforms(arrivals, departures):
'''Minimum railway platforms needed'''
arrivals.sort()
departures.sort()
platforms = 0
max_platforms = 0
i = j = 0
while i < len(arrivals):
if arrivals[i] < departures[j]:
platforms += 1
max_platforms = max(max_platforms, platforms)
i += 1
else:
platforms -= 1
j += 1
return max_platforms
# 5. Gas Station Circuit
def can_complete_circuit(gas, cost):
'''Find starting gas station to complete circuit'''
if sum(gas) < sum(cost):
return -1
total = 0
start = 0
for i in range(len(gas)):
total += gas[i] - cost[i]
if total < 0:
total = 0
start = i + 1
return start
# 6. Jump Game II - Minimum Jumps
def min_jumps(nums):
'''Minimum jumps to reach end'''
if len(nums) <= 1:
return 0
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
jumps += 1
current_end = farthest
if current_end >= len(nums) - 1:
break
return jumps
# 7. Remove K Digits - Minimum Number
def remove_k_digits(num, k):
'''Remove k digits to get smallest number'''
stack = []
for digit in num:
while k > 0 and stack and stack[-1] > digit:
stack.pop()
k -= 1
stack.append(digit)
# Remove remaining k digits from end
stack = stack[:-k] if k > 0 else stack
# Remove leading zeros
result = ''.join(stack).lstrip('0')
return result if result else '0'PythonAnswer: Greedy algorithms: activity selection sorts by end time; Huffman builds optimal codes; fractional knapsack uses value/weight ratio; gas station maintains running sum; greedy choice property + optimal substructure prove correctness.
Q160. Understand NP-complete problems and approximations
# NP-Complete Problems and Approximation Algorithms
# 1. Traveling Salesman Problem - DP with Bitmask
def tsp_dynamic(dist):
'''TSP using dynamic programming - O(n^2 * 2^n)'''
n = len(dist)
# dp[mask][i] = min cost visiting cities in mask, ending at i
dp = [[float('inf')] * n for _ in range(1 << n)]
dp[1][0] = 0 # Start at city 0
for mask in range(1 << n):
for last in range(n):
if dp[mask][last] == float('inf'):
continue
for nxt in range(n):
if mask & (1 << nxt):
continue
new_mask = mask | (1 << nxt)
dp[new_mask][nxt] = min(
dp[new_mask][nxt],
dp[mask][last] + dist[last][nxt]
)
# Return to start
full_mask = (1 << n) - 1
result = min(dp[full_mask][i] + dist[i][0] for i in range(1, n))
return result
# 2. Vertex Cover - 2-Approximation
def vertex_cover_approx(edges, n):
'''2-approximation for minimum vertex cover'''
covered_edges = set()
vertex_cover = set()
for u, v in edges:
if (u, v) not in covered_edges:
vertex_cover.add(u)
vertex_cover.add(v)
# Mark all edges incident to u and v as covered
for x, y in edges:
if x == u or y == u or x == v or y == v:
covered_edges.add((x, y))
return list(vertex_cover)
# 3. Set Cover - Greedy Approximation
def set_cover_greedy(universe, subsets):
'''Greedy approximation for set cover'''
uncovered = set(universe)
covered_sets = []
while uncovered:
# Find subset covering most uncovered elements
best_set = max(subsets, key=lambda s: len(s & uncovered))
covered_sets.append(best_set)
uncovered -= best_set
subsets.remove(best_set)
return covered_sets
# 4. Bin Packing - First Fit Decreasing
def bin_packing_ffd(items, bin_capacity):
'''First Fit Decreasing for bin packing'''
items.sort(reverse=True)
bins = []
for item in items:
# Try to fit in existing bin
placed = False
for bin in bins:
if sum(bin) + item <= bin_capacity:
bin.append(item)
placed = True
break
# Create new bin if needed
if not placed:
bins.append([item])
return bins
# 5. Graph Coloring - Greedy Approximation
def graph_coloring(graph, n):
'''Greedy graph coloring'''
# graph[i] = list of neighbors of vertex i
colors = [-1] * n
for vertex in range(n):
# Find colors of neighbors
neighbor_colors = {colors[neighbor]
for neighbor in graph.get(vertex, [])
if colors[neighbor] != -1}
# Assign smallest available color
color = 0
while color in neighbor_colors:
color += 1
colors[vertex] = color
return colors
# 6. Maximum Independent Set (Tree) - Exact Solution
def max_independent_set_tree(tree, root):
'''Maximum independent set in tree - O(n)'''
# tree[node] = list of children
memo = {}
def dp(node, parent_included):
'''Returns max independent set size'''
if (node, parent_included) in memo:
return memo[(node, parent_included)]
if parent_included:
# Can't include current node
result = sum(dp(child, False) for child in tree.get(node, []))
else:
# Can choose to include or exclude
include = 1 + sum(dp(child, True) for child in tree.get(node, []))
exclude = sum(dp(child, False) for child in tree.get(node, []))
result = max(include, exclude)
memo[(node, parent_included)] = result
return result
return dp(root, False)
# 7. Hamiltonian Path - Backtracking
def hamiltonian_path(graph, n):
'''Find Hamiltonian path using backtracking'''
path = []
visited = [False] * n
def backtrack(v, count):
path.append(v)
visited[v] = True
if count == n:
return True
for neighbor in graph.get(v, []):
if not visited[neighbor]:
if backtrack(neighbor, count + 1):
return True
# Backtrack
path.pop()
visited[v] = False
return False
# Try starting from each vertex
for start in range(n):
if backtrack(start, 1):
return path
path.clear()
visited = [False] * n
return None
# 8. Subset Sum - Pseudo-polynomial DP
def subset_sum_exists(nums, target):
'''Check if subset with given sum exists'''
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
# Traverse backward
for i in range(target, num - 1, -1):
dp[i] = dp[i] or dp[i - num]
return dp[target]PythonAnswer: NP-complete problems: TSP uses DP with bitmask for exact solution O(n²·2ⁿ); vertex cover has 2-approximation; set cover uses greedy ln(n)-approximation; bin packing FFD is 11/9-approximation; recognize when to use approximation vs exact.
Q161. Implement advanced matrix algorithms
# Advanced Matrix Algorithms and Linear Algebra
# 1. Matrix Multiplication - Strassen's Algorithm Concept
def matrix_multiply(A, B):
'''Standard matrix multiplication - O(n^3)'''
n = len(A)
C = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
for k in range(n):
C[i][j] += A[i][k] * B[k][j]
return C
# Note: Strassen's achieves O(n^2.807) but has high constant factors
# Only beneficial for very large matrices
# 2. Matrix Exponentiation - Fast Power
def matrix_power(matrix, n):
'''Compute matrix^n using binary exponentiation - O(k^3 log n)'''
def multiply(A, B):
size = len(A)
C = [[0] * size for _ in range(size)]
for i in range(size):
for j in range(size):
for k in range(size):
C[i][j] += A[i][k] * B[k][j]
return C
size = len(matrix)
result = [[1 if i == j else 0 for j in range(size)] for i in range(size)]
base = [row[:] for row in matrix]
while n > 0:
if n % 2 == 1:
result = multiply(result, base)
base = multiply(base, base)
n //= 2
return result
# Application: Fibonacci in O(log n)
def fibonacci_matrix(n):
'''Compute nth Fibonacci using matrix exponentiation'''
if n <= 1:
return n
matrix = [[1, 1], [1, 0]]
result = matrix_power(matrix, n - 1)
return result[0][0]
# 3. Sparse Matrix Operations
class SparseMatrix:
'''Efficient sparse matrix representation'''
def __init__(self, rows, cols):
self.rows = rows
self.cols = cols
self.data = {} # (i, j): value
def set(self, i, j, value):
if value != 0:
self.data[(i, j)] = value
elif (i, j) in self.data:
del self.data[(i, j)]
def get(self, i, j):
return self.data.get((i, j), 0)
def multiply(self, other):
'''Sparse matrix multiplication'''
if self.cols != other.rows:
raise ValueError("Incompatible dimensions")
result = SparseMatrix(self.rows, other.cols)
# Group by column for efficiency
other_by_col = {}
for (i, j), val in other.data.items():
if j not in other_by_col:
other_by_col[j] = {}
other_by_col[j][i] = val
for (i, k), val1 in self.data.items():
if k in other_by_col:
for j, val2 in other_by_col[k].items():
current = result.get(i, j)
result.set(i, j, current + val1 * val2)
return result
# 4. LU Decomposition
def lu_decomposition(A):
'''LU decomposition of matrix A = LU'''
n = len(A)
L = [[0.0] * n for _ in range(n)]
U = [[0.0] * n for _ in range(n)]
for i in range(n):
# Upper triangular
for k in range(i, n):
sum_val = sum(L[i][j] * U[j][k] for j in range(i))
U[i][k] = A[i][k] - sum_val
# Lower triangular
for k in range(i, n):
if i == k:
L[i][i] = 1
else:
sum_val = sum(L[k][j] * U[j][i] for j in range(i))
L[k][i] = (A[k][i] - sum_val) / U[i][i]
return L, U
# 5. Transpose and Trace
def matrix_operations(A):
'''Common matrix operations'''
n = len(A)
# Transpose
transpose = [[A[j][i] for j in range(n)] for i in range(n)]
# Trace
trace = sum(A[i][i] for i in range(n))
# Determinant (2x2)
det = None
if n == 2:
det = A[0][0] * A[1][1] - A[0][1] * A[1][0]
return transpose, trace, det
# 6. Rotate Matrix 90 Degrees
def rotate_matrix_90(matrix):
'''Rotate NxN matrix 90 degrees clockwise in-place'''
n = len(matrix)
# Transpose
for i in range(n):
for j in range(i + 1, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
# Reverse each row
for i in range(n):
matrix[i].reverse()
return matrixPythonAnswer: Matrix algorithms: standard multiplication O(n³); Strassen’s O(n^2.807); matrix exponentiation for Fibonacci O(log n); sparse matrices save space; LU decomposition for solving systems; rotate via transpose + reverse.
Q162. Implement probabilistic data structures
# Probabilistic Data Structures and Algorithms
# 1. Bloom Filter - Space-Efficient Set Membership
import hashlib
class BloomFilter:
'''Probabilistic set membership test'''
def __init__(self, size, num_hashes):
self.size = size
self.num_hashes = num_hashes
self.bit_array = [False] * size
def _hash(self, item, seed):
'''Generate hash with seed'''
h = hashlib.md5((str(item) + str(seed)).encode())
return int(h.hexdigest(), 16) % self.size
def add(self, item):
'''Add item to filter'''
for i in range(self.num_hashes):
index = self._hash(item, i)
self.bit_array[index] = True
def contains(self, item):
'''Check if item might be in set (no false negatives)'''
for i in range(self.num_hashes):
index = self._hash(item, i)
if not self.bit_array[index]:
return False
return True # Might be false positive
def false_positive_rate(self, n):
'''Estimate false positive rate for n items'''
# (1 - e^(-kn/m))^k where k=hashes, m=size, n=items
import math
k, m = self.num_hashes, self.size
return (1 - math.e ** (-k * n / m)) ** k
# 2. Count-Min Sketch - Frequency Estimation
class CountMinSketch:
'''Estimate frequency of items in stream'''
def __init__(self, width, depth):
self.width = width
self.depth = depth
self.table = [[0] * width for _ in range(depth)]
def _hash(self, item, seed):
'''Generate hash with seed'''
h = hashlib.md5((str(item) + str(seed)).encode())
return int(h.hexdigest(), 16) % self.width
def add(self, item, count=1):
'''Add item with count'''
for i in range(self.depth):
index = self._hash(item, i)
self.table[i][index] += count
def estimate(self, item):
'''Estimate frequency (always >= true frequency)'''
return min(self.table[i][self._hash(item, i)]
for i in range(self.depth))
# 3. HyperLogLog - Cardinality Estimation
import math
class HyperLogLog:
'''Estimate cardinality of multiset'''
def __init__(self, precision):
self.precision = precision
self.m = 1 << precision # 2^precision
self.registers = [0] * self.m
# Alpha constant for bias correction
if self.m >= 128:
self.alpha = 0.7213 / (1 + 1.079 / self.m)
elif self.m >= 64:
self.alpha = 0.709
elif self.m >= 32:
self.alpha = 0.697
else:
self.alpha = 0.673
def _hash(self, item):
'''Hash item to 64-bit value'''
h = hashlib.md5(str(item).encode())
return int(h.hexdigest()[:16], 16)
def _leading_zeros(self, bits):
'''Count leading zeros + 1'''
if bits == 0:
return 64
count = 1
while (bits & (1 << 63)) == 0:
count += 1
bits <<= 1
return count
def add(self, item):
'''Add item to sketch'''
h = self._hash(item)
# Use first p bits for register index
j = h & ((1 << self.precision) - 1)
# Use remaining bits for leading zeros
w = h >> self.precision
# Update register with max leading zeros
self.registers[j] = max(self.registers[j], self._leading_zeros(w))
def cardinality(self):
'''Estimate cardinality'''
# Raw estimate
raw = self.alpha * (self.m ** 2) / sum(2 ** (-x) for x in self.registers)
# Small range correction
if raw <= 2.5 * self.m:
zeros = self.registers.count(0)
if zeros != 0:
return self.m * math.log(self.m / zeros)
# Large range correction
if raw <= (1 << 32) / 30:
return raw
return -(1 << 32) * math.log(1 - raw / (1 << 32))
# 4. Skip List - Probabilistic Balanced Structure
import random
class SkipNode:
def __init__(self, value, level):
self.value = value
self.forward = [None] * (level + 1)
class SkipList:
'''Probabilistic alternative to balanced trees'''
def __init__(self, max_level=16, p=0.5):
self.max_level = max_level
self.p = p
self.header = SkipNode(None, max_level)
self.level = 0
def _random_level(self):
'''Generate random level'''
level = 0
while random.random() < self.p and level < self.max_level:
level += 1
return level
def search(self, target):
'''Search for value'''
current = self.header
for i in range(self.level, -1, -1):
while current.forward[i] and current.forward[i].value < target:
current = current.forward[i]
current = current.forward[0]
return current and current.value == target
def insert(self, value):
'''Insert value'''
update = [None] * (self.max_level + 1)
current = self.header
# Find position
for i in range(self.level, -1, -1):
while current.forward[i] and current.forward[i].value < value:
current = current.forward[i]
update[i] = current
# Generate level for new node
new_level = self._random_level()
if new_level > self.level:
for i in range(self.level + 1, new_level + 1):
update[i] = self.header
self.level = new_level
# Create and insert node
new_node = SkipNode(value, new_level)
for i in range(new_level + 1):
new_node.forward[i] = update[i].forward[i]
update[i].forward[i] = new_nodePythonAnswer: Probabilistic structures: Bloom filter for set membership with no false negatives; Count-Min Sketch estimates frequencies; HyperLogLog estimates cardinality in O(1) space; Skip List provides O(log n) operations with high probability.
Q163. Master online algorithm techniques
# Online Algorithms and Competitive Analysis
# 1. Reservoir Sampling - Sample k items from stream
import random
def reservoir_sampling(stream, k):
'''Sample k items uniformly from stream of unknown length'''
reservoir = []
for i, item in enumerate(stream):
if i < k:
reservoir.append(item)
else:
# Random index from 0 to i
j = random.randint(0, i)
if j < k:
reservoir[j] = item
return reservoir
# 2. Weighted Reservoir Sampling
def weighted_reservoir_sampling(stream_with_weights, k):
'''Sample k items with weights from stream'''
import heapq
heap = []
for item, weight in stream_with_weights:
# Generate key = random^(1/weight)
key = random.random() ** (1.0 / weight)
if len(heap) < k:
heapq.heappush(heap, (key, item))
elif key > heap[0][0]:
heapq.heapreplace(heap, (key, item))
return [item for key, item in heap]
# 3. Online Median Finder
class MedianFinder:
'''Maintain median of stream in O(log n) per insert'''
def __init__(self):
import heapq
self.small = [] # Max heap (negative values)
self.large = [] # Min heap
def add_num(self, num):
'''Add number to stream'''
import heapq
# Add to max heap (small)
heapq.heappush(self.small, -num)
# Balance: move largest from small to large
heapq.heappush(self.large, -heapq.heappop(self.small))
# Maintain size: small has same or one more element
if len(self.small) < len(self.large):
heapq.heappush(self.small, -heapq.heappop(self.large))
def find_median(self):
'''Get current median'''
if len(self.small) > len(self.large):
return -self.small[0]
return (-self.small[0] + self.large[0]) / 2.0
# 4. Sliding Window Maximum
from collections import deque
def max_sliding_window(nums, k):
'''Maximum in each window of size k'''
if not nums:
return []
dq = deque()
result = []
for i, num in enumerate(nums):
# Remove elements outside window
while dq and dq[0] < i - k + 1:
dq.popleft()
# Remove smaller elements (they won't be max)
while dq and nums[dq[-1]] < num:
dq.pop()
dq.append(i)
# Add to result when window is full
if i >= k - 1:
result.append(nums[dq[0]])
return result
# 5. LRU Cache - Online Caching
class LRUCache:
'''Least Recently Used cache - O(1) operations'''
def __init__(self, capacity):
from collections import OrderedDict
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key):
if key not in self.cache:
return -1
# Move to end (most recent)
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
# Remove least recent (first item)
self.cache.popitem(last=False)
# 6. Page Replacement - LRU vs FIFO
def page_replacement_lru(pages, capacity):
'''Simulate LRU page replacement'''
from collections import OrderedDict
cache = OrderedDict()
page_faults = 0
for page in pages:
if page not in cache:
page_faults += 1
if len(cache) >= capacity:
cache.popitem(last=False)
cache[page] = True
else:
cache.move_to_end(page)
return page_faults
# 7. Ski Rental Problem - Online Decision Making
def ski_rental_analysis(days_needed, rental_cost, buy_cost):
'''Analyze competitive ratio of ski rental'''
# Strategy: Rent until cost equals buying, then buy
threshold = buy_cost // rental_cost
if days_needed <= threshold:
# Just rent
total_cost = days_needed * rental_cost
optimal = days_needed * rental_cost
else:
# Rent then buy
total_cost = threshold * rental_cost + buy_cost
optimal = buy_cost
competitive_ratio = total_cost / optimal
return {
'total_cost': total_cost,
'optimal': optimal,
'competitive_ratio': competitive_ratio,
'strategy': 'rent' if days_needed <= threshold else 'buy_after_threshold'
}PythonAnswer: Online algorithms: reservoir sampling maintains uniform random sample; weighted sampling uses random keys; median finder with two heaps; sliding window maximum with monotonic deque; LRU cache with O(1) operations; competitive analysis measures worst-case ratio.
Q164. Implement advanced string matching
# Advanced String Matching and Pattern Recognition
# 1. Aho-Corasick - Multiple Pattern Matching
from collections import deque, defaultdict
class AhoCorasick:
'''Multiple pattern matching in O(n + m + z)'''
def __init__(self):
self.goto = defaultdict(dict)
self.fail = {}
self.output = defaultdict(list)
self.state_count = 0
def add_pattern(self, pattern):
'''Add pattern to trie'''
state = 0
for char in pattern:
if char not in self.goto[state]:
self.state_count += 1
self.goto[state][char] = self.state_count
state = self.goto[state][char]
self.output[state].append(pattern)
def build_failure_function(self):
'''Build failure links using BFS'''
queue = deque()
# All states reachable from root fail to root
for char in self.goto[0]:
state = self.goto[0][char]
self.fail[state] = 0
queue.append(state)
# BFS to build failure function
while queue:
r = queue.popleft()
for char, state in self.goto[r].items():
queue.append(state)
# Find failure state
fail_state = self.fail[r]
while fail_state != 0 and char not in self.goto[fail_state]:
fail_state = self.fail[fail_state]
self.fail[state] = self.goto[fail_state].get(char, 0)
# Merge outputs from failure state
self.output[state].extend(self.output[self.fail[state]])
def search(self, text):
'''Find all pattern occurrences'''
self.build_failure_function()
state = 0
results = []
for i, char in enumerate(text):
# Follow failure links
while state != 0 and char not in self.goto[state]:
state = self.fail[state]
state = self.goto[state].get(char, 0)
# Record matches
if self.output[state]:
for pattern in self.output[state]:
results.append((i - len(pattern) + 1, pattern))
return results
# 2. Suffix Automaton - All Substrings Recognition
class SuffixAutomaton:
'''Recognize all substrings in O(n) space'''
class State:
def __init__(self):
self.length = 0
self.link = -1
self.next = {}
def __init__(self, s):
self.states = [self.State()]
self.last = 0
for char in s:
self.extend(char)
def extend(self, char):
'''Add character to automaton'''
cur = len(self.states)
self.states.append(self.State())
self.states[cur].length = self.states[self.last].length + 1
p = self.last
# Add transitions
while p != -1 and char not in self.states[p].next:
self.states[p].next[char] = cur
p = self.states[p].link
if p == -1:
self.states[cur].link = 0
else:
q = self.states[p].next[char]
if self.states[p].length + 1 == self.states[q].length:
self.states[cur].link = q
else:
# Clone state q
clone = len(self.states)
self.states.append(self.State())
self.states[clone].length = self.states[p].length + 1
self.states[clone].next = self.states[q].next.copy()
self.states[clone].link = self.states[q].link
# Update links
while p != -1 and self.states[p].next.get(char) == q:
self.states[p].next[char] = clone
p = self.states[p].link
self.states[q].link = self.states[cur].link = clone
self.last = cur
def contains(self, pattern):
'''Check if pattern is substring'''
state = 0
for char in pattern:
if char not in self.states[state].next:
return False
state = self.states[state].next[char]
return True
# 3. Longest Common Substring - Suffix Array
def longest_common_substring(s1, s2):
'''Find longest common substring using suffix array'''
# Combine strings with separator
combined = s1 + '#' + s2 + '$'
n = len(combined)
# Build suffix array
suffixes = [(combined[i:], i) for i in range(n)]
suffixes.sort()
# Find longest common prefix between adjacent suffixes
max_length = 0
result = ""
for i in range(1, n):
suffix1, idx1 = suffixes[i - 1]
suffix2, idx2 = suffixes[i]
# Check if from different strings
if (idx1 < len(s1)) != (idx2 < len(s1)):
# Find LCP
lcp = 0
while (lcp < len(suffix1) and lcp < len(suffix2) and
suffix1[lcp] == suffix2[lcp]):
lcp += 1
if lcp > max_length:
max_length = lcp
result = suffix1[:lcp]
return result
# 4. Wildcard Pattern Matching - DP
def wildcard_match(s, p):
'''Match with * (any sequence) and ? (single char)'''
m, n = len(s), len(p)
# dp[i][j] = s[:i] matches p[:j]
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
# Handle leading *
for j in range(1, n + 1):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 1]
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j - 1] == '*':
# * matches empty or any sequence
dp[i][j] = dp[i - 1][j] or dp[i][j - 1]
elif p[j - 1] == '?' or s[i - 1] == p[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
return dp[m][n]
# 5. Regular Expression with . and *
def regex_match(s, p):
'''Match with . (any char) and * (0+ of previous)'''
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
# Handle patterns like a*, a*b*, etc.
for j in range(2, n + 1, 2):
if p[j - 1] == '*':
dp[0][j] = dp[0][j - 2]
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j - 1] == '*':
# * can match zero or more
dp[i][j] = dp[i][j - 2] # Zero occurrences
if p[j - 2] == '.' or p[j - 2] == s[i - 1]:
dp[i][j] = dp[i][j] or dp[i - 1][j]
elif p[j - 1] == '.' or p[j - 1] == s[i - 1]:
dp[i][j] = dp[i - 1][j - 1]
return dp[m][n]PythonAnswer: Advanced string matching: Aho-Corasick finds multiple patterns simultaneously O(n+m+z); suffix automaton recognizes all substrings; LCS via suffix array; wildcard matching with DP; regex matching handles . and * operators.
Q165. Master divide and conquer techniques
# Advanced Divide and Conquer Algorithms
# 1. Count Inversions - Modified Merge Sort
def count_inversions(arr):
'''Count inversions using merge sort - O(n log n)'''
def merge_count(arr, temp, left, mid, right):
i = left
j = mid + 1
k = left
inv_count = 0
while i <= mid and j <= right:
if arr[i] <= arr[j]:
temp[k] = arr[i]
i += 1
else:
temp[k] = arr[j]
inv_count += (mid - i + 1)
j += 1
k += 1
while i <= mid:
temp[k] = arr[i]
i += 1
k += 1
while j <= right:
temp[k] = arr[j]
j += 1
k += 1
for i in range(left, right + 1):
arr[i] = temp[i]
return inv_count
def merge_sort_count(arr, temp, left, right):
inv_count = 0
if left < right:
mid = (left + right) // 2
inv_count += merge_sort_count(arr, temp, left, mid)
inv_count += merge_sort_count(arr, temp, mid + 1, right)
inv_count += merge_count(arr, temp, left, mid, right)
return inv_count
n = len(arr)
temp = [0] * n
return merge_sort_count(arr, temp, 0, n - 1)
# 2. Count of Range Sum
def count_range_sum(nums, lower, upper):
'''Count subarrays with sum in range [lower, upper]'''
def merge_count(sums, lower, upper, left, mid, right):
count = 0
j = k = mid + 1
for i in range(left, mid + 1):
# Find range [j, k) where sums[i] + lower <= sums[x] <= sums[i] + upper
while j <= right and sums[j] - sums[i] < lower:
j += 1
while k <= right and sums[k] - sums[i] <= upper:
k += 1
count += k - j
# Merge
sorted_sums = sorted(sums[left:right + 1])
sums[left:right + 1] = sorted_sums
return count
def merge_sort_count(sums, lower, upper, left, right):
if left >= right:
return 0
mid = (left + right) // 2
count = merge_sort_count(sums, lower, upper, left, mid)
count += merge_sort_count(sums, lower, upper, mid + 1, right)
count += merge_count(sums, lower, upper, left, mid, right)
return count
# Compute prefix sums
prefix_sums = [0]
for num in nums:
prefix_sums.append(prefix_sums[-1] + num)
return merge_sort_count(prefix_sums, lower, upper, 0, len(prefix_sums) - 1)
# 3. Maximum Subarray - Divide and Conquer
def max_subarray_dc(nums):
'''Find maximum subarray sum using divide and conquer'''
def max_crossing_sum(nums, left, mid, right):
'''Maximum sum crossing the midpoint'''
# Left side
left_sum = float('-inf')
temp_sum = 0
for i in range(mid, left - 1, -1):
temp_sum += nums[i]
left_sum = max(left_sum, temp_sum)
# Right side
right_sum = float('-inf')
temp_sum = 0
for i in range(mid + 1, right + 1):
temp_sum += nums[i]
right_sum = max(right_sum, temp_sum)
return left_sum + right_sum
def max_subarray_helper(nums, left, right):
if left == right:
return nums[left]
mid = (left + right) // 2
left_max = max_subarray_helper(nums, left, mid)
right_max = max_subarray_helper(nums, mid + 1, right)
cross_max = max_crossing_sum(nums, left, mid, right)
return max(left_max, right_max, cross_max)
return max_subarray_helper(nums, 0, len(nums) - 1)
# 4. Median of Two Sorted Arrays
def find_median_sorted_arrays(nums1, nums2):
'''Find median of two sorted arrays in O(log(min(m,n)))'''
# Ensure nums1 is smaller
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m, n = len(nums1), len(nums2)
left, right = 0, m
while left <= right:
partition1 = (left + right) // 2
partition2 = (m + n + 1) // 2 - partition1
max_left1 = float('-inf') if partition1 == 0 else nums1[partition1 - 1]
min_right1 = float('inf') if partition1 == m else nums1[partition1]
max_left2 = float('-inf') if partition2 == 0 else nums2[partition2 - 1]
min_right2 = float('inf') if partition2 == n else nums2[partition2]
if max_left1 <= min_right2 and max_left2 <= min_right1:
# Found partition
if (m + n) % 2 == 0:
return (max(max_left1, max_left2) + min(min_right1, min_right2)) / 2
else:
return max(max_left1, max_left2)
elif max_left1 > min_right2:
right = partition1 - 1
else:
left = partition1 + 1
# 5. Kth Largest Element - QuickSelect
import random
def find_kth_largest(nums, k):
'''Find kth largest using QuickSelect - O(n) average'''
def partition(left, right, pivot_index):
pivot = nums[pivot_index]
# Move pivot to end
nums[pivot_index], nums[right] = nums[right], nums[pivot_index]
store_index = left
for i in range(left, right):
if nums[i] < pivot:
nums[store_index], nums[i] = nums[i], nums[store_index]
store_index += 1
# Move pivot to final position
nums[right], nums[store_index] = nums[store_index], nums[right]
return store_index
def select(left, right, k_smallest):
if left == right:
return nums[left]
# Random pivot
pivot_index = random.randint(left, right)
pivot_index = partition(left, right, pivot_index)
if k_smallest == pivot_index:
return nums[k_smallest]
elif k_smallest < pivot_index:
return select(left, pivot_index - 1, k_smallest)
else:
return select(pivot_index + 1, right, k_smallest)
return select(0, len(nums) - 1, len(nums) - k)
# 6. Pow(x, n) - Fast Exponentiation
def my_pow(x, n):
'''Compute x^n using divide and conquer - O(log n)'''
if n == 0:
return 1
if n < 0:
x = 1 / x
n = -n
def helper(x, n):
if n == 1:
return x
half = helper(x, n // 2)
if n % 2 == 0:
return half * half
else:
return half * half * x
return helper(x, n)PythonAnswer: Divide and conquer: count inversions via modified merge sort O(n log n); range sum uses merge with counting; QuickSelect finds kth element O(n) average; median of sorted arrays O(log min(m,n)); fast exponentiation O(log n).
Q166. Master advanced backtracking patterns
# Advanced Backtracking and Constraint Satisfaction
# 1. N-Queens II - Count Solutions
def total_n_queens(n):
'''Count all solutions to N-Queens problem'''
def backtrack(row, cols, diag1, diag2):
if row == n:
return 1
count = 0
for col in range(n):
# Check if position is safe
if col in cols or (row - col) in diag1 or (row + col) in diag2:
continue
# Place queen and recurse
cols.add(col)
diag1.add(row - col)
diag2.add(row + col)
count += backtrack(row + 1, cols, diag1, diag2)
# Backtrack
cols.remove(col)
diag1.remove(row - col)
diag2.remove(row + col)
return count
return backtrack(0, set(), set(), set())
# 2. Sudoku Solver
def solve_sudoku(board):
'''Solve Sudoku using backtracking'''
def is_valid(board, row, col, num):
# Check row
if num in board[row]:
return False
# Check column
if num in [board[i][col] for i in range(9)]:
return False
# Check 3x3 box
box_row, box_col = 3 * (row // 3), 3 * (col // 3)
for i in range(box_row, box_row + 3):
for j in range(box_col, box_col + 3):
if board[i][j] == num:
return False
return True
def solve():
for i in range(9):
for j in range(9):
if board[i][j] == '.':
for num in '123456789':
if is_valid(board, i, j, num):
board[i][j] = num
if solve():
return True
board[i][j] = '.'
return False
return True
solve()
# 3. Word Search II - Trie + Backtracking
class TrieNode:
def __init__(self):
self.children = {}
self.word = None
def find_words(board, words):
'''Find all words in board using Trie'''
# Build Trie
root = TrieNode()
for word in words:
node = root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.word = word
m, n = len(board), len(board[0])
result = []
def backtrack(i, j, node):
char = board[i][j]
if char not in node.children:
return
next_node = node.children[char]
# Found word
if next_node.word:
result.append(next_node.word)
next_node.word = None # Avoid duplicates
# Mark as visited
board[i][j] = '#'
# Explore neighbors
for di, dj in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
ni, nj = i + di, j + dj
if 0 <= ni < m and 0 <= nj < n and board[ni][nj] != '#':
backtrack(ni, nj, next_node)
# Restore
board[i][j] = char
# Prune Trie
if not next_node.children:
del node.children[char]
for i in range(m):
for j in range(n):
if board[i][j] in root.children:
backtrack(i, j, root)
return result
# 4. Combination Sum III
def combination_sum_3(k, n):
'''Find k numbers that sum to n (using 1-9 once)'''
result = []
def backtrack(start, path, remaining):
if len(path) == k:
if remaining == 0:
result.append(path[:])
return
for i in range(start, 10):
if i > remaining:
break
path.append(i)
backtrack(i + 1, path, remaining - i)
path.pop()
backtrack(1, [], n)
return result
# 5. Partition to K Equal Sum Subsets - Optimized
def can_partition_k_subsets_optimized(nums, k):
'''Partition array into k equal sum subsets'''
total = sum(nums)
if total % k != 0:
return False
target = total // k
nums.sort(reverse=True)
if nums[0] > target:
return False
used = [False] * len(nums)
def backtrack(group, start, current_sum):
if group == k:
return True
if current_sum == target:
return backtrack(group + 1, 0, 0)
for i in range(start, len(nums)):
if used[i] or current_sum + nums[i] > target:
continue
# Skip duplicates in same recursive level
if i > 0 and not used[i-1] and nums[i] == nums[i-1]:
continue
used[i] = True
if backtrack(group, i + 1, current_sum + nums[i]):
return True
used[i] = False
return False
return backtrack(0, 0, 0)
# 6. Generate Parentheses with Constraints
def generate_parentheses_balanced(n):
'''Generate all valid parentheses combinations'''
result = []
def backtrack(current, open_count, close_count):
if len(current) == 2 * n:
result.append(current)
return
if open_count < n:
backtrack(current + '(', open_count + 1, close_count)
if close_count < open_count:
backtrack(current + ')', open_count, close_count + 1)
backtrack('', 0, 0)
return result
# 7. Expression Add Operators
def add_operators(num, target):
'''Insert +, -, * to get target'''
result = []
def backtrack(index, path, value, last):
if index == len(num):
if value == target:
result.append(path)
return
for i in range(index, len(num)):
# Skip leading zeros
if i > index and num[index] == '0':
break
current = int(num[index:i+1])
if index == 0:
backtrack(i + 1, str(current), current, current)
else:
# Addition
backtrack(i + 1, path + '+' + str(current),
value + current, current)
# Subtraction
backtrack(i + 1, path + '-' + str(current),
value - current, -current)
# Multiplication (need to undo last operation)
backtrack(i + 1, path + '*' + str(current),
value - last + last * current, last * current)
backtrack(0, '', 0, 0)
return resultPythonAnswer: Advanced backtracking: N-Queens uses sets for O(1) conflict checking; Sudoku validates constraints; Word Search II combines Trie with backtracking; partition optimizes with sorting; expression operators track last value for multiplication.
Q167. Apply max flow to real-world problems
# Advanced Graph Algorithms - Maximum Flow Applications
# 1. Maximum Bipartite Matching - Using Flow
def max_bipartite_matching_flow(graph_left, n_left, n_right):
'''Maximum matching via max flow'''
from collections import defaultdict, deque
# Build flow network: source -> left -> right -> sink
# source = 0, sink = n_left + n_right + 1
source = 0
sink = n_left + n_right + 1
# Build capacity graph
capacity = defaultdict(lambda: defaultdict(int))
# Source to left nodes
for i in range(1, n_left + 1):
capacity[source][i] = 1
# Left to right (original edges)
for left, rights in graph_left.items():
for right in rights:
capacity[left][n_left + right] = 1
# Right to sink
for i in range(1, n_right + 1):
capacity[n_left + i][sink] = 1
def bfs(source, sink, parent):
'''Find augmenting path'''
visited = {source}
queue = deque([source])
while queue:
u = queue.popleft()
for v in range(sink + 1):
if v not in visited and capacity[u][v] > 0:
visited.add(v)
queue.append(v)
parent[v] = u
if v == sink:
return True
return False
parent = {}
max_flow = 0
while bfs(source, sink, parent):
# Find minimum capacity
path_flow = float('inf')
s = sink
while s != source:
path_flow = min(path_flow, capacity[parent[s]][s])
s = parent[s]
# Update capacities
v = sink
while v != source:
u = parent[v]
capacity[u][v] -= path_flow
capacity[v][u] += path_flow
v = parent[v]
max_flow += path_flow
parent = {}
return max_flow
# 2. Project Selection Problem
def max_profit_projects(projects, dependencies):
'''Maximum profit with dependencies using min-cut'''
# projects = [(profit, cost)]
# dependencies = [(i, j)] - project i requires j
from collections import defaultdict, deque
n = len(projects)
source = n
sink = n + 1
# Build flow network
capacity = defaultdict(lambda: defaultdict(int))
total_profit = 0
for i, (profit, cost) in enumerate(projects):
if profit > 0:
capacity[source][i] = profit
total_profit += profit
if cost > 0:
capacity[i][sink] = cost
# Add dependency edges (infinite capacity)
for i, j in dependencies:
capacity[i][j] = float('inf')
# Find min-cut (max-flow)
def edmonds_karp():
def bfs():
visited = {source}
queue = deque([source])
parent = {}
while queue:
u = queue.popleft()
for v in range(sink + 1):
if v not in visited and capacity[u][v] > 0:
visited.add(v)
parent[v] = u
queue.append(v)
if v == sink:
return parent
return None
max_flow = 0
while True:
parent = bfs()
if not parent:
break
# Find path flow
path_flow = float('inf')
v = sink
while v != source:
u = parent[v]
path_flow = min(path_flow, capacity[u][v])
v = u
# Update capacities
v = sink
while v != source:
u = parent[v]
capacity[u][v] -= path_flow
capacity[v][u] += path_flow
v = u
max_flow += path_flow
return max_flow
min_cut = edmonds_karp()
return total_profit - min_cut
# 3. Minimum Cost Maximum Flow - Cycle Canceling
def min_cost_max_flow(graph, source, sink, k):
'''Find max flow with minimum cost'''
# graph[u] = [(v, capacity, cost)]
from collections import defaultdict, deque
import heapq
# Build residual graph
residual = defaultdict(lambda: defaultdict(lambda: [0, 0]))
for u in graph:
for v, cap, cost in graph[u]:
residual[u][v] = [cap, cost]
residual[v][u] = [0, -cost]
def spfa(source, sink):
'''Shortest path with negative edges'''
dist = defaultdict(lambda: float('inf'))
dist[source] = 0
parent = {}
in_queue = {source}
queue = deque([source])
while queue:
u = queue.popleft()
in_queue.remove(u)
for v in residual[u]:
cap, cost = residual[u][v]
if cap > 0 and dist[u] + cost < dist[v]:
dist[v] = dist[u] + cost
parent[v] = u
if v not in in_queue:
queue.append(v)
in_queue.add(v)
if dist[sink] == float('inf'):
return None, float('inf')
return parent, dist[sink]
total_cost = 0
total_flow = 0
for _ in range(k):
parent, path_cost = spfa(source, sink)
if not parent:
break
# Find bottleneck
flow = float('inf')
v = sink
while v != source:
u = parent[v]
flow = min(flow, residual[u][v][0])
v = u
# Update flow
v = sink
while v != source:
u = parent[v]
residual[u][v][0] -= flow
residual[v][u][0] += flow
v = u
total_flow += flow
total_cost += flow * path_cost
return total_flow, total_cost
# 4. Image Segmentation - Graph Cut
def image_segmentation(image, foreground_seeds, background_seeds):
'''Segment image using graph cuts'''
from collections import defaultdict, deque
h, w = len(image), len(image[0])
# Node IDs: 0 = source, 1..h*w = pixels, h*w+1 = sink
source = 0
sink = h * w + 1
def pixel_id(i, j):
return i * w + j + 1
# Build graph
capacity = defaultdict(lambda: defaultdict(int))
# Connect seeds to source/sink
for i, j in foreground_seeds:
capacity[source][pixel_id(i, j)] = float('inf')
for i, j in background_seeds:
capacity[pixel_id(i, j)][sink] = float('inf')
# Connect neighboring pixels
for i in range(h):
for j in range(w):
pid = pixel_id(i, j)
# Right neighbor
if j + 1 < w:
nid = pixel_id(i, j + 1)
weight = 100 / (1 + abs(image[i][j] - image[i][j+1]))
capacity[pid][nid] = weight
capacity[nid][pid] = weight
# Bottom neighbor
if i + 1 < h:
nid = pixel_id(i + 1, j)
weight = 100 / (1 + abs(image[i][j] - image[i+1][j]))
capacity[pid][nid] = weight
capacity[nid][pid] = weight
# Run max-flow/min-cut
def max_flow():
def bfs():
visited = {source}
queue = deque([source])
parent = {}
while queue:
u = queue.popleft()
for v in capacity[u]:
if v not in visited and capacity[u][v] > 0:
visited.add(v)
parent[v] = u
queue.append(v)
if v == sink:
return parent
return None
while True:
parent = bfs()
if not parent:
break
# Augment flow
flow = float('inf')
v = sink
while v != source:
u = parent[v]
flow = min(flow, capacity[u][v])
v = u
v = sink
while v != source:
u = parent[v]
capacity[u][v] -= flow
capacity[v][u] += flow
v = u
max_flow()
# Find reachable nodes from source (foreground)
visited = {source}
queue = deque([source])
while queue:
u = queue.popleft()
for v in capacity[u]:
if v not in visited and capacity[u][v] > 0:
visited.add(v)
queue.append(v)
# Return segmentation
segmentation = [[0] * w for _ in range(h)]
for i in range(h):
for j in range(w):
if pixel_id(i, j) in visited:
segmentation[i][j] = 1
return segmentationPythonAnswer: Max flow applications: bipartite matching via flow network; project selection as min-cut; min-cost max-flow for optimization; image segmentation with graph cuts; pattern: model problem as source-sink flow with capacity constraints.
Q168. Apply DP optimization techniques
# Advanced DP Optimization Techniques
# 1. Convex Hull Trick - Linear DP Optimization
class ConvexHullTrick:
'''Optimize DP transitions with linear functions'''
def __init__(self):
self.lines = [] # (slope, intercept)
def _bad(self, l1, l2, l3):
'''Check if l2 is unnecessary'''
# Cross product test
return (l3[1] - l1[1]) * (l1[0] - l2[0]) <= (l2[1] - l1[1]) * (l1[0] - l3[0])
def add_line(self, slope, intercept):
'''Add line y = slope*x + intercept (slopes must be increasing)'''
new_line = (slope, intercept)
while len(self.lines) >= 2 and self._bad(self.lines[-2], self.lines[-1], new_line):
self.lines.pop()
self.lines.append(new_line)
def query(self, x):
'''Find minimum y value at x'''
if not self.lines:
return float('inf')
# Binary search for best line
left, right = 0, len(self.lines) - 1
while left < right:
mid = (left + right) // 2
m1, c1 = self.lines[mid]
m2, c2 = self.lines[mid + 1]
if m1 * x + c1 > m2 * x + c2:
left = mid + 1
else:
right = mid
m, c = self.lines[left]
return m * x + c
# Example: Optimal Tree Cutting
def optimal_tree_cutting(heights, costs):
'''Minimum cost to cut trees with DP and CHT'''
n = len(heights)
# dp[i] = min cost to cut first i trees
dp = [float('inf')] * (n + 1)
dp[0] = 0
cht = ConvexHullTrick()
cht.add_line(0, 0)
for i in range(1, n + 1):
# Query for best previous cut
dp[i] = cht.query(heights[i-1]) + costs[i-1]
# Add current state
cht.add_line(-i, dp[i] + i * heights[i-1])
return dp[n]
# 2. Divide and Conquer DP Optimization
def divide_conquer_dp(costs):
'''Optimize DP with divide and conquer - O(kn log n)'''
# Problem: split array into k partitions minimizing cost
n = len(costs)
k = 3 # Number of partitions
# dp[i][j] = min cost to partition first j elements into i groups
dp = [[float('inf')] * (n + 1) for _ in range(k + 1)]
dp[0][0] = 0
# opt[i][j] = optimal split point for dp[i][j]
opt = [[0] * (n + 1) for _ in range(k + 1)]
def cost(i, j):
'''Cost of partition from i to j'''
return sum(costs[i:j])
def compute(group, left, right, opt_left, opt_right):
'''Compute dp[group][mid] for mid in [left, right]'''
if left > right:
return
mid = (left + right) // 2
best_cost = float('inf')
best_opt = opt_left
for split in range(opt_left, min(mid, opt_right) + 1):
current_cost = dp[group - 1][split] + cost(split, mid)
if current_cost < best_cost:
best_cost = current_cost
best_opt = split
dp[group][mid] = best_cost
opt[group][mid] = best_opt
# Recurse
compute(group, left, mid - 1, opt_left, best_opt)
compute(group, mid + 1, right, best_opt, opt_right)
for i in range(1, k + 1):
compute(i, 1, n, 0, n)
return dp[k][n]
# 3. Knuth Optimization - Matrix Chain Multiplication
def matrix_chain_multiplication_optimized(dimensions):
'''Optimal matrix multiplication order with Knuth optimization'''
n = len(dimensions) - 1
# dp[i][j] = min multiplications for matrices i to j
dp = [[0] * n for _ in range(n)]
# opt[i][j] = optimal split point
opt = [[0] * n for _ in range(n)]
# Initialize diagonal
for i in range(n):
opt[i][i] = i
# Fill DP table
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = float('inf')
# Use Knuth optimization: opt[i][j-1] <= opt[i][j] <= opt[i+1][j]
start = opt[i][j-1] if j > 0 else i
end = opt[i+1][j] if i + 1 < n else j
for k in range(start, min(end + 1, j)):
cost = (dp[i][k] + dp[k+1][j] +
dimensions[i] * dimensions[k+1] * dimensions[j+1])
if cost < dp[i][j]:
dp[i][j] = cost
opt[i][j] = k
return dp[0][n-1]
# 4. Monotone Queue Optimization - Sliding Window DP
def sliding_window_dp(arr, k):
'''DP with sliding window maximum optimization'''
from collections import deque
n = len(arr)
# dp[i] = optimal value ending at i
dp = [0] * n
dp[0] = arr[0]
# Monotonic deque for max in sliding window
dq = deque([0])
for i in range(1, n):
# Remove elements outside window
while dq and dq[0] < i - k:
dq.popleft()
# Compute dp[i]
if dq:
dp[i] = arr[i] + dp[dq[0]]
else:
dp[i] = arr[i]
# Maintain monotonic property
while dq and dp[dq[-1]] <= dp[i]:
dq.pop()
dq.append(i)
return max(dp)
# 5. Space Optimization - Rolling Array
def fibonacci_space_optimized(n):
'''Fibonacci with O(1) space'''
if n <= 1:
return n
prev2, prev1 = 0, 1
for i in range(2, n + 1):
current = prev1 + prev2
prev2 = prev1
prev1 = current
return prev1
# General rolling array pattern
def dp_rolling_array(n, k):
'''DP with rolling array - only keep last k rows'''
# Instead of dp[i][j], use dp[i % k][j]
dp = [[0] * n for _ in range(k)]
for i in range(n):
for j in range(n):
# Use i % k instead of i
dp[i % k][j] = dp[(i - 1) % k][j] + 1
return dp[(n - 1) % k]PythonAnswer: DP optimizations: Convex Hull Trick optimizes linear transitions O(n log n); Divide-Conquer DP reduces from O(kn²) to O(kn log n); Knuth optimization for optimal BST; monotonic queue for sliding window; rolling array reduces space to O(k).
Q169. Implement advanced self-balancing trees
# Advanced Tree Data Structures
# 1. Splay Tree - Self-Adjusting BST
class SplayNode:
def __init__(self, key):
self.key = key
self.left = None
self.right = None
class SplayTree:
'''Self-adjusting binary search tree with amortized O(log n)'''
def __init__(self):
self.root = None
def _right_rotate(self, x):
'''Rotate right at x'''
y = x.left
x.left = y.right
y.right = x
return y
def _left_rotate(self, x):
'''Rotate left at x'''
y = x.right
x.right = y.left
y.left = x
return y
def _splay(self, root, key):
'''Splay key to root'''
if not root or root.key == key:
return root
if key < root.key:
if not root.left:
return root
# Zig-Zig (Left-Left)
if key < root.left.key:
root.left.left = self._splay(root.left.left, key)
root = self._right_rotate(root)
# Zig-Zag (Left-Right)
elif key > root.left.key:
root.left.right = self._splay(root.left.right, key)
if root.left.right:
root.left = self._left_rotate(root.left)
return self._right_rotate(root) if root.left else root
else:
if not root.right:
return root
# Zag-Zig (Right-Left)
if key < root.right.key:
root.right.left = self._splay(root.right.left, key)
if root.right.left:
root.right = self._right_rotate(root.right)
# Zag-Zag (Right-Right)
elif key > root.right.key:
root.right.right = self._splay(root.right.right, key)
root = self._left_rotate(root)
return self._left_rotate(root) if root.right else root
def search(self, key):
'''Search and splay to root'''
self.root = self._splay(self.root, key)
return self.root and self.root.key == key
def insert(self, key):
'''Insert key and splay'''
if not self.root:
self.root = SplayNode(key)
return
self.root = self._splay(self.root, key)
if self.root.key == key:
return
new_node = SplayNode(key)
if key < self.root.key:
new_node.right = self.root
new_node.left = self.root.left
self.root.left = None
else:
new_node.left = self.root
new_node.right = self.root.right
self.root.right = None
self.root = new_node
# 2. Treap - Randomized BST
import random
class TreapNode:
def __init__(self, key):
self.key = key
self.priority = random.random()
self.left = None
self.right = None
class Treap:
'''Binary search tree with random priorities'''
def __init__(self):
self.root = None
def _rotate_right(self, y):
x = y.left
y.left = x.right
x.right = y
return x
def _rotate_left(self, x):
y = x.right
x.right = y.left
y.left = x
return y
def _insert(self, root, key):
if not root:
return TreapNode(key)
if key < root.key:
root.left = self._insert(root.left, key)
# Maintain heap property
if root.left.priority > root.priority:
root = self._rotate_right(root)
elif key > root.key:
root.right = self._insert(root.right, key)
if root.right.priority > root.priority:
root = self._rotate_left(root)
return root
def insert(self, key):
self.root = self._insert(self.root, key)
def _search(self, root, key):
if not root:
return False
if key == root.key:
return True
elif key < root.key:
return self._search(root.left, key)
else:
return self._search(root.right, key)
def search(self, key):
return self._search(self.root, key)
# 3. Red-Black Tree - Balanced BST
class RBNode:
def __init__(self, key):
self.key = key
self.color = 'RED'
self.left = None
self.right = None
self.parent = None
class RedBlackTree:
'''Self-balancing BST with red-black properties'''
def __init__(self):
self.NIL = RBNode(None)
self.NIL.color = 'BLACK'
self.root = self.NIL
def _left_rotate(self, x):
y = x.right
x.right = y.left
if y.left != self.NIL:
y.left.parent = x
y.parent = x.parent
if x.parent is None:
self.root = y
elif x == x.parent.left:
x.parent.left = y
else:
x.parent.right = y
y.left = x
x.parent = y
def _right_rotate(self, y):
x = y.left
y.left = x.right
if x.right != self.NIL:
x.right.parent = y
x.parent = y.parent
if y.parent is None:
self.root = x
elif y == y.parent.right:
y.parent.right = x
else:
y.parent.left = x
x.right = y
y.parent = x
def insert(self, key):
node = RBNode(key)
node.left = self.NIL
node.right = self.NIL
parent = None
current = self.root
while current != self.NIL:
parent = current
if node.key < current.key:
current = current.left
else:
current = current.right
node.parent = parent
if parent is None:
self.root = node
elif node.key < parent.key:
parent.left = node
else:
parent.right = node
self._fix_insert(node)
def _fix_insert(self, k):
while k.parent and k.parent.color == 'RED':
if k.parent == k.parent.parent.left:
u = k.parent.parent.right
if u.color == 'RED':
k.parent.color = 'BLACK'
u.color = 'BLACK'
k.parent.parent.color = 'RED'
k = k.parent.parent
else:
if k == k.parent.right:
k = k.parent
self._left_rotate(k)
k.parent.color = 'BLACK'
k.parent.parent.color = 'RED'
self._right_rotate(k.parent.parent)
else:
u = k.parent.parent.left
if u.color == 'RED':
k.parent.color = 'BLACK'
u.color = 'BLACK'
k.parent.parent.color = 'RED'
k = k.parent.parent
else:
if k == k.parent.left:
k = k.parent
self._right_rotate(k)
k.parent.color = 'BLACK'
k.parent.parent.color = 'RED'
self._left_rotate(k.parent.parent)
self.root.color = 'BLACK'PythonAnswer: Advanced trees: Splay tree moves accessed nodes to root (amortized O(log n)); Treap uses random priorities for balance; Red-Black tree maintains 5 properties ensuring O(log n) height; all provide guaranteed logarithmic operations.
Q170. Master advanced hashing techniques
# Advanced Hashing and Hash-Based Algorithms
# 1. Rolling Hash - Rabin-Karp for Multiple Patterns
class RollingHash:
'''Polynomial rolling hash for string matching'''
def __init__(self, text, pattern_length):
self.text = text
self.pattern_length = pattern_length
self.base = 256
self.mod = 10**9 + 7
# Precompute base^(m-1) mod mod
self.h = 1
for _ in range(pattern_length - 1):
self.h = (self.h * self.base) % self.mod
# Compute initial hash
self.current_hash = 0
for i in range(pattern_length):
self.current_hash = (self.current_hash * self.base + ord(text[i])) % self.mod
def roll(self, old_idx, new_idx):
'''Roll hash from old_idx to new_idx'''
# Remove leading character
self.current_hash = (self.current_hash - ord(self.text[old_idx]) * self.h) % self.mod
# Add trailing character
self.current_hash = (self.current_hash * self.base + ord(self.text[new_idx])) % self.mod
# Handle negative values
self.current_hash = (self.current_hash + self.mod) % self.mod
return self.current_hash
@staticmethod
def compute_hash(s, base=256, mod=10**9 + 7):
'''Compute hash of string'''
h = 0
for char in s:
h = (h * base + ord(char)) % mod
return h
def rabin_karp_multiple(text, patterns):
'''Find all occurrences of multiple patterns'''
results = {pattern: [] for pattern in patterns}
if not text or not patterns:
return results
# Group patterns by length
by_length = {}
for pattern in patterns:
length = len(pattern)
if length not in by_length:
by_length[length] = []
by_length[length].append(pattern)
# Search for each length group
for length, pattern_group in by_length.items():
if length > len(text):
continue
# Compute pattern hashes
pattern_hashes = {RollingHash.compute_hash(p): p for p in pattern_group}
# Rolling hash for text
roller = RollingHash(text, length)
# Check first window
if roller.current_hash in pattern_hashes:
pattern = pattern_hashes[roller.current_hash]
if text[:length] == pattern:
results[pattern].append(0)
# Check remaining windows
for i in range(1, len(text) - length + 1):
h = roller.roll(i - 1, i + length - 1)
if h in pattern_hashes:
pattern = pattern_hashes[h]
if text[i:i+length] == pattern:
results[pattern].append(i)
return results
# 2. Consistent Hashing - Distributed Systems
class ConsistentHash:
'''Consistent hashing for load balancing'''
def __init__(self, nodes=None, virtual_nodes=150):
import hashlib
self.virtual_nodes = virtual_nodes
self.ring = {}
self.sorted_keys = []
if nodes:
for node in nodes:
self.add_node(node)
def _hash(self, key):
'''Hash function'''
import hashlib
return int(hashlib.md5(str(key).encode()).hexdigest(), 16)
def add_node(self, node):
'''Add node to ring'''
for i in range(self.virtual_nodes):
virtual_key = f"{node}:{i}"
h = self._hash(virtual_key)
self.ring[h] = node
self.sorted_keys.append(h)
self.sorted_keys.sort()
def remove_node(self, node):
'''Remove node from ring'''
for i in range(self.virtual_nodes):
virtual_key = f"{node}:{i}"
h = self._hash(virtual_key)
if h in self.ring:
del self.ring[h]
self.sorted_keys.remove(h)
def get_node(self, key):
'''Get node for key'''
if not self.ring:
return None
h = self._hash(key)
# Binary search for first node >= h
import bisect
idx = bisect.bisect_right(self.sorted_keys, h)
if idx == len(self.sorted_keys):
idx = 0
return self.ring[self.sorted_keys[idx]]
# 3. Perfect Hashing - Two-Level Hashing
class PerfectHash:
'''Perfect hashing with O(1) worst-case lookup'''
def __init__(self, keys):
import random
self.n = len(keys)
self.p = 10**9 + 7
# Find universal hash for first level
while True:
self.a1 = random.randint(1, self.p - 1)
self.b1 = random.randint(0, self.p - 1)
# Create buckets
buckets = [[] for _ in range(self.n)]
for key in keys:
h = self._hash1(key)
buckets[h].append(key)
# Check if any bucket is too large
if all(len(bucket) ** 2 <= self.n for bucket in buckets):
break
# Build second level hash tables
self.tables = []
for bucket in buckets:
if not bucket:
self.tables.append(None)
continue
m = len(bucket) ** 2
# Find collision-free hash
while True:
a2 = random.randint(1, self.p - 1)
b2 = random.randint(0, self.p - 1)
table = [None] * m
collision = False
for key in bucket:
h = ((a2 * hash(key) + b2) % self.p) % m
if table[h] is not None:
collision = True
break
table[h] = key
if not collision:
self.tables.append({'a': a2, 'b': b2, 'table': table})
break
def _hash1(self, key):
'''First level hash'''
return ((self.a1 * hash(key) + self.b1) % self.p) % self.n
def lookup(self, key):
'''O(1) worst-case lookup'''
h1 = self._hash1(key)
if self.tables[h1] is None:
return False
table_info = self.tables[h1]
m = len(table_info['table'])
h2 = ((table_info['a'] * hash(key) + table_info['b']) % self.p) % m
return table_info['table'][h2] == key
# 4. Cuckoo Hashing - Worst-Case O(1) Lookups
class CuckooHash:
'''Cuckoo hashing with two hash functions'''
def __init__(self, size=101):
import hashlib
self.size = size
self.table1 = [None] * size
self.table2 = [None] * size
self.max_loop = 500
def _hash1(self, key):
import hashlib
return int(hashlib.md5(str(key).encode()).hexdigest(), 16) % self.size
def _hash2(self, key):
import hashlib
return int(hashlib.sha1(str(key).encode()).hexdigest(), 16) % self.size
def insert(self, key):
'''Insert key with cuckoo eviction'''
for _ in range(self.max_loop):
h1 = self._hash1(key)
if self.table1[h1] is None:
self.table1[h1] = key
return True
# Evict from table1
key, self.table1[h1] = self.table1[h1], key
h2 = self._hash2(key)
if self.table2[h2] is None:
self.table2[h2] = key
return True
# Evict from table2
key, self.table2[h2] = self.table2[h2], key
# Rehash if too many evictions
return False
def search(self, key):
'''O(1) worst-case search'''
h1 = self._hash1(key)
h2 = self._hash2(key)
return self.table1[h1] == key or self.table2[h2] == keyPythonAnswer: Advanced hashing: Rolling hash enables O(n+m) pattern matching; consistent hashing balances distributed loads; perfect hashing achieves O(1) worst-case with two-level scheme; Cuckoo hashing uses eviction for O(1) operations.
Q171. Implement specialized sorting algorithms
# Advanced Sorting Algorithms and Order Statistics
# 1. Counting Sort - Linear Time for Small Range
def counting_sort(arr, max_val):
'''Sort in O(n + k) time where k is range'''
if not arr:
return arr
# Count occurrences
count = [0] * (max_val + 1)
for num in arr:
count[num] += 1
# Cumulative count
for i in range(1, len(count)):
count[i] += count[i - 1]
# Build output
output = [0] * len(arr)
for num in reversed(arr):
output[count[num] - 1] = num
count[num] -= 1
return output
# 2. Radix Sort - Multi-Digit Sorting
def radix_sort(arr):
'''Sort using radix sort - O(d * (n + k))'''
if not arr:
return arr
# Find maximum number to know number of digits
max_num = max(arr)
# Sort by each digit
exp = 1
while max_num // exp > 0:
counting_sort_digit(arr, exp)
exp *= 10
return arr
def counting_sort_digit(arr, exp):
'''Counting sort by digit'''
n = len(arr)
output = [0] * n
count = [0] * 10
# Count occurrences
for num in arr:
digit = (num // exp) % 10
count[digit] += 1
# Cumulative count
for i in range(1, 10):
count[i] += count[i - 1]
# Build output (stable)
for i in range(n - 1, -1, -1):
digit = (arr[i] // exp) % 10
output[count[digit] - 1] = arr[i]
count[digit] -= 1
# Copy to original array
for i in range(n):
arr[i] = output[i]
# 3. Bucket Sort - Uniform Distribution
def bucket_sort(arr):
'''Sort uniformly distributed data in O(n) average'''
if not arr:
return arr
n = len(arr)
# Create buckets
min_val, max_val = min(arr), max(arr)
bucket_range = (max_val - min_val) / n
buckets = [[] for _ in range(n)]
# Distribute elements
for num in arr:
if num == max_val:
idx = n - 1
else:
idx = int((num - min_val) / bucket_range)
buckets[idx].append(num)
# Sort each bucket and concatenate
result = []
for bucket in buckets:
result.extend(sorted(bucket))
return result
# 4. K-th Order Statistic - Median of Medians
def kth_order_statistic(arr, k):
'''Find k-th smallest in O(n) worst-case'''
def median_of_medians(arr, left, right):
'''Find good pivot'''
if right - left < 5:
return sorted(arr[left:right+1])[len(arr[left:right+1]) // 2]
# Divide into groups of 5
medians = []
for i in range(left, right + 1, 5):
group = arr[i:min(i + 5, right + 1)]
medians.append(sorted(group)[len(group) // 2])
# Recursively find median of medians
return kth_order_statistic(medians, len(medians) // 2)
def partition(arr, left, right, pivot):
'''Partition around pivot'''
# Move pivot to end
pivot_idx = arr.index(pivot, left, right + 1)
arr[pivot_idx], arr[right] = arr[right], arr[pivot_idx]
store_idx = left
for i in range(left, right):
if arr[i] < pivot:
arr[i], arr[store_idx] = arr[store_idx], arr[i]
store_idx += 1
arr[store_idx], arr[right] = arr[right], arr[store_idx]
return store_idx
def select(arr, left, right, k):
if left == right:
return arr[left]
# Find median of medians
pivot = median_of_medians(arr, left, right)
# Partition
pivot_idx = partition(arr, left, right, pivot)
# Recurse
if k == pivot_idx:
return arr[k]
elif k < pivot_idx:
return select(arr, left, pivot_idx - 1, k)
else:
return select(arr, pivot_idx + 1, right, k)
return select(arr[:], 0, len(arr) - 1, k)
# 5. External Sorting - K-way Merge
import heapq
def external_sort(files, output_file, chunk_size=1000):
'''Sort data larger than memory using external merge sort'''
# Phase 1: Create sorted runs
run_files = []
for file in files:
data = []
# Read chunks
with open(file, 'r') as f:
for line in f:
data.append(int(line.strip()))
if len(data) >= chunk_size:
# Sort and write run
data.sort()
run_file = f"run_{len(run_files)}.txt"
with open(run_file, 'w') as rf:
for num in data:
rf.write(f"{num}
")
run_files.append(run_file)
data = []
# Write remaining data
if data:
data.sort()
run_file = f"run_{len(run_files)}.txt"
with open(run_file, 'w') as rf:
for num in data:
rf.write(f"{num}
")
run_files.append(run_file)
# Phase 2: K-way merge
file_handles = [open(f, 'r') for f in run_files]
# Initialize heap with first element from each run
heap = []
for i, fh in enumerate(file_handles):
line = fh.readline()
if line:
heapq.heappush(heap, (int(line.strip()), i))
# Merge
with open(output_file, 'w') as out:
while heap:
val, file_idx = heapq.heappop(heap)
out.write(f"{val}
")
# Read next from same file
line = file_handles[file_idx].readline()
if line:
heapq.heappush(heap, (int(line.strip()), file_idx))
# Cleanup
for fh in file_handles:
fh.close()
# 6. Pancake Sorting - Reversals Only
def pancake_sort(arr):
'''Sort using only reversals (flip operation)'''
def flip(arr, k):
'''Reverse arr[0:k+1]'''
arr[:k+1] = arr[:k+1][::-1]
n = len(arr)
for curr_size in range(n, 1, -1):
# Find index of maximum in arr[0:curr_size]
max_idx = arr[:curr_size].index(max(arr[:curr_size]))
if max_idx != curr_size - 1:
# Flip max element to front
if max_idx != 0:
flip(arr, max_idx)
# Flip to correct position
flip(arr, curr_size - 1)
return arrPythonAnswer: Specialized sorting: Counting sort O(n+k) for small ranges; Radix sort O(d(n+k)) for multi-digit; Bucket sort O(n) for uniform distribution; Median-of-medians guarantees O(n) for k-th element; external sort handles disk-based data.
Q172. Understand cache-oblivious algorithms
# Cache-Oblivious Algorithms and Memory Hierarchy
# 1. Cache-Oblivious Matrix Multiplication
def cache_oblivious_matrix_mult(A, B, C, n, row_a=0, col_a=0, row_b=0, col_b=0, row_c=0, col_c=0):
'''Cache-oblivious matrix multiplication using divide and conquer'''
if n == 1:
C[row_c][col_c] += A[row_a][col_a] * B[row_b][col_b]
return
m = n // 2
# Divide matrices into quadrants and recurse
# C11 = A11*B11 + A12*B21
cache_oblivious_matrix_mult(A, B, C, m, row_a, col_a, row_b, col_b, row_c, col_c)
cache_oblivious_matrix_mult(A, B, C, m, row_a, col_a+m, row_b+m, col_b, row_c, col_c)
# C12 = A11*B12 + A12*B22
cache_oblivious_matrix_mult(A, B, C, m, row_a, col_a, row_b, col_b+m, row_c, col_c+m)
cache_oblivious_matrix_mult(A, B, C, m, row_a, col_a+m, row_b+m, col_b+m, row_c, col_c+m)
# C21 = A21*B11 + A22*B21
cache_oblivious_matrix_mult(A, B, C, m, row_a+m, col_a, row_b, col_b, row_c+m, col_c)
cache_oblivious_matrix_mult(A, B, C, m, row_a+m, col_a+m, row_b+m, col_b, row_c+m, col_c)
# C22 = A21*B12 + A22*B22
cache_oblivious_matrix_mult(A, B, C, m, row_a+m, col_a, row_b, col_b+m, row_c+m, col_c+m)
cache_oblivious_matrix_mult(A, B, C, m, row_a+m, col_a+m, row_b+m, col_b+m, row_c+m, col_c+m)
# 2. Van Emde Boas Layout - Cache-Friendly Tree
class VEBLayout:
'''Cache-oblivious tree layout'''
def __init__(self, tree, n):
self.n = n
self.layout = [None] * n
self.index = 0
self.build_layout(tree, 0, n)
def build_layout(self, tree, left, right):
'''Build Van Emde Boas layout recursively'''
if left >= right or not tree:
return
size = right - left
if size == 1:
self.layout[self.index] = tree
self.index += 1
return
# Find middle level
h = size.bit_length() - 1
mid_level = h // 2
mid_size = 1 << mid_level
# Process top recursively
self.build_layout(tree, left, left + mid_size)
# Process bottom subtrees
queue = [tree]
for _ in range(mid_level):
next_level = []
for node in queue:
if node and node.left:
next_level.append(node.left)
if node and node.right:
next_level.append(node.right)
queue = next_level
for subtree in queue:
if subtree:
self.build_layout(subtree, left + mid_size, right)
# 3. Cache-Oblivious Sorting - Funnelsort
class Funnelsort:
'''Cache-oblivious sorting algorithm'''
@staticmethod
def sort(arr):
'''Sort array using funnelsort'''
if len(arr) <= 1:
return arr
# Split into sqrt(n) groups
import math
k = int(math.sqrt(len(arr)))
if k == 0:
k = 1
# Recursively sort each group
groups = []
for i in range(0, len(arr), k):
group = Funnelsort.sort(arr[i:i+k])
groups.append(group)
# Merge using k-way merge with funnel
return Funnelsort._k_way_merge(groups)
@staticmethod
def _k_way_merge(groups):
'''Merge k sorted groups'''
import heapq
heap = []
# Initialize heap
for i, group in enumerate(groups):
if group:
heapq.heappush(heap, (group[0], i, 0))
result = []
while heap:
val, group_idx, elem_idx = heapq.heappop(heap)
result.append(val)
# Add next element from same group
if elem_idx + 1 < len(groups[group_idx]):
heapq.heappush(heap, (
groups[group_idx][elem_idx + 1],
group_idx,
elem_idx + 1
))
return result
# 4. Cache-Oblivious Priority Queue
class CacheObliviousPriorityQueue:
'''Priority queue with cache-oblivious structure'''
def __init__(self):
self.buffer_size = 16 # Tunable parameter
self.buffers = [[]]
def insert(self, item):
'''Insert item'''
self.buffers[0].append(item)
# Cascade when buffer full
if len(self.buffers[0]) >= self.buffer_size:
self._cascade(0)
def _cascade(self, level):
'''Cascade full buffer to next level'''
if level >= len(self.buffers) - 1:
self.buffers.append([])
# Merge current with next level
merged = sorted(self.buffers[level] + self.buffers[level + 1])
self.buffers[level] = []
self.buffers[level + 1] = merged
# Continue cascade if needed
if len(self.buffers[level + 1]) >= self.buffer_size * (2 ** (level + 1)):
self._cascade(level + 1)
def extract_min(self):
'''Extract minimum element'''
# Find minimum across all buffers
min_val = None
min_level = -1
for i, buffer in enumerate(self.buffers):
if buffer:
if min_val is None or buffer[0] < min_val:
min_val = buffer[0]
min_level = i
if min_level >= 0:
return self.buffers[min_level].pop(0)
return None
# 5. Memory Hierarchy Model - Analysis
def analyze_cache_complexity(algorithm_name):
'''Analyze cache complexity of algorithms'''
complexities = {
'naive_matrix_mult': 'O(n^3 / B + n^2) I/Os',
'cache_oblivious_matrix_mult': 'O(n^3 / (B * sqrt(M)) + n^2 / B) I/Os',
'binary_search': 'O(log_B n) I/Os',
'scanning': 'O(n / B) I/Os',
'sorting': 'O((n / B) * log_(M/B) (n / B)) I/Os',
'funnelsort': 'Optimal O((n / B) * log_(M/B) (n / B)) I/Os',
}
return complexities.get(algorithm_name, 'Unknown')PythonAnswer: Cache-oblivious algorithms: work efficiently without knowing cache parameters; matrix multiplication via recursive blocking; Van Emde Boas layout for trees; Funnelsort achieves optimal sorting I/O complexity; analyze in terms of B (block size) and M (memory).
Q173. Apply amortized analysis techniques
# Amortized Analysis - Aggregate, Accounting, Potential Methods
# 1. Dynamic Array - Doubling Strategy
class DynamicArray:
'''Dynamic array with amortized O(1) append'''
def __init__(self):
self.capacity = 1
self.size = 0
self.array = [None] * self.capacity
self.total_cost = 0 # For analysis
def append(self, item):
'''Append with amortized O(1) cost'''
if self.size == self.capacity:
# Resize: O(n) actual cost
self._resize()
self.total_cost += self.size
self.array[self.size] = item
self.size += 1
self.total_cost += 1 # Insert cost
def _resize(self):
'''Double capacity'''
self.capacity *= 2
new_array = [None] * self.capacity
for i in range(self.size):
new_array[i] = self.array[i]
self.array = new_array
def amortized_cost_per_operation(self):
'''Calculate amortized cost'''
if self.size == 0:
return 0
return self.total_cost / self.size
# Amortized Analysis:
# - n operations: 1 + 2 + 4 + 8 + ... + n (resizes) + n (inserts)
# - Total: 3n - 1 = O(n)
# - Amortized: O(n) / n = O(1)
# 2. Fibonacci Heap - Amortized Efficient Operations
class FibonacciHeapNode:
def __init__(self, key):
self.key = key
self.degree = 0
self.mark = False
self.parent = None
self.child = None
self.left = self
self.right = self
class FibonacciHeap:
'''Fibonacci heap with amortized O(1) insert and decrease-key'''
def __init__(self):
self.min_node = None
self.total_nodes = 0
self.potential = 0 # For potential method analysis
def insert(self, key):
'''Insert with amortized O(1)'''
node = FibonacciHeapNode(key)
if self.min_node is None:
self.min_node = node
else:
# Add to root list
self._add_to_root_list(node)
if key < self.min_node.key:
self.min_node = node
self.total_nodes += 1
self.potential += 1 # One more tree
return node
def _add_to_root_list(self, node):
'''Add node to root list'''
node.left = self.min_node
node.right = self.min_node.right
self.min_node.right = node
node.right.left = node
def extract_min(self):
'''Extract minimum with amortized O(log n)'''
z = self.min_node
if z is None:
return None
# Add children to root list
if z.child:
child = z.child
while True:
next_child = child.right
self._add_to_root_list(child)
child.parent = None
child = next_child
if child == z.child:
break
# Remove z from root list
z.left.right = z.right
z.right.left = z.left
if z == z.right:
self.min_node = None
else:
self.min_node = z.right
self._consolidate()
self.total_nodes -= 1
return z.key
def _consolidate(self):
'''Consolidate trees'''
import math
max_degree = int(math.log2(self.total_nodes)) + 1
degree_table = [None] * max_degree
# Process each root
roots = []
current = self.min_node
if current:
while True:
roots.append(current)
current = current.right
if current == self.min_node:
break
for root in roots:
degree = root.degree
while degree_table[degree] is not None:
other = degree_table[degree]
if root.key > other.key:
root, other = other, root
# Link other under root
self._link(other, root)
degree_table[degree] = None
degree += 1
degree_table[degree] = root
# Rebuild root list and find new min
self.min_node = None
for node in degree_table:
if node:
if self.min_node is None:
self.min_node = node
node.left = node.right = node
else:
self._add_to_root_list(node)
if node.key < self.min_node.key:
self.min_node = node
def _link(self, y, x):
'''Make y child of x'''
# Remove y from root list
y.left.right = y.right
y.right.left = y.left
# Make y child of x
y.parent = x
if x.child is None:
x.child = y
y.left = y.right = y
else:
y.left = x.child
y.right = x.child.right
x.child.right = y
y.right.left = y
x.degree += 1
y.mark = False
# 3. Splay Tree - Amortized O(log n)
# (See Q169 for implementation)
# Amortized Analysis using Potential Method:
def potential_method_example():
'''Demonstrate potential method for dynamic array'''
class AnalyzedArray:
def __init__(self):
self.arr = []
self.capacity = 1
def potential(self):
'''Potential function: 2 * size - capacity'''
return 2 * len(self.arr) - self.capacity
def append(self, item):
actual_cost = 1
if len(self.arr) == self.capacity:
# Resize needed
actual_cost += len(self.arr) # Copy cost
self.capacity *= 2
old_potential = self.potential()
self.arr.append(item)
new_potential = self.potential()
amortized_cost = actual_cost + (new_potential - old_potential)
return {
'actual_cost': actual_cost,
'old_potential': old_potential,
'new_potential': new_potential,
'amortized_cost': amortized_cost
}
# Demo
arr = AnalyzedArray()
for i in range(5):
print(f"Operation {i}: {arr.append(i)}")
# 4. Union-Find with Path Compression - Inverse Ackermann
class UnionFindAmortized:
'''Union-Find with amortized nearly O(1) operations'''
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
'''Find with path compression - amortized O(α(n))'''
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
'''Union by rank - amortized O(α(n))'''
px, py = self.find(x), self.find(y)
if px == py:
return False
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
return True
# α(n) is inverse Ackermann function - grows extremely slowly
# For all practical n, α(n) ≤ 4PythonAnswer: Amortized analysis: Dynamic array doubling achieves O(1) amortized via aggregate/potential method; Fibonacci heap: O(1) insert, O(log n) extract-min amortized; Splay tree: O(log n) amortized; Union-Find: O(α(n)) amortized with path compression.
Q174. Master advanced randomized algorithms
# Advanced Randomized Algorithms
# 1. Randomized Quicksort - Expected O(n log n)
import random
def randomized_quicksort(arr):
'''Quicksort with random pivot - expected O(n log n)'''
def partition(arr, low, high):
# Random pivot
pivot_idx = random.randint(low, high)
arr[pivot_idx], arr[high] = arr[high], arr[pivot_idx]
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
def quicksort_helper(arr, low, high):
if low < high:
pi = partition(arr, low, high)
quicksort_helper(arr, low, pi - 1)
quicksort_helper(arr, pi + 1, high)
quicksort_helper(arr, 0, len(arr) - 1)
return arr
# 2. Randomized Selection - Expected O(n)
def randomized_select(arr, k):
'''Find k-th smallest in expected O(n)'''
def partition(arr, low, high):
pivot_idx = random.randint(low, high)
arr[pivot_idx], arr[high] = arr[high], arr[pivot_idx]
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
def select(arr, low, high, k):
if low == high:
return arr[low]
pivot_idx = partition(arr, low, high)
if k == pivot_idx:
return arr[k]
elif k < pivot_idx:
return select(arr, low, pivot_idx - 1, k)
else:
return select(arr, pivot_idx + 1, high, k)
return select(arr[:], 0, len(arr) - 1, k)
# 3. Miller-Rabin Primality Test
def miller_rabin(n, k=5):
'''Probabilistic primality test - error probability ≤ 4^(-k)'''
if n < 2:
return False
if n == 2 or n == 3:
return True
if n % 2 == 0:
return False
# Write n-1 as 2^r * d
r, d = 0, n - 1
while d % 2 == 0:
r += 1
d //= 2
# Witness loop
for _ in range(k):
a = random.randint(2, n - 2)
x = pow(a, d, n)
if x == 1 or x == n - 1:
continue
for _ in range(r - 1):
x = pow(x, 2, n)
if x == n - 1:
break
else:
return False
return True
# 4. Karger's Min-Cut Algorithm
def karger_min_cut(graph):
'''Find minimum cut with probability ≥ 1/n^2'''
import random
# graph = {node: {neighbor: weight}}
def contract_edge(graph):
'''Contract random edge'''
# Choose random edge
edges = []
for u in graph:
for v in graph[u]:
if u < v: # Avoid duplicates
edges.append((u, v))
if not edges:
return None
u, v = random.choice(edges)
# Merge v into u
for neighbor in graph[v]:
if neighbor == u:
continue
if neighbor in graph[u]:
graph[u][neighbor] += graph[v][neighbor]
else:
graph[u][neighbor] = graph[v][neighbor]
# Update neighbor's edge
graph[neighbor][u] = graph[neighbor].get(u, 0) + graph[neighbor][v]
del graph[neighbor][v]
# Remove v
del graph[v]
del graph[u][v]
return graph
# Run contraction until 2 nodes remain
while len(graph) > 2:
graph = contract_edge(graph)
if graph is None:
return float('inf')
# Count edges between remaining nodes
if len(graph) < 2:
return 0
nodes = list(graph.keys())
return sum(graph[nodes[0]].values())
# 5. Randomized Load Balancing - Power of Two Choices
class LoadBalancer:
'''Load balancer using power of two choices'''
def __init__(self, num_servers):
self.servers = [0] * num_servers
self.num_servers = num_servers
def assign_task(self, task_weight=1):
'''Assign task to less loaded of two random servers'''
# Choose two random servers
s1 = random.randint(0, self.num_servers - 1)
s2 = random.randint(0, self.num_servers - 1)
# Assign to less loaded
if self.servers[s1] <= self.servers[s2]:
self.servers[s1] += task_weight
return s1
else:
self.servers[s2] += task_weight
return s2
def max_load(self):
'''Maximum load on any server'''
return max(self.servers)
def average_load(self):
'''Average load'''
return sum(self.servers) / self.num_servers
# Expected max load: O(log log n) vs O(log n) for random choice
# 6. Probabilistic Data Structures - Skip List
# (See Q162 for implementation)
# 7. Monte Carlo vs Las Vegas Algorithms
def monte_carlo_vs_las_vegas():
'''
Monte Carlo: Always fast, might be wrong
- Example: Miller-Rabin primality test
- Guaranteed running time, bounded error probability
Las Vegas: Always correct, might be slow
- Example: Randomized Quicksort
- Expected running time, always correct answer
Zero-sided error: Always correct when it gives answer
- Example: Randomized Min-Cut (always finds some cut)
One-sided error: Wrong in one direction only
- Example: Primality test (never says prime is composite)
Two-sided error: Can be wrong either way
- Example: Some polynomial identity tests
'''
return {
'monte_carlo': 'Fast, bounded error',
'las_vegas': 'Correct, expected time',
'tradeoff': 'Can convert between them sometimes'
}PythonAnswer: Randomized algorithms: QuickSort/QuickSelect with random pivot avoids worst-case; Miller-Rabin tests primality probabilistically; Karger’s min-cut finds global minimum; power of two choices improves load balancing exponentially; Monte Carlo vs Las Vegas tradeoffs.
Q175. Synthesize algorithm design paradigms
# Algorithm Design Paradigms - Comprehensive Overview
# Summary of Major Algorithmic Techniques
def algorithm_design_paradigms():
'''Overview of algorithm design strategies'''
paradigms = {
'1. Divide and Conquer': {
'description': 'Split problem, solve subproblems, combine solutions',
'examples': [
'Merge Sort - O(n log n)',
'Quick Sort - O(n log n) expected',
'Binary Search - O(log n)',
'Karatsuba Multiplication - O(n^1.58)',
'Closest Pair - O(n log n)',
'Median of Medians - O(n)',
],
'when_to_use': 'Problem has optimal substructure and overlapping sub-problems can be avoided',
'master_theorem': 'T(n) = aT(n/b) + f(n)'
},
'2. Dynamic Programming': {
'description': 'Solve overlapping subproblems once, store results',
'examples': [
'Fibonacci - O(n)',
'Longest Common Subsequence - O(mn)',
'Knapsack - O(nW)',
'Matrix Chain Multiplication - O(n^3)',
'Edit Distance - O(mn)',
'Bellman-Ford - O(VE)',
],
'when_to_use': 'Optimal substructure + overlapping subproblems',
'optimization_techniques': [
'Memoization (top-down)',
'Tabulation (bottom-up)',
'Space optimization (rolling array)',
'Convex hull trick',
'Divide and conquer DP',
]
},
'3. Greedy Algorithms': {
'description': 'Make locally optimal choice at each step',
'examples': [
'Dijkstra - O(E log V)',
'Prim's MST - O(E log V)',
'Kruskal's MST - O(E log E)',
'Huffman Coding - O(n log n)',
'Activity Selection - O(n log n)',
'Fractional Knapsack - O(n log n)',
],
'when_to_use': 'Greedy choice property + optimal substructure',
'proof_techniques': [
'Exchange argument',
'Staying ahead',
'Structural induction',
]
},
'4. Backtracking': {
'description': 'Build solution incrementally, abandon if invalid',
'examples': [
'N-Queens - O(N!)',
'Sudoku Solver - exponential',
'Subset Sum - O(2^n)',
'Graph Coloring - exponential',
'Hamiltonian Path - O(n!)',
],
'when_to_use': 'Need to explore all possibilities with pruning',
'optimization_techniques': [
'Constraint propagation',
'Variable ordering',
'Value ordering',
'Symmetry breaking',
]
},
'5. Graph Algorithms': {
'description': 'Traverse, search, or optimize on graphs',
'categories': {
'Traversal': ['BFS - O(V+E)', 'DFS - O(V+E)'],
'Shortest Paths': [
'Dijkstra - O(E log V)',
'Bellman-Ford - O(VE)',
'Floyd-Warshall - O(V^3)',
'A* - heuristic',
],
'MST': ['Prim - O(E log V)', 'Kruskal - O(E log E)'],
'Flow': [
'Ford-Fulkerson - O(E * max_flow)',
'Edmonds-Karp - O(VE^2)',
'Dinic - O(V^2 * E)',
],
'Special': [
'Topological Sort - O(V+E)',
'SCC (Tarjan) - O(V+E)',
'Bipartite Matching - O(VE)',
]
}
},
'6. String Algorithms': {
'description': 'Pattern matching and string processing',
'examples': [
'KMP - O(n+m)',
'Rabin-Karp - O(n+m) expected',
'Boyer-Moore - O(n/m) best case',
'Aho-Corasick - O(n+m+z)',
'Suffix Array - O(n log n)',
'Manacher - O(n)',
]
},
'7. Computational Geometry': {
'description': 'Solve geometric problems',
'examples': [
'Convex Hull (Graham Scan) - O(n log n)',
'Closest Pair - O(n log n)',
'Line Intersection - O(n log n)',
'Point in Polygon - O(n)',
]
},
'8. Randomized Algorithms': {
'description': 'Use randomness for efficiency or simplicity',
'types': {
'Las Vegas': 'Always correct, expected time',
'Monte Carlo': 'Always fast, bounded error',
},
'examples': [
'Randomized QuickSort - O(n log n) expected',
'Miller-Rabin - polynomial time, small error',
'Karger Min-Cut - O(n^2) expected',
]
},
'9. Approximation Algorithms': {
'description': 'Find near-optimal solutions for NP-hard problems',
'examples': [
'Vertex Cover - 2-approximation',
'Set Cover - O(log n)-approximation',
'TSP - 2-approximation (metric)',
'Bin Packing (FFD) - 11/9-approximation',
]
},
'10. Online Algorithms': {
'description': 'Process data without knowing future',
'examples': [
'Ski Rental - 2-competitive',
'Paging (LRU) - k-competitive',
'Load Balancing - O(log n)-competitive',
],
'analysis': 'Competitive ratio vs offline optimal'
},
}
return paradigms
# Problem-Solving Framework
def problem_solving_steps():
'''Systematic approach to algorithm problems'''
steps = [
'1. Understand the problem',
' - Read carefully, identify inputs/outputs',
' - Clarify constraints and edge cases',
' - Work through examples',
'2. Design the algorithm',
' - Identify applicable paradigm',
' - Consider brute force first',
' - Optimize using patterns',
'3. Analyze complexity',
' - Time complexity: Big O, Omega, Theta',
' - Space complexity: auxiliary space',
' - Trade-offs between time and space',
'4. Implement carefully',
' - Write clean, readable code',
' - Handle edge cases',
' - Use meaningful variable names',
'5. Test thoroughly',
' - Small inputs',
' - Edge cases (empty, single element)',
' - Large inputs',
' - Special cases',
'6. Optimize if needed',
' - Profile bottlenecks',
' - Consider different data structures',
' - Apply algorithmic optimizations',
]
return steps
# Complexity Classes
def complexity_classes():
'''Common complexity classes'''
classes = {
'O(1)': 'Constant - hash table lookup, array access',
'O(log n)': 'Logarithmic - binary search, balanced tree ops',
'O(n)': 'Linear - array scan, linear search',
'O(n log n)': 'Linearithmic - efficient sorting, divide & conquer',
'O(n^2)': 'Quadratic - nested loops, bubble sort',
'O(n^3)': 'Cubic - naive matrix multiplication',
'O(2^n)': 'Exponential - recursive Fibonacci, subset enumeration',
'O(n!)': 'Factorial - permutation generation, TSP brute force',
}
return classesPythonAnswer: Algorithm design paradigms: Divide & conquer breaks problems recursively; DP optimizes overlapping subproblems; Greedy makes local optimal choices; Backtracking explores with pruning; Graph algorithms for network problems; String algorithms for pattern matching; choose based on problem structure.
Q176. Implement advanced shortest path variants
# Advanced Shortest Path Algorithms and Variants
# 1. A* Search Algorithm with Heuristic
import heapq
from collections import defaultdict
def a_star_search(graph, start, goal, heuristic):
'''A* pathfinding with admissible heuristic - optimal if h(n) ≤ actual cost'''
# Priority queue: (f_score, node, path)
open_set = [(heuristic(start, goal), start, [start])]
g_score = defaultdict(lambda: float('inf'))
g_score[start] = 0
visited = set()
while open_set:
f, current, path = heapq.heappop(open_set)
if current in visited:
continue
visited.add(current)
if current == goal:
return path, g_score[current]
for neighbor, weight in graph.get(current, []):
if neighbor in visited:
continue
tentative_g = g_score[current] + weight
if tentative_g < g_score[neighbor]:
g_score[neighbor] = tentative_g
f_score = tentative_g + heuristic(neighbor, goal)
heapq.heappush(open_set, (f_score, neighbor, path + [neighbor]))
return None, float('inf')
# Example heuristic for grid: Manhattan distance
def manhattan_distance(pos1, pos2):
return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1])
# 2. Bidirectional Dijkstra
def bidirectional_dijkstra(graph, start, goal):
'''Dijkstra from both ends - faster for single pair'''
def dijkstra_step(queue, distances, visited, other_distances):
if not queue:
return None
dist, node = heapq.heappop(queue)
if node in visited:
return None
visited.add(node)
# Check if we meet the other search
if node in other_distances:
return dist + other_distances[node]
for neighbor, weight in graph.get(node, []):
if neighbor not in visited:
new_dist = dist + weight
if new_dist < distances[neighbor]:
distances[neighbor] = new_dist
heapq.heappush(queue, (new_dist, neighbor))
return None
# Forward search
forward_queue = [(0, start)]
forward_dist = defaultdict(lambda: float('inf'))
forward_dist[start] = 0
forward_visited = set()
# Backward search
backward_queue = [(0, goal)]
backward_dist = defaultdict(lambda: float('inf'))
backward_dist[goal] = 0
backward_visited = set()
best_distance = float('inf')
while forward_queue or backward_queue:
# Alternate between searches
result = dijkstra_step(forward_queue, forward_dist, forward_visited, backward_dist)
if result is not None:
best_distance = min(best_distance, result)
result = dijkstra_step(backward_queue, backward_dist, backward_visited, forward_dist)
if result is not None:
best_distance = min(best_distance, result)
# Check termination
if forward_queue and backward_queue:
if forward_queue[0][0] + backward_queue[0][0] >= best_distance:
break
return best_distance if best_distance != float('inf') else -1
# 3. All-Pairs Shortest Paths - Johnson's Algorithm
def johnson_all_pairs(graph, n):
'''All-pairs shortest paths in O(V^2 log V + VE) - faster than Floyd-Warshall for sparse graphs'''
# Add new vertex s connected to all with weight 0
extended_graph = defaultdict(list)
for u in range(n):
for v, w in graph.get(u, []):
extended_graph[u].append((v, w))
# Add source vertex n
for u in range(n):
extended_graph[n].append((u, 0))
# Run Bellman-Ford from new vertex to get h values
def bellman_ford(source):
dist = [float('inf')] * (n + 1)
dist[source] = 0
for _ in range(n):
for u in extended_graph:
for v, w in extended_graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
# Check negative cycles
for u in extended_graph:
for v, w in extended_graph[u]:
if dist[u] + w < dist[v]:
return None # Negative cycle
return dist
h = bellman_ford(n)
if h is None:
return None
# Reweight edges: w'(u,v) = w(u,v) + h(u) - h(v)
reweighted = defaultdict(list)
for u in range(n):
for v, w in graph.get(u, []):
new_weight = w + h[u] - h[v]
reweighted[u].append((v, new_weight))
# Run Dijkstra from each vertex
all_pairs = {}
for u in range(n):
dist = [float('inf')] * n
dist[u] = 0
pq = [(0, u)]
while pq:
d, node = heapq.heappop(pq)
if d > dist[node]:
continue
for v, w in reweighted.get(node, []):
if dist[node] + w < dist[v]:
dist[v] = dist[node] + w
heapq.heappush(pq, (dist[v], v))
# Restore original distances
all_pairs[u] = {}
for v in range(n):
if dist[v] != float('inf'):
all_pairs[u][v] = dist[v] - h[u] + h[v]
return all_pairs
# 4. K Shortest Paths - Yen's Algorithm
def k_shortest_paths(graph, start, goal, k):
'''Find k shortest paths from start to goal'''
def dijkstra_with_forbidden(graph, start, goal, forbidden_edges):
'''Modified Dijkstra avoiding forbidden edges'''
dist = {start: 0}
prev = {}
pq = [(0, start)]
while pq:
d, u = heapq.heappop(pq)
if u == goal:
# Reconstruct path
path = []
node = goal
while node in prev:
path.append(node)
node = prev[node]
path.append(start)
return list(reversed(path)), d
if d > dist.get(u, float('inf')):
continue
for v, w in graph.get(u, []):
if (u, v) in forbidden_edges:
continue
new_dist = d + w
if new_dist < dist.get(v, float('inf')):
dist[v] = new_dist
prev[v] = u
heapq.heappush(pq, (new_dist, v))
return None, float('inf')
# Find first shortest path
first_path, first_cost = dijkstra_with_forbidden(graph, start, goal, set())
if first_path is None:
return []
paths = [(first_cost, first_path)]
candidates = []
for k_i in range(1, k):
prev_path = paths[-1][1]
# For each node in previous path
for i in range(len(prev_path) - 1):
spur_node = prev_path[i]
root_path = prev_path[:i+1]
# Remove edges that would create duplicate paths
forbidden = set()
for path_cost, path in paths:
if len(path) > i and path[:i+1] == root_path:
if i + 1 < len(path):
forbidden.add((path[i], path[i+1]))
# Find spur path
spur_path, spur_cost = dijkstra_with_forbidden(
graph, spur_node, goal, forbidden
)
if spur_path:
total_path = root_path[:-1] + spur_path
total_cost = sum(
dict(graph[total_path[j]])[total_path[j+1]]
for j in range(len(total_path)-1)
)
if (total_cost, total_path) not in candidates:
candidates.append((total_cost, total_path))
if not candidates:
break
# Add best candidate
candidates.sort()
paths.append(candidates.pop(0))
return paths
# 5. Constrained Shortest Path
def shortest_path_with_constraint(graph, start, goal, max_edges):
'''Shortest path using at most max_edges edges'''
# dp[node][edges_used] = min cost
dp = defaultdict(lambda: defaultdict(lambda: float('inf')))
dp[start][0] = 0
for k in range(max_edges):
for node in graph:
if dp[node][k] == float('inf'):
continue
for neighbor, weight in graph[node]:
dp[neighbor][k+1] = min(
dp[neighbor][k+1],
dp[node][k] + weight
)
# Find minimum across all edge counts
return min(dp[goal][k] for k in range(max_edges + 1))PythonAnswer: Advanced shortest paths: A* uses admissible heuristic for optimal pathfinding; bidirectional search halves exploration; Johnson’s algorithm combines Bellman-Ford reweighting with Dijkstra for all-pairs; Yen’s finds k-shortest paths; constrained paths with DP.
Q177. Master advanced tree query structures
# Advanced Tree Algorithms - Lowest Common Ancestor and Range Queries
# 1. Binary Lifting for LCA - O(log n) queries after O(n log n) preprocessing
class BinaryLifting:
'''Efficient LCA queries using binary lifting'''
def __init__(self, tree, root=0):
self.n = len(tree)
self.tree = tree
self.root = root
# Compute depth and parent
self.depth = [0] * self.n
self.log = self.n.bit_length()
# up[node][i] = 2^i-th ancestor of node
self.up = [[-1] * self.log for _ in range(self.n)]
self._dfs(root, -1, 0)
def _dfs(self, node, parent, d):
'''Preprocess tree'''
self.depth[node] = d
self.up[node][0] = parent
# Binary lifting: up[node][i] = up[up[node][i-1]][i-1]
for i in range(1, self.log):
if self.up[node][i-1] != -1:
self.up[node][i] = self.up[self.up[node][i-1]][i-1]
for child in self.tree.get(node, []):
if child != parent:
self._dfs(child, node, d + 1)
def lca(self, u, v):
'''Find LCA of u and v in O(log n)'''
# Make u deeper
if self.depth[u] < self.depth[v]:
u, v = v, u
# Bring u to same level as v
diff = self.depth[u] - self.depth[v]
for i in range(self.log):
if (diff >> i) & 1:
u = self.up[u][i]
if u == v:
return u
# Binary search for LCA
for i in range(self.log - 1, -1, -1):
if self.up[u][i] != self.up[v][i]:
u = self.up[u][i]
v = self.up[v][i]
return self.up[u][0]
def distance(self, u, v):
'''Distance between u and v'''
lca = self.lca(u, v)
return self.depth[u] + self.depth[v] - 2 * self.depth[lca]
def kth_ancestor(self, node, k):
'''Find k-th ancestor of node'''
for i in range(self.log):
if (k >> i) & 1:
node = self.up[node][i]
if node == -1:
return -1
return node
# 2. Heavy-Light Decomposition
class HeavyLightDecomposition:
'''Decompose tree into heavy paths for efficient queries'''
def __init__(self, tree, root=0):
self.tree = tree
self.n = len(tree)
self.root = root
# Arrays for HLD
self.parent = [0] * self.n
self.depth = [0] * self.n
self.heavy = [-1] * self.n
self.head = [0] * self.n
self.pos = [0] * self.n
self.current_pos = 0
# Build HLD
self._dfs(root)
self._decompose(root, root)
def _dfs(self, node, p=-1):
'''Compute subtree sizes and mark heavy children'''
size = 1
max_subtree = 0
for child in self.tree.get(node, []):
if child == p:
continue
self.parent[child] = node
self.depth[child] = self.depth[node] + 1
child_size = self._dfs(child, node)
size += child_size
if child_size > max_subtree:
max_subtree = child_size
self.heavy[node] = child
return size
def _decompose(self, node, h, p=-1):
'''Decompose into heavy paths'''
self.head[node] = h
self.pos[node] = self.current_pos
self.current_pos += 1
# Process heavy child first
if self.heavy[node] != -1:
self._decompose(self.heavy[node], h, node)
# Process light children
for child in self.tree.get(node, []):
if child != p and child != self.heavy[node]:
self._decompose(child, child, node)
def lca(self, u, v):
'''Find LCA using HLD in O(log n)'''
while self.head[u] != self.head[v]:
if self.depth[self.head[u]] > self.depth[self.head[v]]:
u = self.parent[self.head[u]]
else:
v = self.parent[self.head[v]]
return u if self.depth[u] < self.depth[v] else v
def path_query(self, u, v, segment_tree):
'''Query path from u to v using segment tree'''
result = 0
while self.head[u] != self.head[v]:
if self.depth[self.head[u]] > self.depth[self.head[v]]:
# Query from u to head of u's chain
result += segment_tree.query(self.pos[self.head[u]], self.pos[u])
u = self.parent[self.head[u]]
else:
result += segment_tree.query(self.pos[self.head[v]], self.pos[v])
v = self.parent[self.head[v]]
# Both in same chain
if self.depth[u] > self.depth[v]:
result += segment_tree.query(self.pos[v], self.pos[u])
else:
result += segment_tree.query(self.pos[u], self.pos[v])
return result
# 3. Link-Cut Trees (Simplified)
class LinkCutTree:
'''Dynamic tree data structure for link/cut operations'''
class Node:
def __init__(self, val):
self.val = val
self.parent = None
self.left = None
self.right = None
self.path_parent = None # For connecting preferred paths
def __init__(self, n):
self.nodes = [self.Node(i) for i in range(n)]
def _is_root(self, node):
'''Check if node is root of its splay tree'''
return (node.parent is None or
(node.parent.left != node and node.parent.right != node))
def _rotate(self, node):
'''Rotate node up (splay operation)'''
parent = node.parent
grandparent = parent.parent if parent else None
if parent.left == node:
parent.left = node.right
if node.right:
node.right.parent = parent
node.right = parent
else:
parent.right = node.left
if node.left:
node.left.parent = parent
node.left = parent
node.parent = grandparent
parent.parent = node
if grandparent:
if grandparent.left == parent:
grandparent.left = node
else:
grandparent.right = node
def _splay(self, node):
'''Splay node to root of its tree'''
while not self._is_root(node):
parent = node.parent
if self._is_root(parent):
self._rotate(node)
else:
grandparent = parent.parent
if (grandparent.left == parent) == (parent.left == node):
# Zig-zig
self._rotate(parent)
self._rotate(node)
else:
# Zig-zag
self._rotate(node)
self._rotate(node)
def access(self, node):
'''Make path from node to root preferred path'''
self._splay(node)
node.right = None
while node.path_parent:
parent = node.path_parent
self._splay(parent)
parent.right = node
self._splay(node)
def link(self, u, v):
'''Add edge between u and v'''
self.access(self.nodes[u])
self.nodes[u].path_parent = self.nodes[v]
def cut(self, u):
'''Remove edge from u to its parent'''
self.access(self.nodes[u])
self.nodes[u].left.parent = None
self.nodes[u].left = NonePythonAnswer: Advanced tree queries: Binary lifting enables O(log n) LCA after O(n log n) preprocessing; Heavy-Light Decomposition splits tree into O(log n) heavy paths for range queries; Link-Cut trees support dynamic connectivity with amortized O(log n) operations.
Q178. Design advanced data structures
# Advanced Data Structure Design Patterns
# 1. Lazy Propagation Segment Tree
class LazySegmentTree:
'''Segment tree with lazy propagation for range updates'''
def __init__(self, arr):
self.n = len(arr)
self.tree = [0] * (4 * self.n)
self.lazy = [0] * (4 * self.n)
self._build(arr, 0, 0, self.n - 1)
def _build(self, arr, node, start, end):
if start == end:
self.tree[node] = arr[start]
return
mid = (start + end) // 2
self._build(arr, 2*node + 1, start, mid)
self._build(arr, 2*node + 2, mid + 1, end)
self.tree[node] = self.tree[2*node + 1] + self.tree[2*node + 2]
def _push(self, node, start, end):
'''Push lazy value down'''
if self.lazy[node] != 0:
self.tree[node] += self.lazy[node] * (end - start + 1)
if start != end:
self.lazy[2*node + 1] += self.lazy[node]
self.lazy[2*node + 2] += self.lazy[node]
self.lazy[node] = 0
def update_range(self, left, right, val):
'''Add val to all elements in range [left, right]'''
self._update_range(0, 0, self.n - 1, left, right, val)
def _update_range(self, node, start, end, left, right, val):
self._push(node, start, end)
if start > right or end < left:
return
if start >= left and end <= right:
self.lazy[node] += val
self._push(node, start, end)
return
mid = (start + end) // 2
self._update_range(2*node + 1, start, mid, left, right, val)
self._update_range(2*node + 2, mid + 1, end, left, right, val)
self._push(2*node + 1, start, mid)
self._push(2*node + 2, mid + 1, end)
self.tree[node] = self.tree[2*node + 1] + self.tree[2*node + 2]
def query_range(self, left, right):
'''Get sum of range [left, right]'''
return self._query_range(0, 0, self.n - 1, left, right)
def _query_range(self, node, start, end, left, right):
if start > right or end < left:
return 0
self._push(node, start, end)
if start >= left and end <= right:
return self.tree[node]
mid = (start + end) // 2
return (self._query_range(2*node + 1, start, mid, left, right) +
self._query_range(2*node + 2, mid + 1, end, left, right))
# 2. Persistent Segment Tree (Path Copying)
class PersistentSegmentTree:
'''Segment tree that preserves all versions'''
class Node:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def __init__(self, arr):
self.n = len(arr)
self.versions = [self._build(arr, 0, self.n - 1)]
def _build(self, arr, start, end):
if start == end:
return self.Node(arr[start])
mid = (start + end) // 2
left = self._build(arr, start, mid)
right = self._build(arr, mid + 1, end)
return self.Node(left.val + right.val, left, right)
def update(self, version, index, value):
'''Create new version with updated value'''
new_root = self._update(self.versions[version], 0, self.n - 1, index, value)
self.versions.append(new_root)
return len(self.versions) - 1
def _update(self, node, start, end, index, value):
if start == end:
return self.Node(value)
mid = (start + end) // 2
if index <= mid:
new_left = self._update(node.left, start, mid, index, value)
new_node = self.Node(new_left.val + node.right.val, new_left, node.right)
else:
new_right = self._update(node.right, mid + 1, end, index, value)
new_node = self.Node(node.left.val + new_right.val, node.left, new_right)
return new_node
def query(self, version, left, right):
'''Query range in specific version'''
return self._query(self.versions[version], 0, self.n - 1, left, right)
def _query(self, node, start, end, left, right):
if start > right or end < left:
return 0
if start >= left and end <= right:
return node.val
mid = (start + end) // 2
return (self._query(node.left, start, mid, left, right) +
self._query(node.right, mid + 1, end, left, right))
# 3. Wavelet Tree - Range Queries on Arrays
class WaveletTree:
'''Support range queries like kth smallest, count < x in range'''
def __init__(self, arr, min_val=None, max_val=None):
self.arr = arr
self.min_val = min_val if min_val is not None else min(arr)
self.max_val = max_val if max_val is not None else max(arr)
# Build tree
self.left = None
self.right = None
self.bitmap = []
if self.min_val < self.max_val:
mid = (self.min_val + self.max_val) // 2
left_arr = []
right_arr = []
for val in arr:
if val <= mid:
self.bitmap.append(0)
left_arr.append(val)
else:
self.bitmap.append(1)
right_arr.append(val)
# Prefix sums for rank queries
self.prefix = [0]
for bit in self.bitmap:
self.prefix.append(self.prefix[-1] + bit)
if left_arr:
self.left = WaveletTree(left_arr, self.min_val, mid)
if right_arr:
self.right = WaveletTree(right_arr, mid + 1, self.max_val)
def rank(self, index, bit):
'''Count occurrences of bit in bitmap[0:index]'''
if bit == 1:
return self.prefix[index]
else:
return index - self.prefix[index]
def kth_smallest(self, left, right, k):
'''Find kth smallest in arr[left:right+1]'''
if self.min_val == self.max_val:
return self.min_val
# Count elements going to left subtree
left_count = self.rank(right + 1, 0) - self.rank(left, 0)
if k <= left_count:
# Search in left subtree
new_left = self.rank(left, 0)
new_right = self.rank(right + 1, 0) - 1
return self.left.kth_smallest(new_left, new_right, k)
else:
# Search in right subtree
new_left = self.rank(left, 1)
new_right = self.rank(right + 1, 1) - 1
return self.right.kth_smallest(new_left, new_right, k - left_count)
def count_less(self, left, right, x):
'''Count elements < x in arr[left:right+1]'''
if self.min_val >= x:
return 0
if self.max_val < x:
return right - left + 1
mid = (self.min_val + self.max_val) // 2
# Count in left subtree
new_left = self.rank(left, 0)
new_right = self.rank(right + 1, 0) - 1
result = 0
if new_left <= new_right:
result += self.left.count_less(new_left, new_right, x)
# Count in right subtree if needed
if x > mid + 1:
new_left = self.rank(left, 1)
new_right = self.rank(right + 1, 1) - 1
if new_left <= new_right:
result += self.right.count_less(new_left, new_right, x)
return result
# 4. Van Emde Boas Tree (vEB) - O(log log U) operations
class VEBTree:
'''Van Emde Boas tree for integer sets with universe size U'''
def __init__(self, u):
self.u = u
self.min = None
self.max = None
if u > 2:
sqrt_u = int(u ** 0.5)
self.summary = VEBTree(sqrt_u)
self.clusters = [None] * sqrt_u
def _high(self, x):
sqrt_u = int(self.u ** 0.5)
return x // sqrt_u
def _low(self, x):
sqrt_u = int(self.u ** 0.5)
return x % sqrt_u
def _index(self, high, low):
sqrt_u = int(self.u ** 0.5)
return high * sqrt_u + low
def insert(self, x):
'''Insert x in O(log log U)'''
if self.min is None:
self.min = self.max = x
return
if x < self.min:
x, self.min = self.min, x
if self.u > 2:
high = self._high(x)
low = self._low(x)
if self.clusters[high] is None:
self.clusters[high] = VEBTree(int(self.u ** 0.5))
self.summary.insert(high)
self.clusters[high].insert(low)
if x > self.max:
self.max = x
def member(self, x):
'''Check if x exists in O(log log U)'''
if x == self.min or x == self.max:
return True
if self.u == 2:
return False
high = self._high(x)
if self.clusters[high] is None:
return False
return self.clusters[high].member(self._low(x))
def successor(self, x):
'''Find smallest element > x in O(log log U)'''
if self.u == 2:
if x == 0 and self.max == 1:
return 1
return None
if self.min is not None and x < self.min:
return self.min
high = self._high(x)
low = self._low(x)
if (self.clusters[high] is not None and
self.clusters[high].max is not None and
low < self.clusters[high].max):
offset = self.clusters[high].successor(low)
return self._index(high, offset)
succ_cluster = self.summary.successor(high)
if succ_cluster is None:
return None
offset = self.clusters[succ_cluster].min
return self._index(succ_cluster, offset)PythonAnswer: Advanced data structures: Lazy propagation enables O(log n) range updates; Persistent segment tree preserves all versions via path copying; Wavelet tree supports kth smallest in range; vEB tree achieves O(log log U) operations for integer sets.
Q179. Implement parallel and distributed algorithms
# Parallel and Distributed Algorithms
# 1. MapReduce Pattern
from collections import defaultdict
from concurrent.futures import ProcessPoolExecutor, as_completed
import multiprocessing as mp
class MapReduce:
'''Generic MapReduce framework'''
def __init__(self, map_func, reduce_func, num_workers=None):
self.map_func = map_func
self.reduce_func = reduce_func
self.num_workers = num_workers or mp.cpu_count()
def __call__(self, data):
'''Execute MapReduce on data'''
# Map phase
mapped_data = self._map_phase(data)
# Shuffle phase - group by key
shuffled = self._shuffle_phase(mapped_data)
# Reduce phase
result = self._reduce_phase(shuffled)
return result
def _map_phase(self, data):
'''Parallel map phase'''
chunk_size = max(1, len(data) // self.num_workers)
chunks = [data[i:i+chunk_size] for i in range(0, len(data), chunk_size)]
mapped_results = []
with ProcessPoolExecutor(max_workers=self.num_workers) as executor:
futures = [executor.submit(self._map_chunk, chunk)
for chunk in chunks]
for future in as_completed(futures):
mapped_results.extend(future.result())
return mapped_results
def _map_chunk(self, chunk):
'''Map a single chunk'''
results = []
for item in chunk:
results.extend(self.map_func(item))
return results
def _shuffle_phase(self, mapped_data):
'''Group mapped data by key'''
shuffled = defaultdict(list)
for key, value in mapped_data:
shuffled[key].append(value)
return shuffled
def _reduce_phase(self, shuffled):
'''Parallel reduce phase'''
results = {}
with ProcessPoolExecutor(max_workers=self.num_workers) as executor:
futures = {executor.submit(self.reduce_func, key, values): key
for key, values in shuffled.items()}
for future in as_completed(futures):
key = futures[future]
results[key] = future.result()
return results
# Example: Word Count
def word_count_example():
'''Word count using MapReduce'''
def map_words(line):
return [(word.lower(), 1) for word in line.split()]
def reduce_counts(word, counts):
return sum(counts)
mr = MapReduce(map_words, reduce_counts)
documents = [
"hello world",
"hello python world",
"python programming"
]
return mr(documents)
# 2. Parallel Merge Sort
def parallel_merge_sort(arr, num_workers=None):
'''Merge sort using parallel processing'''
num_workers = num_workers or mp.cpu_count()
if len(arr) <= 1:
return arr
if len(arr) < 1000: # Sequential threshold
return sorted(arr)
mid = len(arr) // 2
with ProcessPoolExecutor(max_workers=2) as executor:
left_future = executor.submit(parallel_merge_sort, arr[:mid], num_workers)
right_future = executor.submit(parallel_merge_sort, arr[mid:], num_workers)
left = left_future.result()
right = right_future.result()
# Merge
return merge(left, right)
def merge(left, right):
'''Merge two sorted arrays'''
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# 3. Parallel Prefix Sum (Scan)
def parallel_prefix_sum(arr):
'''Compute prefix sums in parallel - O(log n) time with O(n) processors'''
n = len(arr)
if n == 1:
return arr
# Up-sweep phase (reduce)
depth = n.bit_length() - 1
for d in range(depth):
stride = 2 ** (d + 1)
indices = range(stride - 1, n, stride)
with ProcessPoolExecutor() as executor:
def update(i):
left = i - 2**d
return arr[i] + arr[left]
results = list(executor.map(update, indices))
for idx, i in enumerate(indices):
arr[i] = results[idx]
# Set last element to 0 and down-sweep
arr[-1] = 0
for d in range(depth - 1, -1, -1):
stride = 2 ** (d + 1)
indices = range(stride - 1, n, stride)
with ProcessPoolExecutor() as executor:
def update(i):
left = i - 2**d
temp = arr[left]
arr[left] = arr[i]
arr[i] = arr[i] + temp
list(executor.map(update, indices))
return arr
# 4. Work-Stealing Scheduler
from collections import deque
from threading import Thread, Lock
import queue
class WorkStealingScheduler:
'''Work-stealing task scheduler for load balancing'''
def __init__(self, num_workers):
self.num_workers = num_workers
self.queues = [deque() for _ in range(num_workers)]
self.locks = [Lock() for _ in range(num_workers)]
self.workers = []
self.done = False
def add_task(self, task):
'''Add task to least loaded queue'''
min_queue = min(range(self.num_workers),
key=lambda i: len(self.queues[i]))
with self.locks[min_queue]:
self.queues[min_queue].append(task)
def _worker(self, worker_id):
'''Worker thread that processes tasks and steals work'''
my_queue = self.queues[worker_id]
my_lock = self.locks[worker_id]
while not self.done:
task = None
# Try to get from own queue
with my_lock:
if my_queue:
task = my_queue.popleft()
# If no task, try to steal
if task is None:
task = self._steal_task(worker_id)
if task is None:
continue
# Execute task
task()
def _steal_task(self, worker_id):
'''Steal task from another worker's queue'''
for victim_id in range(self.num_workers):
if victim_id == worker_id:
continue
with self.locks[victim_id]:
victim_queue = self.queues[victim_id]
if len(victim_queue) > 1: # Only steal if victim has multiple tasks
return victim_queue.pop() # Steal from end
return None
def start(self):
'''Start all worker threads'''
self.workers = [Thread(target=self._worker, args=(i,))
for i in range(self.num_workers)]
for worker in self.workers:
worker.start()
def stop(self):
'''Stop all workers'''
self.done = True
for worker in self.workers:
worker.join()
# 5. Distributed Consensus - Simplified Raft
class RaftNode:
'''Simplified Raft consensus algorithm node'''
def __init__(self, node_id, peers):
self.node_id = node_id
self.peers = peers
# Persistent state
self.current_term = 0
self.voted_for = None
self.log = []
# Volatile state
self.commit_index = 0
self.last_applied = 0
# Leader state
self.next_index = {}
self.match_index = {}
self.state = 'follower' # follower, candidate, leader
def request_vote(self, term, candidate_id, last_log_index, last_log_term):
'''Handle vote request from candidate'''
# If candidate's term is old, reject
if term < self.current_term:
return False, self.current_term
# Update term if newer
if term > self.current_term:
self.current_term = term
self.voted_for = None
self.state = 'follower'
# Check if we can vote for this candidate
if self.voted_for is None or self.voted_for == candidate_id:
# Check if candidate's log is at least as up-to-date
my_last_term = self.log[-1][0] if self.log else 0
my_last_index = len(self.log)
if (last_log_term > my_last_term or
(last_log_term == my_last_term and last_log_index >= my_last_index)):
self.voted_for = candidate_id
return True, self.current_term
return False, self.current_term
def append_entries(self, term, leader_id, prev_log_index,
prev_log_term, entries, leader_commit):
'''Handle append entries RPC from leader'''
if term < self.current_term:
return False, self.current_term
self.current_term = term
self.state = 'follower'
# Check if log contains entry at prev_log_index with prev_log_term
if prev_log_index > 0:
if (len(self.log) < prev_log_index or
self.log[prev_log_index - 1][0] != prev_log_term):
return False, self.current_term
# Append new entries
self.log = self.log[:prev_log_index]
self.log.extend(entries)
# Update commit index
if leader_commit > self.commit_index:
self.commit_index = min(leader_commit, len(self.log))
return True, self.current_termPythonAnswer: Parallel algorithms: MapReduce enables distributed processing with map/shuffle/reduce phases; parallel merge sort achieves speedup on multicore; prefix sum scan in O(log n) parallel time; work-stealing balances load; Raft provides distributed consensus.
Q180. Design approximation algorithms for NP-hard problems
# Approximation Algorithms for NP-Hard Problems
# 1. Vertex Cover - 2-Approximation
def vertex_cover_approx(edges):
'''2-approximation for minimum vertex cover'''
cover = set()
remaining_edges = set(edges)
while remaining_edges:
# Pick arbitrary edge
u, v = remaining_edges.pop()
# Add both endpoints to cover
cover.add(u)
cover.add(v)
# Remove all edges incident to u or v
remaining_edges = {(a, b) for a, b in remaining_edges
if a not in {u, v} and b not in {u, v}}
return cover
# Proof: OPT must include at least one endpoint of each edge we pick
# We pick both endpoints, so |cover| ≤ 2|OPT|
# 2. Set Cover - Greedy O(log n) Approximation
def set_cover_greedy(universe, subsets):
'''Greedy algorithm for set cover - O(log n) approximation'''
covered = set()
selected = []
while covered != universe:
# Find set that covers most uncovered elements
best_set = max(subsets, key=lambda s: len(s - covered))
if not (best_set - covered):
break
selected.append(best_set)
covered |= best_set
return selected
# Analysis: Greedy picks set covering most elements each iteration
# Can prove approximation ratio is H(n) = O(log n) where n = |universe|
# 3. Traveling Salesman - 2-Approximation (Metric TSP)
def tsp_approx(graph, n):
'''2-approximation for metric TSP using MST'''
# Step 1: Find MST using Prim's algorithm
import heapq
visited = {0}
mst_edges = []
pq = [(weight, 0, neighbor)
for neighbor, weight in graph[0]]
heapq.heapify(pq)
while pq and len(visited) < n:
weight, u, v = heapq.heappop(pq)
if v in visited:
continue
visited.add(v)
mst_edges.append((u, v, weight))
for neighbor, edge_weight in graph[v]:
if neighbor not in visited:
heapq.heappush(pq, (edge_weight, v, neighbor))
# Step 2: Build MST adjacency list
mst = [[] for _ in range(n)]
for u, v, _ in mst_edges:
mst[u].append(v)
mst[v].append(u)
# Step 3: DFS to get preorder traversal
tour = []
def dfs(node, parent=-1):
tour.append(node)
for neighbor in mst[node]:
if neighbor != parent:
dfs(neighbor, node)
dfs(0)
tour.append(0) # Return to start
# Calculate tour cost
tour_cost = sum(dict(graph[tour[i]])[tour[i+1]]
for i in range(len(tour) - 1))
return tour, tour_cost
# Proof: MST cost ≤ OPT (removing one edge from optimal tour gives spanning tree)
# Tour cost ≤ 2 * MST (we traverse each MST edge at most twice)
# By triangle inequality: shortcutting doesn't increase cost
# Therefore: tour_cost ≤ 2 * OPT
# 4. Bin Packing - First Fit and First Fit Decreasing
def bin_packing_first_fit(items, bin_capacity):
'''First Fit - no more than 2*OPT bins'''
bins = []
for item in items:
# Try to fit in existing bin
placed = False
for bin_items in bins:
if sum(bin_items) + item <= bin_capacity:
bin_items.append(item)
placed = True
break
if not placed:
bins.append([item])
return bins
def bin_packing_ffd(items, bin_capacity):
'''First Fit Decreasing - better approximation'''
# Sort items in decreasing order
sorted_items = sorted(items, reverse=True)
return bin_packing_first_fit(sorted_items, bin_capacity)
# FFD uses at most (11/9)OPT + 6/9 bins
# 5. Knapsack - FPTAS (Fully Polynomial Time Approximation Scheme)
def knapsack_fptas(weights, values, capacity, epsilon):
'''(1-ε)-approximation for knapsack in O(n³/ε)'''
n = len(weights)
if n == 0:
return 0
# Scale values
max_value = max(values)
scale = epsilon * max_value / n
scaled_values = [int(v / scale) for v in values]
# DP on scaled values
max_scaled_value = sum(scaled_values)
# dp[v] = minimum weight to achieve scaled value v
dp = [float('inf')] * (max_scaled_value + 1)
dp[0] = 0
for i in range(n):
v = scaled_values[i]
w = weights[i]
# Process in reverse to avoid using same item twice
for val in range(max_scaled_value, v - 1, -1):
if dp[val - v] != float('inf'):
dp[val] = min(dp[val], dp[val - v] + w)
# Find maximum value achievable within capacity
result = 0
for val in range(max_scaled_value + 1):
if dp[val] <= capacity:
# Unscale value
result = max(result, int(val * scale))
return result
# Analysis: Scaling reduces value range from V to n/ε
# DP runs in O(n² / ε) time
# Solution is within (1-ε) of optimal
# 6. MAX-SAT - Randomized 1/2-Approximation
import random
def max_sat_random(clauses, num_vars):
'''Randomized 1/2-approximation for MAX-SAT'''
# Random assignment
assignment = [random.choice([True, False]) for _ in range(num_vars)]
# Count satisfied clauses
satisfied = 0
for clause in clauses:
# Clause is list of (var_index, is_positive) tuples
clause_satisfied = any(
assignment[var] == is_positive
for var, is_positive in clause
)
if clause_satisfied:
satisfied += 1
return assignment, satisfied
# Expected value: Each clause with k literals has probability 1 - (1/2)^k of being satisfied
# E[satisfied] ≥ (1/2) * total_clauses
# 7. Load Balancing - Greedy Approximation
def load_balancing(jobs, m):
'''Assign jobs to m machines to minimize makespan'''
# Sort jobs in decreasing order (LPT - Longest Processing Time)
sorted_jobs = sorted(enumerate(jobs), key=lambda x: x[1], reverse=True)
# Track load on each machine
machines = [[] for _ in range(m)]
loads = [0] * m
for job_id, duration in sorted_jobs:
# Assign to least loaded machine
min_machine = min(range(m), key=lambda i: loads[i])
machines[min_machine].append(job_id)
loads[min_machine] += duration
makespan = max(loads)
return machines, makespan
# LPT gives (4/3 - 1/(3m)) approximation ratioPythonAnswer: Approximation algorithms: Vertex cover achieves 2-approximation; set cover greedy gives O(log n) ratio; metric TSP via MST achieves 2-approximation; FFD for bin packing; FPTAS for knapsack gives (1-ε)-approximation; randomized MAX-SAT; LPT load balancing.
Q181. Analyze online algorithms and competitive ratios
# Online Algorithms and Competitive Analysis
# 1. Ski Rental Problem
class SkiRental:
'''Classic online algorithm problem - rent vs buy decision'''
def __init__(self, rent_cost, buy_cost):
self.rent_cost = rent_cost
self.buy_cost = buy_cost
self.threshold = buy_cost / rent_cost # Buy after this many days
def deterministic_strategy(self, days):
'''Deterministic 2-competitive algorithm'''
if days < self.threshold:
# Rent for all days
return days * self.rent_cost
else:
# Buy immediately
return self.buy_cost
def randomized_strategy(self, days):
'''Randomized e/(e-1) ≈ 1.58 competitive algorithm'''
import random
import math
e = math.e
# Randomize buy day between 1 and threshold
buy_day = random.uniform(1, self.threshold)
if days < buy_day:
return days * self.rent_cost
else:
return buy_day * self.rent_cost + self.buy_cost
# Competitive ratio: Algorithm cost ≤ c * Optimal cost
# Deterministic: c = 2
# Randomized: c = e/(e-1) ≈ 1.58
# 2. Paging - LRU and Marking Algorithm
class PagingLRU:
'''LRU paging algorithm - k-competitive where k = cache size'''
def __init__(self, cache_size):
from collections import OrderedDict
self.cache = OrderedDict()
self.cache_size = cache_size
self.faults = 0
def access(self, page):
'''Access page - returns True if fault'''
if page in self.cache:
# Move to end (most recently used)
self.cache.move_to_end(page)
return False
# Page fault
self.faults += 1
if len(self.cache) >= self.cache_size:
# Evict least recently used (first item)
self.cache.popitem(last=False)
self.cache[page] = True
return True
class PagingMarking:
'''Marking algorithm - also k-competitive'''
def __init__(self, cache_size):
self.cache = set()
self.marked = set()
self.cache_size = cache_size
self.faults = 0
def access(self, page):
'''Access page with marking'''
if page in self.cache:
self.marked.add(page)
return False
# Page fault
self.faults += 1
if len(self.cache) >= self.cache_size:
# If all pages marked, unmark all
if len(self.marked) == self.cache_size:
self.marked.clear()
# Evict unmarked page
unmarked = self.cache - self.marked
if unmarked:
evict = unmarked.pop()
self.cache.remove(evict)
self.cache.add(page)
self.marked.add(page)
return True
# 3. k-Server Problem - Greedy and Work Function Algorithm
class KServerGreedy:
'''k-server problem - move closest server to request'''
def __init__(self, k, distance_func):
self.k = k
self.servers = [] # Server positions
self.distance = distance_func
self.total_cost = 0
def serve(self, request):
'''Serve request by moving closest server'''
if not self.servers:
# Initially place servers
self.servers.append(request)
return 0
if request in self.servers:
return 0 # Already have server there
# Find closest server
closest_idx = min(range(len(self.servers)),
key=lambda i: self.distance(self.servers[i], request))
cost = self.distance(self.servers[closest_idx], request)
self.servers[closest_idx] = request
self.total_cost += cost
return cost
# Greedy is k-competitive for k-server on a line
# 4. Online Matching - Greedy Algorithm
def online_bipartite_matching(left_nodes, right_arrivals):
'''Online bipartite matching - greedy 1/2-competitive'''
matching = {}
matched_left = set()
for right_node, neighbors in right_arrivals:
# neighbors is list of left nodes that right_node connects to
# Find unmatched neighbor
for left in neighbors:
if left not in matched_left:
matching[right_node] = left
matched_left.add(left)
break
return matching
# Greedy achieves 1/2 competitive ratio
# Ranking algorithm (random permutation of left nodes) achieves 1 - 1/e ≈ 0.63
# 5. Secretary Problem - Optimal Stopping
def secretary_problem(candidates):
'''Hire best candidate with 1/e probability'''
import math
n = len(candidates)
# Observe first n/e candidates
observe = int(n / math.e)
# Track best in observation phase
best_observed = max(candidates[:observe])
# Hire first candidate better than best observed
for i in range(observe, n):
if candidates[i] > best_observed:
return i, candidates[i]
# If no one better, hire last candidate
return n - 1, candidates[-1]
# Optimal strategy: reject first n/e, then accept first better than all rejected
# Success probability: 1/e ≈ 0.368
# 6. Metrical Task Systems - Work Function Algorithm
class MetricalTaskSystem:
'''General framework for online problems on metric spaces'''
def __init__(self, states, distance_matrix):
self.states = states
self.distance = distance_matrix
self.current_state = 0
# Work function w[i] = min cost to serve all requests so far ending in state i
self.work = [0] * len(states)
def serve_request(self, cost_vector):
'''
Serve request with cost_vector[i] = cost to serve in state i
Returns state to move to
'''
# Compute new work function
new_work = [float('inf')] * len(self.states)
for j in range(len(self.states)):
for i in range(len(self.states)):
new_work[j] = min(
new_work[j],
self.work[i] + self.distance[i][j] + cost_vector[j]
)
self.work = new_work
# Move to state minimizing work[state] + distance from current
best_state = min(
range(len(self.states)),
key=lambda s: self.work[s] + self.distance[self.current_state][s]
)
cost = self.distance[self.current_state][best_state]
self.current_state = best_state
return best_state, cost
# Work Function Algorithm is 2k-1 competitive for k-state MTS
# 7. Online Convex Optimization - Online Gradient Descent
class OnlineGradientDescent:
'''Online learning with convex loss functions'''
def __init__(self, dimension, learning_rate):
import numpy as np
self.w = np.zeros(dimension)
self.eta = learning_rate
self.regret = 0
def predict(self, x):
'''Make prediction for input x'''
import numpy as np
return np.dot(self.w, x)
def update(self, x, true_y):
'''Update after seeing true label'''
import numpy as np
# Compute gradient of squared loss
pred = self.predict(x)
loss = (pred - true_y) ** 2
gradient = 2 * (pred - true_y) * x
# Update weights
self.w = self.w - self.eta * gradient
# Project to bounded set if needed
norm = np.linalg.norm(self.w)
if norm > 1:
self.w = self.w / norm
return loss
# Regret bound: O(√T) for T rounds with proper learning ratePythonAnswer: Online algorithms: Ski rental achieves 2-competitive deterministic, e/(e-1) randomized; LRU/Marking are k-competitive for paging; greedy k-server; online matching 1/2-competitive; secretary problem 1/e success; work function for MTS; online gradient descent with O(√T) regret.
Q182. Implement streaming algorithms for massive data
# Streaming Algorithms for Massive Data
# 1. Count-Min Sketch - Frequency Estimation
import hashlib
class CountMinSketch:
'''Estimate frequency of elements in stream with bounded error'''
def __init__(self, width, depth):
'''
width: number of buckets per hash function
depth: number of hash functions
Error bound: ε = e/width, probability δ = 1 - (1/2)^depth
'''
self.width = width
self.depth = depth
self.table = [[0] * width for _ in range(depth)]
self.count = 0
def _hash(self, item, seed):
'''Hash function with seed'''
h = hashlib.md5(f"{item}{seed}".encode()).hexdigest()
return int(h, 16) % self.width
def add(self, item, count=1):
'''Add item to sketch'''
for i in range(self.depth):
bucket = self._hash(item, i)
self.table[i][bucket] += count
self.count += count
def estimate(self, item):
'''Estimate frequency of item'''
return min(self.table[i][self._hash(item, i)]
for i in range(self.depth))
def merge(self, other):
'''Merge two sketches'''
if self.width != other.width or self.depth != other.depth:
raise ValueError("Incompatible sketches")
for i in range(self.depth):
for j in range(self.width):
self.table[i][j] += other.table[i][j]
self.count += other.count
# Analysis: Uses O(width * depth) space
# Guarantees: estimate(x) ≥ freq(x)
# With probability 1-δ: estimate(x) ≤ freq(x) + ε * total_count
# 2. HyperLogLog - Cardinality Estimation
class HyperLogLog:
'''Estimate number of distinct elements with small memory'''
def __init__(self, precision=14):
'''
precision: log2(m) where m is number of registers
Typical error: 1.04/√m
'''
self.precision = precision
self.m = 1 << precision # 2^precision registers
self.registers = [0] * self.m
self.alpha = self._get_alpha()
def _get_alpha(self):
'''Bias correction constant'''
if self.m >= 128:
return 0.7213 / (1 + 1.079 / self.m)
elif self.m >= 64:
return 0.709
elif self.m >= 32:
return 0.697
else:
return 0.673
def _hash(self, item):
'''64-bit hash'''
h = hashlib.sha256(str(item).encode()).hexdigest()
return int(h[:16], 16) # Use first 64 bits
def _rho(self, bits, max_width):
'''Position of first 1-bit (counting from right)'''
rho = max_width - bits.bit_length() + 1
return rho if rho <= max_width else max_width
def add(self, item):
'''Add element to set'''
h = self._hash(item)
# Use first p bits for register index
j = h & ((1 << self.precision) - 1)
# Use remaining bits for position of first 1
w = h >> self.precision
self.registers[j] = max(self.registers[j], self._rho(w, 64 - self.precision))
def cardinality(self):
'''Estimate number of distinct elements'''
raw_estimate = self.alpha * (self.m ** 2) / sum(2 ** -x for x in self.registers)
# Apply bias correction for small/large cardinalities
if raw_estimate <= 2.5 * self.m:
# Small range correction
zeros = self.registers.count(0)
if zeros != 0:
return self.m * math.log(self.m / zeros)
if raw_estimate <= (1/30) * (1 << 32):
return raw_estimate
else:
# Large range correction
return -1 * (1 << 32) * math.log(1 - raw_estimate / (1 << 32))
def merge(self, other):
'''Merge two HyperLogLogs'''
if self.precision != other.precision:
raise ValueError("Cannot merge HLLs with different precision")
self.registers = [max(a, b) for a, b in zip(self.registers, other.registers)]
# 3. Reservoir Sampling - Uniform Random Sample from Stream
import random
def reservoir_sampling(stream, k):
'''Maintain uniform random sample of size k from stream'''
reservoir = []
for i, item in enumerate(stream):
if i < k:
reservoir.append(item)
else:
# Randomly replace element with probability k/(i+1)
j = random.randint(0, i)
if j < k:
reservoir[j] = item
return reservoir
# Proof: Each element has exactly k/n probability of being in reservoir
# 4. Misra-Gries Algorithm - Heavy Hitters
class MisraGries:
'''Find elements with frequency > n/k using O(k) space'''
def __init__(self, k):
self.k = k
self.counters = {}
def process(self, item):
'''Process one item from stream'''
if item in self.counters:
self.counters[item] += 1
elif len(self.counters) < self.k - 1:
self.counters[item] = 1
else:
# Decrement all counters
self.counters = {key: val - 1
for key, val in self.counters.items()}
# Remove zeros
self.counters = {key: val
for key, val in self.counters.items()
if val > 0}
def get_heavy_hitters(self, threshold):
'''Return items that might have frequency > threshold'''
return list(self.counters.keys())
# Guarantees: If freq(x) > n/k, then x is in counters
# May have false positives with freq(x) ≥ n/k - n/k = (k-1)n/k
# 5. DGIM Algorithm - Count 1s in Sliding Window
class DGIMAlgorithm:
'''Count 1s in last N bits with O(log²N) space'''
def __init__(self, window_size):
self.N = window_size
self.timestamp = 0
# buckets[size] = list of (timestamp, size) pairs
self.buckets = {}
def update(self, bit):
'''Process new bit'''
self.timestamp += 1
# Remove buckets outside window
for size in list(self.buckets.keys()):
self.buckets[size] = [(t, s) for t, s in self.buckets[size]
if self.timestamp - t < self.N]
if not self.buckets[size]:
del self.buckets[size]
if bit == 1:
# Add new bucket of size 1
if 1 not in self.buckets:
self.buckets[1] = []
self.buckets[1].append((self.timestamp, 1))
# Merge buckets if we have too many of same size
self._merge_buckets()
def _merge_buckets(self):
'''Maintain at most 2 buckets of each size'''
sizes = sorted(self.buckets.keys())
for size in sizes:
while len(self.buckets[size]) > 2:
# Merge two oldest buckets
t1, s1 = self.buckets[size].pop(0)
t2, s2 = self.buckets[size].pop(0)
new_size = s1 + s2
new_timestamp = max(t1, t2)
if new_size not in self.buckets:
self.buckets[new_size] = []
self.buckets[new_size].append((new_timestamp, new_size))
def count(self):
'''Estimate number of 1s in window'''
total = 0
# Sum all complete buckets
for size, bucket_list in self.buckets.items():
for timestamp, s in bucket_list:
if self.timestamp - timestamp < self.N:
total += s
# Subtract half of oldest bucket (may extend beyond window)
if self.buckets:
oldest_size = min(self.buckets.keys())
if self.buckets[oldest_size]:
total -= oldest_size // 2
return total
# Error: At most 50% error for each bucket
# Overall error: O(1) multiplicative error
# 6. Flajolet-Martin Algorithm - Distinct Elements
class FlajoletMartin:
'''Estimate distinct elements using bit patterns'''
def __init__(self, num_hash=32):
self.num_hash = num_hash
self.max_trailing = [0] * num_hash
def _hash(self, item, seed):
h = hashlib.md5(f"{item}{seed}".encode()).hexdigest()
return int(h, 16)
def _trailing_zeros(self, n):
'''Count trailing zeros in binary representation'''
if n == 0:
return 64
count = 0
while (n & 1) == 0:
count += 1
n >>= 1
return count
def add(self, item):
'''Add item to stream'''
for i in range(self.num_hash):
h = self._hash(item, i)
trailing = self._trailing_zeros(h)
self.max_trailing[i] = max(self.max_trailing[i], trailing)
def estimate(self):
'''Estimate number of distinct elements'''
import statistics
# Use median to reduce variance
estimates = [2 ** r for r in self.max_trailing]
return statistics.median(estimates)
# Expected estimate ≈ distinct count
# Standard deviation ≈ distinct countPythonAnswer: Streaming algorithms: Count-Min Sketch estimates frequency with bounded error; HyperLogLog counts distinct elements in O(m) space with 1.04/√m error; reservoir sampling maintains uniform sample; Misra-Gries finds heavy hitters; DGIM counts 1s in sliding window; Flajolet-Martin estimates cardinality.
Q183. Design external memory algorithms for disk-based data
# External Memory Algorithms for Disk-Based Data
# 1. External Merge Sort
import heapq
import tempfile
import os
class ExternalMergeSort:
'''Sort data larger than RAM using disk'''
def __init__(self, memory_limit=1000000):
'''memory_limit: number of items that fit in memory'''
self.memory_limit = memory_limit
self.temp_files = []
def sort(self, input_file, output_file):
'''Sort large file using external merge sort'''
# Phase 1: Create sorted runs
runs = self._create_sorted_runs(input_file)
# Phase 2: K-way merge
self._k_way_merge(runs, output_file)
# Cleanup
self._cleanup()
def _create_sorted_runs(self, input_file):
'''Create sorted runs that fit in memory'''
runs = []
buffer = []
with open(input_file, 'r') as f:
for line in f:
buffer.append(line.strip())
if len(buffer) >= self.memory_limit:
# Sort and write to disk
buffer.sort()
run_file = self._write_run(buffer)
runs.append(run_file)
buffer = []
# Handle remaining data
if buffer:
buffer.sort()
run_file = self._write_run(buffer)
runs.append(run_file)
return runs
def _write_run(self, data):
'''Write sorted run to temporary file'''
temp = tempfile.NamedTemporaryFile(mode='w', delete=False)
self.temp_files.append(temp.name)
for item in data:
temp.write(item + '\n')
temp.close()
return temp.name
def _k_way_merge(self, runs, output_file):
'''Merge k sorted runs using heap'''
# Open all run files
files = [open(run, 'r') for run in runs]
# Initialize heap with first element from each run
heap = []
for i, f in enumerate(files):
line = f.readline().strip()
if line:
heapq.heappush(heap, (line, i))
# Merge
with open(output_file, 'w') as out:
while heap:
value, file_idx = heapq.heappop(heap)
out.write(value + '\n')
# Read next element from same file
line = files[file_idx].readline().strip()
if line:
heapq.heappush(heap, (line, file_idx))
# Close all files
for f in files:
f.close()
def _cleanup(self):
'''Delete temporary files'''
for temp_file in self.temp_files:
if os.path.exists(temp_file):
os.remove(temp_file)
# I/O Complexity: O((N/B) log_{M/B} (N/B)) where B=block size, M=memory size
# 2. External Hash Join
class ExternalHashJoin:
'''Join two large tables using external hashing'''
def __init__(self, memory_limit=1000000):
self.memory_limit = memory_limit
self.num_partitions = 10
self.temp_files = []
def join(self, table1_file, table2_file, output_file, key_idx=0):
'''Perform hash join on two files'''
# Phase 1: Partition both tables
partitions1 = self._partition(table1_file, key_idx)
partitions2 = self._partition(table2_file, key_idx)
# Phase 2: Join corresponding partitions
with open(output_file, 'w') as out:
for i in range(self.num_partitions):
matches = self._join_partition(partitions1[i], partitions2[i], key_idx)
for match in matches:
out.write(','.join(match) + '\n')
# Cleanup
self._cleanup()
def _partition(self, input_file, key_idx):
'''Partition file by hash of key'''
# Create partition files
partition_files = []
partition_writers = []
for i in range(self.num_partitions):
temp = tempfile.NamedTemporaryFile(mode='w', delete=False)
self.temp_files.append(temp.name)
partition_files.append(temp.name)
partition_writers.append(temp)
# Distribute records to partitions
with open(input_file, 'r') as f:
for line in f:
record = line.strip().split(',')
key = record[key_idx]
partition = hash(key) % self.num_partitions
partition_writers[partition].write(line)
# Close writers
for writer in partition_writers:
writer.close()
return partition_files
def _join_partition(self, partition1, partition2, key_idx):
'''Join two partitions that fit in memory'''
# Build hash table for smaller partition
hash_table = {}
with open(partition1, 'r') as f:
for line in f:
record = line.strip().split(',')
key = record[key_idx]
if key not in hash_table:
hash_table[key] = []
hash_table[key].append(record)
# Probe with larger partition
matches = []
with open(partition2, 'r') as f:
for line in f:
record = line.strip().split(',')
key = record[key_idx]
if key in hash_table:
for match in hash_table[key]:
matches.append(match + record)
return matches
def _cleanup(self):
for temp_file in self.temp_files:
if os.path.exists(temp_file):
os.remove(temp_file)
# 3. B-Tree for External Storage
class BTreeNode:
'''Node in B-tree optimized for disk access'''
def __init__(self, leaf=True):
self.keys = []
self.children = []
self.leaf = leaf
self.next_leaf = None # For range queries
class BTree:
'''B-tree with high fanout for minimal disk seeks'''
def __init__(self, t=100):
'''t: minimum degree (node has at least t-1 keys)'''
self.root = BTreeNode()
self.t = t
def search(self, key, node=None):
'''Search for key - O(log_t N) disk accesses'''
if node is None:
node = self.root
# Find position in current node
i = 0
while i < len(node.keys) and key > node.keys[i]:
i += 1
# Check if found
if i < len(node.keys) and key == node.keys[i]:
return True
# Search child if not leaf
if node.leaf:
return False
else:
return self.search(key, node.children[i])
def insert(self, key):
'''Insert key into B-tree'''
root = self.root
if len(root.keys) == 2 * self.t - 1:
# Root is full, split it
new_root = BTreeNode(leaf=False)
new_root.children.append(self.root)
self._split_child(new_root, 0)
self.root = new_root
self._insert_non_full(self.root, key)
def _insert_non_full(self, node, key):
'''Insert into node that is not full'''
i = len(node.keys) - 1
if node.leaf:
# Insert into sorted position
node.keys.append(None)
while i >= 0 and key < node.keys[i]:
node.keys[i + 1] = node.keys[i]
i -= 1
node.keys[i + 1] = key
else:
# Find child to insert into
while i >= 0 and key < node.keys[i]:
i -= 1
i += 1
if len(node.children[i].keys) == 2 * self.t - 1:
# Child is full, split it
self._split_child(node, i)
if key > node.keys[i]:
i += 1
self._insert_non_full(node.children[i], key)
def _split_child(self, parent, i):
'''Split full child'''
t = self.t
full_child = parent.children[i]
new_child = BTreeNode(leaf=full_child.leaf)
# Move second half of keys to new child
mid = t - 1
new_child.keys = full_child.keys[mid + 1:]
full_child.keys = full_child.keys[:mid]
if not full_child.leaf:
new_child.children = full_child.children[mid + 1:]
full_child.children = full_child.children[:mid + 1]
# Insert middle key into parent
parent.keys.insert(i, full_child.keys[mid])
parent.children.insert(i + 1, new_child)
def range_query(self, start, end):
'''Return all keys in range [start, end]'''
result = []
# Find leftmost leaf containing start
node = self._find_leaf(start)
# Scan leaves until we exceed end
while node:
for key in node.keys:
if start <= key <= end:
result.append(key)
elif key > end:
return result
node = node.next_leaf
return result
def _find_leaf(self, key):
'''Find leaf node that would contain key'''
node = self.root
while not node.leaf:
i = 0
while i < len(node.keys) and key > node.keys[i]:
i += 1
node = node.children[i]
return nodePythonAnswer: External memory algorithms: External merge sort handles data larger than RAM with O((N/B)log(N/B)) I/Os; external hash join partitions and joins large tables; B-tree with high fanout minimizes disk seeks achieving O(log_t N) complexity for searches and insertions.
Q184. Master advanced string algorithms
# Advanced String Algorithms and Pattern Matching
# 1. Suffix Automaton - Linear Time Construction
class SuffixAutomaton:
'''Minimal automaton accepting all suffixes - O(n) construction'''
class State:
def __init__(self):
self.transitions = {} # char -> state
self.link = None # suffix link
self.length = 0 # length of longest string in this state
def __init__(self):
self.states = [self.State()]
self.last = 0 # Index of state corresponding to whole string
self.size = 1
def add_char(self, c):
'''Add character to automaton'''
cur = self.size
self.size += 1
self.states.append(self.State())
self.states[cur].length = self.states[self.last].length + 1
# Add transitions from all suffix states
p = self.last
while p != -1 and c not in self.states[p].transitions:
self.states[p].transitions[c] = cur
if p == 0:
p = -1
else:
p = self.states[p].link
if p == -1:
self.states[cur].link = 0
else:
q = self.states[p].transitions[c]
if self.states[p].length + 1 == self.states[q].length:
self.states[cur].link = q
else:
# Clone state q
clone = self.size
self.size += 1
self.states.append(self.State())
self.states[clone].length = self.states[p].length + 1
self.states[clone].transitions = self.states[q].transitions.copy()
self.states[clone].link = self.states[q].link
# Update links
while p != -1 and self.states[p].transitions.get(c) == q:
self.states[p].transitions[c] = clone
if p == 0:
p = -1
else:
p = self.states[p].link
self.states[q].link = clone
self.states[cur].link = clone
self.last = cur
def build(self, s):
'''Build suffix automaton for string s'''
for c in s:
self.add_char(c)
def contains(self, pattern):
'''Check if pattern is substring in O(|pattern|)'''
state = 0
for c in pattern:
if c not in self.states[state].transitions:
return False
state = self.states[state].transitions[c]
return True
def count_occurrences(self, pattern):
'''Count occurrences of pattern'''
state = 0
for c in pattern:
if c not in self.states[state].transitions:
return 0
state = self.states[state].transitions[c]
# Count paths from this state to terminal states
return self._count_paths(state)
def _count_paths(self, state):
'''Count number of terminal states reachable'''
if not self.states[state].transitions:
return 1
total = 1 # This state itself
for next_state in self.states[state].transitions.values():
total += self._count_paths(next_state)
return total
# 2. Manacher's Algorithm - Longest Palindromic Substring
def manacher_longest_palindrome(s):
'''Find longest palindromic substring in O(n)'''
# Preprocess: insert '#' between characters
t = '#'.join('^{}$'.format(s))
n = len(t)
# p[i] = radius of palindrome centered at i
p = [0] * n
center = right = 0
for i in range(1, n - 1):
# Use previously computed values
if i < right:
mirror = 2 * center - i
p[i] = min(right - i, p[mirror])
# Try to expand palindrome centered at i
while t[i + p[i] + 1] == t[i - p[i] - 1]:
p[i] += 1
# Update center if palindrome extends past right
if i + p[i] > right:
center, right = i, i + p[i]
# Find maximum palindrome
max_len = max(p)
center_idx = p.index(max_len)
# Extract original palindrome
start = (center_idx - max_len) // 2
return s[start:start + max_len]
# Time: O(n) because right only increases, total expansion is O(n)
# 3. Z-Algorithm - Pattern Matching
def z_algorithm(s):
'''Compute Z-array: Z[i] = length of longest substring starting at i that is prefix of s'''
n = len(s)
z = [0] * n
z[0] = n
left = right = 0
for i in range(1, n):
# Use previously computed values
if i <= right:
z[i] = min(right - i + 1, z[i - left])
# Try to extend
while i + z[i] < n and s[z[i]] == s[i + z[i]]:
z[i] += 1
# Update window
if i + z[i] - 1 > right:
left, right = i, i + z[i] - 1
return z
def pattern_matching_z(text, pattern):
'''Find all occurrences of pattern in text using Z-algorithm'''
# Concatenate pattern$text
s = pattern + '$' + text
z = z_algorithm(s)
# Find positions where Z[i] = len(pattern)
m = len(pattern)
matches = []
for i in range(m + 1, len(s)):
if z[i] == m:
matches.append(i - m - 1) # Position in original text
return matches
# 4. Ukkonen's Algorithm - Suffix Tree Construction
class SuffixTree:
'''Build suffix tree in O(n) using Ukkonen's algorithm'''
class Node:
def __init__(self):
self.children = {}
self.suffix_link = None
self.start = -1
self.end = [None] # Use list for global end
def __init__(self, text):
self.text = text
self.root = self.Node()
self.active_node = self.root
self.active_edge = -1
self.active_length = 0
self.remaining = 0
self.end = [-1] # Global end pointer
self._build()
def _build(self):
'''Build suffix tree using Ukkonen's algorithm'''
for i in range(len(self.text)):
self._extend(i)
def _extend(self, pos):
'''Extend tree with character at position pos'''
self.end[0] = pos
self.remaining += 1
last_new_node = None
while self.remaining > 0:
if self.active_length == 0:
self.active_edge = pos
if self.text[self.active_edge] not in self.active_node.children:
# Create new leaf
self.active_node.children[self.text[self.active_edge]] = self._create_node(pos)
if last_new_node:
last_new_node.suffix_link = self.active_node
last_new_node = None
else:
# Walk down if needed
next_node = self.active_node.children[self.text[self.active_edge]]
if self._walk_down(next_node):
continue
# Check if current character matches
if self.text[next_node.start + self.active_length] == self.text[pos]:
if last_new_node and self.active_node != self.root:
last_new_node.suffix_link = self.active_node
self.active_length += 1
break
# Split edge
split = self._create_node(next_node.start, next_node.start + self.active_length - 1)
self.active_node.children[self.text[self.active_edge]] = split
split.children[self.text[pos]] = self._create_node(pos)
next_node.start += self.active_length
split.children[self.text[next_node.start]] = next_node
if last_new_node:
last_new_node.suffix_link = split
last_new_node = split
self.remaining -= 1
if self.active_node == self.root and self.active_length > 0:
self.active_length -= 1
self.active_edge = pos - self.remaining + 1
elif self.active_node != self.root:
self.active_node = self.active_node.suffix_link or self.root
def _create_node(self, start, end=None):
node = self.Node()
node.start = start
node.end = [end] if end is not None else self.end
return node
def _walk_down(self, node):
edge_length = node.end[0] - node.start + 1
if self.active_length >= edge_length:
self.active_edge += edge_length
self.active_length -= edge_length
self.active_node = node
return True
return False
def search(self, pattern):
'''Search for pattern in O(m) where m = len(pattern)'''
node = self.root
i = 0
while i < len(pattern):
if pattern[i] not in node.children:
return False
child = node.children[pattern[i]]
edge_len = child.end[0] - child.start + 1
for j in range(min(edge_len, len(pattern) - i)):
if self.text[child.start + j] != pattern[i + j]:
return False
i += edge_len
node = child
return TruePythonAnswer: Advanced strings: Suffix automaton provides O(n) construction accepting all suffixes with O(m) pattern matching; Manacher’s finds longest palindrome in O(n); Z-algorithm computes prefix matches in O(n); Ukkonen’s builds suffix tree in O(n) enabling O(m) pattern searches.
Q185. Implement advanced computational geometry algorithms
# Advanced Computational Geometry Algorithms
# 1. Voronoi Diagram - Fortune's Algorithm
from collections import namedtuple
import heapq
import math
Point = namedtuple('Point', ['x', 'y'])
class VoronoiDiagram:
'''Construct Voronoi diagram using Fortune's sweep line algorithm'''
class Arc:
'''Arc on beach line'''
def __init__(self, site, left=None, right=None):
self.site = site
self.left = left
self.right = right
self.event = None
class Event:
'''Site or circle event'''
def __init__(self, x, point, arc=None, is_site=True):
self.x = x
self.point = point
self.arc = arc
self.is_site = is_site
self.valid = True
def __lt__(self, other):
return self.x < other.x
def __init__(self, points):
self.sites = sorted(points, key=lambda p: (p.x, p.y))
self.edges = []
self.vertices = []
self.beach_line = None
self.events = []
def compute(self):
'''Compute Voronoi diagram'''
# Initialize event queue with site events
for site in self.sites:
heapq.heappush(self.events, self.Event(site.x, site, is_site=True))
# Process events
while self.events:
event = heapq.heappop(self.events)
if not event.valid:
continue
self.sweep_line = event.x
if event.is_site:
self._handle_site_event(event)
else:
self._handle_circle_event(event)
return self.edges, self.vertices
def _handle_site_event(self, event):
'''Handle site event - add new arc to beach line'''
site = event.point
if self.beach_line is None:
self.beach_line = self.Arc(site)
return
# Find arc above new site
arc = self._find_arc_above(site.y)
if arc.event:
arc.event.valid = False
# Create new arcs
left = self.Arc(arc.site)
right = self.Arc(arc.site)
middle = self.Arc(site, left, right)
left.right = middle
right.left = middle
# Check for circle events
self._check_circle_event(left)
self._check_circle_event(right)
def _handle_circle_event(self, event):
'''Handle circle event - arc disappears'''
arc = event.arc
# Add vertex
vertex = event.point
self.vertices.append(vertex)
# Remove arc from beach line
if arc.left:
arc.left.right = arc.right
if arc.right:
arc.right.left = arc.left
# Add edges
# ... (edge construction logic)
# Check new circle events
if arc.left:
self._check_circle_event(arc.left)
if arc.right:
self._check_circle_event(arc.right)
def _find_arc_above(self, y):
'''Find arc on beach line above point y'''
arc = self.beach_line
while arc:
# Check if point is in this arc's region
# ... (parabola intersection logic)
if y < self._get_y_on_arc(arc, y):
if arc.left:
arc = arc.left
else:
break
else:
if arc.right:
arc = arc.right
else:
break
return arc
def _get_y_on_arc(self, arc, x):
'''Get y-coordinate on parabola at x'''
site = arc.site
dp = 2 * (site.x - self.sweep_line)
if dp == 0:
return float('inf')
a1 = 1 / dp
b1 = -2 * site.y / dp
c1 = (site.y ** 2 + site.x ** 2 - self.sweep_line ** 2) / dp
return a1 * x * x + b1 * x + c1
def _check_circle_event(self, arc):
'''Check if arc will disappear - create circle event'''
if not arc.left or not arc.right:
return
# Check if convergence point exists
convergence = self._get_convergence(arc.left.site, arc.site, arc.right.site)
if convergence:
x, y = convergence
radius = math.sqrt((x - arc.site.x)**2 + (y - arc.site.y)**2)
event_x = x + radius
event = self.Event(event_x, Point(x, y), arc, is_site=False)
arc.event = event
heapq.heappush(self.events, event)
def _get_convergence(self, p1, p2, p3):
'''Find circle through three points'''
# Use determinant formula
a = p1.x - p2.x
b = p1.y - p2.y
c = p3.x - p2.x
d = p3.y - p2.y
det = a * d - b * c
if abs(det) < 1e-10:
return None
# ... (convergence point calculation)
return None
# Time Complexity: O(n log n)
# 2. Delaunay Triangulation - Incremental Algorithm
class DelaunayTriangulation:
'''Construct Delaunay triangulation using incremental algorithm'''
class Triangle:
def __init__(self, p1, p2, p3):
self.vertices = [p1, p2, p3]
self.neighbors = [None, None, None]
def circumcircle_contains(self, point):
'''Check if point is inside circumcircle'''
p1, p2, p3 = self.vertices
# Use determinant test
ax, ay = p1.x - point.x, p1.y - point.y
bx, by = p2.x - point.x, p2.y - point.y
cx, cy = p3.x - point.x, p3.y - point.y
det = (ax * ax + ay * ay) * (bx * cy - cx * by) - (bx * bx + by * by) * (ax * cy - cx * ay) + (cx * cx + cy * cy) * (ax * by - bx * ay)
return det > 0
def __init__(self, points):
self.points = points
self.triangles = []
def compute(self):
'''Compute Delaunay triangulation'''
# Create super triangle containing all points
super_triangle = self._create_super_triangle()
self.triangles = [super_triangle]
# Add points one by one
for point in self.points:
self._add_point(point)
# Remove triangles connected to super triangle vertices
self._remove_super_triangle()
return self.triangles
def _create_super_triangle(self):
'''Create triangle containing all points'''
# Find bounding box
min_x = min(p.x for p in self.points)
max_x = max(p.x for p in self.points)
min_y = min(p.y for p in self.points)
max_y = max(p.y for p in self.points)
dx = max_x - min_x
dy = max_y - min_y
delta = max(dx, dy) * 2
# Create large triangle
p1 = Point(min_x - delta, min_y - delta)
p2 = Point(max_x + delta, min_y - delta)
p3 = Point(min_x + dx / 2, max_y + delta)
return self.Triangle(p1, p2, p3)
def _add_point(self, point):
'''Add point to triangulation'''
bad_triangles = []
# Find triangles whose circumcircle contains point
for triangle in self.triangles:
if triangle.circumcircle_contains(point):
bad_triangles.append(triangle)
# Find boundary of polygonal hole
polygon = []
for triangle in bad_triangles:
for i in range(3):
edge = (triangle.vertices[i], triangle.vertices[(i+1)%3])
# Check if edge is shared with another bad triangle
is_shared = False
for other in bad_triangles:
if other == triangle:
continue
for j in range(3):
other_edge = (other.vertices[j], other.vertices[(j+1)%3])
if (edge[0] == other_edge[1] and edge[1] == other_edge[0]):
is_shared = True
break
if is_shared:
break
if not is_shared:
polygon.append(edge)
# Remove bad triangles
for triangle in bad_triangles:
self.triangles.remove(triangle)
# Create new triangles from point to polygon edges
for edge in polygon:
new_triangle = self.Triangle(edge[0], edge[1], point)
self.triangles.append(new_triangle)
def _remove_super_triangle(self):
'''Remove triangles connected to super triangle vertices'''
super_vertices = self.triangles[0].vertices
self.triangles = [t for t in self.triangles
if not any(v in super_vertices for v in t.vertices)]
# 3. Line Segment Intersection - Bentley-Ottmann Algorithm
class SegmentIntersection:
'''Find all intersections among n line segments in O((n+k) log n)'''
def __init__(self, segments):
self.segments = segments
self.intersections = []
def find_intersections(self):
'''Find all intersections using sweep line'''
events = []
# Create events for segment endpoints
for i, seg in enumerate(self.segments):
p1, p2 = seg
if p1.x > p2.x or (p1.x == p2.x and p1.y > p2.y):
p1, p2 = p2, p1
events.append((p1, 'start', i))
events.append((p2, 'end', i))
events.sort(key=lambda e: (e[0].x, e[0].y))
# Status structure (balanced BST in practice)
status = []
for point, event_type, seg_id in events:
if event_type == 'start':
# Add segment to status
status.append(seg_id)
status.sort(key=lambda i: self._y_at_x(self.segments[i], point.x))
# Check intersection with neighbors
idx = status.index(seg_id)
if idx > 0:
self._check_intersection(seg_id, status[idx-1], point.x)
if idx < len(status) - 1:
self._check_intersection(seg_id, status[idx+1], point.x)
else: # end
idx = status.index(seg_id)
# Check if neighbors intersect
if 0 < idx < len(status) - 1:
self._check_intersection(status[idx-1], status[idx+1], point.x)
status.remove(seg_id)
return self.intersections
def _y_at_x(self, segment, x):
'''Get y-coordinate of segment at x'''
p1, p2 = segment
if p2.x == p1.x:
return p1.y
t = (x - p1.x) / (p2.x - p1.x)
return p1.y + t * (p2.y - p1.y)
def _check_intersection(self, seg1_id, seg2_id, sweep_x):
'''Check if two segments intersect'''
seg1 = self.segments[seg1_id]
seg2 = self.segments[seg2_id]
intersection = self._segments_intersect(seg1, seg2)
if intersection and intersection.x >= sweep_x:
self.intersections.append((seg1_id, seg2_id, intersection))
def _segments_intersect(self, seg1, seg2):
'''Compute intersection point of two segments'''
p1, p2 = seg1
p3, p4 = seg2
# Use parametric form
# ... (intersection calculation)
return NonePythonAnswer: Advanced geometry: Voronoi diagram via Fortune’s sweep line in O(n log n) creates regions of points closest to each site; Delaunay triangulation (dual of Voronoi) via incremental insertion; Bentley-Ottmann finds all segment intersections in O((n+k) log n) where k is intersection count.
Q186. Solve advanced graph theory problems
# Advanced Graph Theory - Matching and Coloring
# 1. Maximum Bipartite Matching - Hopcroft-Karp Algorithm
from collections import deque, defaultdict
class HopcroftKarp:
'''Maximum bipartite matching in O(E√V)'''
def __init__(self, graph, left_size, right_size):
'''graph[u] = list of neighbors in right partition'''
self.graph = graph
self.left_size = left_size
self.right_size = right_size
self.pair_left = {} # left -> right
self.pair_right = {} # right -> left
self.dist = {}
def max_matching(self):
'''Find maximum matching'''
matching = 0
while self._bfs():
for u in range(self.left_size):
if u not in self.pair_left:
if self._dfs(u):
matching += 1
return matching
def _bfs(self):
'''Build level graph'''
queue = deque()
for u in range(self.left_size):
if u not in self.pair_left:
self.dist[u] = 0
queue.append(u)
else:
self.dist[u] = float('inf')
self.dist[None] = float('inf')
while queue:
u = queue.popleft()
if self.dist[u] < self.dist[None]:
for v in self.graph.get(u, []):
paired_u = self.pair_right.get(v)
if self.dist.get(paired_u, float('inf')) == float('inf'):
self.dist[paired_u] = self.dist[u] + 1
if paired_u is not None:
queue.append(paired_u)
return self.dist[None] != float('inf')
def _dfs(self, u):
'''Find augmenting path using DFS'''
if u is None:
return True
for v in self.graph.get(u, []):
paired_u = self.pair_right.get(v)
if self.dist.get(paired_u, float('inf')) == self.dist[u] + 1:
if self._dfs(paired_u):
self.pair_left[u] = v
self.pair_right[v] = u
return True
self.dist[u] = float('inf')
return False
# 2. Graph Coloring - Greedy and Backtracking
class GraphColoring:
'''Graph coloring algorithms'''
def __init__(self, graph, n):
self.graph = graph
self.n = n
def greedy_coloring(self):
'''Greedy coloring - uses at most Δ+1 colors'''
colors = {}
for node in range(self.n):
# Find colors used by neighbors
neighbor_colors = {colors[neighbor]
for neighbor in self.graph.get(node, [])
if neighbor in colors}
# Assign smallest available color
color = 0
while color in neighbor_colors:
color += 1
colors[node] = color
return colors
def chromatic_number_backtrack(self, k):
'''Check if graph is k-colorable using backtracking'''
colors = [-1] * self.n
def is_safe(node, color):
for neighbor in self.graph.get(node, []):
if colors[neighbor] == color:
return False
return True
def backtrack(node):
if node == self.n:
return True
for color in range(k):
if is_safe(node, color):
colors[node] = color
if backtrack(node + 1):
return True
colors[node] = -1
return False
return backtrack(0), colors
# 3. Minimum Vertex Cover - Approximation
def vertex_cover_2_approx(edges):
'''2-approximation for minimum vertex cover'''
cover = set()
remaining = set(edges)
while remaining:
u, v = remaining.pop()
cover.add(u)
cover.add(v)
# Remove edges incident to u or v
remaining = {(a, b) for a, b in remaining
if a not in {u, v} and b not in {u, v}}
return cover
# 4. Steiner Tree Problem - Approximation
def steiner_tree_approx(graph, terminals):
'''2-approximation for Steiner tree in graphs'''
import heapq
# Build complete graph on terminals with shortest paths
terminal_graph = {}
for s in terminals:
# Dijkstra from s
dist = {s: 0}
pq = [(0, s)]
while pq:
d, u = heapq.heappop(pq)
if d > dist.get(u, float('inf')):
continue
for v, w in graph.get(u, []):
if dist.get(v, float('inf')) > d + w:
dist[v] = d + w
heapq.heappush(pq, (d + w, v))
terminal_graph[s] = [(t, dist[t]) for t in terminals if t != s]
# Find MST on terminal graph
mst_edges = []
visited = {terminals[0]}
pq = [(d, terminals[0], t) for t, d in terminal_graph[terminals[0]]]
heapq.heapify(pq)
while pq and len(visited) < len(terminals):
d, u, v = heapq.heappop(pq)
if v in visited:
continue
visited.add(v)
mst_edges.append((u, v, d))
for t, dist in terminal_graph[v]:
if t not in visited:
heapq.heappush(pq, (dist, v, t))
return mst_edges
# 5. Graph Isomorphism - Canonical Labeling
class GraphIsomorphism:
'''Check if two graphs are isomorphic'''
def __init__(self, g1, g2, n):
self.g1 = g1
self.g2 = g2
self.n = n
def are_isomorphic(self):
'''Check isomorphism using backtracking'''
# Quick checks
if not self._compatible_degrees():
return False
# Try to find mapping
mapping = {}
return self._backtrack(0, mapping, set())
def _compatible_degrees(self):
'''Check if degree sequences match'''
deg1 = sorted([len(self.g1.get(i, [])) for i in range(self.n)])
deg2 = sorted([len(self.g2.get(i, [])) for i in range(self.n)])
return deg1 == deg2
def _backtrack(self, v1, mapping, used):
'''Try to extend mapping'''
if v1 == self.n:
return True
for v2 in range(self.n):
if v2 in used:
continue
if self._is_compatible(v1, v2, mapping):
mapping[v1] = v2
used.add(v2)
if self._backtrack(v1 + 1, mapping, used):
return True
del mapping[v1]
used.remove(v2)
return False
def _is_compatible(self, v1, v2, mapping):
'''Check if v1 -> v2 mapping is compatible'''
# Check degrees match
if len(self.g1.get(v1, [])) != len(self.g2.get(v2, [])):
return False
# Check edges to already mapped vertices
for u1 in self.g1.get(v1, []):
if u1 in mapping:
u2 = mapping[u1]
if (u2 not in self.g2.get(v2, [])) != (v2 not in self.g2.get(u2, [])):
return False
return TruePythonAnswer: Advanced graph theory: Hopcroft-Karp finds maximum bipartite matching in O(E√V); greedy coloring uses Δ+1 colors; vertex cover 2-approximation; Steiner tree 2-approximation via MST on terminals; graph isomorphism via backtracking with pruning based on degree sequences.
Q187. Apply linear programming and optimization techniques
# Linear Programming and Optimization Techniques
# 1. Simplex Algorithm (Simplified)
import numpy as np
class SimplexSolver:
'''Solve linear programs using simplex method'''
def __init__(self):
pass
def solve(self, c, A, b):
'''
Maximize c^T x subject to Ax <= b, x >= 0
c: objective coefficients (n,)
A: constraint matrix (m, n)
b: constraint bounds (m,)
'''
m, n = A.shape
# Convert to standard form with slack variables
# Maximize c^T x subject to Ax + s = b, x,s >= 0
# Tableau: [A | I | b]
# [c | 0 | 0]
tableau = np.zeros((m + 1, n + m + 1))
# Constraint rows
tableau[:m, :n] = A
tableau[:m, n:n+m] = np.eye(m)
tableau[:m, -1] = b
# Objective row (negate for maximization)
tableau[m, :n] = -c
# Basic variables are slack variables initially
basic = list(range(n, n + m))
while True:
# Find entering variable (most negative in objective row)
obj_row = tableau[m, :-1]
entering = np.argmin(obj_row)
if obj_row[entering] >= -1e-10:
break # Optimal solution found
# Find leaving variable (minimum ratio test)
ratios = []
for i in range(m):
if tableau[i, entering] > 1e-10:
ratios.append((tableau[i, -1] / tableau[i, entering], i))
if not ratios:
return None, None # Unbounded
_, leaving_row = min(ratios)
# Pivot
pivot = tableau[leaving_row, entering]
tableau[leaving_row] /= pivot
for i in range(m + 1):
if i != leaving_row:
tableau[i] -= tableau[i, entering] * tableau[leaving_row]
basic[leaving_row] = entering
# Extract solution
x = np.zeros(n)
for i, var in enumerate(basic):
if var < n:
x[var] = tableau[i, -1]
obj_value = tableau[m, -1]
return x, obj_value
# 2. Interior Point Method (Barrier Method)
class InteriorPointSolver:
'''Solve LP using barrier method'''
def solve(self, c, A, b, x0=None, mu=10, beta=0.5, epsilon=1e-6):
'''
Minimize c^T x subject to Ax = b, x > 0
Using barrier function -μ Σ log(x_i)
'''
n = len(c)
m = A.shape[0]
# Initialize x (feasible point)
if x0 is None:
x = np.ones(n)
else:
x = x0.copy()
while mu > epsilon:
# Solve barrier problem using Newton's method
for _ in range(100):
# Gradient and Hessian of barrier function
grad = c - mu / x
hess = np.diag(mu / (x ** 2))
# KKT system: [H A^T] [dx ] = [-g]
# [A 0 ] [dlam] [ 0]
kkt = np.block([
[hess, A.T],
[A, np.zeros((m, m))]
])
rhs = np.concatenate([-grad, np.zeros(m)])
solution = np.linalg.solve(kkt, rhs)
dx = solution[:n]
# Line search
alpha = 1.0
while np.any(x + alpha * dx <= 0):
alpha *= beta
x += alpha * dx
# Check convergence
if np.linalg.norm(dx) < epsilon:
break
# Decrease barrier parameter
mu *= beta
return x, np.dot(c, x)
# 3. Integer Linear Programming - Branch and Bound
class BranchAndBound:
'''Solve integer linear programs'''
def __init__(self, simplex_solver):
self.solver = simplex_solver
self.best_solution = None
self.best_value = float('inf')
def solve_ilp(self, c, A, b, integer_vars):
'''
Minimize c^T x subject to Ax <= b, x >= 0, x[i] integer for i in integer_vars
'''
# Solve LP relaxation
x, obj = self.solver.solve(-c, A, b) # Simplex maximizes
obj = -obj
if x is None or obj >= self.best_value:
return
# Check if solution is integer
all_integer = all(abs(x[i] - round(x[i])) < 1e-6
for i in integer_vars)
if all_integer:
if obj < self.best_value:
self.best_value = obj
self.best_solution = x
return
# Branch on fractional variable
for i in integer_vars:
if abs(x[i] - round(x[i])) > 1e-6:
# Branch: x[i] <= floor(x[i]) or x[i] >= ceil(x[i])
floor_val = int(x[i])
ceil_val = floor_val + 1
# Left branch: add constraint x[i] <= floor_val
new_A = np.vstack([A, np.eye(1, len(c), i)])
new_b = np.append(b, floor_val)
self.solve_ilp(c, new_A, new_b, integer_vars)
# Right branch: add constraint -x[i] <= -ceil_val
new_A = np.vstack([A, -np.eye(1, len(c), i)])
new_b = np.append(b, -ceil_val)
self.solve_ilp(c, new_A, new_b, integer_vars)
break
def get_solution(self):
return self.best_solution, self.best_value
# 4. Network Flow as Linear Program
def network_flow_lp(graph, source, sink):
'''Formulate max flow as linear program'''
# Variables: f[u][v] for each edge
# Maximize: Σ f[source][v] - Σ f[v][source]
# Subject to:
# f[u][v] <= capacity[u][v]
# Σ f[u][v] - Σ f[v][u] = 0 for all v != source, sink (flow conservation)
# f[u][v] >= 0
edges = []
edge_to_idx = {}
idx = 0
for u in graph:
for v, cap in graph[u]:
edges.append((u, v, cap))
edge_to_idx[(u, v)] = idx
idx += 1
n_edges = len(edges)
nodes = set()
for u, v, _ in edges:
nodes.add(u)
nodes.add(v)
# Objective: maximize flow out of source
c = np.zeros(n_edges)
for i, (u, v, _) in enumerate(edges):
if u == source:
c[i] = -1 # Maximize (negate for minimization)
elif v == source:
c[i] = 1
# Constraints: flow conservation + capacity
n_nodes = len(nodes)
A = []
b = []
# Flow conservation for each node except source/sink
for node in nodes:
if node in {source, sink}:
continue
row = np.zeros(n_edges)
for i, (u, v, _) in enumerate(edges):
if v == node:
row[i] = 1
elif u == node:
row[i] = -1
A.append(row)
b.append(0)
# Capacity constraints
for i, (u, v, cap) in enumerate(edges):
row = np.zeros(n_edges)
row[i] = 1
A.append(row)
b.append(cap)
A = np.array(A)
b = np.array(b)
return c, A, bPythonAnswer: Linear programming: Simplex method solves LPs via pivot operations achieving optimal vertex; interior point method uses barrier functions with Newton iterations; branch and bound solves ILP by branching on fractional variables; network flow formulates as LP with flow conservation constraints.
Q188. Solve constraint satisfaction problems efficiently
# Constraint Satisfaction Problems (CSP)
# 1. Generic CSP Solver with Backtracking
class CSPSolver:
'''Solve constraint satisfaction problems'''
def __init__(self, variables, domains, constraints):
'''
variables: list of variable names
domains: dict mapping variable -> list of possible values
constraints: list of (vars, constraint_func) tuples
'''
self.variables = variables
self.domains = domains
self.constraints = constraints
def solve(self):
'''Solve CSP using backtracking'''
assignment = {}
return self.backtrack(assignment)
def backtrack(self, assignment):
'''Backtracking search'''
# Check if assignment is complete
if len(assignment) == len(self.variables):
return assignment
# Select unassigned variable (using MRV heuristic)
var = self.select_unassigned_variable(assignment)
# Try values in order (using LCV heuristic)
for value in self.order_domain_values(var, assignment):
if self.is_consistent(var, value, assignment):
assignment[var] = value
# Forward checking
inferences = self.forward_check(var, value, assignment)
if inferences is not None:
result = self.backtrack(assignment)
if result is not None:
return result
# Restore domains
self.restore_domains(inferences)
del assignment[var]
return None
def select_unassigned_variable(self, assignment):
'''MRV: choose variable with fewest legal values'''
unassigned = [v for v in self.variables if v not in assignment]
return min(unassigned,
key=lambda var: len(self.get_legal_values(var, assignment)))
def order_domain_values(self, var, assignment):
'''LCV: prefer value that rules out fewest choices for neighbors'''
def count_conflicts(value):
conflicts = 0
test_assignment = assignment.copy()
test_assignment[var] = value
for other_var in self.variables:
if other_var not in test_assignment:
legal = self.get_legal_values(other_var, test_assignment)
conflicts += len(self.domains[other_var]) - len(legal)
return conflicts
return sorted(self.domains[var], key=count_conflicts)
def get_legal_values(self, var, assignment):
'''Get values for var that don't violate constraints'''
legal = []
for value in self.domains[var]:
if self.is_consistent(var, value, assignment):
legal.append(value)
return legal
def is_consistent(self, var, value, assignment):
'''Check if var=value is consistent with assignment'''
test_assignment = assignment.copy()
test_assignment[var] = value
for vars_in_constraint, constraint_func in self.constraints:
# Check if all variables in constraint are assigned
if all(v in test_assignment for v in vars_in_constraint):
values = [test_assignment[v] for v in vars_in_constraint]
if not constraint_func(*values):
return False
return True
def forward_check(self, var, value, assignment):
'''Remove inconsistent values from neighbors' domains'''
removed = {}
for other_var in self.variables:
if other_var == var or other_var in assignment:
continue
removed[other_var] = []
for other_value in list(self.domains[other_var]):
test = assignment.copy()
test[var] = value
test[other_var] = other_value
if not self.is_consistent(other_var, other_value, test):
self.domains[other_var].remove(other_value)
removed[other_var].append(other_value)
# Fail if any domain becomes empty
if not self.domains[other_var]:
self.restore_domains(removed)
return None
return removed
def restore_domains(self, removed):
'''Restore removed values to domains'''
for var, values in removed.items():
self.domains[var].extend(values)
# 2. N-Queens using CSP
def solve_n_queens_csp(n):
'''Solve N-Queens using CSP formulation'''
variables = list(range(n)) # One variable per row
domains = {i: list(range(n)) for i in range(n)} # Column for each row
constraints = []
# Add constraints for each pair of rows
for i in range(n):
for j in range(i + 1, n):
# Queens in different rows can't be in same column
# or on same diagonal
def make_constraint(row1, row2):
def constraint(col1, col2):
# Same column check
if col1 == col2:
return False
# Diagonal check
if abs(row1 - row2) == abs(col1 - col2):
return False
return True
return constraint
constraints.append(([i, j], make_constraint(i, j)))
solver = CSPSolver(variables, domains, constraints)
return solver.solve()
# 3. Sudoku using CSP
def solve_sudoku_csp(board):
'''Solve Sudoku using CSP'''
variables = [(i, j) for i in range(9) for j in range(9)]
domains = {}
for i in range(9):
for j in range(9):
if board[i][j] != 0:
domains[(i, j)] = [board[i][j]]
else:
domains[(i, j)] = list(range(1, 10))
constraints = []
# Row constraints
for i in range(9):
for j1 in range(9):
for j2 in range(j1 + 1, 9):
constraints.append((
[(i, j1), (i, j2)],
lambda a, b: a != b
))
# Column constraints
for j in range(9):
for i1 in range(9):
for i2 in range(i1 + 1, 9):
constraints.append((
[(i1, j), (i2, j)],
lambda a, b: a != b
))
# Box constraints
for box_i in range(3):
for box_j in range(3):
cells = [(i, j)
for i in range(box_i * 3, box_i * 3 + 3)
for j in range(box_j * 3, box_j * 3 + 3)]
for idx1 in range(len(cells)):
for idx2 in range(idx1 + 1, len(cells)):
constraints.append((
[cells[idx1], cells[idx2]],
lambda a, b: a != b
))
solver = CSPSolver(variables, domains, constraints)
solution = solver.solve()
if solution:
result = [[0] * 9 for _ in range(9)]
for (i, j), val in solution.items():
result[i][j] = val
return result
return None
# 4. Graph Coloring using CSP
def graph_coloring_csp(graph, k):
'''Color graph with k colors using CSP'''
nodes = list(graph.keys())
variables = nodes
domains = {node: list(range(k)) for node in nodes}
constraints = []
# Adjacent nodes must have different colors
for node in graph:
for neighbor in graph[node]:
if node < neighbor: # Avoid duplicates
constraints.append((
[node, neighbor],
lambda c1, c2: c1 != c2
))
solver = CSPSolver(variables, domains, constraints)
return solver.solve()
# 5. AC-3 Algorithm - Arc Consistency
class AC3:
'''Arc consistency algorithm for CSP preprocessing'''
def __init__(self, csp):
self.csp = csp
def enforce_arc_consistency(self):
'''Make CSP arc-consistent'''
# Initialize queue with all arcs
queue = []
for vars_tuple, _ in self.csp.constraints:
if len(vars_tuple) == 2:
queue.append(vars_tuple)
queue.append((vars_tuple[1], vars_tuple[0]))
while queue:
xi, xj = queue.pop(0)
if self.revise(xi, xj):
if not self.csp.domains[xi]:
return False # CSP is inconsistent
# Add arcs (xk, xi) for all neighbors xk of xi
for constraint_vars, _ in self.csp.constraints:
if xi in constraint_vars:
for xk in constraint_vars:
if xk != xi and xk != xj:
queue.append((xk, xi))
return True
def revise(self, xi, xj):
'''Remove values from xi's domain that are inconsistent with xj'''
revised = False
for vi in list(self.csp.domains[xi]):
# Check if there exists a value vj in xj's domain
# such that (vi, vj) satisfies the constraint
satisfies = False
for vj in self.csp.domains[xj]:
assignment = {xi: vi, xj: vj}
if self.csp.is_consistent(xi, vi, {xj: vj}):
satisfies = True
break
if not satisfies:
self.csp.domains[xi].remove(vi)
revised = True
return revisedPythonAnswer: CSP solving: Generic backtracking solver with MRV (minimum remaining values) variable selection and LCV (least constraining value) ordering; forward checking prunes domains; AC-3 enforces arc consistency; applies to N-Queens, Sudoku, graph coloring with constraint propagation.
Q189. Implement advanced machine learning algorithms from scratch
# Advanced Machine Learning Algorithms from Scratch
# 1. Gradient Boosting Decision Trees
import numpy as np
class GradientBoostingRegressor:
'''Gradient boosting for regression'''
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.trees = []
self.initial_prediction = None
def fit(self, X, y):
'''Fit gradient boosting model'''
# Initialize with mean
self.initial_prediction = np.mean(y)
predictions = np.full(len(y), self.initial_prediction)
for _ in range(self.n_estimators):
# Compute residuals (negative gradient for MSE)
residuals = y - predictions
# Fit tree to residuals
tree = DecisionTreeRegressor(max_depth=self.max_depth)
tree.fit(X, residuals)
# Update predictions
update = tree.predict(X)
predictions += self.learning_rate * update
self.trees.append(tree)
def predict(self, X):
'''Make predictions'''
predictions = np.full(len(X), self.initial_prediction)
for tree in self.trees:
predictions += self.learning_rate * tree.predict(X)
return predictions
class DecisionTreeRegressor:
'''Simple decision tree for regression'''
def __init__(self, max_depth=None):
self.max_depth = max_depth
self.tree = None
def fit(self, X, y):
'''Build decision tree'''
self.tree = self._build_tree(X, y, depth=0)
def _build_tree(self, X, y, depth):
'''Recursively build tree'''
if len(y) == 0:
return {'value': 0}
if self.max_depth is not None and depth >= self.max_depth:
return {'value': np.mean(y)}
# Find best split
best_split = self._find_best_split(X, y)
if best_split is None:
return {'value': np.mean(y)}
feature, threshold = best_split
# Split data
left_mask = X[:, feature] <= threshold
right_mask = ~left_mask
# Build subtrees
left_tree = self._build_tree(X[left_mask], y[left_mask], depth + 1)
right_tree = self._build_tree(X[right_mask], y[right_mask], depth + 1)
return {
'feature': feature,
'threshold': threshold,
'left': left_tree,
'right': right_tree
}
def _find_best_split(self, X, y):
'''Find best split using variance reduction'''
best_gain = 0
best_split = None
current_variance = np.var(y)
for feature in range(X.shape[1]):
thresholds = np.unique(X[:, feature])
for threshold in thresholds:
left_mask = X[:, feature] <= threshold
right_mask = ~left_mask
if np.sum(left_mask) == 0 or np.sum(right_mask) == 0:
continue
# Calculate variance reduction
left_var = np.var(y[left_mask])
right_var = np.var(y[right_mask])
weighted_var = (np.sum(left_mask) * left_var +
np.sum(right_mask) * right_var) / len(y)
gain = current_variance - weighted_var
if gain > best_gain:
best_gain = gain
best_split = (feature, threshold)
return best_split
def predict(self, X):
'''Make predictions'''
return np.array([self._predict_one(x, self.tree) for x in X])
def _predict_one(self, x, node):
'''Predict single instance'''
if 'value' in node:
return node['value']
if x[node['feature']] <= node['threshold']:
return self._predict_one(x, node['left'])
else:
return self._predict_one(x, node['right'])
# 2. K-Means++ Initialization
class KMeansPlusPlus:
'''K-means with smart initialization'''
def __init__(self, k, max_iters=100):
self.k = k
self.max_iters = max_iters
self.centroids = None
def fit(self, X):
'''Fit K-means model'''
# K-means++ initialization
self.centroids = self._initialize_centroids(X)
for _ in range(self.max_iters):
# Assign points to clusters
labels = self._assign_clusters(X)
# Update centroids
new_centroids = self._update_centroids(X, labels)
# Check convergence
if np.allclose(new_centroids, self.centroids):
break
self.centroids = new_centroids
return self
def _initialize_centroids(self, X):
'''K-means++ initialization - O(k) better than random'''
n_samples = len(X)
centroids = []
# Choose first centroid randomly
centroids.append(X[np.random.randint(n_samples)])
for _ in range(1, self.k):
# Compute distance to nearest centroid for each point
distances = np.array([
min(np.linalg.norm(x - c) for c in centroids)
for x in X
])
# Choose next centroid with probability proportional to distance²
probabilities = distances ** 2
probabilities /= probabilities.sum()
idx = np.random.choice(n_samples, p=probabilities)
centroids.append(X[idx])
return np.array(centroids)
def _assign_clusters(self, X):
'''Assign each point to nearest centroid'''
distances = np.linalg.norm(X[:, np.newaxis] - self.centroids, axis=2)
return np.argmin(distances, axis=1)
def _update_centroids(self, X, labels):
'''Update centroids as mean of assigned points'''
centroids = np.zeros((self.k, X.shape[1]))
for i in range(self.k):
cluster_points = X[labels == i]
if len(cluster_points) > 0:
centroids[i] = cluster_points.mean(axis=0)
else:
centroids[i] = self.centroids[i]
return centroids
def predict(self, X):
'''Predict cluster labels'''
return self._assign_clusters(X)
# 3. Principal Component Analysis
class PCA:
'''Principal Component Analysis from scratch'''
def __init__(self, n_components):
self.n_components = n_components
self.components = None
self.mean = None
self.explained_variance = None
def fit(self, X):
'''Fit PCA model'''
# Center data
self.mean = np.mean(X, axis=0)
X_centered = X - self.mean
# Compute covariance matrix
cov = np.cov(X_centered.T)
# Compute eigenvectors and eigenvalues
eigenvalues, eigenvectors = np.linalg.eig(cov)
# Sort by eigenvalue (descending)
idx = eigenvalues.argsort()[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# Store top k components
self.components = eigenvectors[:, :self.n_components]
self.explained_variance = eigenvalues[:self.n_components]
return self
def transform(self, X):
'''Project data onto principal components'''
X_centered = X - self.mean
return np.dot(X_centered, self.components)
def inverse_transform(self, X_transformed):
'''Reconstruct data from components'''
return np.dot(X_transformed, self.components.T) + self.mean
# 4. Support Vector Machine - SMO Algorithm (Simplified)
class SVM:
'''Support Vector Machine with SMO algorithm'''
def __init__(self, C=1.0, kernel='linear', gamma=0.1, max_iters=100):
self.C = C
self.kernel = kernel
self.gamma = gamma
self.max_iters = max_iters
self.alphas = None
self.b = 0
self.X_train = None
self.y_train = None
def _kernel_function(self, x1, x2):
'''Compute kernel function'''
if self.kernel == 'linear':
return np.dot(x1, x2)
elif self.kernel == 'rbf':
return np.exp(-self.gamma * np.linalg.norm(x1 - x2) ** 2)
return 0
def fit(self, X, y):
'''Train SVM using simplified SMO'''
n_samples = len(X)
self.X_train = X
self.y_train = y
self.alphas = np.zeros(n_samples)
self.b = 0
for _ in range(self.max_iters):
alpha_changed = 0
for i in range(n_samples):
# Compute error
prediction = sum(
self.alphas[j] * y[j] * self._kernel_function(X[j], X[i])
for j in range(n_samples)
) + self.b
error_i = prediction - y[i]
# Check KKT conditions
if ((y[i] * error_i < -0.001 and self.alphas[i] < self.C) or
(y[i] * error_i > 0.001 and self.alphas[i] > 0)):
# Select second alpha randomly
j = np.random.choice([k for k in range(n_samples) if k != i])
# Update alphas (simplified)
alpha_i_old = self.alphas[i]
alpha_j_old = self.alphas[j]
# ... (SMO update steps)
alpha_changed += 1
if alpha_changed == 0:
break
return self
def predict(self, X):
'''Make predictions'''
predictions = []
for x in X:
prediction = sum(
self.alphas[i] * self.y_train[i] *
self._kernel_function(self.X_train[i], x)
for i in range(len(self.X_train))
) + self.b
predictions.append(1 if prediction >= 0 else -1)
return np.array(predictions)PythonAnswer: ML algorithms from scratch: Gradient boosting builds ensemble of trees fitted to residuals; K-means++ initialization improves clustering by choosing centroids proportional to distance²; PCA via eigendecomposition of covariance matrix; SVM with SMO algorithm for quadratic programming optimization.
Q190. Synthesize algorithm design techniques and selection strategies
# Comprehensive Algorithm Design Techniques - Final Synthesis
# This question synthesizes all major algorithm design paradigms covered in Q1-Q189
# 1. Problem Analysis Framework
class ProblemAnalyzer:
'''Analyze problem and suggest appropriate algorithm design technique'''
@staticmethod
def analyze(problem_characteristics):
'''
Suggest best approach based on problem characteristics
problem_characteristics: dict with keys like:
- has_optimal_substructure: bool
- has_overlapping_subproblems: bool
- has_greedy_choice_property: bool
- is_optimization: bool
- input_size: int
- time_constraint: str ('polynomial', 'exponential_ok', 'real_time')
- space_constraint: str ('unlimited', 'limited', 'streaming')
'''
suggestions = []
# Dynamic Programming
if (problem_characteristics.get('has_optimal_substructure') and
problem_characteristics.get('has_overlapping_subproblems')):
suggestions.append({
'technique': 'Dynamic Programming',
'approaches': ['Memoization (top-down)', 'Tabulation (bottom-up)'],
'complexity': 'O(n * state_space)',
'examples': ['LCS', 'Knapsack', 'Edit Distance']
})
# Greedy
if (problem_characteristics.get('has_optimal_substructure') and
problem_characteristics.get('has_greedy_choice_property')):
suggestions.append({
'technique': 'Greedy Algorithm',
'approaches': ['Make locally optimal choice at each step'],
'complexity': 'Usually O(n log n) due to sorting',
'examples': ['Huffman Coding', 'Activity Selection', 'Dijkstra']
})
# Divide and Conquer
if problem_characteristics.get('can_divide_into_subproblems'):
suggestions.append({
'technique': 'Divide and Conquer',
'approaches': ['Split, Solve, Merge'],
'complexity': 'Often O(n log n)',
'examples': ['Merge Sort', 'Quick Sort', 'Binary Search']
})
# Backtracking
if (problem_characteristics.get('requires_exhaustive_search') and
problem_characteristics.get('can_prune_search_space')):
suggestions.append({
'technique': 'Backtracking',
'approaches': ['DFS with pruning', 'Constraint propagation'],
'complexity': 'Exponential but pruned',
'examples': ['N-Queens', 'Sudoku', 'Graph Coloring']
})
# Graph Algorithms
if problem_characteristics.get('is_graph_problem'):
graph_type = problem_characteristics.get('graph_type', 'general')
if graph_type == 'dag':
suggestions.append({
'technique': 'Topological Sort + DP',
'complexity': 'O(V + E)',
'examples': ['Longest Path in DAG', 'Course Schedule']
})
elif graph_type == 'tree':
suggestions.append({
'technique': 'Tree DP / DFS',
'complexity': 'O(V)',
'examples': ['Tree Diameter', 'LCA', 'Tree DP']
})
else:
suggestions.append({
'technique': 'Graph Traversal / Shortest Path',
'algorithms': ['BFS', 'DFS', 'Dijkstra', 'Bellman-Ford'],
'complexity': 'O(V + E) to O(VE)',
'examples': ['Connected Components', 'Shortest Path']
})
# Approximation Algorithms
if (problem_characteristics.get('is_np_hard') and
problem_characteristics.get('approximate_solution_ok')):
suggestions.append({
'technique': 'Approximation Algorithm',
'approaches': ['Greedy approximation', 'LP relaxation'],
'guarantee': 'Within factor of optimal',
'examples': ['Vertex Cover (2-approx)', 'TSP (2-approx)']
})
# Streaming / Online
if problem_characteristics.get('space_constraint') == 'streaming':
suggestions.append({
'technique': 'Streaming Algorithm',
'data_structures': ['Count-Min Sketch', 'HyperLogLog', 'Bloom Filter'],
'complexity': 'O(log n) space',
'examples': ['Frequency Estimation', 'Distinct Count']
})
# Randomized
if problem_characteristics.get('randomization_acceptable'):
suggestions.append({
'technique': 'Randomized Algorithm',
'types': ['Las Vegas (always correct)', 'Monte Carlo (probably correct)'],
'examples': ['QuickSort', 'Miller-Rabin', 'Skip List']
})
return suggestions
# 2. Complexity Analysis Helper
class ComplexityAnalyzer:
'''Analyze time and space complexity'''
@staticmethod
def analyze_time(algorithm_type, n, extra_params=None):
'''Estimate time complexity'''
complexities = {
'constant': ('O(1)', 1),
'logarithmic': ('O(log n)', np.log2(n) if n > 0 else 0),
'linear': ('O(n)', n),
'linearithmic': ('O(n log n)', n * np.log2(n) if n > 0 else 0),
'quadratic': ('O(n²)', n ** 2),
'cubic': ('O(n³)', n ** 3),
'exponential': ('O(2ⁿ)', 2 ** min(n, 20)), # Cap for display
'factorial': ('O(n!)', 'Too large to compute'),
}
return complexities.get(algorithm_type, ('Unknown', None))
@staticmethod
def compare_algorithms(algorithms, n):
'''Compare multiple algorithms at given input size'''
results = []
for name, complexity_type in algorithms:
notation, value = ComplexityAnalyzer.analyze_time(complexity_type, n)
results.append((name, notation, value))
return sorted(results, key=lambda x: x[2] if isinstance(x[2], (int, float)) else float('inf'))
# 3. Algorithm Selection Decision Tree
def select_algorithm(problem_type, constraints):
'''
Decision tree for algorithm selection
Returns recommended algorithm and rationale
'''
decision_tree = {
'sorting': {
'stable_required': {
True: ('Merge Sort', 'O(n log n) stable'),
False: {
'in_place_required': {
True: ('Quick Sort', 'O(n log n) average, in-place'),
False: ('Heap Sort', 'O(n log n) worst-case')
}
}
}
},
'searching': {
'data_sorted': {
True: ('Binary Search', 'O(log n)'),
False: ('Linear Search', 'O(n)')
}
},
'shortest_path': {
'negative_weights': {
True: ('Bellman-Ford', 'O(VE), handles negative'),
False: {
'single_source': {
True: ('Dijkstra', 'O(E log V)'),
False: ('Floyd-Warshall', 'O(V³) all-pairs')
}
}
}
},
'string_matching': {
'pattern_length': {
'small': ('KMP', 'O(n + m), preprocesses pattern'),
'large': ('Boyer-Moore', 'O(n/m) best case'),
'multiple_patterns': ('Aho-Corasick', 'O(n + m + z)')
}
}
}
# Navigate decision tree
current = decision_tree.get(problem_type)
for constraint_key, constraint_value in constraints.items():
if isinstance(current, dict) and constraint_key in current:
current = current[constraint_key]
if isinstance(current, dict):
current = current.get(constraint_value, current)
return current
# 4. Performance Profiling
import time
import tracemalloc
class PerformanceProfiler:
'''Profile algorithm performance'''
@staticmethod
def profile(func, *args, **kwargs):
'''Profile function execution'''
# Start memory tracking
tracemalloc.start()
# Time execution
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
# Get memory usage
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return {
'result': result,
'time_seconds': end_time - start_time,
'memory_current_mb': current / 1024 / 1024,
'memory_peak_mb': peak / 1024 / 1024
}
# 5. Algorithm Correctness Verification
class AlgorithmVerifier:
'''Verify algorithm correctness'''
@staticmethod
def verify_sorting(algorithm, test_cases=100):
'''Verify sorting algorithm'''
import random
for _ in range(test_cases):
arr = [random.randint(-1000, 1000) for _ in range(random.randint(0, 100))]
original = arr.copy()
result = algorithm(arr)
# Check sorted
if result != sorted(original):
return False, f"Failed on input: {original}"
return True, "All tests passed"
@staticmethod
def verify_graph_algorithm(algorithm, generate_graph, verify_property, test_cases=50):
'''Verify graph algorithm'''
for _ in range(test_cases):
graph = generate_graph()
result = algorithm(graph)
if not verify_property(graph, result):
return False, f"Failed on graph: {graph}"
return True, "All tests passed"
# Summary: When designing algorithms, consider:
# 1. Problem characteristics (optimal substructure, greedy property, etc.)
# 2. Constraints (time, space, online vs offline)
# 3. Input characteristics (sorted, size, distribution)
# 4. Solution quality (exact vs approximate)
# 5. Implementation complexity
# 6. Testability and verification
print('''
ALGORITHM DESIGN SYNTHESIS:
1. DIVIDE & CONQUER: Break into subproblems, solve, merge
- Merge Sort O(n log n), Binary Search O(log n)
2. DYNAMIC PROGRAMMING: Optimal substructure + overlapping subproblems
- Bottom-up or memoization, O(states × transitions)
3. GREEDY: Locally optimal choices lead to global optimum
- Requires proof of greedy choice property
4. BACKTRACKING: Exhaustive search with pruning
- DFS with constraint checking
5. GRAPH ALGORITHMS: Model as graph, choose appropriate traversal
- BFS/DFS O(V+E), Dijkstra O(E log V), Flow O(VE²)
6. STRING ALGORITHMS: Pattern matching, suffix structures
- KMP O(n+m), Suffix Array O(n log n)
7. COMPUTATIONAL GEOMETRY: Sweep line, divide & conquer
- Convex Hull O(n log n), Line Intersection O((n+k) log n)
8. APPROXIMATION: For NP-hard, guarantee factor of optimal
- Vertex Cover 2-approx, Set Cover O(log n)-approx
9. RANDOMIZED: Trade certainty for expected performance
- QuickSort O(n log n) expected, Bloom filters
10. STREAMING: Sublinear space for massive data
- Count-Min Sketch, HyperLogLog O(log n) space
''')PythonAnswer: Algorithm design synthesis: Problem analyzer suggests techniques based on characteristics (optimal substructure, greedy property, constraints); decision tree for algorithm selection; complexity analyzer compares options; performance profiler tracks time/space; verifier ensures correctness. Key paradigms: D&C, DP, Greedy, Backtracking, Graph, String, Geometry, Approximation, Randomized, Streaming.
Q191. Implement advanced number theory algorithms
# Advanced Number Theory and Cryptography
# 1. Extended Euclidean Algorithm
def extended_gcd(a, b):
'''
Find gcd(a,b) and coefficients x,y such that ax + by = gcd(a,b)
Used for modular inverse computation
'''
if b == 0:
return a, 1, 0
gcd, x1, y1 = extended_gcd(b, a % b)
# Update x and y using results from recursive call
x = y1
y = x1 - (a // b) * y1
return gcd, x, y
def modular_inverse(a, m):
'''Find modular inverse of a under modulo m using Extended Euclidean'''
gcd, x, _ = extended_gcd(a, m)
if gcd != 1:
return None # Inverse doesn't exist
return (x % m + m) % m
# 2. Chinese Remainder Theorem
def chinese_remainder_theorem(remainders, moduli):
'''
Solve system of congruences:
x ≡ a1 (mod m1)
x ≡ a2 (mod m2)
...
x ≡ an (mod mn)
'''
# Compute product of all moduli
M = 1
for m in moduli:
M *= m
x = 0
for ai, mi in zip(remainders, moduli):
Mi = M // mi
# Find modular inverse of Mi mod mi
yi = modular_inverse(Mi, mi)
if yi is None:
return None
x += ai * Mi * yi
return x % M
# Example usage
# Solve: x ≡ 2 (mod 3), x ≡ 3 (mod 5), x ≡ 2 (mod 7)
# Result: x = 23
# 3. Fast Modular Exponentiation
def mod_exp(base, exp, mod):
'''Compute (base^exp) % mod efficiently in O(log exp)'''
result = 1
base = base % mod
while exp > 0:
# If exp is odd, multiply base with result
if exp & 1:
result = (result * base) % mod
# exp must be even now
exp >>= 1 # exp = exp // 2
base = (base * base) % mod
return result
# 4. Miller-Rabin Primality Test
import random
def miller_rabin(n, k=5):
'''
Probabilistic primality test
Returns False if n is composite, True if n is probably prime
Error probability: 4^(-k)
'''
if n < 2:
return False
# Handle small primes
small_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
if n in small_primes:
return True
if any(n % p == 0 for p in small_primes):
return False
# Write n-1 as 2^r * d
r, d = 0, n - 1
while d % 2 == 0:
r += 1
d //= 2
# Witness loop
for _ in range(k):
a = random.randrange(2, n - 1)
x = mod_exp(a, d, n)
if x == 1 or x == n - 1:
continue
for _ in range(r - 1):
x = mod_exp(x, 2, n)
if x == n - 1:
break
else:
return False
return True
# 5. RSA Encryption (Educational Implementation)
class RSA:
'''Simple RSA implementation for educational purposes'''
def __init__(self, bits=16):
'''Generate RSA keys (use small bits for demo)'''
self.bits = bits
self.public_key = None
self.private_key = None
self._generate_keys()
def _generate_prime(self):
'''Generate a prime number'''
while True:
p = random.randrange(2**(self.bits-1), 2**self.bits)
if miller_rabin(p):
return p
def _generate_keys(self):
'''Generate public and private keys'''
# Generate two distinct primes
p = self._generate_prime()
q = self._generate_prime()
while p == q:
q = self._generate_prime()
# Compute n and φ(n)
n = p * q
phi = (p - 1) * (q - 1)
# Choose e such that 1 < e < φ(n) and gcd(e, φ(n)) = 1
e = 65537 # Common choice
while extended_gcd(e, phi)[0] != 1:
e = random.randrange(2, phi)
# Compute d = e^(-1) mod φ(n)
d = modular_inverse(e, phi)
self.public_key = (e, n)
self.private_key = (d, n)
def encrypt(self, message, public_key=None):
'''Encrypt message using public key'''
if public_key is None:
public_key = self.public_key
e, n = public_key
# Convert message to number (simplified)
if isinstance(message, str):
message = int.from_bytes(message.encode(), 'big')
# Encrypt: c = m^e mod n
ciphertext = mod_exp(message, e, n)
return ciphertext
def decrypt(self, ciphertext):
'''Decrypt ciphertext using private key'''
d, n = self.private_key
# Decrypt: m = c^d mod n
message = mod_exp(ciphertext, d, n)
return message
# 6. Discrete Logarithm - Baby-Step Giant-Step
def baby_step_giant_step(g, h, p):
'''
Solve g^x ≡ h (mod p) for x
Time: O(√p), Space: O(√p)
'''
import math
n = int(math.sqrt(p)) + 1
# Baby step: compute g^j mod p for j = 0, 1, ..., n-1
baby_steps = {}
power = 1
for j in range(n):
if power == h:
return j
baby_steps[power] = j
power = (power * g) % p
# Giant step: compute h * (g^(-n))^i for i = 1, 2, ...
# g^(-n) = (g^n)^(-1) mod p
factor = mod_exp(g, n * (p - 2), p) # Using Fermat's little theorem
gamma = h
for i in range(1, n + 1):
gamma = (gamma * factor) % p
if gamma in baby_steps:
return i * n + baby_steps[gamma]
return None
# 7. Pollard's Rho Algorithm for Integer Factorization
def pollard_rho(n):
'''Factor n using Pollard's rho algorithm'''
if n % 2 == 0:
return 2
x = random.randint(2, n - 1)
y = x
c = random.randint(1, n - 1)
d = 1
def f(x):
return (x * x + c) % n
while d == 1:
x = f(x)
y = f(f(y))
d = extended_gcd(abs(x - y), n)[0]
if d != n:
return d
return None
# 8. Euler's Totient Function
def euler_totient(n):
'''Compute φ(n) - count of numbers ≤ n coprime to n'''
result = n
# Find all prime factors
p = 2
while p * p <= n:
if n % p == 0:
# Remove factor p
while n % p == 0:
n //= p
# Apply formula: φ(n) = n * (1 - 1/p)
result -= result // p
p += 1
if n > 1:
result -= result // n
return result
# 9. Primitive Root Modulo n
def primitive_root(p):
'''Find a primitive root modulo prime p'''
if not miller_rabin(p):
return None
phi = p - 1
# Find prime factors of phi
factors = set()
n = phi
for i in range(2, int(n**0.5) + 1):
while n % i == 0:
factors.add(i)
n //= i
if n > 1:
factors.add(n)
# Check each candidate
for g in range(2, p):
is_primitive = True
for factor in factors:
if mod_exp(g, phi // factor, p) == 1:
is_primitive = False
break
if is_primitive:
return g
return NonePythonAnswer: Number theory: Extended GCD finds Bézout coefficients for modular inverse; Chinese Remainder Theorem solves congruence systems; fast modular exponentiation in O(log n); Miller-Rabin probabilistic primality with error 4^(-k); RSA encryption/decryption; baby-step giant-step for discrete log in O(√p); Pollard’s rho factorization; Euler’s totient function.
Q192. Implement advanced matrix algorithms and linear algebra
# Advanced Matrix Algorithms and Linear Algebra
import numpy as np
# 1. Strassen's Matrix Multiplication
def strassen_multiply(A, B):
'''
Multiply matrices using Strassen's algorithm
Time: O(n^2.807) vs O(n^3) for standard multiplication
'''
n = len(A)
# Base case: use standard multiplication for small matrices
if n <= 64:
return np.dot(A, B)
# Ensure n is power of 2
if n & (n - 1) != 0:
# Pad to next power of 2
next_power = 1
while next_power < n:
next_power *= 2
A_padded = np.zeros((next_power, next_power))
B_padded = np.zeros((next_power, next_power))
A_padded[:n, :n] = A
B_padded[:n, :n] = B
result = strassen_multiply(A_padded, B_padded)
return result[:n, :n]
# Split matrices into quadrants
mid = n // 2
A11 = A[:mid, :mid]
A12 = A[:mid, mid:]
A21 = A[mid:, :mid]
A22 = A[mid:, mid:]
B11 = B[:mid, :mid]
B12 = B[:mid, mid:]
B21 = B[mid:, :mid]
B22 = B[mid:, mid:]
# Compute 7 products (Strassen's formulas)
M1 = strassen_multiply(A11 + A22, B11 + B22)
M2 = strassen_multiply(A21 + A22, B11)
M3 = strassen_multiply(A11, B12 - B22)
M4 = strassen_multiply(A22, B21 - B11)
M5 = strassen_multiply(A11 + A12, B22)
M6 = strassen_multiply(A21 - A11, B11 + B12)
M7 = strassen_multiply(A12 - A22, B21 + B22)
# Combine results
C11 = M1 + M4 - M5 + M7
C12 = M3 + M5
C21 = M2 + M4
C22 = M1 - M2 + M3 + M6
# Construct result matrix
C = np.zeros((n, n))
C[:mid, :mid] = C11
C[:mid, mid:] = C12
C[mid:, :mid] = C21
C[mid:, mid:] = C22
return C
# 2. LU Decomposition with Partial Pivoting
def lu_decomposition(A):
'''
Decompose A = PLU where P is permutation, L is lower, U is upper
Used for solving linear systems efficiently
'''
n = len(A)
A = A.copy().astype(float)
# Initialize permutation matrix
P = np.eye(n)
L = np.eye(n)
U = np.zeros((n, n))
for k in range(n):
# Find pivot
pivot_row = k + np.argmax(np.abs(A[k:, k]))
# Swap rows in A and P
if pivot_row != k:
A[[k, pivot_row]] = A[[pivot_row, k]]
P[[k, pivot_row]] = P[[pivot_row, k]]
if k > 0:
L[[k, pivot_row], :k] = L[[pivot_row, k], :k]
# Compute L and U for this column
for i in range(k + 1, n):
L[i, k] = A[i, k] / A[k, k]
A[i, k:] -= L[i, k] * A[k, k:]
U = A
return P, L, U
def solve_linear_system(A, b):
'''Solve Ax = b using LU decomposition'''
P, L, U = lu_decomposition(A)
# Solve PLUx = b
# First: Ly = Pb (forward substitution)
Pb = np.dot(P, b)
y = np.zeros(len(b))
for i in range(len(b)):
y[i] = Pb[i] - np.dot(L[i, :i], y[:i])
# Then: Ux = y (backward substitution)
x = np.zeros(len(b))
for i in range(len(b) - 1, -1, -1):
x[i] = (y[i] - np.dot(U[i, i+1:], x[i+1:])) / U[i, i]
return x
# 3. QR Decomposition using Gram-Schmidt
def qr_decomposition_gs(A):
'''
Decompose A = QR using Gram-Schmidt orthogonalization
Q is orthogonal, R is upper triangular
'''
m, n = A.shape
Q = np.zeros((m, n))
R = np.zeros((n, n))
for j in range(n):
# Start with j-th column of A
v = A[:, j].copy()
# Subtract projections onto previous columns
for i in range(j):
R[i, j] = np.dot(Q[:, i], A[:, j])
v -= R[i, j] * Q[:, i]
# Normalize
R[j, j] = np.linalg.norm(v)
Q[:, j] = v / R[j, j]
return Q, R
# 4. Eigenvalue Computation - Power Iteration
def power_iteration(A, num_iterations=100):
'''
Find dominant eigenvalue and eigenvector using power iteration
Converges to eigenvector with largest |λ|
'''
n = A.shape[0]
# Start with random vector
v = np.random.rand(n)
v = v / np.linalg.norm(v)
for _ in range(num_iterations):
# Multiply by A
v_new = np.dot(A, v)
# Normalize
v_new = v_new / np.linalg.norm(v_new)
v = v_new
# Compute eigenvalue: λ = v^T A v
eigenvalue = np.dot(v, np.dot(A, v))
return eigenvalue, v
# 5. Singular Value Decomposition (SVD) Application
def low_rank_approximation(A, k):
'''
Compute best rank-k approximation of A using SVD
Minimizes Frobenius norm ||A - A_k||_F
'''
# Compute SVD: A = UΣV^T
U, s, Vt = np.linalg.svd(A, full_matrices=False)
# Keep only top k singular values
s_k = np.zeros_like(s)
s_k[:k] = s[:k]
# Reconstruct approximation
Sigma_k = np.diag(s_k)
A_k = U @ Sigma_k @ Vt
return A_k
# 6. Matrix Exponential
def matrix_exponential(A, terms=20):
'''
Compute e^A = I + A + A²/2! + A³/3! + ...
Used in solving differential equations
'''
n = A.shape[0]
result = np.eye(n)
A_power = np.eye(n)
factorial = 1
for k in range(1, terms):
factorial *= k
A_power = A_power @ A
result += A_power / factorial
return result
# 7. Cholesky Decomposition
def cholesky_decomposition(A):
'''
Decompose positive definite A = LL^T
More efficient than LU for symmetric positive definite matrices
'''
n = A.shape[0]
L = np.zeros((n, n))
for i in range(n):
for j in range(i + 1):
if i == j:
# Diagonal element
sum_sq = sum(L[i, k]**2 for k in range(j))
L[i, j] = np.sqrt(A[i, i] - sum_sq)
else:
# Off-diagonal element
sum_prod = sum(L[i, k] * L[j, k] for k in range(j))
L[i, j] = (A[i, j] - sum_prod) / L[j, j]
return L
# 8. Matrix Inversion using Gauss-Jordan
def matrix_inverse_gauss_jordan(A):
'''Compute A^(-1) using Gauss-Jordan elimination'''
n = A.shape[0]
# Create augmented matrix [A | I]
augmented = np.hstack([A.copy().astype(float), np.eye(n)])
# Forward elimination
for i in range(n):
# Find pivot
pivot_row = i + np.argmax(np.abs(augmented[i:, i]))
if augmented[pivot_row, i] == 0:
raise ValueError("Matrix is singular")
# Swap rows
augmented[[i, pivot_row]] = augmented[[pivot_row, i]]
# Scale pivot row
augmented[i] /= augmented[i, i]
# Eliminate column
for j in range(n):
if i != j:
augmented[j] -= augmented[j, i] * augmented[i]
# Extract inverse from right half
return augmented[:, n:]
# 9. Determinant using LU Decomposition
def determinant_lu(A):
'''Compute determinant using LU decomposition'''
P, L, U = lu_decomposition(A)
# det(A) = det(P) * det(L) * det(U)
# det(P) = (-1)^(number of swaps)
# det(L) = 1 (unit diagonal)
# det(U) = product of diagonal elements
det_P = np.linalg.det(P)
det_U = np.prod(np.diag(U))
return det_P * det_UPythonAnswer: Matrix algorithms: Strassen’s multiplication achieves O(n^2.807); LU decomposition with partial pivoting solves linear systems efficiently; QR via Gram-Schmidt for least squares; power iteration finds dominant eigenvalue; SVD for low-rank approximation; matrix exponential for differential equations; Cholesky for positive definite matrices; Gauss-Jordan inversion.
Q193. Master advanced data compression algorithms
# Advanced Data Compression Algorithms
# 1. Huffman Coding - Optimal Prefix Code
import heapq
from collections import Counter, defaultdict
class HuffmanNode:
def __init__(self, char=None, freq=0, left=None, right=None):
self.char = char
self.freq = freq
self.left = left
self.right = right
def __lt__(self, other):
return self.freq < other.freq
class HuffmanCoding:
'''Huffman coding for lossless compression'''
def __init__(self):
self.codes = {}
self.reverse_codes = {}
self.root = None
def build_tree(self, text):
'''Build Huffman tree from frequency analysis'''
# Count frequencies
freq = Counter(text)
# Create priority queue of leaf nodes
heap = [HuffmanNode(char, f) for char, f in freq.items()]
heapq.heapify(heap)
# Build tree bottom-up
while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
merged = HuffmanNode(
freq=left.freq + right.freq,
left=left,
right=right
)
heapq.heappush(heap, merged)
self.root = heap[0]
# Generate codes
self._generate_codes(self.root, "")
def _generate_codes(self, node, code):
'''Generate binary codes for each character'''
if node.char is not None:
self.codes[node.char] = code
self.reverse_codes[code] = node.char
return
if node.left:
self._generate_codes(node.left, code + "0")
if node.right:
self._generate_codes(node.right, code + "1")
def encode(self, text):
'''Encode text using Huffman codes'''
if not self.codes:
self.build_tree(text)
encoded = "".join(self.codes[char] for char in text)
return encoded
def decode(self, encoded):
'''Decode binary string using Huffman tree'''
decoded = []
current = self.root
for bit in encoded:
if bit == '0':
current = current.left
else:
current = current.right
if current.char is not None:
decoded.append(current.char)
current = self.root
return "".join(decoded)
# 2. LZW (Lempel-Ziv-Welch) Compression
class LZW:
'''LZW compression - builds dictionary dynamically'''
@staticmethod
def compress(data):
'''Compress data using LZW algorithm'''
# Initialize dictionary with single characters
dict_size = 256
dictionary = {chr(i): i for i in range(dict_size)}
result = []
current = ""
for char in data:
combined = current + char
if combined in dictionary:
current = combined
else:
# Output code for current
result.append(dictionary[current])
# Add combined to dictionary
dictionary[combined] = dict_size
dict_size += 1
current = char
# Output code for remaining string
if current:
result.append(dictionary[current])
return result
@staticmethod
def decompress(compressed):
'''Decompress LZW-compressed data'''
# Initialize dictionary
dict_size = 256
dictionary = {i: chr(i) for i in range(dict_size)}
result = []
previous = chr(compressed[0])
result.append(previous)
for code in compressed[1:]:
if code in dictionary:
entry = dictionary[code]
elif code == dict_size:
entry = previous + previous[0]
else:
raise ValueError("Invalid compressed data")
result.append(entry)
# Add to dictionary
dictionary[dict_size] = previous + entry[0]
dict_size += 1
previous = entry
return "".join(result)
# 3. Run-Length Encoding
def run_length_encode(data):
'''Simple run-length encoding'''
if not data:
return []
encoded = []
current_char = data[0]
count = 1
for char in data[1:]:
if char == current_char:
count += 1
else:
encoded.append((current_char, count))
current_char = char
count = 1
encoded.append((current_char, count))
return encoded
def run_length_decode(encoded):
'''Decode run-length encoded data'''
return "".join(char * count for char, count in encoded)
# 4. Burrows-Wheeler Transform
def burrows_wheeler_transform(text):
'''
BWT: Reversible transformation that groups similar characters
Used as preprocessing for compression
'''
# Add end-of-text marker
text += '$'
# Generate all rotations
rotations = [text[i:] + text[:i] for i in range(len(text))]
# Sort rotations
rotations.sort()
# Take last column
bwt = "".join(rotation[-1] for rotation in rotations)
# Find index of original string
original_index = rotations.index(text)
return bwt, original_index
def inverse_burrows_wheeler(bwt, original_index):
'''Invert BWT to recover original text'''
n = len(bwt)
# Build table of (char, original_index) pairs
table = sorted((bwt[i], i) for i in range(n))
# Reconstruct original text
result = []
index = original_index
for _ in range(n):
char, index = table[index]
result.append(char)
# Remove end marker and reverse
return "".join(result[:-1])
# 5. Arithmetic Coding (Simplified)
class ArithmeticCoding:
'''Arithmetic coding for near-optimal compression'''
def __init__(self):
self.precision = 32 # bits of precision
def encode(self, text):
'''Encode text using arithmetic coding'''
# Calculate symbol frequencies
freq = Counter(text)
total = len(text)
# Build cumulative probability ranges
ranges = {}
cumulative = 0
for char in sorted(freq.keys()):
prob = freq[char] / total
ranges[char] = (cumulative, cumulative + prob)
cumulative += prob
# Encode
low = 0.0
high = 1.0
for char in text:
range_width = high - low
char_low, char_high = ranges[char]
high = low + range_width * char_high
low = low + range_width * char_low
# Return value in final range
return (low + high) / 2, ranges
def decode(self, encoded_value, ranges, length):
'''Decode arithmetic coded value'''
# Invert ranges for lookup
inverse_ranges = {v: k for k, v in ranges.items()}
result = []
value = encoded_value
for _ in range(length):
# Find which range value falls into
for (low, high), char in inverse_ranges.items():
if low <= value < high:
result.append(char)
# Update value for next iteration
range_width = high - low
value = (value - low) / range_width
break
return "".join(result)
# 6. Delta Encoding
def delta_encode(data):
'''Encode data as differences between consecutive values'''
if not data:
return []
encoded = [data[0]]
for i in range(1, len(data)):
encoded.append(data[i] - data[i-1])
return encoded
def delta_decode(encoded):
'''Decode delta-encoded data'''
if not encoded:
return []
decoded = [encoded[0]]
for i in range(1, len(encoded)):
decoded.append(decoded[-1] + encoded[i])
return decoded
# 7. Dictionary-Based Compression
class DictionaryCompression:
'''Generic dictionary-based compression'''
def __init__(self, window_size=4096, lookahead_size=18):
self.window_size = window_size
self.lookahead_size = lookahead_size
def compress(self, text):
'''LZ77-style compression'''
result = []
pos = 0
while pos < len(text):
# Find longest match in sliding window
best_length = 0
best_offset = 0
window_start = max(0, pos - self.window_size)
for offset in range(1, pos - window_start + 1):
length = 0
while (length < self.lookahead_size and
pos + length < len(text) and
text[pos - offset + length] == text[pos + length]):
length += 1
if length > best_length:
best_length = length
best_offset = offset
if best_length > 2: # Worth encoding as reference
result.append((best_offset, best_length))
pos += best_length
else:
result.append(text[pos])
pos += 1
return resultPythonAnswer: Compression algorithms: Huffman coding builds optimal prefix tree from frequencies; LZW dynamically builds dictionary during compression; Run-length encodes repeated characters; Burrows-Wheeler Transform groups similar chars as preprocessing; Arithmetic coding achieves near-entropy compression; Delta encoding for sequential data; LZ77 dictionary-based with sliding window.
Q194. Apply game theory and solve combinatorial games
# Game Theory and Combinatorial Game Algorithms
# 1. Nim Game - Sprague-Grundy Theorem
def nim_game(piles):
'''
Determine winner in Nim game
XOR of all pile sizes determines winner
If XOR = 0: losing position, else: winning position
'''
xor_sum = 0
for pile in piles:
xor_sum ^= pile
return "First player wins" if xor_sum != 0 else "Second player wins"
def find_winning_move(piles):
'''Find winning move in Nim game'''
xor_sum = 0
for pile in piles:
xor_sum ^= pile
if xor_sum == 0:
return None # Already losing position
# Find pile where removal creates XOR = 0
for i, pile in enumerate(piles):
target = pile ^ xor_sum
if target < pile:
return (i, pile - target) # (pile_index, stones_to_remove)
return None
# 2. Grundy Numbers (Nimbers)
def grundy_number(position, mex_cache=None):
'''
Compute Grundy number (nimber) for game position
Grundy number = MEX of reachable positions
MEX = Minimum EXcludant (smallest non-negative integer not in set)
'''
if mex_cache is None:
mex_cache = {}
if position in mex_cache:
return mex_cache[position]
# Base case
if position == 0:
return 0
# Find all reachable positions
reachable = set()
# Example: can remove 1, 2, or 3 stones
for move in [1, 2, 3]:
if position >= move:
next_position = position - move
reachable.add(grundy_number(next_position, mex_cache))
# Compute MEX
mex = 0
while mex in reachable:
mex += 1
mex_cache[position] = mex
return mex
# 3. Minimax Algorithm with Alpha-Beta Pruning
class TicTacToe:
'''Tic-tac-toe with optimal AI using minimax'''
def __init__(self):
self.board = [[' ' for _ in range(3)] for _ in range(3)]
def is_winner(self, player):
'''Check if player has won'''
# Check rows
for row in self.board:
if all(cell == player for cell in row):
return True
# Check columns
for col in range(3):
if all(self.board[row][col] == player for row in range(3)):
return True
# Check diagonals
if all(self.board[i][i] == player for i in range(3)):
return True
if all(self.board[i][2-i] == player for i in range(3)):
return True
return False
def is_full(self):
'''Check if board is full'''
return all(cell != ' ' for row in self.board for cell in row)
def get_available_moves(self):
'''Get list of available positions'''
moves = []
for i in range(3):
for j in range(3):
if self.board[i][j] == ' ':
moves.append((i, j))
return moves
def minimax(self, is_maximizing, alpha=float('-inf'), beta=float('inf')):
'''
Minimax with alpha-beta pruning
Returns: (best_score, best_move)
'''
# Terminal states
if self.is_winner('X'):
return 1, None
if self.is_winner('O'):
return -1, None
if self.is_full():
return 0, None
moves = self.get_available_moves()
if is_maximizing:
best_score = float('-inf')
best_move = None
for move in moves:
i, j = move
self.board[i][j] = 'X'
score, _ = self.minimax(False, alpha, beta)
self.board[i][j] = ' '
if score > best_score:
best_score = score
best_move = move
alpha = max(alpha, score)
if beta <= alpha:
break # Beta cutoff
return best_score, best_move
else:
best_score = float('inf')
best_move = None
for move in moves:
i, j = move
self.board[i][j] = 'O'
score, _ = self.minimax(True, alpha, beta)
self.board[i][j] = ' '
if score < best_score:
best_score = score
best_move = move
beta = min(beta, score)
if beta <= alpha:
break # Alpha cutoff
return best_score, best_move
# 4. Monte Carlo Tree Search (MCTS)
import math
import random
class MCTSNode:
'''Node in MCTS tree'''
def __init__(self, state, parent=None):
self.state = state
self.parent = parent
self.children = []
self.visits = 0
self.wins = 0
def ucb1(self, exploration=1.41):
'''UCB1 formula for node selection'''
if self.visits == 0:
return float('inf')
exploitation = self.wins / self.visits
exploration_term = exploration * math.sqrt(math.log(self.parent.visits) / self.visits)
return exploitation + exploration_term
def is_fully_expanded(self):
'''Check if all children have been expanded'''
return len(self.children) == len(self.state.get_legal_moves())
class MCTS:
'''Monte Carlo Tree Search for game playing'''
def __init__(self, iterations=1000):
self.iterations = iterations
def search(self, root_state):
'''Run MCTS to find best move'''
root = MCTSNode(root_state)
for _ in range(self.iterations):
# Selection
node = self._select(root)
# Expansion
if not node.state.is_terminal():
node = self._expand(node)
# Simulation
reward = self._simulate(node.state)
# Backpropagation
self._backpropagate(node, reward)
# Return best child
return max(root.children, key=lambda c: c.visits)
def _select(self, node):
'''Select most promising node using UCB1'''
while not node.state.is_terminal() and node.is_fully_expanded():
node = max(node.children, key=lambda c: c.ucb1())
return node
def _expand(self, node):
'''Expand node by adding a child'''
untried_moves = [m for m in node.state.get_legal_moves()
if not any(c.state.last_move == m for c in node.children)]
if untried_moves:
move = random.choice(untried_moves)
new_state = node.state.make_move(move)
child = MCTSNode(new_state, parent=node)
node.children.append(child)
return child
return node
def _simulate(self, state):
'''Simulate random playout from state'''
current_state = state.copy()
while not current_state.is_terminal():
moves = current_state.get_legal_moves()
move = random.choice(moves)
current_state = current_state.make_move(move)
return current_state.get_reward()
def _backpropagate(self, node, reward):
'''Backpropagate reward up the tree'''
while node is not None:
node.visits += 1
node.wins += reward
node = node.parent
# 5. Nash Equilibrium for 2-Player Games
def find_nash_equilibrium_2x2(payoff_matrix_A, payoff_matrix_B):
'''
Find Nash equilibrium in 2x2 game
payoff_matrix_A[i][j] = payoff to player A when A plays i, B plays j
'''
# Check for pure strategy Nash equilibria
for i in range(2):
for j in range(2):
# Check if (i, j) is Nash equilibrium
# Player A: is i best response to B playing j?
a_payoffs = [payoff_matrix_A[k][j] for k in range(2)]
a_best = i == a_payoffs.index(max(a_payoffs))
# Player B: is j best response to A playing i?
b_payoffs = [payoff_matrix_B[i][k] for k in range(2)]
b_best = j == b_payoffs.index(max(b_payoffs))
if a_best and b_best:
return ('pure', i, j)
# Find mixed strategy Nash equilibrium
# Player A mixes with probability p, Player B with probability q
# A's expected payoff equations
# When B plays mixed(q): p*A[0][0]*q + p*A[0][1]*(1-q) = (1-p)*A[1][0]*q + (1-p)*A[1][1]*(1-q)
# Solve for q that makes A indifferent
num = payoff_matrix_A[1][1] - payoff_matrix_A[1][0]
den = (payoff_matrix_A[0][0] - payoff_matrix_A[0][1] -
payoff_matrix_A[1][0] + payoff_matrix_A[1][1])
if den != 0:
q = num / den
else:
q = 0.5
# Solve for p that makes B indifferent
num = payoff_matrix_B[1][1] - payoff_matrix_B[0][1]
den = (payoff_matrix_B[0][0] - payoff_matrix_B[1][0] -
payoff_matrix_B[0][1] + payoff_matrix_B[1][1])
if den != 0:
p = num / den
else:
p = 0.5
return ('mixed', p, q)
# 6. Backward Induction for Sequential Games
class GameTree:
'''Game tree for sequential games'''
class Node:
def __init__(self, player, value=None):
self.player = player
self.value = value
self.children = []
def __init__(self, root):
self.root = root
def backward_induction(self, node=None):
'''Solve game using backward induction'''
if node is None:
node = self.root
# Leaf node
if not node.children:
return node.value
# Recursively solve children
child_values = [self.backward_induction(child) for child in node.children]
# Current player chooses best option
if node.player == 'MAX':
return max(child_values)
else:
return min(child_values)PythonAnswer: Game theory: Nim game solved via XOR (Sprague-Grundy); Grundy numbers compute MEX for game positions; Minimax with alpha-beta pruning for perfect play in Tic-tac-toe; Monte Carlo Tree Search balances exploration/exploitation using UCB1; Nash equilibrium for strategic games; backward induction for sequential games.
Q195. Design advanced concurrent and parallel algorithms
# Advanced Concurrent and Parallel Algorithms
import threading
import multiprocessing
import queue
import time
# 1. Lock-Free Stack using CAS (Compare-and-Swap)
class LockFreeStack:
'''Lock-free stack using atomic operations'''
class Node:
def __init__(self, value, next_node=None):
self.value = value
self.next = next_node
def __init__(self):
self.head = None
self.lock = threading.Lock() # Only for simulation of CAS
def push(self, value):
'''Push value onto stack'''
new_node = self.Node(value)
while True:
# Read current head
old_head = self.head
new_node.next = old_head
# Try to CAS
with self.lock: # Simulating atomic CAS
if self.head == old_head:
self.head = new_node
return
def pop(self):
'''Pop value from stack'''
while True:
# Read current head
old_head = self.head
if old_head is None:
return None
new_head = old_head.next
# Try to CAS
with self.lock:
if self.head == old_head:
self.head = new_head
return old_head.value
# 2. Read-Write Lock
class ReadWriteLock:
'''Allow multiple readers or single writer'''
def __init__(self):
self.readers = 0
self.writers = 0
self.read_ready = threading.Condition(threading.Lock())
self.write_ready = threading.Condition(threading.Lock())
def acquire_read(self):
'''Acquire read lock'''
with self.read_ready:
while self.writers > 0:
self.read_ready.wait()
self.readers += 1
def release_read(self):
'''Release read lock'''
with self.read_ready:
self.readers -= 1
if self.readers == 0:
self.write_ready.notify_all()
def acquire_write(self):
'''Acquire write lock'''
with self.write_ready:
while self.writers > 0 or self.readers > 0:
self.write_ready.wait()
self.writers += 1
def release_write(self):
'''Release write lock'''
with self.write_ready:
self.writers -= 1
self.write_ready.notify_all()
self.read_ready.notify_all()
# 3. Barrier Synchronization
class Barrier:
'''Synchronization barrier for threads'''
def __init__(self, num_threads):
self.num_threads = num_threads
self.count = 0
self.mutex = threading.Lock()
self.barrier = threading.Semaphore(0)
def wait(self):
'''Wait at barrier until all threads arrive'''
with self.mutex:
self.count += 1
if self.count == self.num_threads:
# Last thread releases everyone
for _ in range(self.num_threads):
self.barrier.release()
# Wait for release
self.barrier.acquire()
# 4. Producer-Consumer with Bounded Buffer
class BoundedBuffer:
'''Thread-safe bounded buffer'''
def __init__(self, capacity):
self.capacity = capacity
self.buffer = []
self.lock = threading.Lock()
self.not_empty = threading.Condition(self.lock)
self.not_full = threading.Condition(self.lock)
def produce(self, item):
'''Add item to buffer'''
with self.not_full:
while len(self.buffer) >= self.capacity:
self.not_full.wait()
self.buffer.append(item)
self.not_empty.notify()
def consume(self):
'''Remove and return item from buffer'''
with self.not_empty:
while len(self.buffer) == 0:
self.not_empty.wait()
item = self.buffer.pop(0)
self.not_full.notify()
return item
# 5. Parallel Merge Sort with Thread Pool
def parallel_merge_sort_threaded(arr, num_threads=4):
'''Merge sort using thread pool'''
if len(arr) <= 1:
return arr
if len(arr) < 1000 or num_threads <= 1:
# Sequential for small arrays or no threads left
return sorted(arr)
mid = len(arr) // 2
# Create threads for left and right halves
left_result = [None]
right_result = [None]
def sort_left():
left_result[0] = parallel_merge_sort_threaded(arr[:mid], num_threads // 2)
def sort_right():
right_result[0] = parallel_merge_sort_threaded(arr[mid:], num_threads // 2)
left_thread = threading.Thread(target=sort_left)
right_thread = threading.Thread(target=sort_right)
left_thread.start()
right_thread.start()
left_thread.join()
right_thread.join()
# Merge results
return merge_sorted(left_result[0], right_result[0])
def merge_sorted(left, right):
'''Merge two sorted arrays'''
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# 6. Concurrent Hash Map
class ConcurrentHashMap:
'''Thread-safe hash map with fine-grained locking'''
def __init__(self, num_buckets=16):
self.num_buckets = num_buckets
self.buckets = [[] for _ in range(num_buckets)]
self.locks = [threading.Lock() for _ in range(num_buckets)]
def _hash(self, key):
'''Hash key to bucket index'''
return hash(key) % self.num_buckets
def put(self, key, value):
'''Insert key-value pair'''
bucket_idx = self._hash(key)
with self.locks[bucket_idx]:
bucket = self.buckets[bucket_idx]
# Update existing key
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
# Insert new key
bucket.append((key, value))
def get(self, key):
'''Get value for key'''
bucket_idx = self._hash(key)
with self.locks[bucket_idx]:
bucket = self.buckets[bucket_idx]
for k, v in bucket:
if k == key:
return v
return None
def remove(self, key):
'''Remove key from map'''
bucket_idx = self._hash(key)
with self.locks[bucket_idx]:
bucket = self.buckets[bucket_idx]
for i, (k, v) in enumerate(bucket):
if k == key:
del bucket[i]
return v
return None
# 7. Work-Stealing Deque
class WorkStealingDeque:
'''Deque supporting work stealing'''
def __init__(self):
self.items = []
self.lock = threading.Lock()
def push_bottom(self, item):
'''Owner pushes to bottom'''
with self.lock:
self.items.append(item)
def pop_bottom(self):
'''Owner pops from bottom'''
with self.lock:
if not self.items:
return None
return self.items.pop()
def steal_top(self):
'''Thief steals from top'''
with self.lock:
if not self.items:
return None
return self.items.pop(0)
# 8. Parallel Prefix Sum (Scan) using Threads
def parallel_prefix_sum(arr, num_threads=4):
'''Compute prefix sums in parallel'''
n = len(arr)
if n == 0:
return []
result = arr.copy()
# Sequential for small arrays
if n < 100:
for i in range(1, n):
result[i] += result[i-1]
return result
# Up-sweep phase
d = 0
while (1 << (d + 1)) < n:
stride = 1 << (d + 1)
def update_range(start, end):
for i in range(start, min(end, n), stride):
if i + (1 << d) < n:
result[i + stride - 1] += result[i + (1 << d) - 1]
# Parallelize updates
chunk_size = max(1, n // num_threads)
threads = []
for t in range(num_threads):
start = t * chunk_size
end = start + chunk_size if t < num_threads - 1 else n
thread = threading.Thread(target=update_range, args=(start, end))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
d += 1
# Down-sweep phase (simplified for clarity)
result[-1] = 0
# ... (down-sweep implementation)
return resultPythonAnswer: Concurrent algorithms: Lock-free stack using CAS avoids blocking; read-write locks allow multiple readers; barrier synchronization coordinates threads; bounded buffer for producer-consumer; parallel merge sort with thread pool; concurrent hash map with fine-grained locking; work-stealing deque for load balancing; parallel prefix sum with up/down-sweep phases.
Q196. Design URL shortener with base62 encoding
# Real-World Algorithm Applications and Problem-Solving
# Q196: Design a URL Shortener System
class URLShortener:
'''
Design URL shortener like bit.ly
Requirements: Generate short URLs, redirect to original
'''
def __init__(self, base_url="http://short.url/"):
self.base_url = base_url
self.url_to_short = {}
self.short_to_url = {}
self.counter = 0
# Base62 encoding for compact URLs
self.alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def encode_base62(self, num):
'''Convert number to base62 string'''
if num == 0:
return self.alphabet[0]
result = []
while num > 0:
result.append(self.alphabet[num % 62])
num //= 62
return ''.join(reversed(result))
def decode_base62(self, s):
'''Convert base62 string to number'''
num = 0
for char in s:
num = num * 62 + self.alphabet.index(char)
return num
def shorten(self, long_url):
'''Create short URL for long URL'''
# Check if already shortened
if long_url in self.url_to_short:
return self.base_url + self.url_to_short[long_url]
# Generate new short code
short_code = self.encode_base62(self.counter)
self.counter += 1
# Store mappings
self.url_to_short[long_url] = short_code
self.short_to_url[short_code] = long_url
return self.base_url + short_code
def expand(self, short_url):
'''Get original URL from short URL'''
short_code = short_url.replace(self.base_url, "")
return self.short_to_url.get(short_code)
# Q197: Design Rate Limiter
import time
from collections import deque
class RateLimiter:
'''Rate limiter using sliding window algorithm'''
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = {} # user_id -> deque of timestamps
def allow_request(self, user_id):
'''Check if request is allowed for user'''
current_time = time.time()
if user_id not in self.requests:
self.requests[user_id] = deque()
user_requests = self.requests[user_id]
# Remove old requests outside window
while user_requests and user_requests[0] <= current_time - self.window_seconds:
user_requests.popleft()
# Check if under limit
if len(user_requests) < self.max_requests:
user_requests.append(current_time)
return True
return False
class TokenBucketRateLimiter:
'''Rate limiter using token bucket algorithm'''
def __init__(self, capacity, refill_rate):
'''
capacity: max tokens
refill_rate: tokens added per second
'''
self.capacity = capacity
self.refill_rate = refill_rate
self.buckets = {} # user_id -> (tokens, last_update)
def allow_request(self, user_id, tokens_needed=1):
'''Check if request is allowed'''
current_time = time.time()
if user_id not in self.buckets:
self.buckets[user_id] = (self.capacity, current_time)
tokens, last_update = self.buckets[user_id]
# Refill tokens based on time elapsed
time_passed = current_time - last_update
tokens = min(self.capacity, tokens + time_passed * self.refill_rate)
# Check if enough tokens
if tokens >= tokens_needed:
tokens -= tokens_needed
self.buckets[user_id] = (tokens, current_time)
return True
self.buckets[user_id] = (tokens, current_time)
return False
# Q198: LRU Cache with Expiration
class LRUCacheWithExpiration:
'''LRU cache that also handles time-based expiration'''
class Node:
def __init__(self, key, value, expiration):
self.key = key
self.value = value
self.expiration = expiration
self.prev = None
self.next = None
def __init__(self, capacity, default_ttl=3600):
self.capacity = capacity
self.default_ttl = default_ttl
self.cache = {}
# Doubly linked list for LRU
self.head = self.Node(0, 0, 0)
self.tail = self.Node(0, 0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def _remove_node(self, node):
'''Remove node from linked list'''
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_head(self, node):
'''Add node to head of list'''
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key):
'''Get value from cache'''
current_time = time.time()
if key not in self.cache:
return None
node = self.cache[key]
# Check expiration
if node.expiration < current_time:
self._remove_node(node)
del self.cache[key]
return None
# Move to head (most recently used)
self._remove_node(node)
self._add_to_head(node)
return node.value
def put(self, key, value, ttl=None):
'''Add or update value in cache'''
current_time = time.time()
ttl = ttl if ttl is not None else self.default_ttl
expiration = current_time + ttl
if key in self.cache:
# Update existing
node = self.cache[key]
node.value = value
node.expiration = expiration
self._remove_node(node)
self._add_to_head(node)
else:
# Add new
if len(self.cache) >= self.capacity:
# Remove LRU (tail.prev)
lru = self.tail.prev
self._remove_node(lru)
del self.cache[lru.key]
new_node = self.Node(key, value, expiration)
self.cache[key] = new_node
self._add_to_head(new_node)
# Q199: Consistent Hashing Implementation
import bisect
class ConsistentHashing:
'''Consistent hashing for distributed systems'''
def __init__(self, num_virtual_nodes=150):
self.num_virtual_nodes = num_virtual_nodes
self.ring = [] # Sorted list of (hash, server_id)
self.servers = set()
def _hash(self, key):
'''Hash function'''
import hashlib
return int(hashlib.md5(str(key).encode()).hexdigest(), 16)
def add_server(self, server_id):
'''Add server to ring'''
self.servers.add(server_id)
# Add virtual nodes
for i in range(self.num_virtual_nodes):
virtual_key = f"{server_id}:{i}"
hash_value = self._hash(virtual_key)
bisect.insort(self.ring, (hash_value, server_id))
def remove_server(self, server_id):
'''Remove server from ring'''
self.servers.discard(server_id)
# Remove virtual nodes
self.ring = [(h, s) for h, s in self.ring if s != server_id]
def get_server(self, key):
'''Find server responsible for key'''
if not self.ring:
return None
hash_value = self._hash(key)
# Binary search for first server >= hash_value
idx = bisect.bisect_right(self.ring, (hash_value, ''))
if idx == len(self.ring):
idx = 0
return self.ring[idx][1]
# Q200: Design Real-Time Leaderboard
import heapq
from collections import defaultdict
class Leaderboard:
'''
Real-time leaderboard with efficient updates and queries
Supports: update score, get top K, get rank
'''
def __init__(self):
self.scores = {} # player_id -> score
self.rank_cache = {} # player_id -> rank (invalidated on update)
self.cache_valid = False
def update_score(self, player_id, score):
'''Update player's score'''
self.scores[player_id] = score
self.cache_valid = False
def add_score(self, player_id, delta):
'''Add to player's score'''
self.scores[player_id] = self.scores.get(player_id, 0) + delta
self.cache_valid = False
def get_top_k(self, k):
'''Get top K players - O(n log k) using min-heap'''
if k >= len(self.scores):
return sorted(self.scores.items(), key=lambda x: -x[1])
# Use min-heap of size k
heap = []
for player_id, score in self.scores.items():
if len(heap) < k:
heapq.heappush(heap, (score, player_id))
elif score > heap[0][0]:
heapq.heapreplace(heap, (score, player_id))
# Sort and return
return sorted(heap, key=lambda x: -x[0])
def get_rank(self, player_id):
'''Get player's rank - O(n log n) with caching'''
if player_id not in self.scores:
return None
if not self.cache_valid:
self._rebuild_rank_cache()
return self.rank_cache.get(player_id)
def _rebuild_rank_cache(self):
'''Rebuild rank cache'''
sorted_players = sorted(self.scores.items(),
key=lambda x: -x[1])
self.rank_cache = {player_id: rank + 1
for rank, (player_id, _) in enumerate(sorted_players)}
self.cache_valid = True
def get_players_in_range(self, start_rank, end_rank):
'''Get players in rank range'''
if not self.cache_valid:
self._rebuild_rank_cache()
sorted_players = sorted(self.scores.items(), key=lambda x: -x[1])
return sorted_players[start_rank-1:end_rank]PythonAnswer: URL shortener: Base62 encoding converts counter to compact string; bidirectional mapping enables expansion; counter ensures uniqueness; supports custom short codes and analytics.
Q197. Implement rate limiter algorithms
# Real-World Algorithm Applications and Problem-Solving
# Q196: Design a URL Shortener System
class URLShortener:
'''
Design URL shortener like bit.ly
Requirements: Generate short URLs, redirect to original
'''
def __init__(self, base_url="http://short.url/"):
self.base_url = base_url
self.url_to_short = {}
self.short_to_url = {}
self.counter = 0
# Base62 encoding for compact URLs
self.alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def encode_base62(self, num):
'''Convert number to base62 string'''
if num == 0:
return self.alphabet[0]
result = []
while num > 0:
result.append(self.alphabet[num % 62])
num //= 62
return ''.join(reversed(result))
def decode_base62(self, s):
'''Convert base62 string to number'''
num = 0
for char in s:
num = num * 62 + self.alphabet.index(char)
return num
def shorten(self, long_url):
'''Create short URL for long URL'''
# Check if already shortened
if long_url in self.url_to_short:
return self.base_url + self.url_to_short[long_url]
# Generate new short code
short_code = self.encode_base62(self.counter)
self.counter += 1
# Store mappings
self.url_to_short[long_url] = short_code
self.short_to_url[short_code] = long_url
return self.base_url + short_code
def expand(self, short_url):
'''Get original URL from short URL'''
short_code = short_url.replace(self.base_url, "")
return self.short_to_url.get(short_code)
# Q197: Design Rate Limiter
import time
from collections import deque
class RateLimiter:
'''Rate limiter using sliding window algorithm'''
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = {} # user_id -> deque of timestamps
def allow_request(self, user_id):
'''Check if request is allowed for user'''
current_time = time.time()
if user_id not in self.requests:
self.requests[user_id] = deque()
user_requests = self.requests[user_id]
# Remove old requests outside window
while user_requests and user_requests[0] <= current_time - self.window_seconds:
user_requests.popleft()
# Check if under limit
if len(user_requests) < self.max_requests:
user_requests.append(current_time)
return True
return False
class TokenBucketRateLimiter:
'''Rate limiter using token bucket algorithm'''
def __init__(self, capacity, refill_rate):
'''
capacity: max tokens
refill_rate: tokens added per second
'''
self.capacity = capacity
self.refill_rate = refill_rate
self.buckets = {} # user_id -> (tokens, last_update)
def allow_request(self, user_id, tokens_needed=1):
'''Check if request is allowed'''
current_time = time.time()
if user_id not in self.buckets:
self.buckets[user_id] = (self.capacity, current_time)
tokens, last_update = self.buckets[user_id]
# Refill tokens based on time elapsed
time_passed = current_time - last_update
tokens = min(self.capacity, tokens + time_passed * self.refill_rate)
# Check if enough tokens
if tokens >= tokens_needed:
tokens -= tokens_needed
self.buckets[user_id] = (tokens, current_time)
return True
self.buckets[user_id] = (tokens, current_time)
return False
# Q198: LRU Cache with Expiration
class LRUCacheWithExpiration:
'''LRU cache that also handles time-based expiration'''
class Node:
def __init__(self, key, value, expiration):
self.key = key
self.value = value
self.expiration = expiration
self.prev = None
self.next = None
def __init__(self, capacity, default_ttl=3600):
self.capacity = capacity
self.default_ttl = default_ttl
self.cache = {}
# Doubly linked list for LRU
self.head = self.Node(0, 0, 0)
self.tail = self.Node(0, 0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def _remove_node(self, node):
'''Remove node from linked list'''
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_head(self, node):
'''Add node to head of list'''
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key):
'''Get value from cache'''
current_time = time.time()
if key not in self.cache:
return None
node = self.cache[key]
# Check expiration
if node.expiration < current_time:
self._remove_node(node)
del self.cache[key]
return None
# Move to head (most recently used)
self._remove_node(node)
self._add_to_head(node)
return node.value
def put(self, key, value, ttl=None):
'''Add or update value in cache'''
current_time = time.time()
ttl = ttl if ttl is not None else self.default_ttl
expiration = current_time + ttl
if key in self.cache:
# Update existing
node = self.cache[key]
node.value = value
node.expiration = expiration
self._remove_node(node)
self._add_to_head(node)
else:
# Add new
if len(self.cache) >= self.capacity:
# Remove LRU (tail.prev)
lru = self.tail.prev
self._remove_node(lru)
del self.cache[lru.key]
new_node = self.Node(key, value, expiration)
self.cache[key] = new_node
self._add_to_head(new_node)
# Q199: Consistent Hashing Implementation
import bisect
class ConsistentHashing:
'''Consistent hashing for distributed systems'''
def __init__(self, num_virtual_nodes=150):
self.num_virtual_nodes = num_virtual_nodes
self.ring = [] # Sorted list of (hash, server_id)
self.servers = set()
def _hash(self, key):
'''Hash function'''
import hashlib
return int(hashlib.md5(str(key).encode()).hexdigest(), 16)
def add_server(self, server_id):
'''Add server to ring'''
self.servers.add(server_id)
# Add virtual nodes
for i in range(self.num_virtual_nodes):
virtual_key = f"{server_id}:{i}"
hash_value = self._hash(virtual_key)
bisect.insort(self.ring, (hash_value, server_id))
def remove_server(self, server_id):
'''Remove server from ring'''
self.servers.discard(server_id)
# Remove virtual nodes
self.ring = [(h, s) for h, s in self.ring if s != server_id]
def get_server(self, key):
'''Find server responsible for key'''
if not self.ring:
return None
hash_value = self._hash(key)
# Binary search for first server >= hash_value
idx = bisect.bisect_right(self.ring, (hash_value, ''))
if idx == len(self.ring):
idx = 0
return self.ring[idx][1]
# Q200: Design Real-Time Leaderboard
import heapq
from collections import defaultdict
class Leaderboard:
'''
Real-time leaderboard with efficient updates and queries
Supports: update score, get top K, get rank
'''
def __init__(self):
self.scores = {} # player_id -> score
self.rank_cache = {} # player_id -> rank (invalidated on update)
self.cache_valid = False
def update_score(self, player_id, score):
'''Update player's score'''
self.scores[player_id] = score
self.cache_valid = False
def add_score(self, player_id, delta):
'''Add to player's score'''
self.scores[player_id] = self.scores.get(player_id, 0) + delta
self.cache_valid = False
def get_top_k(self, k):
'''Get top K players - O(n log k) using min-heap'''
if k >= len(self.scores):
return sorted(self.scores.items(), key=lambda x: -x[1])
# Use min-heap of size k
heap = []
for player_id, score in self.scores.items():
if len(heap) < k:
heapq.heappush(heap, (score, player_id))
elif score > heap[0][0]:
heapq.heapreplace(heap, (score, player_id))
# Sort and return
return sorted(heap, key=lambda x: -x[0])
def get_rank(self, player_id):
'''Get player's rank - O(n log n) with caching'''
if player_id not in self.scores:
return None
if not self.cache_valid:
self._rebuild_rank_cache()
return self.rank_cache.get(player_id)
def _rebuild_rank_cache(self):
'''Rebuild rank cache'''
sorted_players = sorted(self.scores.items(),
key=lambda x: -x[1])
self.rank_cache = {player_id: rank + 1
for rank, (player_id, _) in enumerate(sorted_players)}
self.cache_valid = True
def get_players_in_range(self, start_rank, end_rank):
'''Get players in rank range'''
if not self.cache_valid:
self._rebuild_rank_cache()
sorted_players = sorted(self.scores.items(), key=lambda x: -x[1])
return sorted_players[start_rank-1:end_rank]PythonAnswer: Rate limiting: Sliding window tracks timestamps in deque, removes expired; token bucket refills at constant rate, allows bursts up to capacity; both support per-user limits and distributed scenarios.
Q198. Design LRU cache with time-based expiration
# Real-World Algorithm Applications and Problem-Solving
# Q196: Design a URL Shortener System
class URLShortener:
'''
Design URL shortener like bit.ly
Requirements: Generate short URLs, redirect to original
'''
def __init__(self, base_url="http://short.url/"):
self.base_url = base_url
self.url_to_short = {}
self.short_to_url = {}
self.counter = 0
# Base62 encoding for compact URLs
self.alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def encode_base62(self, num):
'''Convert number to base62 string'''
if num == 0:
return self.alphabet[0]
result = []
while num > 0:
result.append(self.alphabet[num % 62])
num //= 62
return ''.join(reversed(result))
def decode_base62(self, s):
'''Convert base62 string to number'''
num = 0
for char in s:
num = num * 62 + self.alphabet.index(char)
return num
def shorten(self, long_url):
'''Create short URL for long URL'''
# Check if already shortened
if long_url in self.url_to_short:
return self.base_url + self.url_to_short[long_url]
# Generate new short code
short_code = self.encode_base62(self.counter)
self.counter += 1
# Store mappings
self.url_to_short[long_url] = short_code
self.short_to_url[short_code] = long_url
return self.base_url + short_code
def expand(self, short_url):
'''Get original URL from short URL'''
short_code = short_url.replace(self.base_url, "")
return self.short_to_url.get(short_code)
# Q197: Design Rate Limiter
import time
from collections import deque
class RateLimiter:
'''Rate limiter using sliding window algorithm'''
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = {} # user_id -> deque of timestamps
def allow_request(self, user_id):
'''Check if request is allowed for user'''
current_time = time.time()
if user_id not in self.requests:
self.requests[user_id] = deque()
user_requests = self.requests[user_id]
# Remove old requests outside window
while user_requests and user_requests[0] <= current_time - self.window_seconds:
user_requests.popleft()
# Check if under limit
if len(user_requests) < self.max_requests:
user_requests.append(current_time)
return True
return False
class TokenBucketRateLimiter:
'''Rate limiter using token bucket algorithm'''
def __init__(self, capacity, refill_rate):
'''
capacity: max tokens
refill_rate: tokens added per second
'''
self.capacity = capacity
self.refill_rate = refill_rate
self.buckets = {} # user_id -> (tokens, last_update)
def allow_request(self, user_id, tokens_needed=1):
'''Check if request is allowed'''
current_time = time.time()
if user_id not in self.buckets:
self.buckets[user_id] = (self.capacity, current_time)
tokens, last_update = self.buckets[user_id]
# Refill tokens based on time elapsed
time_passed = current_time - last_update
tokens = min(self.capacity, tokens + time_passed * self.refill_rate)
# Check if enough tokens
if tokens >= tokens_needed:
tokens -= tokens_needed
self.buckets[user_id] = (tokens, current_time)
return True
self.buckets[user_id] = (tokens, current_time)
return False
# Q198: LRU Cache with Expiration
class LRUCacheWithExpiration:
'''LRU cache that also handles time-based expiration'''
class Node:
def __init__(self, key, value, expiration):
self.key = key
self.value = value
self.expiration = expiration
self.prev = None
self.next = None
def __init__(self, capacity, default_ttl=3600):
self.capacity = capacity
self.default_ttl = default_ttl
self.cache = {}
# Doubly linked list for LRU
self.head = self.Node(0, 0, 0)
self.tail = self.Node(0, 0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def _remove_node(self, node):
'''Remove node from linked list'''
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_head(self, node):
'''Add node to head of list'''
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key):
'''Get value from cache'''
current_time = time.time()
if key not in self.cache:
return None
node = self.cache[key]
# Check expiration
if node.expiration < current_time:
self._remove_node(node)
del self.cache[key]
return None
# Move to head (most recently used)
self._remove_node(node)
self._add_to_head(node)
return node.value
def put(self, key, value, ttl=None):
'''Add or update value in cache'''
current_time = time.time()
ttl = ttl if ttl is not None else self.default_ttl
expiration = current_time + ttl
if key in self.cache:
# Update existing
node = self.cache[key]
node.value = value
node.expiration = expiration
self._remove_node(node)
self._add_to_head(node)
else:
# Add new
if len(self.cache) >= self.capacity:
# Remove LRU (tail.prev)
lru = self.tail.prev
self._remove_node(lru)
del self.cache[lru.key]
new_node = self.Node(key, value, expiration)
self.cache[key] = new_node
self._add_to_head(new_node)
# Q199: Consistent Hashing Implementation
import bisect
class ConsistentHashing:
'''Consistent hashing for distributed systems'''
def __init__(self, num_virtual_nodes=150):
self.num_virtual_nodes = num_virtual_nodes
self.ring = [] # Sorted list of (hash, server_id)
self.servers = set()
def _hash(self, key):
'''Hash function'''
import hashlib
return int(hashlib.md5(str(key).encode()).hexdigest(), 16)
def add_server(self, server_id):
'''Add server to ring'''
self.servers.add(server_id)
# Add virtual nodes
for i in range(self.num_virtual_nodes):
virtual_key = f"{server_id}:{i}"
hash_value = self._hash(virtual_key)
bisect.insort(self.ring, (hash_value, server_id))
def remove_server(self, server_id):
'''Remove server from ring'''
self.servers.discard(server_id)
# Remove virtual nodes
self.ring = [(h, s) for h, s in self.ring if s != server_id]
def get_server(self, key):
'''Find server responsible for key'''
if not self.ring:
return None
hash_value = self._hash(key)
# Binary search for first server >= hash_value
idx = bisect.bisect_right(self.ring, (hash_value, ''))
if idx == len(self.ring):
idx = 0
return self.ring[idx][1]
# Q200: Design Real-Time Leaderboard
import heapq
from collections import defaultdict
class Leaderboard:
'''
Real-time leaderboard with efficient updates and queries
Supports: update score, get top K, get rank
'''
def __init__(self):
self.scores = {} # player_id -> score
self.rank_cache = {} # player_id -> rank (invalidated on update)
self.cache_valid = False
def update_score(self, player_id, score):
'''Update player's score'''
self.scores[player_id] = score
self.cache_valid = False
def add_score(self, player_id, delta):
'''Add to player's score'''
self.scores[player_id] = self.scores.get(player_id, 0) + delta
self.cache_valid = False
def get_top_k(self, k):
'''Get top K players - O(n log k) using min-heap'''
if k >= len(self.scores):
return sorted(self.scores.items(), key=lambda x: -x[1])
# Use min-heap of size k
heap = []
for player_id, score in self.scores.items():
if len(heap) < k:
heapq.heappush(heap, (score, player_id))
elif score > heap[0][0]:
heapq.heapreplace(heap, (score, player_id))
# Sort and return
return sorted(heap, key=lambda x: -x[0])
def get_rank(self, player_id):
'''Get player's rank - O(n log n) with caching'''
if player_id not in self.scores:
return None
if not self.cache_valid:
self._rebuild_rank_cache()
return self.rank_cache.get(player_id)
def _rebuild_rank_cache(self):
'''Rebuild rank cache'''
sorted_players = sorted(self.scores.items(),
key=lambda x: -x[1])
self.rank_cache = {player_id: rank + 1
for rank, (player_id, _) in enumerate(sorted_players)}
self.cache_valid = True
def get_players_in_range(self, start_rank, end_rank):
'''Get players in rank range'''
if not self.cache_valid:
self._rebuild_rank_cache()
sorted_players = sorted(self.scores.items(), key=lambda x: -x[1])
return sorted_players[start_rank-1:end_rank]PythonAnswer: LRU cache with expiration: Doubly linked list maintains recency order; hash map provides O(1) lookup; each node stores expiration time; get() checks expiration before returning; put() evicts LRU when at capacity.
Q199. Implement consistent hashing for distributed systems
# Real-World Algorithm Applications and Problem-Solving
# Q196: Design a URL Shortener System
class URLShortener:
'''
Design URL shortener like bit.ly
Requirements: Generate short URLs, redirect to original
'''
def __init__(self, base_url="http://short.url/"):
self.base_url = base_url
self.url_to_short = {}
self.short_to_url = {}
self.counter = 0
# Base62 encoding for compact URLs
self.alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def encode_base62(self, num):
'''Convert number to base62 string'''
if num == 0:
return self.alphabet[0]
result = []
while num > 0:
result.append(self.alphabet[num % 62])
num //= 62
return ''.join(reversed(result))
def decode_base62(self, s):
'''Convert base62 string to number'''
num = 0
for char in s:
num = num * 62 + self.alphabet.index(char)
return num
def shorten(self, long_url):
'''Create short URL for long URL'''
# Check if already shortened
if long_url in self.url_to_short:
return self.base_url + self.url_to_short[long_url]
# Generate new short code
short_code = self.encode_base62(self.counter)
self.counter += 1
# Store mappings
self.url_to_short[long_url] = short_code
self.short_to_url[short_code] = long_url
return self.base_url + short_code
def expand(self, short_url):
'''Get original URL from short URL'''
short_code = short_url.replace(self.base_url, "")
return self.short_to_url.get(short_code)
# Q197: Design Rate Limiter
import time
from collections import deque
class RateLimiter:
'''Rate limiter using sliding window algorithm'''
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = {} # user_id -> deque of timestamps
def allow_request(self, user_id):
'''Check if request is allowed for user'''
current_time = time.time()
if user_id not in self.requests:
self.requests[user_id] = deque()
user_requests = self.requests[user_id]
# Remove old requests outside window
while user_requests and user_requests[0] <= current_time - self.window_seconds:
user_requests.popleft()
# Check if under limit
if len(user_requests) < self.max_requests:
user_requests.append(current_time)
return True
return False
class TokenBucketRateLimiter:
'''Rate limiter using token bucket algorithm'''
def __init__(self, capacity, refill_rate):
'''
capacity: max tokens
refill_rate: tokens added per second
'''
self.capacity = capacity
self.refill_rate = refill_rate
self.buckets = {} # user_id -> (tokens, last_update)
def allow_request(self, user_id, tokens_needed=1):
'''Check if request is allowed'''
current_time = time.time()
if user_id not in self.buckets:
self.buckets[user_id] = (self.capacity, current_time)
tokens, last_update = self.buckets[user_id]
# Refill tokens based on time elapsed
time_passed = current_time - last_update
tokens = min(self.capacity, tokens + time_passed * self.refill_rate)
# Check if enough tokens
if tokens >= tokens_needed:
tokens -= tokens_needed
self.buckets[user_id] = (tokens, current_time)
return True
self.buckets[user_id] = (tokens, current_time)
return False
# Q198: LRU Cache with Expiration
class LRUCacheWithExpiration:
'''LRU cache that also handles time-based expiration'''
class Node:
def __init__(self, key, value, expiration):
self.key = key
self.value = value
self.expiration = expiration
self.prev = None
self.next = None
def __init__(self, capacity, default_ttl=3600):
self.capacity = capacity
self.default_ttl = default_ttl
self.cache = {}
# Doubly linked list for LRU
self.head = self.Node(0, 0, 0)
self.tail = self.Node(0, 0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def _remove_node(self, node):
'''Remove node from linked list'''
node.prev.next = node.next
node.next.prev = node.prev
def _add_to_head(self, node):
'''Add node to head of list'''
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
def get(self, key):
'''Get value from cache'''
current_time = time.time()
if key not in self.cache:
return None
node = self.cache[key]
# Check expiration
if node.expiration < current_time:
self._remove_node(node)
del self.cache[key]
return None
# Move to head (most recently used)
self._remove_node(node)
self._add_to_head(node)
return node.value
def put(self, key, value, ttl=None):
'''Add or update value in cache'''
current_time = time.time()
ttl = ttl if ttl is not None else self.default_ttl
expiration = current_time + ttl
if key in self.cache:
# Update existing
node = self.cache[key]
node.value = value
node.expiration = expiration
self._remove_node(node)
self._add_to_head(node)
else:
# Add new
if len(self.cache) >= self.capacity:
# Remove LRU (tail.prev)
lru = self.tail.prev
self._remove_node(lru)
del self.cache[lru.key]
new_node = self.Node(key, value, expiration)
self.cache[key] = new_node
self._add_to_head(new_node)
# Q199: Consistent Hashing Implementation
import bisect
class ConsistentHashing:
'''Consistent hashing for distributed systems'''
def __init__(self, num_virtual_nodes=150):
self.num_virtual_nodes = num_virtual_nodes
self.ring = [] # Sorted list of (hash, server_id)
self.servers = set()
def _hash(self, key):
'''Hash function'''
import hashlib
return int(hashlib.md5(str(key).encode()).hexdigest(), 16)
def add_server(self, server_id):
'''Add server to ring'''
self.servers.add(server_id)
# Add virtual nodes
for i in range(self.num_virtual_nodes):
virtual_key = f"{server_id}:{i}"
hash_value = self._hash(virtual_key)
bisect.insort(self.ring, (hash_value, server_id))
def remove_server(self, server_id):
'''Remove server from ring'''
self.servers.discard(server_id)
# Remove virtual nodes
self.ring = [(h, s) for h, s in self.ring if s != server_id]
def get_server(self, key):
'''Find server responsible for key'''
if not self.ring:
return None
hash_value = self._hash(key)
# Binary search for first server >= hash_value
idx = bisect.bisect_right(self.ring, (hash_value, ''))
if idx == len(self.ring):
idx = 0
return self.ring[idx][1]
# Q200: Design Real-Time Leaderboard
import heapq
from collections import defaultdict
class Leaderboard:
'''
Real-time leaderboard with efficient updates and queries
Supports: update score, get top K, get rank
'''
def __init__(self):
self.scores = {} # player_id -> score
self.rank_cache = {} # player_id -> rank (invalidated on update)
self.cache_valid = False
def update_score(self, player_id, score):
'''Update player's score'''
self.scores[player_id] = score
self.cache_valid = False
def add_score(self, player_id, delta):
'''Add to player's score'''
self.scores[player_id] = self.scores.get(player_id, 0) + delta
self.cache_valid = False
def get_top_k(self, k):
'''Get top K players - O(n log k) using min-heap'''
if k >= len(self.scores):
return sorted(self.scores.items(), key=lambda x: -x[1])
# Use min-heap of size k
heap = []
for player_id, score in self.scores.items():
if len(heap) < k:
heapq.heappush(heap, (score, player_id))
elif score > heap[0][0]:
heapq.heapreplace(heap, (score, player_id))
# Sort and return
return sorted(heap, key=lambda x: -x[0])
def get_rank(self, player_id):
'''Get player's rank - O(n log n) with caching'''
if player_id not in self.scores:
return None
if not self.cache_valid:
self._rebuild_rank_cache()
return self.rank_cache.get(player_id)
def _rebuild_rank_cache(self):
'''Rebuild rank cache'''
sorted_players = sorted(self.scores.items(),
key=lambda x: -x[1])
self.rank_cache = {player_id: rank + 1
for rank, (player_id, _) in enumerate(sorted_players)}
self.cache_valid = True
def get_players_in_range(self, start_rank, end_rank):
'''Get players in rank range'''
if not self.cache_valid:
self._rebuild_rank_cache()
sorted_players = sorted(self.scores.items(), key=lambda x: -x[1])
return sorted_players[start_rank-1:end_rank]PythonAnswer: Consistent hashing: Virtual nodes (150 per server) ensure even distribution; binary search finds responsible server in O(log n); adding/removing servers only affects 1/n of keys; sorted ring structure enables efficient lookups.
Discover more from Altgr Blog
Subscribe to get the latest posts sent to your email.
