《连续自然数和》题解

· · 题解

连续自然数和(O(m^3) \sim O(\sqrt{m}) 算法)

题目简述

输入 m,输出所有满足 l+(l+1)+(l+2)+\cdots+(r-2)+(r-1)+r=mlr

l 从小到大的排列输出。

思路详解

O(m^3) 算法(10 分)

不难想到我们可以暴力枚举 lr,判断 l \sim r 之间所有整数的和是不是 m

#include<iostream>
#define ll long long
using namespace std;
ll m;  //2e6的规模,不开 long long 见祖宗 
int main(){
    cin >> m;
    for(ll l = 1;l<m;l++){  //枚举左边界 
        for(ll r = l+1;r<=m;r++){  //枚举右边界 
            ll sum=0;  //求 l~r 之间所有数的和 
            for(ll i = l;i<=r;i++){  
                sum+=i;  //累加 
            }
            if(sum==m){  //判断和是否等于 m 
                cout << l << " " << r << endl;  //如果相等,输出 l 和 r 
            }
        }
    }
    return 0;
}

显然,这个代码对于 2 \times 10^{6} 的规模会严重超时。

O(m^2) 算法(10+15 分)

l \sim r 之间的所有整数的和是多少,不难发现是等差数列,因此直接用等差数列求和公式即可。

#include<iostream>
#define ll long long
using namespace std;
ll m;  //2e6的规模,不开 long long 见祖宗 
int main(){
    cin >> m;
    for(ll l = 1;l<m;l++){  //枚举左边界 
        for(ll r = l+1;r<=m;r++){  //枚举右边界 
            ll sum=(l+r)*(r-l+1)/2;  //套用求和公式 
            if(sum==m){  //判断和是否等于 m 
                cout << l << " " << r << endl;  //如果相等,输出 l 和 r 
            }
        }
    }
    return 0;
}

显然,这个代码对于 2 \times 10^{6} 的规模还是会严重超时。

但是由于学校OJ数据太水了,这个代码也能卡过去。所以我帮学校造了几组数据。

测试点 3 \sim 5 能卡掉你的代码。

O(m \log m) 算法(10+15+20 分)

如果我们已经确定左边界 l,想求一个右边界 r,满足 l \sim r 之间所有整数的和为 m,不难想到二分。

#include<iostream>
#define ll long long
using namespace std;
ll m;  //2e6的规模,不开 long long 见祖宗 
int main(){
    cin >> m;
    for(ll l = 1;l<m;l++){  //枚举左边界 
        int x=l+1,y=m;  //x 和 y 就是枚举 r 的左右边界
        while(x<=y){
            ll r=(x+y)/2;  //二分取中间 
            ll sum=(l+r)*(r-l+1)/2;  //套用求和公式 
            if(sum==m){  //判断和是否等于 m 
                cout << l << " " << r << endl;  //如果相等,输出 l 和 r 
                break;  //记得退出循环,不然就会进入死循环 
            }
            else if(sum<m){ 
                x=r+1;  //如果 r 太小了,就要把 r 的范围调大,因此要把 x 调大 
            }
            else{
                y=r-1;  //如果 r 太大了,就要把 r 的范围调小,因此要把 y 调小 
            }
        } 
    }
    return 0;
}

至此,这个代码已经能通过这道题了。但是由于你发现我添加的测试点 4 的数据规模是 4 \times 10^{7},所以你还需要加油。

O(m) 算法(10+15+20+25 分)

本人在本比赛中的独创算法。

回顾上一个算法,发现如果我们已经确定左边界 l,想求一个右边界 r,满足 l \sim r 之间所有整数的和为 m,不难发现这个 r 是唯一的。

再回顾上一份代码,发现对于每一个 l,我们要求出一个整数 r 满足:

\frac{(l+r) \times (r-l+1)}{2} = m

由于 lm 是固定的(对于每次循环),因此这就变成了关于 r 的一个一元方程!

化简上式:

