Python Algorithms & Data Structures track

Lesson 27 · Part 8: Applied Patterns

Applied Patterns

Two pointers, sliding windows, bitmasking and prefix sums

Part 8: Applied Patterns5 min readFree lesson

Part 08 Applications | Previous: 26_Minimum_Spanning_Tree


Introduction

In the final chapter, we focus intensively on the 4 most common practical patterns that appear in coding tests. Once you have mastered individual algorithms, you should now be able to combine patterns to solve practical problems quickly.

Pattern Core Idea Time Complexity Representative Problems
Two Pointers Move two indices for O(n) search O(n) Two Sum, Subarray Sum
Sliding Window Move the Fixed/Variable window to handle intervals O(n) Maximum Sum Interval, Minimum Value
Bitmasking Express Set/State using integer bits O(2ⁿ) or O(n) TSP, DP with visit state
Prefix Sum Handle interval queries in O(1) using cumulative sum O(n) preprocessing Interval Sum, 2D Interval Sum

Learning Goals

  1. Can distinguish between Two Pointers and Sliding Window and memorize their respective templates.
  2. Can implement code to enumerate subsets or represent DP visit states using bitmasking.
  3. Can solve cumulative sum problems by implementing 1D and 2D prefix sum preprocessing and O(1) queries.

Key Concepts

1. Two Pointers

Basic Idea

Move two indices (pointers) lo and hi according to conditions for O(n) search.

Opposite Direction Type (Sum of two numbers in a sorted array):

lo, hi = 0, n - 1
while lo < hi:
    s = arr[lo] + arr[hi]
    if s == target:
        #Found
    elif s < target:
        lo += 1
    else:
        hi -= 1

Same Direction Type (Partial sum ≥ target):

lo = total = 0
for hi in range(n):
    total += arr[hi]
    while total >= target:
        #Processing [lo, hi] range
        total -= arr[lo]
        lo += 1
Item Opposite Direction Same Direction
Initial lo=0, hi=n-1 lo=hi=0
Condition Sorting required Assume positive values
Movement Move one side each hi always moves forward, lo moves conditionally
Time O(n) O(n)

2. Sliding Window

Fixed Size Window (length k):

window_sum = sum(arr[:k])
max_sum = window_sum
for i in range(k, n):
    window_sum += arr[i] - arr[i - k]  #Add on the right, remove on the left
    max_sum = max(max_sum, window_sum)

Using deque for Minimum/Maximum O(n):

from collections import deque
dq = deque()   #Store index (monotone deque)
for i in range(n):
    while dq and arr[dq[-1]] >= arr[i]:  #Monotonically increasing deque
        dq.pop()
    dq.append(i)
    if dq[0] <= i - k:   #Remove index that went out of the window
        dq.popleft()
    if i >= k - 1:
        print(arr[dq[0]])  #Minimum value of the window

3. Bitmask

Key Bit Operations

Operation Code Meaning
Turn i-th bit ON mask | (1 << i) Include i
Turn i-th bit OFF mask & ~(1 << i) Exclude i
Check i-th bit mask >> i & 1 Whether i is included
All subsets for s in range(1 << n) O(2ⁿ)
Lowest bit mask & (-mask) Lowest 1 bit
Number of bits bin(mask).count('1') popcount

Visited State DP (TSP):

#dp[mask][v] = Minimum cost when visiting the set 'mask' and being at 'v'
dp = [[INF] * n for _ in range(1 << n)]
dp[1][0] = 0   #Only the starting point 0 is visited

for mask in range(1 << n):
    for u in range(n):
        if dp[mask][u] == INF:
            continue
        for v in range(n):
            if mask >> v & 1:  #Already visited
                continue
            new_mask = mask | (1 << v)
            dp[new_mask][v] = min(dp[new_mask][v], dp[mask][u] + dist[u][v])

4. Prefix Sum

1D Prefix Sum (O(n) preprocessing, O(1) query):

prefix = [0] * (n + 1)
for i in range(n):
    prefix[i + 1] = prefix[i] + arr[i]

#Sum of the interval [l, r] (0-indexed)
def query(l, r):
    return prefix[r + 1] - prefix[l]

2D Prefix Sum (O(nm) preprocessing, O(1) query):

#ps[i][j] = sum from top-left (0,0) to (i-1,j-1)
ps = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
    for j in range(1, m + 1):
        ps[i][j] = (arr[i-1][j-1]
                    + ps[i-1][j] + ps[i][j-1]
                    - ps[i-1][j-1])

#sum of the segment from (r1,c1) to (r2,c2) (0-indexed original)
def query2d(r1, c1, r2, c2):
    return (ps[r2+1][c2+1] - ps[r1][c2+1]
            - ps[r2+1][c1] + ps[r1][c1])

2D Prefix Sum Visualization:

ps[i][j] applies the inclusion-exclusion principle:
┌─────────────┐
│     A       │  ps[i-1][j]
├────┬────────┤
│ B  │ target │  target = arr[i-1][j-1]
│    │        │
└────┴────────┘
   ps[i][j-1]

