นากกายสีมิ้นต์ต้นฉบับเลือกช่วงต่อเนื่องสีอำพันจากเส้นขึ้นลงบนพื้นมิ้นต์

LeetCode #53 Maximum Subarray: Kadane จากคำถามว่าจะต่อหรือเริ่มใหม่

น้องกายEngineering

โจทย์ maximum-subarray ให้ array ที่มีอย่างน้อยหนึ่งสมาชิก ต้องหาผลรวมสูงสุดของช่วงต่อเนื่องที่ไม่ว่าง คำว่า “ไม่ว่าง” ทำให้ input ติดลบทั้งหมดตอบสมาชิกที่ติดลบน้อยที่สุด ไม่ใช่ศูนย์

วิธีที่ 1: ขยายทุกจุดเริ่ม

เลือกซ้ายทุกตำแหน่งแล้วสะสมผลรวมเมื่อขยับขวา เวลา O(n²), พื้นที่ O(1) ดีกว่าคำนวณ sum ใหม่ทุกคู่ที่กลายเป็น O(n³) และเหมาะเป็น oracle สำหรับทดสอบข้อมูลเล็ก

วิธีที่ 2: prefix sum กับ minimum prefix

ผลรวมช่วง [i, j) เท่ากับ prefix[j] ลบ prefix[i] ดังนั้นเดินทีละจุด เก็บ prefix ต่ำสุดก่อนหน้าและผลต่างสูงสุดได้ใน O(n)/O(1) วิธีนี้เชื่อมไปโจทย์ prefix อื่นได้ดี แต่ต้องระวังลำดับอัปเดตเพื่อไม่เลือกช่วงว่าง

วิธีที่ 3: Kadane (แนะนำหลัก)

ที่แต่ละค่า มีเพียงสองทาง: ต่อช่วงที่จบก่อนหน้า หรือเริ่มช่วงใหม่ที่ค่าปัจจุบัน เก็บ ending_here = max(value, ending_here + value) และ best เวลา O(n), พื้นที่ O(1) Invariant คือ ending_here เป็นผลรวมสูงสุดของช่วงที่ต้องจบตรงตำแหน่งนี้

วิธีที่ 4: divide-and-conquer

คำตอบอยู่ซ้าย ขวา หรือข้ามกึ่งกลาง รวม prefix/suffix ที่ดีที่สุดแล้ว recurse ได้ O(n log n) มีคุณค่าด้านการสอนและต่อยอดเป็น segment tree แต่ซับซ้อนกว่า Kadane สำหรับคำถามครั้งเดียว

import unittest


def maximum_subarray(nums):
    if not nums:
        raise ValueError("nums must be non-empty")
    ending_here = best = nums[0]
    for value in nums[1:]:
        ending_here = max(value, ending_here + value)
        best = max(best, ending_here)
    return best


def maximum_subarray_bruteforce(nums):
    if not nums:
        raise ValueError("nums must be non-empty")
    best = nums[0]
    for left in range(len(nums)):
        total = 0
        for right in range(left, len(nums)):
            total += nums[right]
            best = max(best, total)
    return best


class MaximumSubarrayTests(unittest.TestCase):
    def test_examples_and_all_negative(self):
        cases = [
            ([-2, 1, -3, 4, -1, 2, 1, -5, 4], 6),
            ([-8, -3, -6], -3),
            ([5], 5),
            ([0, 0], 0),
        ]
        for nums, expected in cases:
            with self.subTest(nums=nums):
                self.assertEqual(maximum_subarray(nums), expected)
                self.assertEqual(maximum_subarray(nums), maximum_subarray_bruteforce(nums))

    def test_empty_input_is_rejected(self):
        with self.assertRaises(ValueError):
            maximum_subarray([])


if __name__ == "__main__":
    unittest.main()

เลือกตามโจทย์

เป้าหมายวิธี
อธิบายจากนิยาม/สร้าง test oracleO(n²)
เชื่อมแนวคิด prefix summinimum prefix
คำตอบมาตรฐานและ production scanKadane
ต้องตอบ query หลายช่วงภายหลังdivide-and-conquer/segment tree

ถ้าต้องคืน indices ให้เก็บจุดเริ่ม candidate เมื่อเลือก value แทน ending_here + value และบันทึกขอบเมื่อ best ดีขึ้น ต้องกำหนด tie-breaking ว่าเอาช่วงสั้นสุดหรือซ้ายสุด มิฉะนั้น implementation ที่ผลรวมเท่ากันอาจคืนคนละช่วง

Recap

Kadane คือ default เพราะ O(n)/O(1) และพิสูจน์ด้วย state “ช่วงที่ต้องจบตรงนี้” อย่าตั้ง best เป็นศูนย์เมื่อโจทย์บังคับช่วงไม่ว่าง และใช้ brute force เทียบผลบนอาร์เรย์สุ่มสั้นเพื่อจับ edge case ครับ