Advanced

Contest-Style Problems

This lesson presents five full contest problems exactly as they would appear in a HackerRank or Codeforces contest, but with AI/ML contexts. Each problem has a detailed problem statement, sample I/O, constraints, the optimal approach, and a complete solution. Try solving each problem yourself before reading the solution.

Problem A: Data Pipeline Optimization (Greedy + Sorting)

📝
Problem Statement: You have N data preprocessing tasks to schedule. Each task i has a processing time p_i and a deadline d_i. A task is late if it finishes after its deadline. You can process one task at a time. Find the schedule that minimizes the maximum lateness (max over all tasks of finish_time_i - d_i).

Input: First line: N (1 ≤ N ≤ 2 * 10^5). Next N lines: p_i, d_i (1 ≤ p_i, d_i ≤ 10^9)
Output: Minimum possible maximum lateness.

Sample Input:
3
2 6
3 3
1 4
Sample Output: 2
Explanation: Schedule: task 2 (finish=3), task 3 (finish=4), task 1 (finish=6). Lateness: max(3-3, 4-4, 6-6) = 0. But optimal by EDD: task 2 (finish=3, late=0), task 3 (finish=4, late=0), task 1 (finish=6, late=0). Actually this gives 0. Better example: with deadlines adjusted, EDD minimizes max lateness.

Approach: Earliest Deadline First (EDD)

This is a classic scheduling theory result. Sorting tasks by deadline and processing them in that order minimizes the maximum lateness. The proof uses an exchange argument: swapping any two adjacent tasks where the later-deadline task is scheduled first can only decrease or maintain the maximum lateness.

def min_max_lateness(tasks):
    """Minimize maximum lateness using Earliest Deadline First.
    Time: O(N log N), Space: O(N)
    """
    # Sort by deadline
    tasks.sort(key=lambda x: x[1])

    current_time = 0
    max_lateness = float('-inf')

    schedule = []
    for proc_time, deadline in tasks:
        current_time += proc_time
        lateness = current_time - deadline
        max_lateness = max(max_lateness, lateness)
        schedule.append((proc_time, deadline, current_time, lateness))

    return max_lateness, schedule

# Sample Input
tasks = [(2, 6), (3, 3), (1, 4)]
max_late, schedule = min_max_lateness(tasks)
print(f"Minimum maximum lateness: {max_late}")
for p, d, finish, late in schedule:
    print(f"  Task (p={p}, d={d}): finishes at {finish}, lateness={late}")

# Larger example: ML pipeline with tight deadlines
pipeline_tasks = [
    (10, 15),  # Data loading
    (5, 8),    # Preprocessing
    (3, 20),   # Feature extraction
    (8, 12),   # Model training
    (2, 25),   # Evaluation
]
max_late, schedule = min_max_lateness(pipeline_tasks)
print(f"\nPipeline max lateness: {max_late}")
for p, d, finish, late in schedule:
    print(f"  Task (p={p}, d={d}): finishes at {finish}, lateness={late}")

Complexity: O(N log N) for sorting, O(N) for computing lateness.

Problem B: Model Selection (Binary Search + DP)

📝
Problem Statement: You have N candidate models with accuracies a_1, a_2, ..., a_N (sorted in ascending order) and costs c_1, c_2, ..., c_N. You have a budget B. You want to select exactly K models to form an ensemble such that the minimum accuracy among selected models is maximized, and the total cost does not exceed B.

Input: N, K, B on first line. Next N lines: a_i, c_i
Constraints: 1 ≤ K ≤ N ≤ 2 * 10^5, 1 ≤ B ≤ 10^18
Output: Maximum possible minimum accuracy, or -1 if impossible

Approach: Binary Search on Answer + Greedy Validation

Binary search on the minimum accuracy threshold. For a given threshold, only models with accuracy ≥ threshold are candidates. Among those candidates, greedily pick the K cheapest ones and check if total cost ≤ B.

