📘 概念導讀

Prefix Sums

前綴和是「預處理換查詢」的代表技法:先花 O(n) 算好「從頭到每個位置的累積和」,之後任何「子陣列區間和」都能在 O(1) 內回答。看到題目反覆詢問不同區間的和(或積、或計數),就該想到它。

核心:區間和 O(1) 查詢

定義 prefix[i] 為前 i 個元素的和(prefix[0] = 0),則子陣列 nums[l..r](含兩端)的和為:

sum(l, r) = prefix[r + 1] - prefix[l]
class PrefixSum(nums: IntArray) {
    // prefix[i] = nums[0] + ... + nums[i-1],多開一格省去邊界判斷
    private val prefix = IntArray(nums.size + 1)

    init {
        for (i in nums.indices) {
            prefix[i + 1] = prefix[i] + nums[i]
        }
    }

    // 回傳 nums[l..r] 的和(閉區間),O(1)
    fun query(l: Int, r: Int): Int = prefix[r + 1] - prefix[l]
}

TIP

多開一格(prefix[0] = 0)是關鍵小技巧:l = 0 時不必特判,公式統一成 prefix[r+1] - prefix[l]

前綴和 + Hash Map:和為 k 的子陣列

當問題不是「查某個固定區間」,而是「有幾個子陣列的和等於 k」時,把前綴和丟進 Hash Map 是經典套路。

關鍵恆等式:子陣列 nums[i..j] 和為 k,等價於 prefix[j+1] - prefix[i] = k,即 prefix[i] = prefix[j+1] - k。於是邊走邊用 Map 記錄「每個前綴和出現幾次」,對當前前綴和 curcur - k 出現過幾次即可。

fun subarraySum(nums: IntArray, k: Int): Int {
    val count = HashMap<Int, Int>()
    count[0] = 1            // 空前綴:和為 0 出現一次
    var cur = 0
    var ans = 0
    for (x in nums) {
        cur += x
        ans += count.getOrDefault(cur - k, 0)
        count[cur] = count.getOrDefault(cur, 0) + 1
    }
    return ans
}

NOTE

count[0] = 1 處理「從頭開始就剛好等於 k」的子陣列,漏了它會少算。同樣的 Map 技巧也能解「和為 k 倍數的子陣列」——把鍵改成 cur % k 的餘數即可。

變體:前綴積

把「和」換成「積」就是前綴積,常用於「除自身以外的乘積」這類題:用左前綴積乘右後綴積,避開除法(也能處理含 0 的情況)。

fun productExceptSelf(nums: IntArray): IntArray {
    val n = nums.size
    val res = IntArray(n) { 1 }
    var left = 1
    for (i in 0 until n) {        // res[i] 先存「左側所有元素的積」
        res[i] = left
        left *= nums[i]
    }
    var right = 1
    for (i in n - 1 downTo 0) {   // 再乘上「右側所有元素的積」
        res[i] *= right
        right *= nums[i]
    }
    return res
}

何時用

跨倉庫導讀