Python DSA (Q101-Q199)

    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 False
    Python

    Answer: 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 None
    Python

    Answer: 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 = node
    Python

    Answer: 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.current
    Python

    Answer: 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()
    Python

    Answer: 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]
                    return
    Python

    Answer: 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)
    Python

    Answer: 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 root
    Python

    Answer: 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}")
    Python

    Answer: 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 True
    Python

    Answer: 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_sorted
    Python

    Answer: 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)
    Python

    Answer: 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_len
    Python

    Answer: 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)
    Python

    Answer: 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_infinite
    Python

    Answer: 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 result
    Python

    Answer: 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 result
    Python

    Answer: 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]
    Python

    Answer: 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]
    Python

    Answer: 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 time
    Python

    Answer: 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 []
    Python

    Answer: 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_sum
    Python

    Answer: 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_xor
    Python

    Answer: 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 == 1
    Python

    Answer: 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.mod
    Python

    Answer: 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 0
    Python

    Answer: 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)
    Python

    Answer: 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 result
    Python

    Answer: 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 total
    Python

    Answer: 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 False
    Python

    Answer: 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]
    Python

    Answer: 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]
    Python

    Answer: 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)
    Python

    Answer: 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 result
    Python

    Answer: 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 0
    Python

    Answer: 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))
    Python

    Answer: 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 result
    Python

    Answer: 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 True
    Python

    Answer: 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 True
    Python

    Answer: 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]
    Python

    Answer: 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
        pass
    Python

    Answer: 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 result
    Python

    Answer: 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 result
    Python

    Answer: 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 root
    Python

    Answer: 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 ""
    Python

    Answer: 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)
    Python

    Answer: 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 True
    Python

    Answer: 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_window
    Python

    Answer: 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 result
    Python

    Answer: 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
    '''
    Python

    Answer: 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]
    Python

    Answer: 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 result
    Python

    Answer: 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)
    Python

    Answer: 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]
    Python

    Answer: 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_value
    Python

    Answer: 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'))
    Python

    Answer: 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.5
    Python

    Answer: 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]
    Python

    Answer: 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'
    Python

    Answer: 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]
    Python

    Answer: 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 matrix
    Python

    Answer: 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_node
    Python

    Answer: 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'
        }
    Python

    Answer: 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]
    Python

    Answer: 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)
    Python

    Answer: 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 result
    Python

    Answer: 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 segmentation
    Python

    Answer: 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]
    Python

    Answer: 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'
    Python

    Answer: 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] == key
    Python

    Answer: 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 arr
    Python

    Answer: 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')
    Python

    Answer: 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) ≤ 4
    Python

    Answer: 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'
        }
    Python

    Answer: 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 classes
    Python

    Answer: 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))
    Python

    Answer: 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 = None
    Python

    Answer: 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)
    Python

    Answer: 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_term
    Python

    Answer: 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 ratio
    Python

    Answer: 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 rate
    Python

    Answer: 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 count
    Python

    Answer: 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 node
    Python

    Answer: 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 True
    Python

    Answer: 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 None
    Python

    Answer: 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 True
    Python

    Answer: 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, b
    Python

    Answer: 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 revised
    Python

    Answer: 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)
    Python

    Answer: 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
    ''')
    Python

    Answer: 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 None
    Python

    Answer: 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_U
    Python

    Answer: 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 result
    Python

    Answer: 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)
    Python

    Answer: 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 result
    Python

    Answer: 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]
    Python

    Answer: 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]
    Python

    Answer: 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]
    Python

    Answer: 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]
    Python

    Answer: 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.

    Leave a Reply

    Your email address will not be published. Required fields are marked *