\begin{aligned} \frac{(l+r) \times (r-l+1)}{2} &= m \\ (l+r) \times (r-l+1) &= 2m \\ l(r-l+1)+r(r-l+1) &= 2m \\ lr-l^{2}+l+r^{2}-lr+r &= 2m \\ -l^{2}+l+r^{2}+r &= 2m \\ r^{2}+r &= 2m+l^{2}-l \end{aligned}

下面,我们用 t 代表等号右侧的 2m+l^{2}-l,则由化简为:

\begin{aligned} r^{2}+r &= 2m+l^{2}-l \\ r^{2}+r &= t \\ r^{2}+r-t &= 0 \end{aligned} 这里使用的是公式法,由于 $r^{2}+r-t = 0$ 已经是一般形式了,因此可以直接用公式法。 首先,确定 $a,b,c$,不难看出:$a=b=1,c=-t$。 判断有没有实数根: $$ \begin{aligned} \Delta &= 1^{2}-4 \times 1 \times (-t) \\ &= 1+ 4t \\ \end{aligned} $$ 不难发现 $\Delta$ 在合法的数据范围内永远大于 $0$,所以这个方程必有至少一个实数根,分别为: $$ x_{1}=\frac{-b+\sqrt{\Delta}}{2a}, x_{2}=\frac{-b-\sqrt{\Delta}}{2a} $$ 这里,我们认为 $\sqrt{\Delta}$ 是 $\Delta$ 的**算术平方根**。 带入 $a$ 和 $b$,得: $$ x_{1}=\frac{\sqrt{\Delta}-1}{2}, x_{2}=\frac{-\sqrt{\Delta}-1}{2} $$ 不难发现,在合法的数据范围内,$x_{1}$ 永远大于 $x_{2}$,因为 $\sqrt{\Delta}$ 是一个正数。因此如果这个 $r$ 合法,我们取 $r=x_{1}$ 来避免负数。 最后判断 $\Delta$ 是不是一个完全平方数,判断下式是否成立即可: $$\sqrt{\Delta}=\left \lfloor \sqrt{\Delta} \right \rfloor$$ 如果成立,则这个 $r$ 是一个合法的根,输出 $l$ 和 $x_{1}$(即 $r$)即可。 ```cpp #include<iostream> #include<cmath> //调用 sqrt 要包含 cmath 库 #define ll long long using namespace std; ll m; //2e6的规模,不开 long long 见祖宗 int main(){ cin >> m; for(ll l = 1;l<m;l++){ //枚举左边界 ll t=2*m+l*l-l; //t 的含义见上文 if(sqrt(1+4*t)==(ll)(sqrt(1+4*t))){ //这里的 sqrt(1+4*t) 就是德尔塔了 cout << l << " " << (ll)(sqrt(1+4*t)-1)/2 << endl; //如果合法,输出r //注意这里最好加上转 ll,不然会出现科学计数法(如1e+06) } } return 0; } ``` 至此,你已经能通过所有数据了! 但是不难发现,枚举 $l$ 时只需枚举到 $\left \lfloor \frac{m}{2} \right \rfloor$ 即可,因此可以优化代码。 ```cpp #include<iostream> #include<cmath> //调用 sqrt 要包含 cmath 库 #define ll long long using namespace std; ll m; //2e6的规模,不开 long long 见祖宗 int main(){ cin >> m; for(ll l = 1;l<=m/2;l++){ //枚举左边界,注意枚举到一半就行 ll t=2*m+l*l-l; //t 的含义见上文 if(sqrt(1+4*t)==(ll)(sqrt(1+4*t))){ //这里的 sqrt(1+4*t) 就是德尔塔了 cout << l << " " << (ll)(sqrt(1+4*t)-1)/2 << endl; //如果合法,输出r //注意这里最好加上转 ll,不然会出现科学计数法(如1e+06) } } return 0; } ``` 因为 `sqrt` 有精度丢失(如输入 `500000000` 会出错),所以这个代码只能通过所有满足 $m \le 4 \times 10^{7}$ 的测试点。测试点 $6$ 能卡掉你的代码。 ~~你可以用 `sqrtl` 以减少精度丢失的发生。~~ ### $O(\sqrt{m})$ 算法($10+15+20+25+30$ 分) 又是一个本人**在本比赛中的**独创算法,加强后的数据在[这里](https://www.luogu.com.cn/problem/U548254)。 先回顾等差数列就和公式,令 $[l,r]$ 之间的所有整数的和为 $m$,有: $$ m=\frac{(l+r) \times (r-l+1)}{2} $$ 去分母得: $$ 2m=(l+r) \times (r-l+1) $$ 因为 $l,r$ 均为整数,所以 $l+r,r-l+1$ 也都是整数。因此,我们只要枚举 $2m$ (注意不是 $m$)的所有因数,即可知道对应的 $l+r$ 和 $r-l+1$ 了。 令 $xy=m$($x,y$ 均为 $2m$ 的因数),若下关于 $l,r$ 的方程组成立,则 $l,r$ 是一组合法的区间: $$ \begin{cases} l+r=x \\ r-l+1=y \end{cases} $$ 很明显,这是一个二元一次方程组,解它就是了。 最后判断解出的 $l,r$ 是否符合范围即可:$0 < l < r < m$。 但是你会发现一个问题:我们的 $l$ 是从大到小排列的,不符合要求,因此,我们只要把答案存起来,最后倒序输出就行。 ```cpp #include<iostream> #include<vector> #define ll long long using namespace std; ll n,m,i,l,r; vector<ll> ansl,ansr; //用于记录答案 int main(){ cin >> n; m=n*2; //记得要乘 2 for(ll i = 1;i<=m;i++){ if(m%i==0){ //判断 i 是不是 m 的因数 if((m/i+i-1)%2==0){ //解方程之前,先看 l 和 r 是不是整数解 r=(m/i+i-1)/2; //解得 r l=m/i-r; //解得 l if(0<l&&l<r&&r<m){ //如果 0<l<r<m,则保存答案 ansl.push_back(l); ansr.push_back(r); } } } } for(int i = ansl.size()-1;i>=0;i--){ //注意要逆序输出 cout << ansl[i] << " " << ansr[i] << endl; } return 0; } ``` 不过这个代码还是 $O(m)$ 的时间复杂度,还是不能通过。 不难发现,$m$ 的任意一个因数 $i$ 能满足 $\frac{m}{i}$ 也是 $m$ 的因数。因此,我们只要枚举到 $\sqrt{m}$ 即可。 ```cpp #include<iostream> #include<vector> #define ll long long using namespace std; ll n,m,i,l,r; vector<ll> ansl,ansr; //用于记录答案 int main(){ cin >> n; m=n*2; //记得要乘 2 for(ll i = 1;i*i<=m;i++){ //优化后只需要枚举到 m 的平方根 if(m%i==0){ //判断 i 是不是 m 的因数 if((m/i+i-1)%2==0){ //解方程之前,先看 l 和 r 是不是整数解 r=(m/i+i-1)/2; //解得 r l=m/i-r; //解得 l if(0<l&&l<r&&r<m){ //如果 0<l<r<m,则保存答案 ansl.push_back(l); ansr.push_back(r); } } } } for(int i = ansl.size()-1;i>=0;i--){ //注意要逆序输出 cout << ansl[i] << " " << ansr[i] << endl; } return 0; } ``` 至此,这道题就结束了。 ## 总结 回顾这道题的解题过程。 有时我们看到一道题,往往会想到暴力法,但是时间复杂度不是很理想。 这个时候就需要我们认真分析代码,一步一步地优化每一个循环和每一步计算。 从近几年的比赛题目不难看出:**OI 正在逐渐从“模版”走向“思维”**,现在很少会考模版题,而是改成考思维题了。同时,现在人们不求算得对,而是求算得又对又快。 锻炼思维,才是 OI 的真正精髓。 (完,全文约 $7000$ 字)