题解:洛谷 P1147 连续自然数和

【题目来源】

洛谷: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

【核心思想】

  1. 问题分析:给定一个正整数 \(M\),需要找出所有长度至少为 2 的连续正整数段,使得这些段内所有整数之和恰好等于 \(M\)。本质是在一个单调递增的无限正整数序列中,寻找所有满足特定区间和条件的连续子数组。由于 \(M\) 最大可达 \(2 \times 10^6\),无法通过枚举所有区间并求和的方式解决,需要利用双指针(滑动窗口)或数学公式进行优化。

  2. 算法选择

    • 双指针(滑动窗口):维护一个窗口 [i, j],表示当前连续正整数段的起始和结束。窗口内的和 sum 会随着指针移动而动态变化。由于序列全是正数,窗口和 sum 具有单调性,这允许我们通过移动 ij 来高效地寻找所有和为 \(M\) 的区间。
    • 数学公式(备选):设连续段首项为 \(i\),项数为 \(k\),则和为 \(k(2i+k-1)/2 = M\)。可以通过枚举项数 \(k\) 并解方程 \(i = (2M/k - k + 1)/2\) 来求解,但双指针实现更直观通用。
  3. 关键步骤

    • 初始化:设置左指针 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】。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(M)\),因为左指针 i 和右指针 j 都只会单向移动,最多各移动 \(M/2\) 次。
    • 空间复杂度:\(O(1)\),仅使用了常数个变量。
  5. 双指针(滑动窗口)的核心思想

    • 单调性驱动:由于所有数为正整数,窗口和 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
posted @ 2026-09-02 22:18  团爸讲算法  阅读(19)  评论(0)    收藏  举报