ps[i][j] = arr + ps_top + ps_left - ps_top_left_diagonal

Example

File Content Core Concept
src/01_two_pointer.py 3 Types of Two-Pointer Patterns Sum of two numbers after sorting, partial sum, duplicate removal
src/02_sliding_window.py Sliding Window Maximum sum of fixed window, variable window, deque minimum
src/03_bitmask.py Bitmasking Enumerating subsets, DP of visit states, TSP
src/04_prefix_sum.py Range Sum 1D/2D preprocessing + O(1) query
python src/01_two_pointer.py
python src/02_sliding_window.py
python src/03_bitmask.py
python src/04_prefix_sum.py

Common Mistakes

  1. Two-Pointer Infinite Loop: lo < hi infinite loop occurs when writing loops without conditions. In the same direction type, hi must move forward.

  2. Sliding Window Deque Out of Range: Check if the index removed from the deque is outside the window before popleft(). while dq and dq[0] <= i - k: dq.popleft()

  3. Bitmask 1-indexed Confusion: Bitmasks are usually 0-indexed. If you receive 1-indexed input, unify the conversion to -1 internally.

  4. Range Sum Off-by-One: prefix[r+1] - prefix[l] vs prefix[r] - prefix[l-1] Unify the index start and prefix definition.

  5. 2D Range Sum Inclusion-Exclusion Sign Error: Mistakenly subtracting +ps[r1][c1]. Formula: ps[r2+1][c2+1] - ps[r1][c2+1] - ps[r2+1][c1] + ps[r1][c1]


Summary

Practical Pattern Summary
━━━━━━━━━━━━━━━━━━━━━
Two Pointers      : O(n), move two indices
Sliding Window    : O(n), move window + deque O(n) min/max
Bitmasking        : O(2ⁿ), represent with Set/State, TSP
Prefix Sum        : O(n) preprocessing, O(1) query, 2D inclusion-exclusion included

Try It Yourself

  1. Try solving Baekjoon 2003 (Sum of Numbers 2) directly using two pointers.
  2. Try solving Baekjoon 11003 (Finding Minimum) with collections.deque.
  3. Solve Baekjoon 11659 (Finding Range Sum 4) with preprocessing for O(1) queries.
  4. Try solving Baekjoon 2098 (Traveling Salesman Problem) using bitmask DP.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/01_two_pointer.pyPython
"""Unit 27 — Practical Patterns: Two Pointer
Topic: Two Pointer Patterns
Time: O(n) per pattern
Space: O(1) ~ O(n)

Description:
    1. Two pointers in opposite directions: Sum of two numbers in a sorted array
    2. Two pointers in the same direction: Minimum subarray with sum at least target
    3. Remove duplicates: Keep only unique elements in a sorted array"""


# ──────────────────────────────────────────────
#1. Two-Pointer in Opposite Directions — Sum of Two Numbers
# ──────────────────────────────────────────────

def two_sum_sorted(arr: list[int], target: int) -> list[tuple[int, int]]:
    """Return all (i, j) pairs with sum equal to target in a sorted array (i < j).

    Time: O(n)
    Space: O(size of result)"""
    result: list[tuple[int, int]] = []
    lo, hi = 0, len(arr) - 1

    while lo < hi:
        s = arr[lo] + arr[hi]
        if s == target:
            result.append((arr[lo], arr[hi]))
            lo += 1
            hi -= 1
            #Skip Duplicates
            while lo < hi and arr[lo] == arr[lo - 1]:
                lo += 1
            while lo < hi and arr[hi] == arr[hi + 1]:
                hi -= 1
        elif s < target:
            lo += 1
        else:
            hi -= 1

    return result


# ──────────────────────────────────────────────
#2. Two-Pointer in Same Direction — Minimum Subarray Sum
# ──────────────────────────────────────────────

def min_subarray_len(arr: list[int], target: int) -> int:
    """Return the length of the shortest contiguous subarray with sum at least target. Return 0 if none exists.

    Time: O(n) — lo and hi each move at most n times
    Space: O(1)"""
    n = len(arr)
    lo = 0
    total = 0
    min_len = n + 1

    for hi in range(n):
        total += arr[hi]

        #Condition Met → Move lo as far right as possible
        while total >= target:
            min_len = min(min_len, hi - lo + 1)
            total -= arr[lo]
            lo += 1

    return min_len if min_len <= n else 0


# ──────────────────────────────────────────────
#3. Two-Pointer — Remove Duplicates in Sorted Array (in-place)
# ──────────────────────────────────────────────

def remove_duplicates(arr: list[int]) -> int:
    """Remove duplicates from a sorted array and return the number of unique elements.
    Result is stored in arr[:result_len].

    Time: O(n)
    Space: O(1)"""
    if not arr:
        return 0

    write = 1   #Next Position to Use

    for read in range(1, len(arr)):
        if arr[read] != arr[write - 1]:
            arr[write] = arr[read]
            write += 1

    return write