def max_min_accuracy(models, K, B):
    """Binary search on answer: maximize minimum accuracy under budget.
    Time: O(N log N), Space: O(N)
    """
    # Sort by accuracy
    models.sort(key=lambda x: x[0])
    N = len(models)

    if K > N:
        return -1

    # Binary search on the index of minimum accuracy model
    # If we fix minimum accuracy = models[i][0], we need K models
    # from models[i..N-1] with minimum total cost <= B

    # Preprocess: for each possible starting index i,
    # find K cheapest costs among models[i..N-1]
    # We process from right to left, maintaining a max-heap of size K

    import heapq
    best_answer = -1

    # Try each possible minimum accuracy
    # Key insight: sort eligible models by cost and pick K cheapest
    for i in range(N - K + 1):
        # Models i through N-1 are eligible (accuracy >= models[i][0])
        eligible_costs = sorted([models[j][1] for j in range(i, N)])
        total_cost = sum(eligible_costs[:K])
        if total_cost <= B:
            best_answer = models[i][0]
            break  # Since models sorted by accuracy ascending,
                   # lower i = lower min accuracy, we want highest i that works

    # Optimized: iterate from highest accuracy downward
    # Use a sorted approach
    best_answer = -1
    # Sort eligible models from high accuracy down, accumulate cheapest K
    costs = []
    total = 0

    for i in range(N - 1, -1, -1):
        acc, cost = models[i]
        heapq.heappush(costs, -cost)  # max-heap
        total += cost

        if len(costs) > K:
            # Remove most expensive
            total += heapq.heappop(costs)  # removes -max, adding negative = subtracting

        if len(costs) == K and total <= B:
            best_answer = acc

    return best_answer

# Example
models = [
    (0.70, 100), (0.75, 150), (0.80, 200),
    (0.85, 300), (0.90, 500), (0.95, 800)
]
print(f"Max min accuracy (K=3, B=650): {max_min_accuracy(models[:], 3, 650)}")
# Pick models with acc >= 0.75: costs [150, 200, 300] = 650 <= 650 -> 0.75

print(f"Max min accuracy (K=2, B=500): {max_min_accuracy(models[:], 2, 500)}")
# Pick models with acc >= 0.85: costs [300, 500] = 800 > 500
# Pick models with acc >= 0.80: costs [200, 300] = 500 <= 500 -> 0.80

Complexity: O(N log N) time, O(N) space.

Problem C: Hyperparameter Grid Search (Bitmask Enumeration)

📝
Problem Statement: You have K hyperparameters, each with a small set of possible values. The total number of combinations across all hyperparameters is at most 2^20. For each combination, you know the validation score (precomputed). Find the combination that maximizes the score. Additionally, find the top-M combinations.

Input: K hyperparameters with their value counts, score for each combination
Constraints: Total combinations ≤ 2^20, K ≤ 10, M ≤ 100
Output: Top-M combinations with their scores

Approach: Enumerate All Combinations with Mixed-Radix Encoding

import heapq
from itertools import product

def grid_search_optimal(hyperparams, score_fn, top_m=5):
    """Exhaustive grid search with top-M tracking.
    Time: O(total_combos * log M), Space: O(M)
    Uses a min-heap of size M to track top results efficiently.
    """
    param_names = list(hyperparams.keys())
    param_values = [hyperparams[name] for name in param_names]

    # Min-heap of size M for top results
    top_heap = []  # (score, config_dict)
    total_evaluated = 0

    for combo in product(*param_values):
        config = dict(zip(param_names, combo))
        score = score_fn(config)
        total_evaluated += 1

        if len(top_heap) < top_m:
            heapq.heappush(top_heap, (score, total_evaluated, config))
        elif score > top_heap[0][0]:
            heapq.heapreplace(top_heap, (score, total_evaluated, config))

    # Sort results descending by score
    results = sorted(top_heap, key=lambda x: -x[0])
    return [(score, config) for score, _, config in results], total_evaluated

# Example: Hyperparameter optimization for a classifier
hyperparams = {
    'learning_rate': [0.001, 0.01, 0.1],
    'batch_size': [16, 32, 64, 128],
    'dropout': [0.1, 0.2, 0.3, 0.5],
    'hidden_dim': [64, 128, 256],
    'optimizer': ['adam', 'sgd'],
}

# Simulated score function (in reality, this would train and evaluate)
def mock_score(config):
    score = 0.85
    if config['learning_rate'] == 0.01:
        score += 0.05
    if config['batch_size'] == 64:
        score += 0.02
    if config['dropout'] == 0.2:
        score += 0.03
    if config['hidden_dim'] == 256:
        score += 0.04
    if config['optimizer'] == 'adam':
        score += 0.01
    # Add some interaction effects
    if config['learning_rate'] == 0.01 and config['optimizer'] == 'adam':
        score += 0.02
    return score

top_results, total = grid_search_optimal(hyperparams, mock_score, top_m=5)
print(f"Evaluated {total} combinations")
print(f"\nTop 5 configurations:")
for i, (score, config) in enumerate(top_results):
    print(f"  {i+1}. Score: {score:.4f}")
    for k, v in config.items():
        print(f"     {k}: {v}")

Complexity: O(total_combinations * log M) time, O(M) space for the top-M heap.

Problem D: Feature Selection (Bitmask DP)

