[每日一題] 第五題:和為 s 的連續正數序列

DRose發表於2020-07-29

輸入一個正整數 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,滑動視窗位於序列的最左側,視窗大小為零。

[每日一題] 第五題:和為 s 的連續正數序列

滑動視窗的重要性質是:視窗的左邊界和右邊界永遠只能向右移動,而不能向左移動。這是為了保證滑動視窗的時間複雜度是 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 開頭的序列,所以視窗的左邊界要向右移動

對於第二個問題,我們可以稍微簡單地證明一下:

[每日一題] 第五題:和為 s 的連續正數序列

我們一開始要找的是 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 協議》,轉載必須註明作者和本文連結

相關文章