# ──────────────────────────────────────────────
#4. Two-Pointer — Count of Subarrays with Sum == Target
# ──────────────────────────────────────────────

def count_subarray_sum(arr: list[int], target: int) -> int:
    """Return the count of contiguous subarrays whose sum is exactly target (positive array).

    Time: O(n)
    Space: O(1)

    Note: Only valid for arrays without negative numbers.
          If negatives exist, use a hashmap (prefix sum)."""
    lo = total = count = 0
    for hi in range(len(arr)):
        total += arr[hi]
        while total > target and lo <= hi:
            total -= arr[lo]
            lo += 1
        if total == target:
            count += 1
    return count


def main() -> None:
    #── 1. Sum of Two Numbers ──
    print("=" * 55)
    print("1. Sum of Two Numbers (Sorted Array, Two-Pointer in Opposite Directions)")
    print("=" * 55)
    arr1 = [-2, -1, 0, 1, 2, 3, 4, 5]
    target1 = 4
    pairs = two_sum_sorted(arr1, target1)
    print(f"Array: {arr1}")
    print(f"  target: {target1}")
    print(f"Result: {pairs}")

    #── 2. Minimum Subarray ──
    print()
    print("=" * 55)
    print("2. Minimum Subarray Length with Sum ≥ Target (Two-Pointer in Same Direction)")
    print("=" * 55)
    cases = [
        ([2, 3, 1, 2, 4, 3], 7),
        ([1, 4, 4], 4),
        ([1, 1, 1, 1], 10),
    ]
    for arr2, tgt in cases:
        result = min_subarray_len(arr2, tgt)
        print(f"arr={arr2}, target={tgt} → Minimum Length: {result}")

    #── 3. Remove Duplicates ──
    print()
    print("=" * 55)
    print("3. Remove Duplicates in Sorted Array (in-place)")
    print("=" * 55)
    arr3 = [1, 1, 2, 2, 3, 4, 4, 5]
    print(f"Original: {arr3}")
    k = remove_duplicates(arr3)
    print(f"After Removal ({k} elements): {arr3[:k]}")

    #── 4. Count of Subarrays with Sum == Target ──
    print()
    print("=" * 55)
    print("4. Number of Subarrays with Sum == Target (Positive Array)")
    print("=" * 55)
    arr4 = [1, 2, 3, 2, 1, 4]
    target4 = 6
    cnt = count_subarray_sum(arr4, target4)
    print(f"Array: {arr4}, Target: {target4}")
    print(f"Number of Subarrays with Sum {target4}: {cnt}")

    #── Complexity Summary ──
    print()
    print("=" * 55)
    print("Two-Pointer Complexity Summary")
    print("=" * 55)
    print("Opposite Directions: O(n), Requires Sorted Array")
    print("Same Direction: O(n), Requires Array with Positive Values")
    print("Remove duplicates: O(n), in-place O(1) space")


if __name__ == "__main__":
    main()
src/02_sliding_window.pyPython
"""Unit 27 — Practical Patterns: Sliding Window
Topic : Sliding Window — Fixed/Variable + Deque Min/Max
Time  : O(n)
Space : O(k) or O(n)

Explanation:
    1. Fixed-size window: Maximum sum of a length k interval
    2. Variable-size window: Minimal interval with a sum at least target (positive)
    3. O(n) window minimum with deque: Baekjoon 11003 style
    4. Maximum number of unique characters in variable-size window (string)"""

from collections import deque


# ──────────────────────────────────────────────
#1. Fixed-size window — Maximum sum
# ──────────────────────────────────────────────

def max_sum_fixed_window(arr: list[int], k: int) -> int:
    """Return the maximum sum of a contiguous subarray of length k.

    Time : O(n)
    Space: O(1)"""
    if len(arr) < k:
        return 0

    window_sum = sum(arr[:k])
    max_sum = window_sum

    for i in range(k, len(arr)):
        window_sum += arr[i] - arr[i - k]   #Add on the right, remove on the left
        max_sum = max(max_sum, window_sum)

    return max_sum


# ──────────────────────────────────────────────
#2. Variable-size window — Minimum subarray with sum ≥ target
# ──────────────────────────────────────────────

def min_window_sum(arr: list[int], target: int) -> int:
    """Return the length of the shortest subarray whose sum is at least target (positive array).

    Time : O(n)
    Space: O(1)"""
    n = len(arr)
    lo = total = 0
    min_len = n + 1

    for hi in range(n):
        total += arr[hi]
        while total >= target:
            min_len = min(min_len, hi - lo + 1)
            total -= arr[lo]
            lo += 1

    return min_len if min_len <= n else 0


# ──────────────────────────────────────────────
#3. Deque sliding window — Minimum of range O(n)
# ──────────────────────────────────────────────