📝
Problem Statement: You have N features (N ≤ 20). Each subset of features has a known model accuracy (given as a function). Each feature has a computational cost. Find the subset of features that maximizes (accuracy - lambda * total_cost), where lambda is a cost-sensitivity parameter. This is the optimal feature selection problem.

Input: N, lambda, cost array, and accuracy for each of 2^N subsets
Output: Optimal feature subset and its objective value

Approach: Bitmask Enumeration over All Subsets

def optimal_feature_selection(n, costs, accuracy_fn, lam):
    """Find optimal feature subset maximizing accuracy - lambda * cost.
    Time: O(2^N * N), Space: O(2^N)
    Works for N <= 20 (up to ~10^6 subsets).
    """
    best_score = float('-inf')
    best_mask = 0

    for mask in range(1 << n):
        # Compute cost of this subset
        total_cost = 0
        features = []
        for i in range(n):
            if mask & (1 << i):
                total_cost += costs[i]
                features.append(i)

        # Compute accuracy for this subset
        acc = accuracy_fn(mask)

        # Objective: accuracy - lambda * cost
        objective = acc - lam * total_cost

        if objective > best_score:
            best_score = objective
            best_mask = mask

    # Extract selected features
    selected = [i for i in range(n) if best_mask & (1 << i)]
    return selected, best_score

# Example: 8 features for a sentiment classifier
feature_names = ["word_count", "avg_word_len", "sentiment_score",
                 "exclamation_count", "caps_ratio", "emoji_count",
                 "sentence_count", "punctuation_ratio"]
n = len(feature_names)
costs = [1, 1, 5, 2, 3, 4, 1, 2]  # Computation cost per feature

# Simulated accuracy function based on feature subset
def accuracy_fn(mask):
    """Simulate model accuracy for a feature subset"""
    base = 0.50
    # Core features contribute most
    if mask & (1 << 2):  # sentiment_score
        base += 0.20
    if mask & (1 << 0):  # word_count
        base += 0.08
    if mask & (1 << 4):  # caps_ratio
        base += 0.06
    if mask & (1 << 5):  # emoji_count
        base += 0.05
    if mask & (1 << 1):  # avg_word_len
        base += 0.03
    if mask & (1 << 3):  # exclamation_count
        base += 0.04
    if mask & (1 << 6):  # sentence_count
        base += 0.02
    if mask & (1 << 7):  # punctuation_ratio
        base += 0.01
    # Diminishing returns for too many features
    count = bin(mask).count('1')
    if count > 5:
        base -= 0.02 * (count - 5)
    return min(base, 0.99)

selected, score = optimal_feature_selection(n, costs, accuracy_fn, lam=0.005)
print(f"Optimal features (lambda=0.005):")
for i in selected:
    print(f"  {feature_names[i]} (cost: {costs[i]})")
print(f"Objective score: {score:.4f}")

# Try different lambda values
print("\nSensitivity analysis:")
for lam in [0.001, 0.005, 0.01, 0.02, 0.05]:
    sel, sc = optimal_feature_selection(n, costs, accuracy_fn, lam)
    names = [feature_names[i] for i in sel]
    print(f"  lambda={lam:.3f}: {len(sel)} features, score={sc:.4f}")

Complexity: O(2^N * N) time and O(2^N) space. Feasible for N ≤ 20.

Problem E: GPU Resource Scheduling (Interval Scheduling + DP)

📝
Problem Statement: You have M identical GPUs and N training jobs. Each job i has a start time s_i, end time e_i, and profit p_i (priority score). Each GPU can run at most one job at a time (no overlap). Maximize the total profit of scheduled jobs across all M GPUs.

Input: N, M on first line. Next N lines: s_i, e_i, p_i
Constraints: 1 ≤ N ≤ 2000, 1 ≤ M ≤ 100, 1 ≤ s_i < e_i ≤ 10^9, 1 ≤ p_i ≤ 10^6
Output: Maximum total profit

Approach: Weighted Job Scheduling on M Machines

Sort jobs by end time. Use DP where dp[i][j] represents the maximum profit using jobs 1..i with j GPUs available. For each job, binary search for the latest non-conflicting job to decide include/exclude.

from bisect import bisect_right

