0Pricing
DSA Interview Prep · บทเรียน

ผลรวมคำนำหน้าและยอดรวมสะสม

สร้างอาร์เรย์ผลรวมคำนำหน้าเพื่อตอบคำถามผลรวมช่วงในเวลา O(1) และประยุกต์เทคนิคนี้กับโจทย์ช่วงย่อย เช่น ช่วงย่อยที่มีผลรวมสูงสุด

ผลรวมคำนำหน้าและยอดรวมสะสม เป็นบทเรียน DSA Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน DSA Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

โจทย์ผลรวมช่วง

เมื่อกำหนดอาร์เรย์ nums คุณต้องตอบการสอบถามหลายรายการในรูปแบบว่า ผลรวมของสมาชิกตั้งแต่ดัชนี i ถึงดัชนี j คือเท่าใด การคำนวณแต่ละรายการแบบตรงไปตรงมาใช้เวลา O(n) ดังนั้นการสอบถาม k รายการจึงมีต้นทุน O(n×k) หากใช้อาร์เรย์ผลรวมตั้งแต่ต้น คุณจะคำนวณผลรวมสะสมล่วงหน้าด้วยเวลา O(n) แล้วตอบการสอบถามแต่ละรายการได้ในเวลา O(1) นี่เป็นหนึ่งในเทคนิคการคำนวณล่วงหน้าที่ใช้กันอย่างแพร่หลายที่สุดในการสัมภาษณ์

# Naive: O(n) per query
def range_sum_naive(nums, i, j):
    return sum(nums[i:j+1])

nums = [1, 3, 5, 7, 9]
print(range_sum_naive(nums, 1, 3))  # 3+5+7 = 15
print(range_sum_naive(nums, 0, 4))  # 1+3+5+7+9 = 25
# For 1000 queries, this takes 5000 operations

การสร้างอาร์เรย์ผลรวมตั้งแต่ต้น

กำหนดให้ prefix[i] เป็นผลรวมของ nums[0] ถึง nums[i-1] (มีช่องเพิ่มอีกหนึ่งช่อง โดยออฟเซ็ต 1 แบบดัชนีเริ่มจากศูนย์ช่วยให้จัดการกรณีขอบเขตได้ง่ายขึ้น) สร้างอาร์เรย์นี้ด้วยเวลา O(n) โดยวนผ่านข้อมูลเพียงรอบเดียว: prefix[i] = prefix[i-1] + nums[i-1] จากนั้นการสอบถามช่วง sum(i, j) จะกลายเป็น prefix[j+1] - prefix[i] ซึ่งเป็นการลบเพียงครั้งเดียวและใช้เวลา O(1)

def build_prefix(nums):
    n = len(nums)
    prefix = [0] * (n + 1)
    for i in range(n):
        prefix[i+1] = prefix[i] + nums[i]
    return prefix

def range_sum(prefix, i, j):
    return prefix[j+1] - prefix[i]  # O(1)

nums = [1, 3, 5, 7, 9]
pre = build_prefix(nums)
print(pre)                    # [0, 1, 4, 9, 16, 25]
print(range_sum(pre, 1, 3))  # 9 - 1 = 8? Wait: 3+5+7=15
# Hmm: prefix[4]-prefix[1] = 16-1 = 15  correct
print(range_sum(pre, 1, 3))  # 15

ผลรวมช่วงย่อยเท่ากับ K

การหาจำนวนช่วงย่อยที่มีผลรวมเท่ากับ k เป็นโจทย์คลาสสิกที่ใช้ตารางแฮชร่วมกับผลรวมตั้งแต่ต้น แนวคิดสำคัญคือ ผลรวมช่วงย่อยตั้งแต่ i ถึง j เท่ากับ prefix[j] - prefix[i-1] หากต้องการให้ค่านี้เท่ากับ k จะได้ว่า prefix[i-1] = prefix[j] - k ขณะสแกนจากซ้ายไปขวาโดยรักษาผลรวมตั้งแต่ต้นสะสมไว้ ให้ค้นหาว่า current_sum - k ปรากฏมาก่อนกี่ครั้ง แล้วนับช่วงย่อยที่ถูกต้องทั้งหมดได้ในเวลา O(n)

from collections import defaultdict

def subarray_sum_k(nums, k):
    count = 0
    current = 0
    freq = defaultdict(int)
    freq[0] = 1  # empty prefix
    for n in nums:
        current += n
        count += freq[current - k]  # how many prior sums give diff=k
        freq[current] += 1
    return count

print(subarray_sum_k([1, 1, 1], 2))    # 2
print(subarray_sum_k([1, 2, 3], 3))    # 2  ([1,2] and [3])

