Leetcode 題解系列 -- 和為s的連續正數序列(滑動視窗)

安歌發表於2022-02-16

休了個不短不長的年假,題解系列繼續開工~

image.png
本專題旨在分享刷Leecode過程發現的一些思路有趣或者有價值的題目。

題目相關

  • 原題地址: https://leetcode-cn.com/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof/]
  • 題目描述:

    輸入一個正整數 target ,輸出所有和為 target 的連續正整數序列(至少含有兩個數)。序列內的數字由小到大排列,不同序列按照首個數字從小到大排列。

    • 示例 1:
      輸入:target = 9
      輸出:[[2,3,4],[4,5]]
    • 示例 2:
      輸入:target = 15
      輸出:[[1,2,3,4,5],[4,5,6],[7,8]]
      限制:1 <= target <= 10^5

思路解析

暴力破解

題目的含義比較清晰,需要求出和為特定值的 連續正整數序列。

首先,這道題的很直接的也會想到一個 -- 暴力破解:

image.png

具體思路

  • 首先尋找是否存在滿足要求的,以1為開頭的序列,所以初始化一個序列為 [ 1 ]
  • 依次往序列裡新增連續的正整數,讓序列變成 [1, 2, 3..i. target], 並且每次新增完一個整數時,對比當前序列所有整數之和sum,與目標值target的關係

    • 如果sum < target,則繼續往序列裡新增下一個數;
    • 如果已經滿足sum = target,那麼儲存當前序列,並且說明以1開始的序列尋找可以停止了
    • 如果直接到sum > target,那麼說明以1開始的序列尋找可以停止了(不存在以1為開頭並且滿足要求的序列);
  • 重複上述步驟,分別尋找以2,3, ... target開頭且滿足題意的序列;

劃重點.png

上面這種思路的話 最壞的情況下,需要外層迴圈(外層迴圈也就是遍歷以1..n開頭的序列)n次,內層迴圈n次(內層迴圈就是尋找當前開頭固定時,不同結尾的序列),那麼總的複雜度就是O(n^2),由於 1 <= target <= 10^5 , 所以O(n^2)明顯超時;

紅色區域表示序列,它的左邊界和右邊界有個特點,都只向右側移動,而整個遍歷的過程,其實就像是一個推拉窗的移動過程,這個演算法也就由此得名。

滑動視窗

從上述過程可以看到,暴力破解的問題在於時間複雜度太高,而之所以高,是因為在遍歷過程存在一些可以跳過的過程, 為了便於理解,我們帶入一個題設中的示例1,target = 9的情況來進行演示。按照暴力破解思路:

  • 首先序列為 [1] , 序列之和 sum = 1 , 1 < 9 繼續迴圈;
  • 序列為 [1, 2] , 序列之和 sum = 3 , 3 < 9 繼續迴圈;
  • 序列為 [1, 2,3] , 序列之和 sum = 6 , 6 < 9 繼續迴圈;
  • 序列為 [1, 2, 3 ,4] , 序列之和 sum = 12 , 12 > 9 ,那 Stop!;
    到此說明不存在以1為開頭切滿足要求的序列,那麼按照前面的思路,接下來是要尋找以2開頭且滿足題意的序列,那麼現在問題來了:

image.png

我們真的有必要從[2]開始嗎? 在找以1開頭的序列時,我們已經發現[1,2,3]之和都小於target了,那序列[2,3]之和肯定也小於9,那為什麼還要按部就班的,先走一次[2]再到[2,3] 再到[2,3,4]呢?

這,就是突破的關鍵!

image.png

所以我們發現,再找完以i開頭的序列之後,跳到尋找以i+1開頭的序列時,是可以跳過一些中間遍歷次數的,可以這麼做:

  • 序列為 [1, 2, 3 ,4] , 序列之和 sum = 12, 12 < 9 ,此時要停止尋找以1為開頭的序列,那麼我們直接去掉序列左邊的值,從[2,3,4]開始尋找以2開頭的序列;
  • 按照規則,[2, 3 ,4] 之和剛好為9,此時儲存當前序列結果,並且停止尋找以2為開頭切滿足要求的序列,接下來準備尋找3開頭的序列,我們同樣去掉此時序列的最左邊值, 從[3, 4]開始運算;

重複上述過程, 會發現,在遍歷過程中,我們的序列如下圖所示(懶得做動圖了,分開看更有利於理解):
image.png
image.png
image.png
image.png

紅色區域表示序列,它的左邊界右邊界有個特點,都只向右側移動,而整個遍歷的過程,其實就像是一個推拉窗的移動過程,這個演算法也就由此得名。

當然,要使用上面的演算法,我們要回答一個問題:相較於暴力破解,滑動視窗確實減少了迴圈次數,但是滑動視窗能否找到所有的解呢?(也就是在上述的跳躍過程導致遺漏呢?)

這個是可以證明的,因為按照前文的遍歷思路:

  • 尋找1開頭的序列時,只要序列之和小於target,則視窗右邊界一直往右擴充,直到找到[1,2,3]時,此時序列值之和還是小於target; 而到[1,2,3,4]時,此時序列之和第一次大於target,說明以1開頭的序列尋找結束;
  • 那麼此時以2開頭的序列 [2,3] < [1,2,3] < target, 說明只需要從[2,3,4]開始尋找就可以了,(讀者朋友也可以拿示例2帶入試試看,加深理解)以此類推,說明滑動視窗的演算法是不會有遺漏的。

完整程式碼

到這裡只需要整理前面的思路,虛擬碼也就出來了:

  • 初始化,設定序列視窗的左右邊界,分別為 1,2 ,然後開始迴圈;
  • 迴圈,當序列內之和小於target時,右邊界右移;
  • 迴圈過程如果發現序列值和等於target,則儲存當前序列,並且把左邊界右移;
  • 迴圈過程如果發現序列值和大於target,則把左邊界右移;
  • 當左邊界追上右邊界時,迴圈結束(可以思考下為什麼?)

那麼實際程式碼如下:

var findContinuousSequence = function(target) {
  const res = [];
  const sum = [0, 1];
  let l = 1; // 左邊界
  let r = 2; // 右邊界
  let s = 3; // 當前序列之和sum
  while(l < r){
      if(s === target) {
          // 滿足題意的序列新增到結果
          res.push(getList(l, r));
      }

      if(s > target) {
          s = s - l;
          l++;
      } else {
          r++;
          s += r; 
      }
  }
  return res;
};

function getList (l, r) {
  const res = [];
  for(let i = l; i<=r; i++) {
      res.push(i)
  }
  return res;
}

那麼滑動視窗的內容就到此為止了~
image.png

此外...

image.png
在文章末尾順便打個小廣告,問下有沒有想來外企955的小夥伴:

  • 面朝大海辦公,不打卡 不考勤 到點就下班,工作生活兩不誤;
  • 假期超長(每年年假+帶薪病假+企業年假 = 20天起步!);
  • 而且很多崗位可以長期遠端辦公! 不再困擾與一線高昂房價難落地的問題;
  • 可內推,base杭州和廈門~

相關文章