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) |