ผลรวมช่วงย่อยสูงสุดด้วยผลรวมตั้งแต่ต้น

เราสามารถมองปัญหาผลรวมช่วงย่อยสูงสุดเป็นปัญหาผลรวมตั้งแต่ต้นได้ โดยสำหรับแต่ละดัชนี j เราต้องการหาค่าสูงสุดของ prefix[j] - prefix[i] สำหรับ i ทุกค่าที่ < j ค่า i ที่เหมาะสมที่สุดสำหรับแต่ละ j คือผลรวมตั้งแต่ต้นที่มีค่าน้อยที่สุดซึ่งพบมาจนถึงตอนนั้น การสแกนจากซ้ายไปขวาพร้อมติดตาม min_prefix ใช้เวลา O(n) วิธีนี้เทียบเท่ากับอัลกอริทึมของ Kadane เมื่อมองผ่านมุมมองของผลรวมตั้งแต่ต้น

def max_subarray_prefix(nums):
    max_sum  = float('-inf')
    min_pre  = 0  # prefix[0] = 0
    current  = 0
    for n in nums:
        current += n
        max_sum = max(max_sum, current - min_pre)
        min_pre = min(min_pre, current)
    return max_sum

print(max_subarray_prefix([-2,1,-3,4,-1,2,1,-5,4]))
# 6  (same as Kadane's)
print(max_subarray_prefix([-1,-2,-3]))
# -1

ผลรวมตั้งแต่ต้นแบบ 2 มิติสำหรับการสอบถามตาราง

ผลรวมตั้งแต่ต้นสามารถขยายไปใช้กับตาราง 2 มิติได้ กำหนดให้ P[i][j] เป็นผลรวมของสมาชิกทั้งหมดในสี่เหลี่ยมตั้งแต่ (0,0) ถึง (i-1,j-1) สร้างค่าด้วยสูตรการรวมและตัดส่วนซ้ำ: P[i][j] = P[i-1][j] + P[i][j-1] - P[i-1][j-1] + grid[i-1][j-1] จากนั้นการสอบถามผลรวมของสี่เหลี่ยมใด ๆ ตั้งแต่ (r1,c1) ถึง (r2,c2) จะตอบได้ในเวลา O(1) โดยใช้การค้นค่าทั้งสี่ตำแหน่ง

def build_2d_prefix(grid):
    R, C = len(grid), len(grid[0])
    P = [[0]*(C+1) for _ in range(R+1)]
    for r in range(1, R+1):
        for c in range(1, C+1):
            P[r][c] = (P[r-1][c] + P[r][c-1]
                       - P[r-1][c-1] + grid[r-1][c-1])
    return P

def rect_sum(P, r1, c1, r2, c2):
    return P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1]

grid = [[3,0,1,4],[5,6,3,2],[1,2,0,1]]
P = build_2d_prefix(grid)
print(rect_sum(P, 0, 0, 1, 1))  # 3+0+5+6 = 14

ผลรวมสะสมสำหรับดัชนีสมดุล

ดัชนีสมดุล คือตำแหน่งที่ผลรวมของสมาชิกทางซ้ายเท่ากับผลรวมทางขวา ให้คำนวณผลรวมทั้งหมดล่วงหน้า จากนั้นสแกนโดยรักษาผลรวมทางซ้ายสะสมไว้ ผลรวมทางขวาคือ total - left_sum - nums[i] ตรวจสอบความเท่ากันในเวลา O(1) ต่อดัชนี จึงใช้เวลา O(n) โดยรวม วิธีนี้แสดงให้เห็นว่าผลรวมสะสมสามารถใช้แทนอาร์เรย์ผลรวมตั้งแต่ต้นสองชุดได้อย่างไร

def find_pivot_index(nums):
    total = sum(nums)
    left_sum = 0
    for i, n in enumerate(nums):
        # right_sum = total - left_sum - nums[i]
        if left_sum == total - left_sum - n:
            return i
        left_sum += n
    return -1

print(find_pivot_index([1, 7, 3, 6, 5, 6]))  # 3
print(find_pivot_index([1, 2, 3]))             # -1

อาร์เรย์ผลคูณโดยไม่รวมตัวเอง

