1、题目描述
【JZ41】小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
知识点:数学,前缀和,滑动窗口
难度:☆
2、解题思路
2.1 穷举法
首先分析出题意关键词:正数、连续。
我们可以通过确定左边界和右边界的方式来穷举出最终结果。
步骤:
1、定义左边界i
,初始值为1,不超过S的一半;
2、定义右边界j
,初始值为i+1
;
3、计算序列[i,i+1,i+2,...,j]
每个元素的和并判断与S
的关系;
3.1 如果等于S
,则找到一组满足要求的序列。继续找下一组,左边界+1
,回到第2步;
3.2 如果小于S
,则序列还可以拓展,右边界+1
,回到第3步;
3.3 如果大于S
,则序列已经不符合要求了,重新确定左边界,左边界+1
,回到第2步;
2.2 前N项和优化
穷举法有三层嵌套的for循环,时间复杂度为O(N3)。
其中最里面一层for循环是计算[i,i+1,i+2,...,j]
的元素和,当右边界+1,则再计算[i,i+1,i+2,...,j+1]
的元素和,其中的[i,i+1,i+2,...,j]
又计算了一次,属于重复计算。
我们可以把这一部分存储到变量temp
,然后右边界+1时,我直接加上j+1
即可。
2.3 滑动窗口法
把左边界i
和右边界j
包含的区域看成是一个窗口。
整个窗口从左往右移动,也就是说,我要扩大窗口时,只能右边界+1,要缩小窗口时,则是左边界+1。
算法步骤如下:
1、开始时,左边界i=1
,右边界j=2
;
2、计算序列和[i,i+1,...,j]
保存到temp
中;
3、判断temp
和S
的关系:
3.1 如果temp<S
,说明窗口过小,需要扩大窗口,先j++
,然后temp
更新为temp+j
;
3.2 如果temp>S
,说明窗口过大,需要缩小窗口,先temp
更新为temp-i
,然后i++
;
3.3 如果temp==S
,则找到符合要求的序列,保存。然后temp
先更新为temp-i
,左边界再+1,继续寻找下一组合适的序列。
4、当i
大于S
的一半时,算法结束。
3、解题代码
3.1 穷举法
package pers.klb.jzoffer.medium;
import java.util.ArrayList;
/**
* @program: JzOffer2021
* @description: 和为S的连续正数序列(穷举法)
* @author: Meumax
* @create: 2020-07-03 12:37
**/
public class ContinuousSequence {
public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
// 要求至少包含两个数,所以最小也是{1,2},此时sum为3
if (sum < 2) {
return new ArrayList<ArrayList<Integer>>();
}
ArrayList<ArrayList<Integer>> lists = new ArrayList<ArrayList<Integer>>();
// i表示左边界
// 因为至少包含两个数字,所以i最大不能超过sum的一半
for (int i = 1; i <= sum / 2; i++) {
// j表示右边界,不能取值到sum
for (int j = i + 1; j < sum; j++) {
// 求和
int temp = 0;
for (int k = i; k <= j; k++) {
temp += k;
}
// 判断是否满足要求
if (temp == sum) { // 说明此时 [i,i+1,i+2,...,j] 正是所求序列
ArrayList<Integer> list = new ArrayList<Integer>();
for (int k = i; k <= j; k++) {
list.add(k);
}
// 保存这个可以的序列
lists.add(list);
} else if (temp > sum) {
// 此时的 [i,i+1,i+2,...,j] 不满足要求
// 此时有边界再往右走都只能是大于sum,要重新调整左边界
break;
}
}
}
return lists;
}
}
时间复杂度:O(N3)
空间复杂度:O(1)
3.2 前N项和优化
package pers.klb.jzoffer.medium;
import java.util.ArrayList;
/**
* @program: JzOffer2021
* @description: 和为S的连续正数序列(前N项和优化)
* @author: Meumax
* @create: 2020-07-03 12:37
**/
public class ContinuousSequence {
public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
// 要求至少包含两个数,所以最小也是{1,2},此时sum为3
if (sum < 2) {
return new ArrayList<ArrayList<Integer>>();
}
ArrayList<ArrayList<Integer>> lists = new ArrayList<ArrayList<Integer>>();
int temp; // 存储序列和
// i为左边界
for (int i = 1; i <= sum / 2; i++) {
temp = i;
// j为右边界
for (int j = i + 1; j < sum; j++) {
// temp = i+(i+1)+(i+2)...+j
temp += j;
if (temp == sum) { // 找到符合要求的右边界 j
ArrayList<Integer> list = new ArrayList<Integer>();
for (int k = i; k <= j; k++) {
list.add(k);
}
lists.add(list);
} else if (temp > sum) { // 当前序列已经没意义了,需要重新确定左边界
break;
}
// temp < sum 当前序列和还达不到sum,继续找右边界
}
}
return lists;
}
}
时间复杂度:O(N2)
空间复杂度:O(1)
3.3 滑动窗口法
package pers.klb.jzoffer.medium;
import java.util.ArrayList;
/**
* @program: JzOffer2021
* @description: 和为S的连续正数序列
* @author: Meumax
* @create: 2020-07-03 12:37
**/
public class ContinuousSequence {
public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
// 要求至少包含两个数,所以最小也是{1,2},此时sum为3
if (sum < 2) {
return new ArrayList<ArrayList<Integer>>();
}
ArrayList<ArrayList<Integer>> lists = new ArrayList<ArrayList<Integer>>();
int i = 1; // 滑动窗口左边界
int j = 2; // 滑动窗口右边界
int temp = i + j; // 序列和
// 当左边界超过sum的一半时,在往下不可能有目标序列
while (i <= sum / 2) {
if (temp < sum) { // 序列和小于目标sum,窗口需要扩大
j++;
temp += j;
} else if (temp > sum) { // 序列和大于目标sum,窗口需要缩小
temp -= i;
i++;
} else { // temp == sum
// 保存符合要求的序列
ArrayList<Integer> list = new ArrayList<Integer>();
for (int k = i; k <= j; k++) {
list.add(k);
}
lists.add(list);
// 左边界+1,继续滑动窗口
temp -= i;
i++;
}
}
return lists;
}
}
时间复杂度:O(N)
空间复杂度:O(1)
4、解题心得
本题是很经典的有序序列处理的问题,第一反应是穷举,穷举后的首要优化的地方就是重复计算,有迭代的意思在里面。
滑动窗口法有个特点就是前面的符合要求的序列的元素个数肯定比后面的少。