【西法帶你學算法】一次搞定前綴和

我花了幾天時間,從力扣中精選了五道相同思想的題目,來幫助你們解套,若是以爲文章對你有用,記得點贊分享,讓我看到你的承認,有動力繼續作下去。git

前四道題都是滑動窗口的子類型,咱們知道滑動窗口適合在題目要求連續的狀況下使用, 而前綴和也是如此。兩者在連續問題中,對於優化時間複雜度有着很重要的意義。 所以若是一道題你能夠用暴力解決出來,並且題目剛好有連續的限制, 那麼滑動窗口和前綴和等技巧就應該被想到。github

除了這幾道題, 還有不少題目都是相似的套路, 你們能夠在學習過程當中進行體會。今天咱們就來一塊兒學習一下。算法

前菜

咱們從一個簡單的問題入手,識別一下這種題的基本形式和套路,爲以後的四道題打基礎。當你瞭解了這個套路以後, 以後作這種題就能夠直接套。數組

須要注意的是這四道題的前置知識都是 滑動窗口, 不熟悉的同窗能夠先看下我以前寫的 滑動窗口專題(思路 + 模板)數據結構

母題 0

有 N 個的正整數放到數組 A 裏,如今要求一個新的數組 B,新數組的第 i 個數 B[i]是原數組 A 第 0 到第 i 個數的和。app

這道題可使用前綴和來解決。 前綴和是一種重要的預處理,能大大下降查詢的時間複雜度。咱們能夠簡單理解爲「數列的前 n 項的和」。這個概念其實很容易理解,即一個數組中,第 n 位存儲的是數組前 n 個數字的和。數據結構和算法

對 [1,2,3,4,5,6] 來講,其前綴和能夠是 pre=[1,3,6,10,15,21]。咱們可使用公式 pre[𝑖]=pre[𝑖−1]+nums[𝑖]獲得每一位前綴和的值,從而經過前綴和進行相應的計算和解題。其實前綴和的概念很簡單,但困難的是如何在題目中使用前綴和以及如何使用前綴和的關係來進行解題。ide

母題 1

若是讓你求一個數組的連續子數組總個數,你會如何求?其中連續指的是數組的索引連續。 好比 [1,3,4],其連續子數組有:[1], [3], [4], [1,3], [3,4] , [1,3,4],你須要返回 6。函數

一種思路是總的連續子數組個數等於:以索引爲 0 結尾的子數組個數 + 以索引爲 1 結尾的子數組個數 + ... + 以索引爲 n - 1 結尾的子數組個數,這無疑是完備的。學習

同時利用母題 0 的前綴和思路, 邊遍歷邊求和。

參考代碼(JS):

function countSubArray(nums) {
  let ans = 0;
  let pre = 0;
  for (_ in nums) {
    pre += 1;
    ans += pre;
  }
  return ans;
}

複雜度分析

  • 時間複雜度:$O(N)$,其中 N 爲數組長度。
  • 空間複雜度:$O(1)$

而因爲以索引爲 i 結尾的子數組個數就是 i + 1,所以這道題能夠直接用等差數列求和公式 (1 + n) * n / 2,其中 n 數組長度。

母題 2

我繼續修改下題目, 若是讓你求一個數組相鄰差爲 1 連續子數組的總個數呢?其實就是索引差 1 的同時,值也差 1。

和上面思路相似,無非就是增長差值的判斷。

參考代碼(JS):

function countSubArray(nums) {
  let ans = 1;
  let pre = 1;
  for (let i = 1; i < nums.length; i++) {
    if (nums[i] - nums[i - 1] == 1) {
      pre += 1;
    } else {
      pre = 0;
    }

    ans += pre;
  }
  return ans;
}

複雜度分析

  • 時間複雜度:$O(N)$,其中 N 爲數組長度。
  • 空間複雜度:$O(1)$

若是我值差只要大於 1 就行呢?其實改下符號就好了,這不就是求上升子序列個數麼?這裏再也不繼續贅述, 你們能夠本身試試。

母題 3

咱們繼續擴展。

