链接:和为S的连续序列
题目详情:
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
分析:
很容易得到一个
O
(
n
2
)
O(n^2)
O(n2)暴力算法,但我们可以观察得到结论:
- 每次搜索到了一个正确的答案后,按暴力的做法会往后继续搜索更大的sum,显然这后面的枚举是没必要的,所以check掉;
- 发现每次枚举起点后,j做了很多次无用功的“回溯操作”,那么我们要找一个比较合适的起点开始检索,有点类似于kmp算法思想的意味;
- 从起点看呢,每次我们找到一个答案后i++,假设上次的区间长度是k,下次找到的区间长度最多只能是k-1,原因是每次移动起点i整个区间每个位置+1,即和至少多了k,为了扣回来,一定是起点至少往后多走1格;
- 综上,我们可以考虑用双指针来维护这个区间操作,复杂度
O
(
n
)
O(n)
O(n)
对于这种双指针做法也称为尺取法,还有一种写法是《挑战程序设计竞赛》中的方法:每次找一个起点后再嵌套一个循环找到终点的极限值,并把和保存住,以后就省去计算等差数列的步骤,以后就只是对存住的值进行加减,在某些不能写出通项公式的问题中这种做法更优,效率也比较高。法一要避免这种多次计算也可以预处理前缀和。
对于法二尺取法的更新答案操作一般是在整个循环的最后,相当于做了这一次指针移动后的一次总结,而法一就要每次移动都要judge一次
Code:
//法一
class Solution {
public:
vector<vector<int> > FindContinuousSequence(int sum) {
int l = 1, r = 2;
int mid = sum / 2;
vector<vector<int> > ans; ans.clear();
while (l <= mid && r < sum) {
long long tmp = (r - l + 1) * (l + r) >> 1;
if (tmp == sum) {
ans.push_back(number(l, r));
l += 2;
r ++; //这个终点已经得不到sum了,必须至少把区间右移,但显然变大,所以长度-1
} else if (tmp < sum) { //和不够,终点后移
r++;
} else { //和过大,长度要-1
l++;
}
}
return ans;
}
vector<int> number(int l, int r) {
vector<int> d; d.clear();
while (l <= r) {
d.push_back(l);
l++;
}
return d;
}
};
//法二
class Solution {
public:
vector<vector<int> > FindContinuousSequence(int sum) {
int l = 1, r = 1;
long long res = 0;
vector<vector<int> > ans; ans.clear();
while (r < sum) {
while (r < sum && res < sum) res += r++;
if (res < sum) break; //说明目前能达到的 最大区间长度都不够sum,显然找不到了
if (res == sum) ans.push_back(number(l, r - 1)); //update
res -= l++; //区间后移
}
return ans;
}
vector<int> number(int l, int r) {
vector<int> d; d.clear();
while (l <= r) {
d.push_back(l);
l++;
}
return d;
}
};
对于这个问题还有另外一种问法: 对于一个任意的自然数,问是否能将其拆分成2个或2个以上的连续自然数之和,写出所有的等式,这是编程之美的一个问题,关于做法有人给出部分的证明戳这里