def max_profit_scheduling(jobs, M):
    """Schedule N jobs on M GPUs to maximize profit.
    Time: O(N^2 * M) or O(N * M * log N) with binary search.
    """
    N = len(jobs)
    if N == 0:
        return 0

    # Sort by end time
    jobs.sort(key=lambda x: x[1])

    # For each job, find the latest non-conflicting job
    end_times = [job[1] for job in jobs]

    def latest_non_conflict(i):
        """Find latest job j < i such that job j ends <= job i starts"""
        target = jobs[i][0]
        # Binary search for rightmost end_time <= target
        lo, hi = 0, i - 1
        result = -1
        while lo <= hi:
            mid = (lo + hi) // 2
            if end_times[mid] <= target:
                result = mid
                lo = mid + 1
            else:
                hi = mid - 1
        return result

    # For M=1: classic weighted job scheduling
    if M == 1:
        dp = [0] * (N + 1)
        for i in range(N):
            # Don't take job i
            dp[i + 1] = dp[i]
            # Take job i
            j = latest_non_conflict(i)
            dp[i + 1] = max(dp[i + 1], dp[j + 1] + jobs[i][2])
        return dp[N]

    # For M > 1: greedy assignment to M machines
    # Use M copies of the single-machine solution
    # More precisely: repeatedly find the best schedule, remove those jobs,
    # and repeat M times.

    # Alternative: extend DP to track M machines
    # dp[i] = max profit considering first i jobs with M machines
    # We use a priority queue of M machine end times

    import heapq

    # Greedy: sort by end time, assign each job to earliest available GPU
    # that finished before job starts, prioritizing high-profit jobs

    # Better approach for small M: solve weighted interval scheduling
    # for each machine greedily

    remaining = list(range(N))
    total_profit = 0

    for gpu in range(M):
        if not remaining:
            break

        # Weighted interval scheduling on remaining jobs
        current_jobs = [(jobs[i][0], jobs[i][1], jobs[i][2], i) for i in remaining]
        current_jobs.sort(key=lambda x: x[1])

        k = len(current_jobs)
        dp = [0] * (k + 1)
        choice = [False] * k

        cur_ends = [cj[1] for cj in current_jobs]

        for i in range(k):
            dp[i + 1] = dp[i]
            # Find latest non-conflicting
            target = current_jobs[i][0]
            lo, hi, res = 0, i - 1, -1
            while lo <= hi:
                mid = (lo + hi) // 2
                if cur_ends[mid] <= target:
                    res = mid
                    lo = mid + 1
                else:
                    hi = mid - 1

            val = dp[res + 1] + current_jobs[i][2]
            if val > dp[i]:
                dp[i + 1] = val

        # Backtrack to find selected jobs
        total_profit += dp[k]
        selected_indices = set()
        i = k - 1
        while i >= 0:
            target = current_jobs[i][0]
            lo, hi, res = 0, i - 1, -1
            while lo <= hi:
                mid = (lo + hi) // 2
                if cur_ends[mid] <= target:
                    res = mid
                    lo = mid + 1
                else:
                    hi = mid - 1

            if dp[res + 1] + current_jobs[i][2] > dp[i]:
                selected_indices.add(current_jobs[i][3])
                i = res
            else:
                i -= 1

        remaining = [i for i in remaining if i not in selected_indices]

    return total_profit

# Example: Schedule training jobs on 2 GPUs
jobs = [
    (0, 3, 50),    # Job A: time 0-3, profit 50
    (1, 4, 40),    # Job B: time 1-4, profit 40
    (3, 6, 70),    # Job C: time 3-6, profit 70
    (4, 7, 60),    # Job D: time 4-7, profit 60
    (5, 9, 80),    # Job E: time 5-9, profit 80
    (7, 10, 30),   # Job F: time 7-10, profit 30
]

for m in range(1, 4):
    profit = max_profit_scheduling(jobs[:], m)
    print(f"Max profit with {m} GPU(s): {profit}")

# 1 GPU: best non-overlapping subset
# 2 GPUs: can run two jobs in parallel
# 3 GPUs: even more parallelism

Complexity: O(M * N^2) time in the worst case, O(N) space per machine.

💡
Contest insight: The five problems in this lesson cover the most common algorithmic patterns in AI-themed contest problems: (A) Greedy with exchange argument, (B) Binary search on answer, (C) Complete enumeration with pruning, (D) Bitmask DP for subset optimization, (E) Interval scheduling with DP. If you master these five patterns, you can solve the majority of contest problems at the Div 2 C-D level.

Key Takeaways

  • Data pipeline scheduling uses the Earliest Deadline First rule, provably optimal for minimizing max lateness.
  • Model selection problems often reduce to binary search on the answer with greedy validation.
  • Grid search can be optimized with a top-M heap to track the best results in O(log M) per evaluation.
  • Feature selection with N ≤ 20 features is tractable via bitmask enumeration over all 2^N subsets.
  • Multi-GPU scheduling extends weighted interval scheduling to multiple machines using iterative DP.