若是我讓你求出不大於 k 的子數組的個數呢?不大於 k 指的是子數組的所有元素都不大於 k。 好比 [1,3,4] 子數組有 [1], [3], [4], [1,3], [3,4] , [1,3,4],不大於 3 的子數組有 [1], [3], [1,3] ,那麼 [1,3,4] 不大於 3 的子數組個數就是 3。 實現函數 atMostK(k, nums)。

參考代碼(JS):

function countSubArray(k, nums) {
  let ans = 0;
  let pre = 0;
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] <= k) {
      pre += 1;
    } else {
      pre = 0;
    }

    ans += pre;
  }
  return ans;
}

複雜度分析

  • 時間複雜度:$O(N)$,其中 N 爲數組長度。
  • 空間複雜度:$O(1)$

母題 4

若是我讓你求出子數組最大值恰好是 k 的子數組的個數呢? 好比 [1,3,4] 子數組有 [1], [3], [4], [1,3], [3,4] , [1,3,4],子數組最大值恰好是 3 的子數組有 [3], [1,3] ,那麼 [1,3,4] 子數組最大值恰好是 3 的子數組個數就是 2。實現函數 exactK(k, nums)。

其實是 exactK 能夠直接利用 atMostK,即 atMostK(k) - atMostK(k - 1),緣由見下方母題 5 部分。

母題 5

若是我讓你求出子數組最大值恰好是 介於 k1 和 k2 的子數組的個數呢?實現函數 betweenK(k1, k2, nums)。

其實是 betweenK 能夠直接利用 atMostK,即 atMostK(k1, nums) - atMostK(k2 - 1, nums),其中 k1 > k2。前提是值是離散的, 好比上面我出的題都是整數。 所以我能夠直接 減 1,由於 1 是兩個整數最小的間隔

如上,小於等於 10 的區域減去 小於 5 的區域就是 大於等於 5 且小於等於 10 的區域

注意我說的是小於 5, 不是小於等於 5。 因爲整數是離散的,最小間隔是 1。所以小於 5 在這裏就等價於 小於等於 4。這就是 betweenK(k1, k2, nums) = atMostK(k1) - atMostK(k2 - 1) 的緣由。

所以不難看出 exactK 其實就是 betweenK 的特殊形式。 當 k1 == k2 的時候, betweenK 等價於 exactK。

所以 atMostK 就是靈魂方法,必定要掌握,不明白建議多看幾遍。

有了上面的鋪墊, 咱們來看下第一道題。

467. 環繞字符串中惟一的子字符串(中等)

題目描述

把字符串 s 看做是「abcdefghijklmnopqrstuvwxyz」的無限環繞字符串,因此 s 看起來是這樣的:"...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd....". 

如今咱們有了另外一個字符串 p 。你須要的是找出 s 中有多少個惟一的 p 的非空子串,尤爲是當你的輸入是字符串 p ,你須要輸出字符串 s 中 p 的不一樣的非空子串的數目。 

注意: p 僅由小寫的英文字母組成,p 的大小可能超過 10000。

 

示例 1:

輸入: "a"
輸出: 1
解釋: 字符串 S 中只有一個"a"子字符。
 

示例 2:

輸入: "cac"
輸出: 2
解釋: 字符串 S 中的字符串「cac」只有兩個子串「a」、「c」。.
 

示例 3:

輸入: "zab"
輸出: 6
解釋: 在字符串 S 中有六個子串「z」、「a」、「b」、「za」、「ab」、「zab」。.
 

前置知識

  • 滑動窗口

思路

題目是讓咱們找 p 在 s 中出現的非空子串數目,而 s 是固定的一個無限循環字符串。因爲 p 的數據範圍是 10^5 ,所以暴力找出全部子串就須要 10^10 次操做了,應該會超時。並且題目不少信息都沒用到,確定不對。

仔細看下題目發現,這不就是母題 2 的變種麼?話很少說, 直接上代碼,看看有多像。

