ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด
ใช้อัลกอริทึมของ Kadane กับช่วงย่อยผลรวมสูงสุด และขยายให้ติดตามทั้งค่าสูงสุดและค่าต่ำสุดสำหรับรูปแบบผลคูณ
ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
ปัญหาอาร์เรย์ย่อยผลรวมสูงสุด
ปัญหา อาร์เรย์ย่อยผลรวมสูงสุด ให้คุณค้นหาอาร์เรย์ย่อยที่อยู่ติดกันภายในอาร์เรย์ตัวเลขหนึ่งมิติซึ่งมีผลรวมมากที่สุด ตัวอย่างเช่น ใน [-2, 1, -3, 4, -1, 2, 1, -5, 4] อาร์เรย์ย่อย [4, -1, 2, 1] ให้ผลรวมสูงสุดเท่ากับ 6 วิธีตรวจสอบทุกอาร์เรย์ย่อยแบบครบทุกกรณีใช้เวลา O(n²) แต่อัลกอริทึมของ Kadane แก้ปัญหานี้ได้ในเวลา O(n)
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
# Brute force: O(n^2)
max_sum = float('-inf')
for i in range(len(nums)):
curr = 0
for j in range(i, len(nums)):
curr += nums[j]
max_sum = max(max_sum, curr)
print(max_sum) # 6แนวคิดของอัลกอริทึมของ Kadane
อัลกอริทึมของ Kadane วนผ่านอาร์เรย์หนึ่งครั้ง โดยรักษา current_sum ซึ่งเป็นผลรวมสะสมไว้ ในแต่ละสมาชิก คุณต้องตัดสินใจว่า จะต่ออาร์เรย์ย่อยเดิม หรือ เริ่มใหม่จากสมาชิกนี้ หาก current_sum กลายเป็นค่าลบ ค่านี้จะมีแต่ทำให้อาร์เรย์ย่อยในอนาคตแย่ลง จึงควรเริ่มใหม่ ความสัมพันธ์เวียนเกิดคือ current_sum = max(num, current_sum + num)
def max_subarray(nums):
max_sum = current_sum = nums[0]
for num in nums[1:]:
# Extend or start fresh?
current_sum = max(num, current_sum + num)
max_sum = max(max_sum, current_sum)
return max_sum
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
print(max_subarray(nums)) # 6การติดตามการทำงานของอัลกอริทึมของ Kadane
มาลองติดตามการทำงานของอัลกอริทึมของ Kadane กับ [-2, 1, -3, 4, -1, 2, 1, -5, 4]: เริ่มด้วย curr=-2, max=-2 เมื่อพบ 1: curr=max(1,-2+1)=1, max=1 เมื่อพบ -3: curr=max(-3,1-3)=-2, max=1 เมื่อพบ 4: curr=max(4,-2+4)=4, max=4 เมื่อพบ -1: curr=3, max=4 เมื่อพบ 2: curr=5, max=5 เมื่อพบ 1: curr=6, max=6 เมื่อพบ -5: curr=1 เมื่อพบ 4: curr=5, max=6 อัลกอริทึมระบุได้อย่างถูกต้องว่าอาร์เรย์ย่อยที่สิ้นสุดที่ดัชนี 6 เป็นคำตอบที่ดีที่สุด
def max_subarray_trace(nums):
curr = max_sum = nums[0]
for i, num in enumerate(nums[1:], 1):
new_curr = max(num, curr + num)
max_sum = max(max_sum, new_curr)
print(f'i={i}, num={num}, curr: {curr}->{new_curr}, max={max_sum}')
curr = new_curr
return max_sum
max_subarray_trace([-2, 1, -3, 4, -1, 2, 1, -5, 4])การคืนค่าอาร์เรย์ย่อยจริง
หากผู้สัมภาษณ์ขอให้คุณ คืนค่าอาร์เรย์ย่อยเอง (ไม่ใช่เพียงผลรวม) คุณต้องติดตามดัชนีเริ่มต้นและสิ้นสุด เมื่อเริ่มใหม่ (เพราะ num > current_sum + num) ให้ปรับปรุงค่า temp_start เมื่อปรับปรุง max_sum ให้บันทึก temp_start เป็น start และบันทึกดัชนีปัจจุบันเป็น end วิธีนี้เพิ่มค่าใช้จ่าย O(1) ให้กับอัลกอริทึมเดิมที่ใช้เวลา O(n)
def max_subarray_indices(nums):
max_sum = curr = nums[0]
start = end = temp_start = 0
for i in range(1, len(nums)):
if nums[i] > curr + nums[i]:
curr = nums[i]
temp_start = i
else:
curr += nums[i]
if curr > max_sum:
max_sum = curr
start, end = temp_start, i
return max_sum, nums[start:end+1]
print(max_subarray_indices([-2, 1, -3, 4, -1, 2, 1, -5, 4]))
# (6, [4, -1, 2, 1])ปัญหาอาร์เรย์ย่อยผลคูณสูงสุด
ปัญหา อาร์เรย์ย่อยผลคูณสูงสุด ซับซ้อนกว่ารูปแบบผลรวมเนื่องจากมี จำนวนลบ จำนวนลบสองจำนวนคูณกันได้จำนวนบวก ดังนั้นผลคูณที่ติดลบมากอาจกลายเป็นค่าสูงสุดได้เมื่อคูณด้วยจำนวนลบอีกจำนวนหนึ่ง สำหรับ [2, 3, -2, 4] คำตอบคือ 6 ([2, 3]) สำหรับ [-2, 0, -1] คำตอบคือ 0 เราต้องติดตามผลคูณทั้ง ค่าสูงสุดและค่าต่ำสุด ในแต่ละขั้นตอน
nums = [2, 3, -2, 4]
# [2,3,-2,4]: products [2, 6, -12, -48]
# subarrays: [2]=2, [2,3]=6, [3]=3, etc.
# max is 6 from subarray [2,3]
nums2 = [-2, 3, -4]
# [-2]*3*[-4] = 24
# negative*negative=positive!
print('Expected:', 24)การติดตามผลคูณทั้งค่าสูงสุดและค่าต่ำสุด
แนวคิดสำคัญคือ ในแต่ละตำแหน่ง ผลคูณปัจจุบันที่มีค่าสูงสุดจะเป็นหนึ่งใน num, max_so_far * num หรือ min_so_far * num (ตัวเลือกสุดท้ายมีประโยชน์เมื่อจำนวนลบเปลี่ยนค่าต่ำสุดให้เป็นค่าสูงสุด) เช่นเดียวกันกับค่าต่ำสุด ให้ปรับปรุงทั้ง สองค่า คือ cur_max และ cur_min พร้อมกันโดยใช้ค่าก่อนหน้า เพื่อหลีกเลี่ยงการนำค่าที่ปรับปรุงแล้วมาใช้ซ้ำในขั้นตอนเดียวกัน
def max_product(nums):
max_prod = min_prod = result = nums[0]
for num in nums[1:]:
# All three candidates for new max
candidates = (num, max_prod * num, min_prod * num)
max_prod, min_prod = max(candidates), min(candidates)
result = max(result, max_prod)
return result
print(max_product([2, 3, -2, 4])) # 6
print(max_product([-2, 3, -4])) # 24
print(max_product([-2, 0, -1])) # 0
print(max_product([-2])) # -2เหตุใดผลคูณต่ำสุดจึงสำคัญ
พิจารณา [-3, -10, 5] หลังประมวลผล -3: max=-3, min=-3 หลังประมวลผล -10: ค่าที่เป็นไปได้คือ (-10, 30, 30) → max=30, min=-10 หลังประมวลผล 5: ค่าที่เป็นไปได้คือ (5, 150, -50) → max=150 หากไม่ติดตาม min_prod คุณจะพลาดการพลิกค่าที่เกิดขึ้นเมื่อค่าต่ำสุดซึ่งติดลบมากถูกคูณด้วยจำนวนลบอีกจำนวนหนึ่ง ให้คำนวณทั้ง max และ min จากค่าเดิมชุดเดียวกันเสมอ เพื่อหลีกเลี่ยงข้อผิดพลาดจากการอ่านค่าที่ล้าสมัย
def max_product_traced(nums):
max_p = min_p = result = nums[0]
for num in nums[1:]:
prev_max, prev_min = max_p, min_p
max_p = max(num, prev_max * num, prev_min * num)
min_p = min(num, prev_max * num, prev_min * num)
result = max(result, max_p)
print(f'num={num}: max_p={max_p}, min_p={min_p}')
return result
max_product_traced([-3, -10, 5])
# max_p after -10: 30 (flip!)
# max_p after 5: 150ศูนย์จะรีเซ็ตผลคูณ
ศูนย์ ในอาร์เรย์จะรีเซ็ตผลคูณสะสมทั้งสองค่าเป็นศูนย์ ซึ่งเท่ากับการแบ่งอาร์เรย์ออกเป็นอาร์เรย์ย่อยอิสระ เมื่อ num = 0 ทั้ง max_prod * 0 = 0 และ min_prod * 0 = 0 ดังนั้นค่าที่เป็นไปได้ทั้งสามค่าจึงกลายเป็น 0 และค่าสูงสุดจากผลลัพธ์ก่อนหน้าจะยังคงอยู่ ไม่จำเป็นต้องเขียนโค้ดกรณีพิเศษ — สูตรทั่วไปจัดการกับศูนย์ได้โดยธรรมชาติ
def max_product(nums):
max_p = min_p = result = nums[0]
for num in nums[1:]:
cands = (num, max_p * num, min_p * num)
max_p, min_p = max(cands), min(cands)
result = max(result, max_p)
return result
# Zero splits array into independent subarrays
print(max_product([3, -1, 4, 0, 2, 5, -1])) # 10 (2*5)
print(max_product([0, 2])) # 2
print(max_product([-1, 0, -2])) # 0ทางเลือก: การกวาดผลคูณจากซ้ายไปขวาและขวาไปซ้าย
อีกวิธีหนึ่งคือกวาดข้อมูลทั้งจาก ซ้ายไปขวา และ ขวาไปซ้าย โดยรีเซ็ตผลคูณสะสมเป็น 1 เมื่อพบศูนย์ อาร์เรย์ย่อยผลคูณสูงสุดจะไม่ข้ามศูนย์ ดังนั้นหากจำนวนลบทำให้ผลลัพธ์แย่ลงในทิศทางหนึ่ง การกวาดย้อนกลับจะตรวจพบการพลิกค่านั้น วิธีนี้ดูสง่างาม แต่ในการสัมภาษณ์มักคาดหวังวิธี ติดตามค่าต่ำสุดและค่าสูงสุด มากกว่า
def max_product_sweep(nums):
result = max(nums)
left = right = 1
n = len(nums)
for i in range(n):
left *= nums[i]
right *= nums[n - 1 - i]
result = max(result, left, right)
if left == 0: left = 1
if right == 0: right = 1
return result
print(max_product_sweep([2, 3, -2, 4])) # 6
print(max_product_sweep([-2, 3, -4])) # 24
print(max_product_sweep([-2, 0, -1])) # 0ความแตกต่างสำคัญระหว่าง Kadane กับผลคูณ
อาร์เรย์ย่อยผลรวมและอาร์เรย์ย่อยผลคูณมีความแตกต่างที่สำคัญ สำหรับผลรวม จำนวนลบเป็นผลเสียเสมอ คุณจึงเริ่มใหม่แบบตะกละ สำหรับผลคูณ จำนวนลบสองจำนวนช่วยเพิ่มผลลัพธ์ คุณจึงต้องติดตามค่าที่อยู่สุดขั้วทั้งสอง นอกจากนี้ ศูนย์เป็นจุดสิ้นสุดสำหรับผลคูณ แต่มีผลเสียต่อผลรวมเพียงเล็กน้อย เมื่ออธิบายในการสัมภาษณ์ ให้กล่าวถึงความแตกต่างเหล่านี้อย่างชัดเจน และอธิบายเหตุผลที่ต้องติดตามค่าต่ำสุดก่อนเขียนโค้ด
# Max Sum Subarray: O(n) time, O(1) space
def max_sum(nums):
curr = result = nums[0]
for n in nums[1:]:
curr = max(n, curr + n) # restart or extend
result = max(result, curr)
return result
# Max Product Subarray: O(n) time, O(1) space
def max_prod(nums):
lo = hi = result = nums[0]
for n in nums[1:]:
lo, hi = min(n, lo*n, hi*n), max(n, lo*n, hi*n)
result = max(result, hi)
return result
print(max_sum([-2, 1, -3, 4, -1, 2, 1])) # 6
print(max_prod([-2, 3, -4])) # 24ความซับซ้อนและเคล็ดลับการสัมภาษณ์
ทั้งอัลกอริทึมของ Kadane (ผลรวมสูงสุด) และการติดตามค่าต่ำสุด/สูงสุด (ผลคูณสูงสุด) ใช้เวลา O(n) และใช้พื้นที่ O(1) เคล็ดลับสำคัญในการสัมภาษณ์: (1) สำหรับผลรวมสูงสุด ให้กล่าวถึงทางเลือกแบบแบ่งและพิชิตที่ใช้เวลา O(n log n) เพื่อแสดงให้เห็นว่าคุณมีความรู้ครอบคลุม (2) สำหรับผลคูณสูงสุด ให้เน้นว่าคุณปรับปรุง min_prod และ max_prod พร้อมกันโดยใช้ค่าก่อนหน้า เพื่อหลีกเลี่ยงการใช้ข้อมูลที่ล้าสมัย (3) ให้ถามให้ชัดเจนเสมอว่าอาร์เรย์ว่างได้หรือไม่ และอาร์เรย์ย่อยต้องไม่ว่างใช่หรือไม่ (ตามธรรมเนียมแล้ว อาร์เรย์ย่อยต้องไม่ว่าง)
# Both run O(n) time, O(1) space
# Kadane handles: all negative (returns least negative)
# Product handles: zeros (resets naturally), negatives (tracks both extremes)
nums_all_neg = [-5, -2, -8]
print('Max sum (all neg):', max(max(nums_all_neg[0:1]),
max(x for x in nums_all_neg))) # -2
# Correct: return the maximum element when all are negativeตรวจสอบความเข้าใจ
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูล & อัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
สรุปบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้: อัลกอริทึมของ Kadane แก้ปัญหาอาร์เรย์ย่อยผลรวมสูงสุดได้ในเวลา O(n) โดยเลือกต่อหรือเริ่มใหม่ที่แต่ละสมาชิก, อาร์เรย์ย่อยผลคูณสูงสุดต้องติดตามทั้งผลคูณสะสมต่ำสุดและสูงสุด เนื่องจากการพลิกค่าที่เกิดจากจำนวนลบ และ ศูนย์จะรีเซ็ตผลคูณสะสมได้โดยธรรมชาติโดยไม่ต้องเขียนโค้ดกรณีพิเศษ ต่อไปเราจะศึกษาโจทย์การแบ่งคำโดยใช้ตาราง DP หนึ่งมิติ
คำถามที่พบบ่อย
บทเรียน “ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด”
ใช้อัลกอริทึมของ Kadane กับช่วงย่อยผลรวมสูงสุด และขยายให้ติดตามทั้งค่าสูงสุดและค่าต่ำสุดสำหรับรูปแบบผลคูณ คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- โจรปล้นบ้าน: ความสัมพันธ์เวียนเกิดแบบเลือกหรือข้าม
- ช่วงย่อยผลรวมสูงสุดและช่วงย่อยผลคูณสูงสุด
- การแบ่งคำและการแบ่งสตริงเป็นส่วน
- ถอดรหัสวิธีและการนับเส้นทาง