Advanced Techniques

Level 2: Intermediate - Master powerful algorithmic patterns and strategies

Advanced Algorithmic Techniques
Sliding Window Technique
Fixed Size Sliding Window
The sliding window technique uses two pointers to create a "window" that slides through the data structure, typically used for subarray/substring problems.
🪟 Real-Life Analogy:
Think of looking through a window on a moving train. As the train moves, you see different scenery through the same window frame. The window size stays the same, but the content changes.

🎯 When to Use Sliding Window:

  • Subarray/Substring problems: Finding optimal contiguous sequences
  • Fixed or variable window size: Depending on problem constraints
  • Optimization problems: Maximum, minimum, or target sum
  • String matching: Pattern finding with constraints
Problem Type Time Complexity Space Complexity
Fixed Window O(n) O(1)
Variable Window O(n) O(k) where k is unique elements
Min Window Substring O(n + m) O(m) where m is pattern length
Backtracking
Systematic Solution Exploration
Backtracking is a systematic method for solving problems by trying partial solutions and abandoning them if they cannot lead to a complete solution.
🧩 Real-Life Analogy:
Think of solving a maze. You try a path, and if you hit a dead end, you backtrack to the last decision point and try a different route. You systematically explore all possibilities.

🎯 Backtracking Template:

  • Choose: Make a choice from available options
  • Explore: Recursively explore the consequences
  • Unchoose: Undo the choice (backtrack) if it doesn't work
  • Base Case: Stop when solution is found or impossible
Greedy Algorithms
Local Optimal Choices
Greedy algorithms make the locally optimal choice at each step, hoping to find a global optimum. They work when the problem has optimal substructure and the greedy choice property.
🍯 Real-Life Analogy:
Think of a bee collecting nectar. At each flower, it chooses the one with the most nectar nearby (local optimum), trusting this strategy will maximize total nectar collected (global optimum).
TypeScript
// Activity Selection Problem
function activitySelection(start: number[], finish: number[]): number[] {
    const n = start.length;
    const activities = Array.from({length: n}, (_, i) => ({index: i, start: start[i], finish: finish[i]}));
    
    // Sort by finish time
    activities.sort((a, b) => a.finish - b.finish);
    
    const selected: number[] = [];
    selected.push(activities[0].index);
    let lastFinish = activities[0].finish;
    
    for (let i = 1; i < n; i++) {
        if (activities[i].start >= lastFinish) {
            selected.push(activities[i].index);
            lastFinish = activities[i].finish;
        }
    }
    
    return selected;
}

// Fractional Knapsack
interface Item {
    value: number;
    weight: number;
    ratio: number;
}

function fractionalKnapsack(values: number[], weights: number[], capacity: number): number {
    const items: Item[] = values.map((value, i) => ({
        value,
        weight: weights[i],
        ratio: value / weights[i]
    }));
    
    // Sort by value-to-weight ratio (descending)
    items.sort((a, b) => b.ratio - a.ratio);
    
    let totalValue = 0;
    let remainingCapacity = capacity;
    
    for (const item of items) {
        if (remainingCapacity >= item.weight) {
            // Take the whole item
            totalValue += item.value;
            remainingCapacity -= item.weight;
        } else {
            // Take fraction of the item
            totalValue += item.value * (remainingCapacity / item.weight);
            break;
        }
    }
    
    return totalValue;
}

// Coin Change (Greedy - works for standard coin systems)
function coinChangeGreedy(coins: number[], amount: number): number[] {
    coins.sort((a, b) => b - a); // Sort in descending order
    const result: number[] = [];
    
    for (const coin of coins) {
        while (amount >= coin) {
            result.push(coin);
            amount -= coin;
        }
    }
    
    return amount === 0 ? result : [];
}

// Example usage
console.log(activitySelection([1, 3, 0, 5, 8, 5], [2, 4, 6, 7, 9, 9])); // [0, 1, 3, 4]
console.log(fractionalKnapsack([60, 100, 120], [10, 20, 30], 50)); // 240
console.log(coinChangeGreedy([25, 10, 5, 1], 67)); // [25, 25, 10, 5, 1, 1]
                                
Python
# Activity Selection Problem
def activity_selection(start, finish):
    n = len(start)
    activities = [(finish[i], start[i], i) for i in range(n)]
    
    # Sort by finish time
    activities.sort()
    
    selected = [activities[0][2]]  # Select first activity
    last_finish = activities[0][0]
    
    for i in range(1, n):
        if activities[i][1] >= last_finish:  # start >= last_finish
            selected.append(activities[i][2])
            last_finish = activities[i][0]
    
    return selected