爲了減小判斷, 我這裏用了一個黑科技, p 前面加了個 ^
class Solution:
    def findSubstringInWraproundString(self, p: str) -> int:
        p = '^' + p
        w = 1
        ans = 0
        for i in range(1,len(p)):
            if ord(p[i])-ord(p[i-1]) == 1 or ord(p[i])-ord(p[i-1]) == -25:
                w += 1
            else:
                w = 1
            ans += w
        return ans

如上代碼是有問題。 好比 cac會被計算爲 3,實際上應該是 2。根本緣由在於 c 被錯誤地計算了兩次。所以一個簡單的思路就是用 set 記錄一下訪問過的子字符串便可。好比:

{
    c,
    abc,
    ab,
    abcd
}

而因爲 set 中的元素必定是連續的,所以上面的數據也能夠用 hashmap 存:

{
    c: 3
    d: 4
    b: 1
}

含義是:

  • 以 b 結尾的子串最大長度爲 1,也就是 b。
  • 以 c 結尾的子串最大長度爲 3,也就是 abc。
  • 以 d 結尾的子串最大長度爲 4,也就是 abcd。

至於 c ,是沒有必要存的。咱們能夠經過母題 2 的方式算出來。

具體算法:

  • 定義一個 len_mapper。key 是 字母, value 是 長度。 含義是以 key 結尾的最長連續子串的長度。
關鍵字是:最長
  • 用一個變量 w 記錄連續子串的長度,遍歷過程根據 w 的值更新 len_mapper
  • 返回 len_mapper 中全部 value 的和。

好比: abc,此時的 len_mapper 爲:

{
    c: 3
    b: 2
    a: 1
}

再好比:abcab,此時的 len_mapper 依舊。

再好比: abcazabc,此時的 len_mapper:

{
    c: 4
    b: 3
    a: 2
    z: 1
}

這就獲得了去重的目的。這種算法是不重不漏的,由於最長的連續子串必定是包含了比它短的連續子串,這個思想和 1297. 子串的最大出現次數 剪枝的方法有殊途同歸之妙。

代碼(Python)

class Solution:
    def findSubstringInWraproundString(self, p: str) -> int:
        p = '^' + p
        len_mapper = collections.defaultdict(lambda: 0)
        w = 1
        for i in range(1,len(p)):
            if ord(p[i])-ord(p[i-1]) == 1 or ord(p[i])-ord(p[i-1]) == -25:
                w += 1
            else:
                w = 1
            len_mapper[p[i]] = max(len_mapper[p[i]], w)
        return sum(len_mapper.values())

複雜度分析

  • 時間複雜度:$O(N)$,其中 $N$ 爲字符串 p 的長度。
  • 空間複雜度:因爲最多存儲 26 個字母, 所以空間其實是常數,故空間複雜度爲 $O(1)$。

795. 區間子數組個數(中等)

題目描述

給定一個元素都是正整數的數組 A ,正整數 L  以及  R (L <= R)。

求連續、非空且其中最大元素知足大於等於 L  小於等於 R 的子數組個數。

例如 :
輸入:
A = [2, 1, 4, 3]
L = 2
R = 3
輸出: 3
解釋: 知足條件的子數組: [2], [2, 1], [3].
注意:

L, R  和  A[i] 都是整數,範圍在  [0, 10^9]。
數組  A  的長度範圍在[1, 50000]。

前置知識

  • 滑動窗口

思路

由母題 5,咱們知道 betweenK 能夠直接利用 atMostK,即 atMostK(k1) - atMostK(k2 - 1),其中 k1 > k2

由母題 2,咱們知道如何求知足必定條件(這裏是元素都小於等於 R)子數組的個數。

這兩個結合一下, 就能夠解決。

代碼(Python)

代碼是否是很像
class Solution:
    def numSubarrayBoundedMax(self, A: List[int], L: int, R: int) -> int:
        def notGreater(R):
            ans = cnt = 0
            for a in A:
                if a <= R: cnt += 1
                else: cnt = 0
                ans += cnt
            return  ans

        return notGreater(R) - notGreater(L - 1)

複雜度分析

  • 時間複雜度:$O(N)$,其中 $N$ 爲數組長度。
  • 空間複雜度:$O(1)$。