def sliding_window_minimum(arr: list[int], k: int) -> list[int]:
    """Return a list of minimum values in sliding windows of size k.

    Principle: Monotonic increasing deque — the front of the deque is the index of the current window's minimum.
    Remove indices from the back that are larger than the new element (unnecessary).

    Time : O(n)  — each element enters and leaves the deque once
    Space: O(k)"""
    result: list[int] = []
    dq: deque[int] = deque()   #Store index

    for i, val in enumerate(arr):
        #Remove elements from the back of the deque that are greater than or equal to the current value (keep monotonic increase)
        while dq and arr[dq[-1]] >= val:
            dq.pop()
        dq.append(i)

        #Remove index that went out of the window
        if dq[0] <= i - k:
            dq.popleft()

        #Save result once the window is complete
        if i >= k - 1:
            result.append(arr[dq[0]])

    return result


# ──────────────────────────────────────────────
#4. Variable window — Maximum length substring with at most k unique characters
# ──────────────────────────────────────────────

def max_len_at_most_k_distinct(s: str, k: int) -> int:
    """Return the length of the longest substring with at most k distinct characters.

    Time : O(n)
    Space: O(k)  (maximum k character types)"""
    freq: dict[str, int] = {}
    lo = max_len = 0

    for hi, ch in enumerate(s):
        freq[ch] = freq.get(ch, 0) + 1

        while len(freq) > k:
            left_ch = s[lo]
            freq[left_ch] -= 1
            if freq[left_ch] == 0:
                del freq[left_ch]
            lo += 1

        max_len = max(max_len, hi - lo + 1)

    return max_len


def main() -> None:
    #── 1. Maximum sum with fixed window ──
    print("=" * 55)
    print("1. Maximum sum with fixed-size window")
    print("=" * 55)
    arr1 = [2, 1, 5, 1, 3, 2]
    k1 = 3
    result1 = max_sum_fixed_window(arr1, k1)
    print(f"Array: {arr1}, k={k1}")
    print(f"Maximum sum: {result1}  (5+1+3=9 or 2+1+5=8 → {result1})")

    #Check sum of all windows
    print("Sum per window:", [sum(arr1[i:i+k1]) for i in range(len(arr1)-k1+1)])

    #── 2. Minimum subarray with variable window ──
    print()
    print("=" * 55)
    print("2. Minimum subarray with sum ≥ target")
    print("=" * 55)
    cases2 = [
        ([2, 3, 1, 2, 4, 3], 7),
        ([1, 4, 4], 4),
    ]
    for arr2, tgt in cases2:
        r = min_window_sum(arr2, tgt)
        print(f"  arr={arr2}, target={tgt} → {r}")

    #── 3. Sliding window minimum (deque) ──
    print()
    print("=" * 55)
    print("3. Sliding window minimum (deque, O(n))")
    print("=" * 55)
    arr3 = [1, 3, -1, -3, 5, 3, 6, 7]
    k3 = 3
    mins = sliding_window_minimum(arr3, k3)
    print(f"Array: {arr3}")
    print(f"k={k3} Window minimums: {mins}")
    print(f"Expected:          [-1, -3, -3, -3, 3, 3]")

    #── 4. Longest substring with at most k unique characters ──
    print()
    print("=" * 55)
    print("4. Longest substring with ≤ k unique characters")
    print("=" * 55)
    test_cases = [("eceba", 2), ("aa", 1), ("aabacbebebe", 3)]
    for s, k in test_cases:
        r = max_len_at_most_k_distinct(s, k)
        print(f"  s={s!r}, k={k} → {r}")

    #── Complexity Summary ──
    print()
    print("=" * 55)
    print("Sliding window complexity summary")
    print("=" * 55)
    print("Fixed window sum/average : O(n),  O(1)")
    print("Variable window sum       : O(n),  O(1)")
    print("Deque minimum             : O(n),  O(k)")
    print("Variable window hashmap: O(n), O(k)")


if __name__ == "__main__":
    main()
src/03_bitmask.pyPython
"""Unit 27 — Practical Pattern: Bitmask
Topic: Bitmask — Subset Enumeration, Visited State DP, TSP
Time: Subset Enumeration O(2ⁿ), TSP O(2ⁿ * n²)
Space: O(2ⁿ * n)

Description:
    1. Basics of bit operations (ON/OFF/CHECK/COUNT)
    2. Subset enumeration O(2ⁿ)
    3. Visited state DP — representing visited sets with bitmask
    4. TSP (Traveling Salesman Problem) bitmask DP"""

import sys

INF = sys.maxsize


# ──────────────────────────────────────────────
#1. Bit Operation Utility
# ──────────────────────────────────────────────

def bit_on(mask: int, i: int) -> int:
    """Set the i-th bit to 1. Time O(1)."""
    return mask | (1 << i)


def bit_off(mask: int, i: int) -> int:
    """Set the i-th bit to 0. Time O(1)."""
    return mask & ~(1 << i)


def bit_check(mask: int, i: int) -> bool:
    """Check if the i-th bit is 1. Time O(1)."""
    return bool(mask >> i & 1)


def bit_count(mask: int) -> int:
    """Number of 1 bits (popcount). Time O(1) (using built-in bin)."""
    return bin(mask).count("1")