เมื่อกำหนดอาร์เรย์ ให้ส่งคืนอาร์เรย์ที่สมาชิกแต่ละตัวเป็นผลคูณของสมาชิกตัวอื่นทั้งหมด ห้ามใช้การหาร ให้ใช้ ผลคูณของส่วนก่อนหน้า และ ผลคูณของส่วนถัดไป: result[i] = (ผลคูณของสมาชิกทั้งหมดก่อน i) × (ผลคูณของสมาชิกทั้งหมดหลัง i) สร้างผลคูณของส่วนก่อนหน้าด้วยการวนจากซ้ายไปขวา จากนั้นคูณผลคูณของส่วนถัดไปด้วยการวนจากขวาไปซ้ายโดยใช้ตัวแปรสะสม จึงไม่ต้องใช้อาร์เรย์เพิ่มเติมสำหรับส่วนถัดไป

def product_except_self(nums):
    n = len(nums)
    result = [1] * n
    # Left pass: result[i] = product of nums[:i]
    prefix = 1
    for i in range(n):
        result[i] = prefix
        prefix *= nums[i]
    # Right pass: multiply in product of nums[i+1:]
    suffix = 1
    for i in range(n-1, -1, -1):
        result[i] *= suffix
        suffix *= nums[i]
    return result

print(product_except_self([1, 2, 3, 4]))
# [24, 12, 8, 6]   O(n) time, O(1) extra space

ผลรวมตั้งแต่ต้นด้วยโมดูโล

โจทย์บางข้อถามหาจำนวนช่วงย่อยที่ผลรวมหารด้วย k ลงตัว โดยใช้ผลรวมตั้งแต่ต้นแบบโมดูโล k: หาก prefix[j] % k == prefix[i] % k แสดงว่า sum(i+1..j) หารด้วย k ลงตัว การใช้ตารางแฮชนับจำนวนแต่ละค่าเศษที่พบขณะสแกนทำให้ใช้เวลา O(n) การกำหนดค่าเริ่มต้นที่สำคัญคือ freq[0] = 1 เพื่อรองรับช่วงย่อยที่เริ่มจากดัชนี 0

from collections import defaultdict

def subarray_div_by_k(nums, k):
    freq = defaultdict(int)
    freq[0] = 1
    current = 0
    count = 0
    for n in nums:
        current = (current + n) % k
        count += freq[current]
        freq[current] += 1
    return count

print(subarray_div_by_k([4, 5, 0, -2, -3, 1], 5))
# 7  (seven subarrays divisible by 5)

อาร์เรย์ผลต่างสำหรับการอัปเดตช่วง

อาร์เรย์ผลต่าง เป็นสิ่งตรงข้ามกับผลรวมตั้งแต่ต้น เมื่อกำหนดอาร์เรย์ ให้คำนวณล่วงหน้าว่า diff[i] = nums[i] - nums[i-1] การเพิ่ม x ให้กับช่วง [l, r] ต้องดำเนินการกับอาร์เรย์ผลต่างเพียงสองครั้งในเวลา O(1): diff[l] += x และ diff[r+1] -= x หลังจากอัปเดตทั้งหมดแล้ว ให้สร้างอาร์เรย์ผลลัพธ์กลับคืนด้วยการวนหาผลรวมตั้งแต่ต้นเพียงรอบเดียว วิธีนี้เปลี่ยนการอัปเดตช่วง k ครั้งจาก O(n×k) เป็น O(n + k)

def apply_range_updates(n, updates):
    # updates: list of (l, r, val)
    diff = [0] * (n + 1)
    for l, r, val in updates:
        diff[l]   += val
        diff[r+1] -= val
    # Reconstruct with prefix sum
    result = []
    running = 0
    for i in range(n):
        running += diff[i]
        result.append(running)
    return result

# Add 3 to [1,3], add 1 to [0,2]
print(apply_range_updates(5, [(1,3,3),(0,2,1)]))
# [1, 4, 4, 3, 0]

ผลรวมตั้งแต่ต้นในโจทย์สัมภาษณ์

ผลรวมตั้งแต่ต้นปรากฏในโจทย์หลายประเภท:

  • การสอบถามช่วง — ผลรวมช่วงย่อย ผลรวมสี่เหลี่ยม
  • การนับช่วงย่อย — ผลรวมเท่ากับ k หารด้วย k ลงตัว
  • โจทย์ผลคูณ — ผลคูณโดยไม่รวมตัวเอง
  • สมดุล — หาดัชนีจุดหมุน
  • การอัปเดตช่วง — อาร์เรย์ผลต่าง
เมื่อพบโจทย์ที่เกี่ยวข้องกับผลรวมสะสมหรือการรวมค่าตามช่วง ให้คิดถึงผลรวมตั้งแต่ต้นก่อน วิธีนี้แทบจะปลดล็อกคำตอบที่ใช้เวลา O(n) จากวิธีลองทำทุกกรณีแบบตรงไปตรงมาซึ่งใช้เวลา O(n²) ได้เสมอ