904. 水果成籃(中等)

題目描述

在一排樹中,第 i 棵樹產生 tree[i] 型的水果。
你能夠從你選擇的任何樹開始,而後重複執行如下步驟:

把這棵樹上的水果放進你的籃子裏。若是你作不到,就停下來。
移動到當前樹右側的下一棵樹。若是右邊沒有樹,就停下來。
請注意,在選擇一顆樹後,你沒有任何選擇:你必須執行步驟 1,而後執行步驟 2,而後返回步驟 1,而後執行步驟 2,依此類推,直至中止。

你有兩個籃子,每一個籃子能夠攜帶任何數量的水果,但你但願每一個籃子只攜帶一種類型的水果。

用這個程序你能收集的水果樹的最大總量是多少?

 

示例 1:

輸入:[1,2,1]
輸出:3
解釋:咱們能夠收集 [1,2,1]。
示例 2:

輸入:[0,1,2,2]
輸出:3
解釋:咱們能夠收集 [1,2,2]
若是咱們從第一棵樹開始,咱們將只能收集到 [0, 1]。
示例 3:

輸入:[1,2,3,2,2]
輸出:4
解釋:咱們能夠收集 [2,3,2,2]
若是咱們從第一棵樹開始,咱們將只能收集到 [1, 2]。
示例 4:

輸入:[3,3,3,1,2,1,1,2,3,3,4]
輸出:5
解釋:咱們能夠收集 [1,2,1,1,2]
若是咱們從第一棵樹或第八棵樹開始,咱們將只能收集到 4 棵水果樹。
 

提示:

1 <= tree.length <= 40000
0 <= tree[i] < tree.length

前置知識

  • 滑動窗口

思路

題目花裏胡哨的。咱們來抽象一下,就是給你一個數組, 讓你選定一個子數組, 這個子數組最多隻有兩種數字,這個選定的子數組最大能夠是多少。

這不就和母題 3 同樣麼?只不過 k 變成了固定值 2。另外因爲題目要求整個窗口最多兩種數字,咱們用哈希表存一下不就行了嗎?

set 是不行了的。 所以咱們不但須要知道幾個數字在窗口, 咱們還要知道每一個數字出現的次數,這樣纔可使用滑動窗口優化時間複雜度。

代碼(Python)

class Solution:
    def totalFruit(self, tree: List[int]) -> int:
        def atMostK(k, nums):
            i = ans = 0
            win = defaultdict(lambda: 0)
            for j in range(len(nums)):
                if win[nums[j]] == 0: k -= 1
                win[nums[j]] += 1
                while k < 0:
                    win[nums[i]] -= 1
                    if win[nums[i]] == 0: k += 1
                    i += 1
                ans = max(ans, j - i + 1)
            return ans

        return atMostK(2, tree)

複雜度分析

  • 時間複雜度:$O(N)$,其中 $N$ 爲數組長度。
  • 空間複雜度:$O(k)$。

992. K 個不一樣整數的子數組(困難)

題目描述

給定一個正整數數組 A,若是 A 的某個子數組中不一樣整數的個數剛好爲 K,則稱 A 的這個連續、不必定獨立的子數組爲好子數組。

(例如,[1,2,3,1,2] 中有 3 個不一樣的整數:1,2,以及 3。)

返回 A 中好子數組的數目。

 

示例 1:

輸入:A = [1,2,1,2,3], K = 2
輸出:7
解釋:剛好由 2 個不一樣整數組成的子數組:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2].
示例 2:

輸入:A = [1,2,1,3,4], K = 3
輸出:3
解釋:剛好由 3 個不一樣整數組成的子數組:[1,2,1,3], [2,1,3], [1,3,4].
 

提示:

1 <= A.length <= 20000
1 <= A[i] <= A.length
1 <= K <= A.length

前置知識

  • 滑動窗口

思路

由母題 5,知:exactK = atMostK(k) - atMostK(k - 1), 所以答案便呼之欲出了。其餘部分和上面的題目 904. 水果成籃 同樣。

實際上和全部的滑動窗口題目都差很少。