def lowest_bit(mask: int) -> int:
    """Keep only the lowest 1 bit. Time O(1)."""
    return mask & (-mask)


# ──────────────────────────────────────────────
#2. Subset Enumeration
# ──────────────────────────────────────────────

def enumerate_subsets(items: list) -> list[list]:
    """Returns all subsets of items (including the empty set).

    Time: O(2ⁿ * n)
    Space: O(2ⁿ * n)"""
    n = len(items)
    result = []
    for mask in range(1 << n):
        subset = [items[i] for i in range(n) if bit_check(mask, i)]
        result.append(subset)
    return result


def enumerate_subsets_of_mask(mask: int) -> list[int]:
    """Enumerate all submasks of the given mask.
    (including empty one)

    Time: O(2^k)  k = popcount(mask)"""
    sub = mask
    result = []
    while sub:
        result.append(sub)
        sub = (sub - 1) & mask
    result.append(0)  #Empty Set
    return result


# ──────────────────────────────────────────────
#3. Visit State DP — Represent visited set with bitmask
# ──────────────────────────────────────────────

def min_cost_visit_all(
    dist: list[list[int]],
    start: int,
) -> int:
    """Minimum cost to visit all nodes exactly once and return to start.
    (small TSP)

    dp[mask][v] = minimum cost when visiting the set mask and currently at position v.

    Time: O(2ⁿ * n²)
    Space: O(2ⁿ * n)"""
    n = len(dist)
    full = (1 << n) - 1
    dp = [[INF] * n for _ in range(1 << n)]
    dp[1 << start][start] = 0

    for mask in range(1 << n):
        for u in range(n):
            if dp[mask][u] == INF:
                continue
            if not bit_check(mask, u):
                continue
            for v in range(n):
                if bit_check(mask, v):
                    continue   #Already visited
                if dist[u][v] == INF:
                    continue
                new_mask = bit_on(mask, v)
                cost = dp[mask][u] + dist[u][v]
                if cost < dp[new_mask][v]:
                    dp[new_mask][v] = cost

    #Return to start after visiting all nodes
    best = INF
    for v in range(n):
        if v == start:
            continue
        if dp[full][v] != INF and dist[v][start] != INF:
            best = min(best, dp[full][v] + dist[v][start])

    return best


def main() -> None:
    #── 1. Bit Operation Basics ──
    print("=" * 55)
    print("1. Bit Operation Basics")
    print("=" * 55)
    mask = 0b1010   #Turn on bits 1 and 3
    print(f"Initial mask: {bin(mask)} ({mask})")
    print(f"Turn on bit 0: {bin(bit_on(mask, 0))}")
    print(f"Turn off bit 1: {bin(bit_off(mask, 1))}")
    print(f"Check bit 3: {bit_check(mask, 3)}")
    print(f"Number of bits: {bit_count(mask)}")
    print(f"Lowest bit: {bin(lowest_bit(mask))}")

    #── 2. Subset Enumeration ──
    print()
    print("=" * 55)
    print("2. Subset Enumeration (n=3)")
    print("=" * 55)
    items = ["A", "B", "C"]
    subsets = enumerate_subsets(items)
    for i, s in enumerate(subsets):
        print(f"mask={bin(i):>5}: {s if s else 'Empty Set'}")

    print()
    print(f"Total {len(subsets)} (2^{len(items)})")

    #── 2b. Sub-masks of a specific mask ──
    print()
    print("=" * 55)
    print("2b. Enumerate sub-masks of mask=0b1011")
    print("=" * 55)
    m = 0b1011
    subs = enumerate_subsets_of_mask(m)
    print(f"mask={bin(m)}: number of sub-masks {len(subs)}")
    for s in subs:
        print(f"    {bin(s)}")

    #── 3. TSP (4 cities) ──
    print()
    print("=" * 55)
    print("3. TSP — Minimum tour cost for 4 cities (bitmask DP)")
    print("=" * 55)
    dist_matrix = [
        [0,   10,  15,  20],
        [10,  0,   35,  25],
        [15,  35,  0,   30],
        [20,  25,  30,  0 ],
    ]
    print("Distance matrix:")
    for i, row in enumerate(dist_matrix):
        print(f"    {row}")

    min_cost = min_cost_visit_all(dist_matrix, start=0)
    print(f"Minimum tour cost: {min_cost}")
    print("  (0→1→3→2→0: 10+25+30+15=80)")

    #── Complexity Summary ──
    print()
    print("=" * 55)
    print("Bitmask Complexity Summary")
    print("=" * 55)
    print("Bit operations      : O(1)")
    print("Subset enumeration : O(2ⁿ * n)")
    print("Submask enumeration : O(2^k), k=popcount")
    print("  TSP DP          : O(2ⁿ * n²)")
    for n_val in [10, 15, 20]:
        ops = (2 ** n_val) * n_val ** 2
        print(f"    n={n_val}: {ops:>12,}  "
              f"{'OK' if ops <= 1e8 else 'TLE risk'}")


if __name__ == "__main__":
    main()
