57题-和为s的连续正数序列

1 题目描述

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
示例1:

输入:target = 9
输出:[[2,3,4],[4,5]]

示例2:

输入:target = 15
输出:[[1,2,3,4,5],[4,5,6],[7,8]]

2 解题思路

方法1:枚举+暴力

枚举每个正整数为起点,判断以它为起点的序列和sum是否等于target即可,由于题目要求序列长度至少大于2,所以枚举的上界为 ⌊ t a r g e t 2 ⌋ \lfloor {target \over 2} \rfloor 2target

class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> res = new ArrayList<>();
        int sum = 0;
        int limit = (target - 1) / 2;
        for (int i = 1;i <= limit;i++) {
            for (int j = i;;j++) {
                sum += j;
                if (sum > target) {
                    sum = 0;
                    break;
                }
                else if (sum == target) {
                    int[] tmp = new int[j - i + 1];
                    for (int k = i;k <= j;k++) {
                        tmp[k - i] = k;
                    }
                    sum = 0;
                    res.add(tmp);
                    break;
                }
            }
        }
        return res.toArray(new int[res.size()][]);
    }
}

复杂度分析:

  • 时间复杂度:外层需要枚举 ⌊ t a r g e t 2 ⌋ \lfloor {target \over 2} \rfloor 2target次,内层判断最多不会超过 O ( t a r g e t ) O(\sqrt {target}) O(target )的时间复杂度,因为我们考虑从1开始累加到 t a r g e t \sqrt {target} target ,由求和公式可以得
    ( 1 + t a r g e t ) ∗ ( t a r g e t ) 2 = t a r g e t + t a r g e t 2 > t a r g e t {{(1+ \sqrt {target})*(\sqrt {target})} \over 2 }= target + {\sqrt {target}\over 2} > target 2(1+target )(target )=target+2target >target
    而如果累加到 t a r g e t − 1 \sqrt {target} -1 target 1,由求和公式可以得
    ( 1 + t a r g e t − 1 ) ∗ ( t a r g e t − 1 ) 2 = t a r g e t − t a r g e t 2 < t a r g e t {{(1+ \sqrt {target}-1)*(\sqrt {target}-1)} \over 2 }= target - {\sqrt {target}\over 2} < target 2(1+target 1)(target 1)=target2target <target
    所以最多累加到 t a r g e t \sqrt {target} target ,而以后从2,3,…开始的数累加的长度必然也不会超过 O ( t a r g e t ) O(\sqrt {target}) O(target )的时间复杂度。最后总时间复杂度为内外层循环复杂度相乘,即 O ( t a r g e t t a r g e t ) O(target \sqrt {target}) O(targettarget )
  • 空间复杂度:O(1),除了答案数组只需要常数的空间存放若干变量。

方法2:滑动窗口

什么是滑动窗口?
滑动窗口可以看成数组中框起来的一部分。在一些数组类题目中,我们可以用滑动窗口来观察可能的候选结果。当滑动窗口从数组的左边滑倒了右边,我们就可以从所有的候选结果中找到最优的结果。

对于这道题来说,数组就是正整数序列[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。滑动窗口只有右边界向右移动(扩大窗口)和左边界向右移动(缩小窗口)两个操作,所以实际上非常简单。

如何用滑动窗口解这道题?
要用滑动窗口解这道题,我们要回答两个问题:

  • 第一个问题,窗口何时扩大,何时缩小?
  • 第二个问题,滑动窗口能找到全部的解吗?

对于第一个问题,回答非常简单:

  • 当窗口的和小于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)。

class Solution {
    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[] tmp = new int[j - i];
                for (int k = i;k < j;k++) {
                    tmp[k - i] = k;
                }
                res.add(tmp);
                sum -= i;
                i++;
            }
        }
        return res.toArray(new int[res.size()][]);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值