# Fractional Knapsack
def fractional_knapsack(values, weights, capacity):
    items = [(values[i] / weights[i], values[i], weights[i]) for i in range(len(values))]
    
    # Sort by value-to-weight ratio (descending)
    items.sort(reverse=True)
    
    total_value = 0
    remaining_capacity = capacity
    
    for ratio, value, weight in items:
        if remaining_capacity >= weight:
            # Take the whole item
            total_value += value
            remaining_capacity -= weight
        else:
            # Take fraction of the item
            total_value += value * (remaining_capacity / weight)
            break
    
    return total_value

# Coin Change (Greedy - works for standard coin systems)
def coin_change_greedy(coins, amount):
    coins.sort(reverse=True)  # Sort in descending order
    result = []
    
    for coin in coins:
        while amount >= coin:
            result.append(coin)
            amount -= coin
    
    return result if amount == 0 else []

# Job Scheduling
def job_scheduling(jobs):  # jobs = [(profit, deadline)]
    # Sort by profit (descending)
    jobs.sort(key=lambda x: x[0], reverse=True)
    
    max_deadline = max(job[1] for job in jobs)
    schedule = [None] * max_deadline
    total_profit = 0
    
    for profit, deadline in jobs:
        # Find latest available slot before deadline
        for slot in range(min(deadline - 1, max_deadline - 1), -1, -1):
            if schedule[slot] is None:
                schedule[slot] = (profit, deadline)
                total_profit += profit
                break
    
    return schedule, total_profit

# Example usage
print(activity_selection([1, 3, 0, 5, 8, 5], [2, 4, 6, 7, 9, 9]))  # [0, 1, 3, 4]
print(fractional_knapsack([60, 100, 120], [10, 20, 30], 50))  # 240.0
print(coin_change_greedy([25, 10, 5, 1], 67))  # [25, 25, 10, 5, 1, 1]
                                
OCaml
(* Activity Selection Problem *)
let activity_selection start finish =
  let n = Array.length start in
  let activities = Array.mapi (fun i _ -> (finish.(i), start.(i), i)) start in
  
  (* Sort by finish time *)
  Array.sort (fun (f1, _, _) (f2, _, _) -> compare f1 f2) activities;
  
  let selected = ref [activities.(0) |> fun (_, _, i) -> i] in
  let last_finish = ref (activities.(0) |> fun (f, _, _) -> f) in
  
  for i = 1 to n - 1 do
    let (finish_time, start_time, index) = activities.(i) in
    if start_time >= !last_finish then (
      selected := index :: !selected;
      last_finish := finish_time
    )
  done;
  
  List.rev !selected

(* Fractional Knapsack *)
let fractional_knapsack values weights capacity =
  let n = Array.length values in
  let items = Array.mapi (fun i _ -> 
    (float_of_int values.(i) /. float_of_int weights.(i), values.(i), weights.(i))
  ) values in
  
  (* Sort by value-to-weight ratio (descending) *)
  Array.sort (fun (r1, _, _) (r2, _, _) -> compare r2 r1) items;
  
  let total_value = ref 0.0 in
  let remaining_capacity = ref capacity in
  
  for i = 0 to n - 1 do
    let (ratio, value, weight) = items.(i) in
    if !remaining_capacity >= weight then (
      (* Take the whole item *)
      total_value := !total_value +. float_of_int value;
      remaining_capacity := !remaining_capacity - weight
    ) else if !remaining_capacity > 0 then (
      (* Take fraction of the item *)
      total_value := !total_value +. (float_of_int value *. float_of_int !remaining_capacity /. float_of_int weight);
      remaining_capacity := 0
    )
  done;
  
  !total_value

(* Coin Change (Greedy) *)
let coin_change_greedy coins amount =
  let sorted_coins = List.sort (fun a b -> compare b a) coins in
  let result = ref [] in
  let remaining = ref amount in
  
  List.iter (fun coin ->
    while !remaining >= coin do
      result := coin :: !result;
      remaining := !remaining - coin
    done
  ) sorted_coins;
  
  if !remaining = 0 then List.rev !result else []

(* Example usage *)
let start_times = [|1; 3; 0; 5; 8; 5|]
let finish_times = [|2; 4; 6; 7; 9; 9|]
let selected_activities = activity_selection start_times finish_times

let values = [|60; 100; 120|]
let weights = [|10; 20; 30|]
let max_value = fractional_knapsack values weights 50

let coins = [25; 10; 5; 1]
let change = coin_change_greedy coins 67
                                

🎯 When Greedy Works:

  • Optimal Substructure: Problem can be broken into subproblems
  • Greedy Choice Property: Local optimum leads to global optimum
  • No Dependencies: Choices don't affect future options
  • Sorting Often Helps: Arrange data to make greedy choice obvious
Problem Time Complexity Space Complexity
Activity Selection O(n log n) O(1)
Fractional Knapsack O(n log n) O(1)
Coin Change (Greedy) O(n) O(1)