src/04_prefix_sum.pyPython
"""Chapter 27 — Practical Patterns: Range Sum (Prefix Sum)
Topic: 1D and 2D Prefix Sum with O(1) Range Query
Time: O(n) preprocessing, O(1) query
Space: O(n) or O(nm)

Explanation:
    1. 1D range sum: prefix[i+1] = prefix[i] + arr[i]
       query(l, r) = prefix[r+1] - prefix[l]
    2. 2D range sum: inclusion-exclusion principle
       ps[i][j] = arr + ps_above + ps_left - ps_top_left_diagonal
       query(r1,c1,r2,c2) = ps[r2+1][c2+1] - ps[r1][c2+1]
                            - ps[r2+1][c1] + ps[r1][c1]
    3. Difference Array: O(1) batch update on a range"""


# ──────────────────────────────────────────────
#1. 1D Range Sum
# ──────────────────────────────────────────────

class PrefixSum1D:
    """1D array range sum query.

    Time: O(n) initialization, O(1) query
    Space: O(n)"""

    def __init__(self, arr: list[int]) -> None:
        n = len(arr)
        self.prefix = [0] * (n + 1)
        for i in range(n):
            self.prefix[i + 1] = self.prefix[i] + arr[i]

    def query(self, l: int, r: int) -> int:
        """Returns the sum of the [l, r] range (0-indexed, inclusive).

        Time: O(1)"""
        return self.prefix[r + 1] - self.prefix[l]

    def range_count_ge(self, l: int, r: int, threshold: int) -> int:
        """[l, r] Count of elements greater than or equal to threshold (sum-based application rather than cumulative frequency)."""
        #Example of simple sum application — in practice, sorting + binary search is needed
        raise NotImplementedError


# ──────────────────────────────────────────────
#2. 2D Range Sum
# ──────────────────────────────────────────────

class PrefixSum2D:
    """2D matrix rectangular range sum query.

    Time: O(nm) initialization, O(1) query
    Space: O(nm)"""

    def __init__(self, grid: list[list[int]]) -> None:
        n = len(grid)
        m = len(grid[0]) if n > 0 else 0
        self.n = n
        self.m = m
        #ps[i][j] = sum of rectangle from (0,0) to (i-1,j-1) (1-indexed)
        self.ps = [[0] * (m + 1) for _ in range(n + 1)]

        for i in range(1, n + 1):
            for j in range(1, m + 1):
                self.ps[i][j] = (
                    grid[i - 1][j - 1]
                    + self.ps[i - 1][j]
                    + self.ps[i][j - 1]
                    - self.ps[i - 1][j - 1]
                )

    def query(self, r1: int, c1: int, r2: int, c2: int) -> int:
        """Sum of rectangular section from (r1,c1) to (r2,c2) (0-indexed, both ends included).

        Inclusion-Exclusion Principle:
            Total - remove top - remove left + restore diagonal

        Time: O(1)"""
        return (
            self.ps[r2 + 1][c2 + 1]
            - self.ps[r1][c2 + 1]
            - self.ps[r2 + 1][c1]
            + self.ps[r1][c1]
        )


# ──────────────────────────────────────────────
#3. Difference Array
# ──────────────────────────────────────────────

class DifferenceArray:
    """Batch section update O(1), final array reconstruction O(n).

    Use: When there are many updates adding val to [l, r] sections.

    Time: O(1) update, O(n) build
    Space: O(n)"""

    def __init__(self, n: int) -> None:
        self.diff = [0] * (n + 1)
        self.n = n

    def update(self, l: int, r: int, val: int) -> None:
        """Add val to [l, r] section (0-indexed). Time O(1)."""
        self.diff[l] += val
        if r + 1 <= self.n:
            self.diff[r + 1] -= val

    def build(self) -> list[int]:
        """Return final array. Time O(n)."""
        result = [0] * self.n
        cur = 0
        for i in range(self.n):
            cur += self.diff[i]
            result[i] = cur
        return result


