Algorithm Search⌘K

Maximum subarray divide and conquer in Python

Split at the midpoint, take the best of the left, right and crossing subarrays, giving Theta(n log n) time.

File
maximum_subarray_divide_and_conquer.py, 70 lines
Level
I0 not checked yet
Toolchain
Python 3.14, pytest 9
Test framework
pytest
Run the tests
python3 -m pytest
maximum_subarray_divide_and_conquer.pyRawGitHub
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/maximum-subarray-divide-and-conquer/. The authors are listed in prov/NXBA0T.jsonl.

from typing import Union

Number = Union[int, float]


def _maxsub(a: list[Number], lo: int, hi: int) -> tuple[Number, int, int]:
    if lo == hi:  # @line L1
        return a[lo], lo, lo
    mid = (lo + hi) // 2  # @line L2
    s_left, i_left, j_left = _maxsub(a, lo, mid)  # @line L3
    s_right, i_right, j_right = _maxsub(a, mid + 1, hi)
    t: Number = 0  # @line L4
    left_sum: Number = a[mid]
    ci = mid
    for i in range(mid, lo - 1, -1):
        t += a[i]
        if t > left_sum or i == mid:
            left_sum = t
            ci = i
    t = 0  # @line L5
    right_sum: Number = a[mid + 1]
    cj = mid + 1
    for j in range(mid + 1, hi + 1):
        t += a[j]
        if t > right_sum or j == mid + 1:
            right_sum = t
            cj = j
    s_cross = left_sum + right_sum  # @line L6
    if s_left >= s_right and s_left >= s_cross:  # @line L7
        return s_left, i_left, j_left
    if s_right >= s_cross:  # @line L8
        return s_right, i_right, j_right
    return s_cross, ci, cj  # @line L9


def max_subarray_dc(a: list[Number]) -> tuple[Number, int, int]:
    """Return (best, i, j) with sum(a[i:j + 1]) == best, the largest sum of a non-empty subarray.

    Indices are 0-based and inclusive. Ties prefer the left half, then the right
    half, then the crossing subarray.
    Precondition: a is not empty (raises ValueError otherwise).
    Cost: Theta(n log n) time, Theta(log n) extra space for the recursion.
    """
    if not a:
        raise ValueError("the array must not be empty")
    return _maxsub(a, 0, len(a) - 1)


def max_subarray_linear_dc(a: list[Number]) -> Number:
    """Return the largest non-empty subarray sum with the four-value combine.

    Each part returns (total, best prefix, best suffix, best). Cost: Theta(n)
    time, Theta(log n) extra space. Precondition: a is not empty.
    """
    if not a:
        raise ValueError("the array must not be empty")

    def solve(lo: int, hi: int) -> tuple[Number, Number, Number, Number]:
        if lo == hi:
            x = a[lo]
            return x, x, x, x
        mid = (lo + hi) // 2
        lt, lp, ls, lb = solve(lo, mid)
        rt, rp, rs, rb = solve(mid + 1, hi)
        return lt + rt, max(lp, lt + rp), max(rs, rt + ls), max(lb, rb, ls + rp)

    return solve(0, len(a) - 1)[3]
test_maximum_subarray_divide_and_conquer.py 45 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/maximum-subarray-divide-and-conquer/. The authors are listed in prov/NXBA0T.jsonl.

import random

import pytest

from maximum_subarray_divide_and_conquer import max_subarray_dc, max_subarray_linear_dc


def brute(a: list[int]) -> int:
    return max(sum(a[i : j + 1]) for i in range(len(a)) for j in range(i, len(a)))


def test_example() -> None:
    assert max_subarray_dc([-2, 1, -3, 4, -1, 2, 1, -5, 4]) == (6, 3, 6)
    assert max_subarray_dc([31, -41, 59, 26, -53, 58, 97, -93, -23, 84]) == (187, 2, 6)
    clrs = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
    assert max_subarray_dc(clrs) == (43, 7, 10)
    assert max_subarray_linear_dc(clrs) == 43


def test_edges() -> None:
    assert max_subarray_dc([5]) == (5, 0, 0)
    assert max_subarray_dc([-5]) == (-5, 0, 0)
    assert max_subarray_dc([-3, -1, -2]) == (-1, 1, 1)
    assert max_subarray_dc([1, 2, 3]) == (6, 0, 2)
    assert max_subarray_dc([1.5, -0.5, 2.0]) == (3.0, 0, 2)
    assert max_subarray_linear_dc([-3, -1, -2]) == -1
    with pytest.raises(ValueError):
        max_subarray_dc([])
    with pytest.raises(ValueError):
        max_subarray_linear_dc([])


def test_against_brute_force() -> None:
    rng = random.Random(20261105)
    for _ in range(2000):
        n = rng.randrange(1, 30)
        a = [rng.randrange(-10, 11) for _ in range(n)]
        best, i, j = max_subarray_dc(a)
        assert best == brute(a)
        assert 0 <= i <= j < n
        assert sum(a[i : j + 1]) == best
        assert max_subarray_linear_dc(a) == best

What the next level needs

  • the tests did not run in this build

Read the full page about Maximum subarray divide and conquer