# Template: prefix sum + hash map for subarray problems
from collections import defaultdict

def subarray_count_template(nums, target):
    """
    Count subarrays with property involving prefix sums.
    Adapt 'target' and lookup condition for each problem.
    """
    freq = defaultdict(int)
    freq[0] = 1          # empty prefix at sum=0
    current = 0
    count = 0
    for n in nums:
        current += n
        count += freq[current - target]  # adjust per problem
        freq[current] += 1
    return count

print(subarray_count_template([1,2,3,2,1], 3))  # 3

ผลรวมสะสมและค่าสูงสุดสะสม

นอกเหนือจากผลรวมตั้งแต่ต้นแล้ว โจทย์จำนวนมากยังใช้ ค่าสูงสุดสะสม หรือ ค่าต่ำสุดสะสม ซึ่งรักษาไว้ด้วยตัวแปรเพียงตัวเดียว โจทย์เวลาที่ดีที่สุดในการซื้อหุ้นใช้ราคาต่ำสุดสะสม ส่วนการคำนวณน้ำฝนที่ขังจากด้านซ้ายใช้ความสูงด้านซ้ายที่เป็นค่าสูงสุดสะสม รูปแบบเหล่านี้ต้องสแกนเพียงครั้งเดียวและใช้พื้นที่เพิ่มเติม O(1) จึงเป็นมาตรฐานสูงสุดทั้งด้านประสิทธิภาพของเวลาและพื้นที่

def max_profit(prices):
    # Running minimum buy price
    min_price = float('inf')
    max_prof  = 0
    for price in prices:
        if price < min_price:
            min_price = price
        elif price - min_price > max_prof:
            max_prof = price - min_price
    return max_prof

def left_max_array(heights):
    # Running max from left for trapping rain water
    n = len(heights)
    left_max = [0] * n
    left_max[0] = heights[0]
    for i in range(1, n):
        left_max[i] = max(left_max[i-1], heights[i])
    return left_max

print(max_profit([7,1,5,3,6,4]))  # 5

ตรวจสอบความเข้าใจอย่างรวดเร็ว

ตรวจสอบความเข้าใจของคุณเกี่ยวกับแนวคิดโครงสร้างข้อมูล & อัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้

ทบทวนบทเรียน

ในบทเรียนนี้ คุณได้เรียนรู้ว่า ผลรวมตั้งแต่ต้นเปลี่ยนการสอบถามช่วงที่ใช้เวลา O(n) ให้เป็นการค้นค่าในเวลา O(1) ด้วยการคำนวณผลรวมสะสมล่วงหน้าในการวนผ่านข้อมูล O(n) เพียงครั้งเดียว การใช้ผลรวมตั้งแต่ต้นร่วมกับตารางแฮชช่วยให้แก้โจทย์การนับช่วงย่อยที่มีผลรวมที่กำหนดหรือมีคุณสมบัติหารลงตัวได้ในเวลา O(n) และ อาร์เรย์ผลต่างเป็นสิ่งตรงข้ามกับผลรวมตั้งแต่ต้น โดยช่วยให้อัปเดตช่วงได้ในเวลา O(1) และสร้างผลลัพธ์กลับคืนด้วยการวนหาผลรวมตั้งแต่ต้นเพียงครั้งเดียวในตอนท้าย ถัดไปเราจะจัดการกับเทคนิคตัวชี้สองตัว โดยเริ่มจากตัวชี้ที่ปลายทั้งสองด้าน

คำถามที่พบบ่อย

บทเรียน “ผลรวมคำนำหน้าและยอดรวมสะสม” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “ผลรวมคำนำหน้าและยอดรวมสะสม” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส DSA Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DSA Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ผลรวมคำนำหน้าและยอดรวมสะสม”

สร้างอาร์เรย์ผลรวมคำนำหน้าเพื่อตอบคำถามผลรวมช่วงในเวลา O(1) และประยุกต์เทคนิคนี้กับโจทย์ช่วงย่อย เช่น ช่วงย่อยที่มีผลรวมสูงสุด คุณปฏิบัติ DSA Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DSA Interview Prep หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน DSA Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน

บทเรียน “ผลรวมคำนำหน้าและยอดรวมสะสม” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน DSA Interview Prep นี้ได้ไหม

ได้ บทเรียน DSA Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. พื้นฐานอาร์เรย์และการดำเนินการในที่เดิม
  2. ผลรวมคำนำหน้าและยอดรวมสะสม
  3. ตัวชี้สองตัว: จากปลายตรงข้าม
  4. ตัวชี้สองตัว: ช้าและเร็ว
← กลับไปที่ DSA Interview Prep