def main() -> None:
    #── 1. 1D Range Sum ──
    print("=" * 55)
    print("1. 1D Range Sum")
    print("=" * 55)
    arr = [3, 1, 4, 1, 5, 9, 2, 6]
    ps1 = PrefixSum1D(arr)
    print(f"Array: {arr}")
    print(f"  prefix: {ps1.prefix}")
    queries = [(0, 3), (2, 5), (0, 7), (3, 3)]
    for l, r in queries:
        ans = ps1.query(l, r)
        brute = sum(arr[l:r+1])
        print(f"query({l},{r}) = {ans}  (direct sum: {brute})")

    #── 2. 2D Range Sum ──
    print()
    print("=" * 55)
    print("2. 2D Range Sum")
    print("=" * 55)
    grid = [
        [1, 2, 3, 4],
        [5, 6, 7, 8],
        [9, 10, 11, 12],
    ]
    ps2 = PrefixSum2D(grid)
    print("Matrix:")
    for row in grid:
        print(f"    {row}")
    print()

    q2d = [
        (0, 0, 1, 1),   # 1+2+5+6=14
        (1, 1, 2, 2),   # 6+7+10+11=34
        (0, 0, 2, 3),   #Total sum = 78
    ]
    for r1, c1, r2, c2 in q2d:
        ans = ps2.query(r1, c1, r2, c2)
        brute = sum(grid[r][c] for r in range(r1, r2+1)
                    for c in range(c1, c2+1))
        print(f"query({r1},{c1},{r2},{c2}) = {ans}  (direct: {brute})")

    #── 3. Difference Array ──
    print()
    print("=" * 55)
    print("3. Difference Array — Range Update")
    print("=" * 55)
    n = 8
    da = DifferenceArray(n)
    updates = [(1, 3, 10), (2, 5, 20), (4, 7, -5)]
    print("Initial: [0] * 8")
    for l, r, v in updates:
        da.update(l, r, v)
        print(f"  update({l},{r},{v:+})")
    result = da.build()
    print(f"Final: {result}")
    print("Direct check:")
    manual = [0] * n
    for l, r, v in updates:
        for i in range(l, r + 1):
            manual[i] += v
    print(f"Expected value: {manual}")
    print(f"Match: {result == manual}")

    #── Complexity Summary ──
    print()
    print("=" * 55)
    print("Summary of Range Sum Complexity")
    print("=" * 55)
    print("1D Preprocessing: O(n),   Query: O(1)")
    print("2D Preprocessing: O(nm),  Query: O(1)")
    print("Difference Array: O(1) update, O(n) reconstruction")
    print("Segment Tree: O(n) preprocessing, O(log n) update+query")


if __name__ == "__main__":
    main()

Assignment

Solve it directly on Baekjoon Online Judge first, then check the reference solution below.


Problem List

# Baekjoon Number Title Difficulty Key Technique
1 2003 Sum of Numbers 2 Silver IV Two Pointers
2 11003 Finding Minimum Platinum V Sliding Window + deque
3 11659 Prefix Sum 4 Silver III 1D Prefix Sum
4 2098 Traveling Salesman Gold I Bitmask DP (TSP)

Problem 1: Baekjoon 2003 — Sum of Numbers 2 (Silver IV)

Problem Summary

Given N numbers A[0..N-1], find the number of cases where the sum of a consecutive subarray equals M.

Input

N M
A[0] A[1] ... A[N-1]

Constraints

  • 1 ≤ N ≤ 10,000
  • 1 ≤ M ≤ 300,000,000
  • All A[i] ≥ 1 (positive → two pointers possible)

Solution Strategy

  • Two pointers in the same direction
  • lo=hi=0, if sum ≥ M then move lo forward, if sum = M then count++

Solution Files

boj_2003.py


Problem 2: Baekjoon 11003 — Finding Minimum (Platinum V)

Problem Summary

For N numbers A, for each i, output the minimum in the range max(0, i-L+1) ~ i.

Input

N L
A[0] A[1] ... A[N-1]

Constraints

  • 1 ≤ N ≤ 5,000,000
  • 1 ≤ L ≤ N
  • -10⁹ ≤ A[i] ≤ 10⁹

Solution Strategy

  • deque sliding window with monotonically increasing deque
  • O(n) time, O(L) space
  • Optimize I/O with sys.stdin.readline + sys.stdout.write

Solution Files

boj_11003.py


Problem 3: Baekjoon 11659 — Prefix Sum 4 (Silver III)

Problem Summary

For N numbers, process M prefix sum queries [l, r].

Input

N M
A[1] A[2] ... A[N]
i j  (sum of the range from i to j)
...

Constraints

  • 1 ≤ N ≤ 100,000
  • 1 ≤ M ≤ 100,000
  • 1-indexed

Solution Strategy

  • prefix[i] = prefix[i-1] + A[i] Preprocessing
  • query(l, r) = prefix[r] - prefix[l-1]

Solution Files

boj_11659.py


Problem 4: Baekjoon 2098 — Traveling Salesman (Gold I)

Problem Summary

Find the minimum cost to visit N cities exactly once and return to the starting point. W[i][j] = cost from i→j (0 if none).

Input

N
W[0][0] ... W[0][N-1]
...

Constraints

  • 1 ≤ N ≤ 16
  • 0 ≤ W[i][j] ≤ 100,000

Solution Strategy

  • Bitmask DP: dp[mask][v] = mask of visited set, minimum cost at current v
  • The minimum value of dp[1<<N - 1][v] + W[v][0] is the answer
  • Both memoization (top-down) and tabulation (bottom-up) are possible

Solution Files

boj_2098.py


Pre-submission Checklist

  • 2003: Since all numbers are positive, two-pointer possible; if sum is M, count++ then move lo forward
  • 11003: N up to 5 million, so sys.stdin.readline + output buffer required
  • 11659: 1-indexed, prefix[0] = 0, query(l, r) = prefix[r] - prefix[l-1]
  • 2098: W[i][j]=0 → treat as no path, starting point can be fixed (0th) since it's cyclic
  • 2098: N=16, space O(2^16 * 16) = 1 million → sufficient
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

