剑指 Offer 57 - II. 和为s的连续正数序列 三种方法,滑动窗口和数学法详解

题目:

输入一个正整数 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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/he-wei-sde-lian-xu-zheng-shu-xu-lie-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析:

题目很容易理解,没什么好分析。

一、不可取的暴力法

遍历暴力求解很简单,不过这道题的返回值要求是二维数组,注意各一维数组的长度不同,所以需要用List最后转回去。

class Solution {
    public int[][] findContinuousSequence(int target) {
        List<List<Integer>> list=new ArrayList();

        for(int i =1 ; i<(target+1)/2 ; i++){
            List<Integer> temp=new ArrayList<>();
            int a=i;
            temp.add(a);
            int sum=a;
            while(sum<target){
                a++;
                temp.add(a);
                sum+=a;
            }
            if(sum==target){
                list.add(temp);
            }
        }
        int a=list.size();
        int[][] ans=new int[a][];
        for(int i=0;i<a;i++){
            List<Integer> temp=list.get(i);
            int b=temp.size();
            int[] in=new int[b];
            for(int j=0;j<b;j++){
                in[j]=temp.get(j);
            }
            ans[i]=in;
        }
        return ans;
    }   
}

二、滑动窗口

其实上面的暴力法,对于每一个固定的左边界 a ,进行累加,做了很多重复工作,从 1 开始的时候 1+2+3+4… 加到不满足,下一次从2开始又要继续 2+3+4+…。

所以我们一般优化成滑动窗口,借助下面的图片来看:

在这里插入图片描述
当计算完 1 开始的一个序列和之后,要从 2 开始,只需要把 1 减去,然后看是否需要右边界扩充,就不用重复计算中间的加和了。

另外,顺便可以通过窗口大小确定一个解数组的 size ,加上结果连续,所以只需要 left 边界的值就可以将结果都存入数组。

具体步骤:

  1. left 左边界从 1 开始,需要 sum 来代表加和;
  2. sum 如果 < target ,就代表要继续累加,right 边界右移;
  3. sum 如果 == target ,代表当前有了一个解,那么就可以 new 一个数组并填入对应的序列;
  4. sum 如果已经 > target ,代表对于当前的 left 没有解, 则 left 只能 ++ ,此时少了一个数,很可能下一次 sum 又< target 了,这不用考虑,因为继续循环就会执行对应的case了。

那么整个循环的范围是什么呢?

可以是 left < target,但是因为至少需要两个元素,所以当 left 超过 target 的一半时就可以结束,因为肯定不会有解了。

很容易写出代码:

class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> list=new ArrayList<>();

        int sum=0;
        int left=1;
        int right=1;
        //范围依然如此,因为到一般之后肯定往后加会更大,则无解
        while( left<(target+1)/2){
            if( sum < target ){
                sum += right;
                right++;
            }else if( sum > target ){
                sum -= left;
                left++;
            }else{
                //说明从left-right是一个解
                int[] res = new int[ right-left ];
                int i=0;
                for( int k=left ; k<right ; k++ ){
                    res[i] = k;
                    i++;
                }
                list.add(res);
                sum -= left;
                left ++;//非正常收缩
            }
        }

        int[][] ans=new int[list.size()][];
        int pos=0;
        for(int[] res:list){
            ans[pos]=res;
            pos++;
        }
        return ans;
    }   
}

三、数学法

数学法是最米奇妙妙屋的解法,而且速度也是最快的。

因为要求的是连续的正整数序列,也就是像示例那样 1+2+3+4+5=15,或者 4+5+6=15 之类,所以答案的正整数序列一定是形如: n + n+1 + n+2+…+ n+m-1

总共有 m 个数的和,这个问题是有公式的,那就是:(首项+末项)*项数/2。

也就是:

( n + n+m-1 )*m / 2

这个题目就是要让他 == target 来求解。

这个式子逆着求解只能采用 枚举 的方式:

  1. 这个式子里有两个未知数,不好求,但是 m 代表了 target 最终分成的连续数字的项数 , 而项数是很有限的,他不会小于 2 (题目给定),上界先不考虑。

  2. 另外,求和公式展开,重新合并可以进行拆分成:m*n + m ( m-1 ) /2
    这里面:

    • m*n 是 m 个结果序列中的最小值 n 的乘积:
    • 右半部分是 m - 1 个项的和,也就是序列里除了最小的 n 外,全都减去 n 之后,的加和。
      这部分加和是一个从 1 … 到 m - 1 的自然数加和。
      直观来看:比如 1+2+3+4+5=15,分成了 1+1+1+1+1,和1+2+3+4两部分。
      再比如 4+5+6,分成 4+4+4 和 1+2 两部分。
      在这里插入图片描述

这样分的好处是,左边对应 m*n ,右边对应 m ( m-1 ) /2 有更明显的特点。

  1. **左边可以被 m 整除。**这个条件就把 m 的范围限制的范围更小。
  2. 右边是 1 开始的 m-1 个数的自然数加和,计算同样方便。

比如target= 9的示例 1 :

  • 我们先用它减去右边的自然数加和的最小 1 。9 - 1 = 8
  • 此时的 m-1 就是 1,(1 到 m -1 的自然数加和) ,所以 m=2
  • 那么如果这种情况能组成解,8 必须要能被 m,也就是 2 整除。
  • 显然它可以被整除,所以我们就得到对应的解,那就是 n = 8/2 = 4 开始逐渐递增的 m 个数,[4,5],两个数。
  • 接着 8 继续-2=6,此时 m-1=2,m=3,如果 9 想要被组成,6就要能被 3 整除,它可以整除,所以得到对应的解,就是 n = 6 / 3 = 2 开始的递增的 m 个数,[ 2 , 3 , 4 ] ,三个数。
  • 以此类推,继续减下去 target 很快被减到 0 ,那时候就可以结束程序。
因此我们可以写出对应的代码:
  1. 条件,target>0;
  2. 维护一个自然数序列 m,从 1 开始;
  3. target - m,得到剩下的值 left ,然后将 m ++ ,也就是从 1 求解出 m = 2 ,如果 left/m 没有余数,那么得到 n=left/m从 n 开始的 m 个数,组成一个解
  4. 因为 m 已经++ 过了,下一轮用 targe - m 就可以继续循环。
  5. 直到条件target>0 结束。

但是这种方法的解,是从大到小的顺序,按照这个题目的顺序要求,最后提交的时候需要将每一个子数组逆序。

class Solution {
    public int[][] findContinuousSequence(int target) {
        List<int[]> list=new ArrayList<>();

        int m=1;
        while( target > 0 ){
            target -= m;
            m ++ ;
            //里面也要加target>0,防止12345结束后,又加012345这样的结果
            if( target>0 && target%m==0 ){
                int[] temp = new int[m];
                int n = target/m;
                for(int i=n; i<n+m ; i++){
                    temp[i-n]=i;
                }
                list.add(temp);
            }
        }
        Collections.reverse(list);
        return list.toArray( new int[list.size()][] );
    }   
}

最后的交换,也可以不用Collections的方法,自己实现一下。

        int size=list.size();
        int[][] ans=new int[size][];
        for(int i=0;i<size;i++){
            ans[i]=list.get( size-i-1 );
        }
        return ans;
展开阅读全文
©️2020 CSDN 皮肤主题: 游动-白 设计师: 上身试试 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值