问题:写一个程序,对于一个正整数,输出它所有可能的连续自然数(两个以上)之和的算式
分析:
设输入的数为N;拆分为k个数;拆分后连续数的第一个数为m。则有
N = m + (m+1) + (m+2) + …… + (m+k-1)
= k*m + 1+2+3+……+(k-1)
= k*m + (1+(k-1))*(k-1)/2
= k*m + k*(k-1)/2
一种简单的方法,因为m与k必然小于N,只要循环遍历m与k可能取的值,就可以得到N的可能拆分。
#include <iostream> using namespace std; int main(int argc, char** argv) { int m;//起始的数 int k;//分的项数 int num;//要拆分的数 cout<<"请输入要拆分的数:"<<endl; while(cin>>num) { for(m=1;m<num;m++) { for(k=2;k<num;k++) { int tmp = k*m+(k-1)*k/2; if(tmp==num) { cout<<"找到一个拆分组合:"; for(int i=0;i<k;i++) { cout<<m+i<<" "; } cout<<endl; } } } cout<<"请输入要拆分的数:"<<endl; } }
但是程序中有k*m、(k-1)*k之类的运算,如果要求是64位数的运算的话,上面列举的成法肯定会溢出。所以要满足64位运算还要对上面的算法进行优化。
首先,考虑拆分后的起始数m。应该不会有m>num/2,因为拆分两项时就有m+(m+1)=2*m+1了。所以有m<num/2。
然后,考虑拆分项数k。有k<=num/m,也很简单,如果有k*m>num,还有那么多比m大的数呢,必然不满足拆分条件了。
另外,考虑从1开始拆分项得到的项数k最多,(1+k)*k/2=num。为了不让后续计算溢出,求出k的最大取值。另num=0xffffffff,求上面二次方程有k<92681。于是得新的程序。
#include <iostream> using namespace std; //64为数拆分版 int main(int argc, char** argv) { unsigned long m;//拆分后起始的数 unsigned long k;//分的项数 unsigned long num;//要拆分的数 cout<<"请输入要拆分的数:"<<endl; while(cin>>num) { for(m=1;m<num/2+1;m++)//拆分后的数中最小的不会比 要拆分的数 的一半还大 { //拆分的项数不会大于 要拆分的数除以拆分后的数中的最小的那个 //92681为(1+k)*k/2=0xffffffff的解,求此解释为了不让后续运算溢出 for(k=2;k<=92681&&k<=num/m;k++) { unsigned long tmp,tmp2; tmp2 = k%2==0?k/2*(k-1):(k-1)/2*k;//有92681限制肯定不会溢出 if(0xffffffff-tmp2<k*m)//保证k*m+tmp2<0xffffffff,此处k*m+tmp2有可能溢出,所以用0xffffffff-tmp2与k*m比较 { break; } //不用担心k*m溢出,因为k最大取num/m tmp = k*m+tmp2;//上面逻辑保证了此处不会溢出 if(tmp>num)//后续计算没意义,直接跳出 break; if(tmp==num) { cout<<"找到一个拆分组合:"; for(unsigned long i=0;i<k;i++) { cout<<m+i<<" "; } cout<<endl; break; } } } cout<<"请输入要拆分的数:"<<endl; } }
上面的算法算是达到目的了,可是它实在是太慢了。输入最大的64位数4294967295要等上十几分钟才有结果,所以要对上面的算法继续优化。利用上面的程序观察输出规律,看看能否从中找出更好的算法。
我发现具体有如下规律:
从3开始每加2后得到的数会有一个2个数的拆分方法,如3+2=5可以拆分为2+3;3+2+2=7可以拆分为3+4。
从6开始每加3后得到的数会有一个3个数的拆分方法。
从10开始每加4后得到的数会有一个4个数的拆分方法。
……
把规律列出如下:
起始数 递增数 拆分格式
3 2 _+_
6=3+3 3 _+_+_
10=6+4 4 _+_+_+_
15=10+5 5 _+_+_+_+_
……
得到如上规律后,对之前的程序进行修改,依次判断输入的数是否满足拆分2、3、4、……个数的要求。如果满足再在此拆分的基础上找出具体的拆分数即可。程序如下:
#include <iostream> using namespace std; //64为数拆分版 int main(int argc, char** argv) { unsigned long i,j; unsigned long num;//要拆分的数 unsigned long cnt=0;//可以有几组拆分 cout<<"请输入要拆分的数:"<<endl; while(cin>>num)//输入要拆分的数 { cnt = 0; unsigned long tmp=1; for(i=2;i<=num/2+1&&i<92681;i++)//最大的数不会超过要拆分数的一半。92681为了防止溢出 { tmp+=i; if(tmp>num)//这个要被减的数都比要拆分的数大了,后续没必要继续运算了 { break; } if((num-tmp)%i==0)//满足规律 { unsigned long begnum; unsigned long sum=0; begnum = num/i-i/2; for(;sum!=num;begnum++)//找出拆分为i个数相加中最小的那个数 { sum = 0; for(j=0;j<i;j++) { sum+=begnum+j; } } cout<<"找到一个拆分组合:"; for(j=0;j<i;j++) { cout<<begnum-1+j<<" ";//打印出来 } cout<<endl; cnt++; } } cout<<"找到拆分个数:"<<cnt<<endl; cout<<"请输入要拆分的数:"<<endl; } }
经过这次优化后程序运行已经很快了。当然这个程序还能优化,比如那个92681,可以根据具体数值换成更小的数,不过要开根,这里就不做讨论了。