代碼(Python)

class Solution:
    def subarraysWithKDistinct(self, A, K):
        return self.atMostK(A, K) - self.atMostK(A, K - 1)

    def atMostK(self, A, K):
        counter = collections.Counter()
        res = i = 0
        for j in range(len(A)):
            if counter[A[j]] == 0:
                K -= 1
            counter[A[j]] += 1
            while K < 0:
                counter[A[i]] -= 1
                if counter[A[i]] == 0:
                    K += 1
                i += 1
            res += j - i + 1
        return res

複雜度分析

  • 時間複雜度:$O(N)$,中 $N$ 爲數組長度。
  • 空間複雜度:$O(k)$。

1109. 航班預訂統計(中等)

題目描述

這裏有  n  個航班,它們分別從 1 到 n 進行編號。

咱們這兒有一份航班預訂表,表中第  i  條預訂記錄  bookings[i] = [i, j, k]  意味着咱們在從  i  到  j  的每一個航班上預訂了 k 個座位。

請你返回一個長度爲 n 的數組  answer,按航班編號順序返回每一個航班上預訂的座位數。



示例:

輸入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
輸出:[10,55,45,25,25]



提示:

1 <= bookings.length <= 20000
1 <= bookings[i][0] <= bookings[i][1] <= n <= 20000
1 <= bookings[i][2] <= 10000

前置知識

  • 前綴和

思路

這道題的題目描述不是很清楚。我簡單分析一下題目:

[i, j, k] 其實表明的是 第 i 站上來了 k 我的, 一直到 第 j 站都在飛機上,到第 j + 1 就不在飛機上了。因此第 i 站到第 j 站的每一站都會所以多 k 我的。

理解了題目只會不難寫出下面的代碼。

class Solution:
    def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
        counter = [0] * n

        for i, j, k in bookings:
            while i <= j:
                counter[i - 1] += k
                i += 1
        return counter

如上的代碼複雜度過高,沒法經過所有的測試用例。

注意到裏層的 while 循環是連續的數組所有加上一個數字,不難想到能夠利用母題 0 的前綴和思路優化。

一種思路就是在 i 的位置 + k, 而後利用前綴和的技巧給 i 到 n 的元素都加上 k。可是題目須要加的是一個區間, j + 1 及其以後的元素會被多加一個 k。一個簡單的技巧就是給 j + 1 的元素減去 k,這樣正負就能夠抵消。

代碼(Python)

class Solution:
    def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]:
        counter = [0] * (n + 1)

        for i, j, k in bookings:
            counter[i - 1] += k
            if j < n: counter[j] -= k
        for i in range(n + 1):
            counter[i] += counter[i - 1]
        return counter[:-1]

複雜度分析

  • 時間複雜度:$O(N)$,中 $N$ 爲數組長度。
  • 空間複雜度:$O(N)$。

總結

這幾道題都是滑動窗口和前綴和的思路。力扣相似的題目還真很多,你們只有多留心,就會發現這個套路。

前綴和的技巧以及滑動窗口的技巧都比較固定,且有模板可套。 難點就在於我怎麼才能想到能夠用這個技巧呢?

我這裏總結了兩點:

  1. 找關鍵字。好比題目中有連續,就應該條件反射想到滑動窗口和前綴和。好比題目求最大最小就想到動態規劃和貪心等等。想到以後,就能夠和題目信息對比快速排除錯誤的算法,找到可行解。這個思考的時間會隨着你的題感增長而下降。
  2. 先寫出暴力解,而後找暴力解的瓶頸, 根據瓶頸就很容易知道應該用什麼數據結構和算法去優化。

最後推薦幾道相似的題目, 供你們練習,必定要本身寫出來才行哦。

你們對此有何見解,歡迎給我留言,我有時間都會一一查看回答。

更多算法套路能夠訪問個人 LeetCode 題解倉庫:https://github.com/azl3979858... 。 目前已經 36K star 啦。

你們也能夠關注個人公衆號《力扣加加》帶你啃下算法這塊硬骨頭。

相關文章
相關標籤/搜索