Circular Kadane algorithm in Python
Answer is the larger of plain Kadane and the total sum minus the minimum subarray sum.
- File
circular_kadane_algorithm.py, 58 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/circular-kadane-algorithm/. The authors are listed in prov/00F99T.jsonl.
from typing import Union
Number = Union[int, float]
def circular_kadane(a: list[Number]) -> Number:
"""Return the largest sum of a non-empty circular subarray of a.
A circular subarray may wrap from the end of a to its start; no item is used twice.
Precondition: a is not empty (raises ValueError otherwise).
Cost: O(n) time, O(1) extra space.
"""
if not a:
raise ValueError("the array must not be empty")
hi = best = lo = worst = total = a[0] # @line L1
for j in range(1, len(a)): # @line L2
hi = a[j] + max(hi, 0) # @line L3
best = max(best, hi)
lo = a[j] + min(lo, 0) # @line L4
worst = min(worst, lo)
total += a[j] # @line L5
if best < 0: # @line L6
return best
return max(best, total - worst) # @line L7
def circular_kadane_arc(a: list[Number]) -> tuple[Number, int, int]:
"""Return (best, start, length): a best circular arc a[start], a[start + 1 mod n], ...
Same precondition and cost as circular_kadane. Ties prefer a plain arc.
"""
if not a:
raise ValueError("the array must not be empty")
n = len(a)
hi = best = lo = worst = total = a[0]
hs = ls = 0
bi, bj = 0, 0
wi, wj = 0, 0
for j in range(1, n):
if hi < 0:
hi, hs = a[j], j
else:
hi += a[j]
if hi > best:
best, bi, bj = hi, hs, j
if lo > 0:
lo, ls = a[j], j
else:
lo += a[j]
if lo < worst:
worst, wi, wj = lo, ls, j
total += a[j]
if best < 0 or best >= total - worst or (wi, wj) == (0, n - 1):
return best, bi, bj - bi + 1
return total - worst, (wj + 1) % n, n - (wj - wi + 1)
test_circular_kadane_algorithm.py 56 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/circular-kadane-algorithm/. The authors are listed in prov/00F99T.jsonl.
import random
import pytest
from circular_kadane_algorithm import circular_kadane, circular_kadane_arc
def brute(a: list[int]) -> int:
n = len(a)
return max(
sum(a[(s + k) % n] for k in range(length))
for s in range(n)
for length in range(1, n + 1)
)
def arc_sum(a: list[int], start: int, length: int) -> int:
return sum(a[(start + k) % len(a)] for k in range(length))
def test_example() -> None:
assert circular_kadane([8, -4, 3, -5, 4]) == 12
assert circular_kadane_arc([8, -4, 3, -5, 4]) == (12, 4, 2)
assert circular_kadane([5, -3, 5]) == 10
assert circular_kadane([1, -2, 3, -2]) == 3
def test_edges() -> None:
assert circular_kadane([-3, -2, -3]) == -2
assert circular_kadane_arc([-3, -2, -3]) == (-2, 1, 1)
assert circular_kadane([7]) == 7
assert circular_kadane([-7]) == -7
assert circular_kadane([5, -10]) == 5
assert circular_kadane([-1, 0]) == 0
assert circular_kadane([1, 2, 3]) == 6
assert circular_kadane([0.5, -1.0, 0.5]) == 1.0
with pytest.raises(ValueError):
circular_kadane([])
with pytest.raises(ValueError):
circular_kadane_arc([])
def test_against_brute_force() -> None:
rng = random.Random(20261105)
for _ in range(3000):
n = rng.randrange(1, 15)
a = [rng.randrange(-10, 11) for _ in range(n)]
expected = brute(a)
assert circular_kadane(a) == expected
value, start, length = circular_kadane_arc(a)
assert value == expected
assert 0 <= start < n and 1 <= length <= n
assert arc_sum(a, start, length) == expected
What the next level needs
- the tests did not run in this build