boj_11003.pyPython
"""Baekjoon 11003 — Finding the Minimum
https://www.acmicpc.net/problem/11003

Difficulty: Platinum V
Time  : O(n)
Space : O(L)

Solution:
    Sliding window minimum using a deque.
    Maintain a monotonically increasing deque: the front of the deque is the index of the minimum in the current window.
    N can be up to 5 million → sys.stdin + sys.stdout.write output optimization is essential."""

import sys
from collections import deque


def solve() -> None:
    input_data = sys.stdin.buffer.read().split()
    idx = 0
    N = int(input_data[idx]); idx += 1
    L = int(input_data[idx]); idx += 1
    A = [int(input_data[idx + i]) for i in range(N)]

    dq: deque[int] = deque()  #Store index
    result = []

    for i in range(N):
        #Remove elements from the back of the deque that are greater than or equal to the current value (keep monotonic increase)
        while dq and A[dq[-1]] >= A[i]:
            dq.pop()
        dq.append(i)

        #Remove indexes exceeding window size L
        if dq[0] < i - L + 1:
            dq.popleft()

        result.append(A[dq[0]])

    sys.stdout.write(" ".join(map(str, result)) + "\n")


if __name__ == "__main__":
    solve()
boj_11659.pyPython
"""Baekjoon 11659 — Finding the Sum of Sections 4
https://www.acmicpc.net/problem/11659

Difficulty: Silver III
Time: O(n + m)  n = number of elements, m = number of queries
Space: O(n)

Solution:
    1-indexed 1D prefix sum.
    prefix[i] = A[1] + ... + A[i]
    query(l, r) = prefix[r] - prefix[l-1]"""

import sys

input = sys.stdin.readline


def solve() -> None:
    N, M = map(int, input().split())
    A = list(map(int, input().split()))

    # 1-indexed prefix
    prefix = [0] * (N + 1)
    for i in range(1, N + 1):
        prefix[i] = prefix[i - 1] + A[i - 1]

    result = []
    for _ in range(M):
        l, r = map(int, input().split())
        result.append(prefix[r] - prefix[l - 1])

    print("\n".join(map(str, result)))


if __name__ == "__main__":
    solve()
boj_2003.pyPython
"""Baekjoon 2003 — Sum of Numbers 2
https://www.acmicpc.net/problem/2003

Difficulty: Silver IV
Time: O(n)
Space: O(n)

Solution:
    Two pointers in the same direction.
    Since all A[i] ≥ 1, it is safe to advance lo when the sum is greater than or equal to M.
    If sum == M, count++ and advance lo."""

import sys

input = sys.stdin.readline


def solve() -> None:
    N, M = map(int, input().split())
    A = list(map(int, input().split()))

    lo = 0
    total = 0
    count = 0

    for hi in range(N):
        total += A[hi]

        #If the sum is M or more, move lo forward
        while total >= M and lo <= hi:
            if total == M:
                count += 1
            total -= A[lo]
            lo += 1

    print(count)


if __name__ == "__main__":
    solve()
boj_2098.pyPython
"""Baekjoon 2098 — Traveling Salesman Problem (TSP)
https://www.acmicpc.net/problem/2098

Difficulty: Gold I
Time  : O(2ⁿ * n²)  n ≤ 16
Space : O(2ⁿ * n)

Solution:
    Bitmask DP (Held-Karp algorithm).

    dp[mask][v] = minimum cost when currently at v with the cities in mask bit set (visited cities).

    Initialization: dp[1 << i][i] = 0  (starting from city i)
                    or dp[1][0] = 0  (starting from 0, same since it's a cycle)

    Transition: for next != v, next not in mask:
            dp[mask | (1<<next)][next] = min(..., dp[mask][v] + W[v][next])

    Answer: min(dp[(1<<n)-1][v] + W[v][0]) for all v

    W[i][j] = 0 means there is no path."""

import sys

input = sys.stdin.readline
INF = sys.maxsize


def solve() -> None:
    N = int(input())
    W = [list(map(int, input().split())) for _ in range(N)]

    FULL = (1 << N) - 1
    dp = [[INF] * N for _ in range(1 << N)]
    dp[1][0] = 0   #Depart from city 0

    for mask in range(1 << N):
        for u in range(N):
            if dp[mask][u] == INF:
                continue
            if not (mask >> u & 1):
                continue

            for v in range(N):
                if mask >> v & 1:    #Already visited
                    continue
                if W[u][v] == 0:     #No route
                    continue
                new_mask = mask | (1 << v)
                cost = dp[mask][u] + W[u][v]
                if cost < dp[new_mask][v]:
                    dp[new_mask][v] = cost

    #Return to 0 after visiting all cities
    ans = INF
    for v in range(1, N):
        if dp[FULL][v] != INF and W[v][0] != 0:
            ans = min(ans, dp[FULL][v] + W[v][0])

    print(ans)


if __name__ == "__main__":
    solve()

Learn this lesson with a mentor

Work through the Python Algorithms & Data Structures track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now