luogu-P1147 连续自然数和

luogu-P1147 连续自然数和

GESP C++ 五级(四级)练习题,双指针(尺取法)和数学计算考点。题目难度⭐⭐★☆☆,适合练习对连续区间和的控制。洛谷难度等级普及−

luogu-P1147 连续自然数和

题目要求

题目描述

对一个给定的正整数 MM,求出所有的连续的正整数段(每一段至少有两个数),这些连续的自然数段中的全部数之和为 MM

例子:1998+1999+2000+2001+2002=100001998+1999+2000+2001+2002 = 10000,所以从 1998199820022002 的一个自然数段为 M=10000M=10000 的一个解。

输入格式

包含一个整数的单独一行给出 MM 的值(10M2,000,00010 \le M \le 2,000,000)。

输出格式

每行两个正整数,给出一个满足条件的连续正整数段中的第一个数和最后一个数,两数之间用一个空格隔开,所有输出行的第一个按从小到大的升序排列,对于给定的输入数据,保证至少有一个解。

输入输出样例 #1

输入 #1
10000
输出 #1
18 142 
297 328 
388 412 
1998 2002

题目分析

题目要求找出和为 MM 的连续正整数序列。设序列为 i,i+1,,ji, i+1, \dots, j

方法一:双指针法(尺取法)

这是一个经典的可以用双指针(Sliding Window)解决的问题。 我们维护一个区间 [l,r][l, r],以及这个区间内所有数字的和 sum。 初始时,从最小的区间 l=1,r=2l=1, r=2 开始,sum = 3

  1. 如果 sum < M:说明当前的和还不够大,我们需要让区间右端点 rr 向右移动(扩大窗口),并加上新的 rr 值。
  2. 如果 sum > M:说明当前的和太大了,我们需要让区间左端点 ll 向右移动(缩小窗口),并减去旧的 ll 值。
  3. 如果 sum == M:找到了一个满足条件的区间!输出 llrr。然后我们可以让 ll 继续向右移动,尝试寻找下一个可能的区间(因为题目要求输出所有解)。

循环终止条件:当 ll 超过 M/2M/2 时即可停止(因为至少两个数,最大的数大概就在 M/2M/2 附近,再往后单个数都接近或超过 MM 了,不可能构成和为 MM 的序列)。

方法二:数学公式法

根据等差数列求和公式,长度为 kk 的连续自然数段(首项为 aa)的和为:

S=k(2a+k1)2=M S = \frac{k(2a + k - 1)}{2} = M

整理得:

k(2a+k1)=2M k(2a + k - 1) = 2M

2a=2Mkk+1 2a = \frac{2M}{k} - k + 1

a=Mkk12 a = \frac{M}{k} - \frac{k-1}{2}

由于 aa 必须是正整数:

  1. 我们可以枚举长度 kkkk 的范围大致是从 22 开始,直到 kk 很大导致 a0a \le 0
  2. 粗略估算,k2Mk \approx \sqrt{2M}。我们可以从 2M\sqrt{2M} 往下枚举到 22

为什么是从大到小枚举 k? 题目要求输出时,第一个数 aa 按从小到大排列。在和 MM 固定的情况下,序列长度 kk 越长,起始数字 aa 就越小。因此,我们优先枚举最大的 kk,就能最先找到最小的 aa,从而直接满足输出顺序要求。


示例代码

方法一、双指针法(推荐)

#include <iostream>

int main() {
    int m;
    std::cin >> m;

    // 双指针初始化
    // l: 区间左端点
    // r: 区间右端点
    // sum: 当前区间 [l, r] 的和
    int l = 1, r = 2;
    long long sum = 3; // 1 + 2 = 3

    // 循环条件:左端点小于中点即可,因为至少两个数
    // 实际上 l < r 也可以作为条件
    while (l <= m / 2) {
        if (sum == m) {
            // 找到一组解,输出
            std::cout << l << " " << r << std::endl;
            // 寻找下一组解,左端点右移
            sum -= l;
            l++;
        } else if (sum < m) {
            // 和太小,右端点右移,增加 sum
            r++;
            sum += r;
        } else {
            // 和太大,左端点右移,减少 sum
            sum -= l;
            l++;
        }
    }

    return 0;
}

另一种写法是,利用等差数列公式计算sum

#include <iostream>

int main() {
    int M;
    std::cin >> M; // 读取目标和 M
    int l = 1, r = 2; // 初始化左指针 l 和右指针 r,表示当前连续子段的起点和终点
    // (l + r) * (r - l + 1) / 2 是等差数列求和公式,用于计算从 l 到 r 的连续整数和
    // 初始 sum 不需赋值,在循环内部计算
    long long current_sum = 0; // 使用 long long 避免 sum 溢出
    while (l <= (M + 1) / 2) { // 循环条件:左端点 l 理论上不会超过 (M+1)/2,因为至少有2个数,如果 l 超过 M/2,最小的两个数之和就可能超过 M 了
        current_sum = (long long)(l + r) * (r - l + 1) / 2; // 计算当前 [l, r] 区间的和
        if (current_sum == M) {
            // 如果当前区间和等于 M,找到一组解
            std::cout << l << " " << r << std::endl;
            l++; // 左指针右移,寻找下一个可能的解
        } else if (current_sum < M) {
            // 如果当前区间和小于 M,说明需要更大的和,右指针右移扩大区间
            r++;
        } else { // current_sum > M
            // 如果当前区间和大于 M,说明和太大,左指针右移缩小区间
            l++;
        }
    }
    return 0;
}

方法二、数学法

#include <iostream>
#include <cmath>

int main() {
    int m;
    std::cin >> m;

    // 根据公式推导,k 的最大值约为 sqrt(2*M)
    // 为了让首项 a 从小到大输出,我们需要让 k 从大到小枚举
    for (int k = sqrt(2 * m); k >= 2; --k) {
        // 判断是否构成整数解
        // 2M = k * (2a + k - 1)
        // 所以 2M 必须能被 k 整除
        long long double_m = 2LL * m;
        if (double_m % k == 0) {
            long long val = double_m / k; 
            // val = 2a + k - 1
            // 2a = val - k + 1
            long long two_a = val - k + 1;
            
            // 2a 必须是偶数,且 a 必须大于 0
            if (two_a % 2 == 0 && two_a > 0) {
                int a = two_a / 2;
                std::cout << a << " " << a + k - 1 << std::endl;
            }
        }
    }

    return 0;
}

本文由coderli.com原创,按照CC BY-NC-SA 4.0 进行授权

所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code

luogu-”系列题目可在 洛谷题库 在线评测。

bcqm-”系列题目可在 编程启蒙题库 在线评测。

GESP/CSP认证交流QQ群: 688906745

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
最后更新于