题解:洛谷 P1147 连续自然数和
【题目来源】
【题目描述】
对一个给定的正整数 \(M\),求出所有的连续的正整数段(每一段至少有两个数),这些连续的自然数段中的全部数之和为 \(M\)。
例子:\(1998+1999+2000+2001+2002 = 10000\),所以从 \(1998\) 到 \(2002\) 的一个自然数段为 \(M=10000\) 的一个解。
【输入】
包含一个整数的单独一行给出 \(M\) 的值(\(10 \le M \le 2,000,000\))。
【输出】
每行两个正整数,给出一个满足条件的连续正整数段中的第一个数和最后一个数,两数之间用一个空格隔开,所有输出行的第一个按从小到大的升序排列,对于给定的输入数据,保证至少有一个解。
【输入样例】
10000
【输出样例】
18 142
297 328
388 412
1998 2002
【核心思想】
-
问题分析:给定一个正整数 \(M\),需要找出所有长度至少为 2 的连续正整数段,使得这些段内所有整数之和恰好等于 \(M\)。本质是在一个单调递增的无限正整数序列中,寻找所有满足特定区间和条件的连续子数组。由于 \(M\) 最大可达 \(2 \times 10^6\),无法通过枚举所有区间并求和的方式解决,需要利用双指针(滑动窗口)或数学公式进行优化。
-
算法选择:
- 双指针(滑动窗口):维护一个窗口
[i, j],表示当前连续正整数段的起始和结束。窗口内的和sum会随着指针移动而动态变化。由于序列全是正数,窗口和sum具有单调性,这允许我们通过移动i或j来高效地寻找所有和为 \(M\) 的区间。 - 数学公式(备选):设连续段首项为 \(i\),项数为 \(k\),则和为 \(k(2i+k-1)/2 = M\)。可以通过枚举项数 \(k\) 并解方程 \(i = (2M/k - k + 1)/2\) 来求解,但双指针实现更直观通用。
- 双指针(滑动窗口):维护一个窗口
-
关键步骤:
- 初始化:设置左指针
i = 1,右指针j = 1,窗口和sum = 1。 - 滑动窗口遍历:循环条件为
i <= M / 2,因为长度至少为 2 的连续正整数段,其首项不可能超过 \(M/2\)【0†L59】。- 扩展窗口:如果当前窗口和
sum小于目标值 \(M\),则将右指针j右移一位,并将新元素j加入窗口和sum中【0†L60-L62】。 - 收缩与判断:如果当前窗口和
sum大于或等于目标值 \(M\)【0†L65】:- 如果
sum == M,则找到一个解,输出左指针i和右指针j【0†L66-L67】。 - 无论是否找到解,都将左指针
i从窗口和中移除,并将i右移一位,以尝试下一个可能的起始位置【0†L69-L70】。
- 如果
- 扩展窗口:如果当前窗口和
- 初始化:设置左指针
-
时间/空间复杂度:
- 时间复杂度:\(O(M)\),因为左指针
i和右指针j都只会单向移动,最多各移动 \(M/2\) 次。 - 空间复杂度:\(O(1)\),仅使用了常数个变量。
- 时间复杂度:\(O(M)\),因为左指针
-
双指针(滑动窗口)的核心思想:
- 单调性驱动:由于所有数为正整数,窗口和
sum随右指针j增加而严格增大,随左指针i增加而严格减小。这种单调性保证了算法的正确性和高效性。 - 有序探索:通过交替进行“扩展”和“收缩”操作,算法系统地探索了所有可能的区间,而不会遗漏任何解,同时避免了重复计算。
- 最优性:相比枚举所有区间(\(O(M^2)\)),滑动窗口将复杂度降为线性,是处理这类“连续子数组和”问题的经典且高效的方法。
- 适用场景:适用于在有序的正数序列中,寻找所有满足特定和的条件连续子段的问题。
- 单调性驱动:由于所有数为正整数,窗口和
【算法标签】
普及- #双指针
【代码详解】
#include <bits/stdc++.h>
using namespace std;
int m; // 存储输入的目标和
int main()
{
// 输入目标和m
cin >> m;
// 初始化滑动窗口的起点i和终点j,以及当前和sum
int i = 1, j = 1, sum = 1;
// 滑动窗口法寻找连续正整数序列
while (i <= m / 2) // 只需要遍历到m/2即可
{
// 如果当前和小于m,扩大窗口右边界
if (sum < m)
{
j++; // 右边界右移
sum += j; // 更新当前和
}
// 如果当前和大于等于m,尝试缩小窗口左边界
if (sum >= m)
{
// 如果找到和为m的序列,输出结果
if (sum == m)
cout << i << " " << j << endl;
sum -= i; // 缩小窗口
i++; // 左边界右移
}
}
return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
int m; // 存储输入的目标和
int sum = 1; // 当前连续序列的和,初始化为1(第一个正整数)
int main()
{
// 输入目标和m
cin >> m;
// 使用滑动窗口法寻找连续正整数序列
for (int i = 1, j = 1; i <= m / 2; i++) // i从1开始,最多只需要遍历到m/2
{
// 当当前和小于m时,扩大窗口右边界
while (sum < m)
{
j++; // 右边界右移
sum += j; // 更新当前和
}
// 当当前和大于等于m时
if (sum >= m)
{
// 如果找到和为m的序列,输出起始和结束位置
if (sum == m)
cout << i << " " << j << endl;
// 缩小窗口左边界(i右移)
sum -= i;
}
}
return 0;
}
【运行结果】
10000
18 142
297 328
388 412
1998 2002
浙公网安备 33010602011771号