輸入一個正整數 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
滑動視窗可以看成陣列中框起來的一個部分。在一些陣列類題目中,我們可以用滑動視窗來觀察可能的候選結果。當滑動視窗從陣列的左邊滑到了右邊,我們就可以從所有的候選結果中找到最優的結果。
對於這道題來說,陣列就是正整數序列 [1,2,3,…,n]
。我們設滑動視窗的左邊界為 i
,右邊界為 j
,則滑動視窗框起來的是一個左閉右開區間 [i, j)
。注意,為了程式設計的方便,滑動視窗一般表示成一個左閉右開區間。在一開始,i=1
, j=1
,滑動視窗位於序列的最左側,視窗大小為零。
滑動視窗的重要性質是:視窗的左邊界和右邊界永遠只能向右移動,而不能向左移動。這是為了保證滑動視窗的時間複雜度是 O(n)
。如果左右邊界向左移動的話,這叫做“回溯”,演算法的時間複雜度就可能不止 O(n)。
在這道題中,我們關注的是滑動視窗中所有數的和。當滑動視窗的右邊界向右移動時,也就是 j = j + 1
,視窗中多了一個數字 j
,視窗的和也就要加上 j
。當滑動視窗的左邊界向右移動時,也就是 i = i + 1
,視窗中少了一個數字 i
,視窗的和也就要減去 i
。滑動視窗只有 右邊界向右移動(擴大視窗) 和 左邊界向右移動(縮小視窗) 兩個操作,所以實際上非常簡單。
要用滑動視窗解這道題,我們要回答兩個問題:
1. 第一個問題,視窗何時擴大,何時縮小?
2. 第二個問題,滑動視窗能找到全部的解嗎?
對於第一個問題,回答非常簡單:
當視窗的和小於
target
的時候,視窗的和需要增加,所以要擴大視窗,視窗的右邊界向右移動當視窗的和大於
target
的時候,視窗的和需要減少,所以要縮小視窗,視窗的左邊界向右移動當視窗的和恰好等於
target
的時候,我們需要記錄此時的結果。設此時的視窗為[i, j)
,那麼我們已經找到了一個i
開頭的序列,也是唯一一個i
開頭的序列,接下來需要找i+1
開頭的序列,所以視窗的左邊界要向右移動
對於第二個問題,我們可以稍微簡單地證明一下:
我們一開始要找的是 1
開頭的序列,只要視窗的和小於 target
,視窗的右邊界會一直向右移動。假設 1+2+⋯+8
小於 target
,再加上一個 9
之後, 發現 1+2+⋯+8+9
又大於 target
了。這說明 1
開頭的序列找不到解。此時滑動視窗的最右元素是 9
。
接下來,我們需要找 2
開頭的序列,我們發現,2 + …… + 8
< 1 + 2 + + 8 < target
。這說明 2
開頭的序列至少要加到 9
。那麼,我們只需要把原先 1~9
的滑動視窗的左邊界向右移動,變成 2~9
的滑動視窗,然後繼續尋找。而右邊界完全不需要向左移動。
以此類推,滑動視窗的左右邊界都不需要向左移動,所以這道題用滑動視窗一定可以得到所有的解。時間複雜度是 O(n)
。
注:這道題當前可以用等差數列的求和公式來計算滑動視窗的和。不過我這裡沒有使用求和公式,是為了展示更通用的解題思路。實際上,把題目中的正整數序列換成任意的遞增整數序列,這個方法都可以解。
public int[][] findContinuousSequence(int target) {
int i = 1; // 滑動視窗的左邊界
int j = 1; // 滑動視窗的右邊界
int sum = 0; // 滑動視窗中數字的和
List<int[]> res = new ArrayList<>();
while (i <= target / 2) {
if (sum < target) {
// 右邊界向右移動
sum += j;
j++;
} else if (sum > target) {
// 左邊界向右移動
sum -= i;
i++;
} else {
// 記錄結果
int[] arr = new int[j-i];
for (int k = i; k < j; k++) {
arr[k-i] = k;
}
res.add(arr);
// 左邊界向右移動
sum -= i;
i++;
}
}
return res.toArray(new int[res.size()][]);
}
1. 方法:首先是採用滑動視窗的方法,迴圈進行判斷。
2. 迴圈條件:迴圈可以是 for 迴圈,也可以是 while 迴圈。以 while 迴圈為例,迴圈的條件是什麼呢?我們的題目是求出所有和為 target
的連續正整數序列(至少含有兩個數),所以迴圈只需要到 target/2 即可,超過這個值之後,兩個連續的值肯定大於 target,所以我們假設我們 <= target/2,那麼我們以示例為例,target/2 為 4,還會取 > target/2 後面的一位,那我們就完全可以用滑動視窗的左半部分做迴圈條件,那麼迴圈條件就為 while(left < target/2)
,left 的起始條件應該為 1,從 0 開始沒有意義,因為我們要找以 1 開頭的序列。
3. 滑動視窗的初始左右邊界:初始左右邊界我們都可以從最左邊界開始,即 1
。
4. 滑動視窗何時移動:注意我們一直是右移。當 當前和
比 target
小的時候,說明該加了,就應該右移 右邊界
。當 當前和
比 target
大的時候,說明該減了,就應該右移 左邊界
。當 當前和
和 target
相等的時候,說明當前記錄符合題目要求,就應該記錄當前值,並右移 右邊界
。
5. 返回結果型別:這個我只能記住了,定義一個 List<int[]>
型別,然後每個結果集存成 int[]
陣列,再呼叫 add()
方法新增。最後透過 toArray()
方法轉換成返回型別。
6. 注意:結果相等的時候也要 ++i
,然後減去 i
的值。
7. 順序問題:一定要先執 sum -= i;
操作,在執行 i++
操作,這樣才不會出錯。
作者:nettee
連結:leetcode-cn.com/problems/he-wei-sd...
來源:力扣(LeetCode)
本作品採用《CC 協議》,轉載必須註明作者和本文連結