搜索与回溯算法之—自然数的拆分
一、问题描述任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。当n=7共14种拆分方法:7=1+1+1+1+1+1+17=1+1+1+1+1+27=1+1+1+1+37=1+1+1+2+27=1+1+1+47=1+1+2+37=1+1+57=1+2+2+27=1+2+47=1+3+37=1+67=2+2+37=2+57=3+4输入:n输出:按字典序输出具体方案输入样例:7输出样例:
一、问题描述
任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。
当n=7共14种拆分方法:
输入:n 输出:按字典序输出具体方案
输入样例:7 输出样例如下图:
提示:该题可以在以下OJ中进行提交测试(不同的OJ,题目输入输出格式稍微有些区别)
二、解题思路
● 思路1:
7=1+1+1+1+1+2 和 7=1+1+1+2+1+1 是同一种拆分情况,也就是说不同的拆分顺序视为同一种拆分情况。那怎么保证呢?我们只需要让下一个拆分出来的数大于等于前一个数即可
● 思路2:
根据题目要求,拆分出来的数要小于n,因此类似于7=7这种情况是不满足条件的,不被视为一种拆分情况
● 思路3:
递归函数的编写需要有两个参数,我们不妨假设第一个为s,第二个为t,s代表的含义为:待拆分的数据,t代表的含义为:t代表当前是第几个拆分出来的数据。此处一定要理解透彻这两个变量的含义。
● 思路4:
对于参数s(请注意,s作为递归函数的参数,它的值是不断改变的)而言,它拆分出来的数据要小于等于s,并且当n==s时,即7=7,7不能作为7拆分出来的数据,这里的确比较难理解,不过之后你只需要根据代码去理解就可以了。
● 思路5:
我们定义如下变量:int a[10001]={1},a数组用来某个数拆分的各个数字,比如a[1]=1,a[2]=1,a[3]=1,a[4]=1,a[5]=1,a[6]=1,a[7]=1,就表示7被拆分为1+1+1+1+1+1+1。比如a[1]=1,a[2]=1,a[3]=2,a[4]=3,就表示7被拆分为1+1+2+3。int total,n; total代表某个数的拆分方案数,n代表要拆分的数
● 思路6:
一定要注意,在尝试每一个数据时,一定要保证当前拆分出来的这个数据大于等于前面一个拆分出来的数据,不然会出现重复的情况,就像思路1中所提及的一样。
三、搜索回溯算法的思想
回溯是搜索算法中的一种控制策略。它的基本思想是:为了求得问题的解,先选择某一种可能情况向前探索,在探索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索,如此反复进行,直至得到解或证明无解。
up提醒各位学习的朋友们,在学习某个算法时,建议你们多做些和该算法有关的题目,等题目理解透彻之后,再回过头来仔细品读算法的含义,如果不写题目,只看算法的含义,只能是一知半解或者完全不懂(打肿脸充胖子)
四、代码实现
#include<iostream> using namespace std; int a[10001]={1},n,total; int search(int,int); int print(int); int search(int s,int t) { int i; for(i=a[t-1];i<=s;i++) //下一个拆分出来的数要大于前一位数字 { if(i<n) //当前数i要小于n,避免出现如7=7 这种情况出现 { a[t]=i; //将当前拆分出来的数i保存到第t个位置 s-=i; //s减去数i,s的值将要继续拆分 if(s==0) print(t); //递归出口 当s=0时,拆分结束输出结果 else search(s,t+1); //当s>0时,继续递归,继续拆分s s+=i; //回溯,加上之前拆分的数,以使产生所有可能的拆分 } } return 0; } int print(int t) { cout<<n<<"="; for (int i=1;i<=t-1;i++) //输出一种拆分方案 { cout<<a[i]<<"+"; } cout<<a[t]<<endl; total++; //方案数累加1 return 0; } int main() { cin>>n; search(n,1); //将要拆分的数n传递给s //cout<<"total="<<total<<endl; //输出拆分的方案数 return 0; }
五、例子代入
有了上述代码之后,我们不妨将具体的例子代入到代码中来看一看整个算法的执行流程,为了演示方便,我们来看看当n=5时,代码的执行流程(主要是跟踪核心代码-search函数的执行流程):
首先主函数调用search(5,1),5代表即将待拆分的数是5,1代表即将要拆分出第1个数。
第1种拆分情况:5=1+1+1+1+1
search(5,1) ==> search(4,2) ==> search(3,3) ==> search(2,4) ==> search(1,5)
当s=5,t=1,s代表要拆分的数是5,t代表当前准备拆分第1个数,首先初始化i=a[t-1]=a[1-1]=a[0]=1,因为按照题目的要求是要按字典序输出,所以拆分顺序肯定是从小到大,从前一个数据开始,现在t=1,没有前一个拆分的数据,就让a[0]=1,到时候拆分的第一个数据就是从a[t-1]开始,也就是a[0]=1开始。if(i<5)成立 a[t]=a[1]=1,拆分出的第一个数据为1,s=5,拆分出1之后,s=s-1=4,因为s不等于0,因此s=4需要继续拆分,递归调用函search(4,2),4代表此时要拆分的数据是4,2代表上一层5即将要拆分第2个数字。
当s=4,t=2,s代表要拆分的数是4,t代表当前准备拆分5的第2个数,首先初始化i=a[t-1]=a[2-1]=a[1]=1,因为按照题目的要求是要按字典序输出,所以拆分顺序肯定是从小到大,从前一个数据开始,现在t=2,前一个拆分的数据是a[t-1]=a[1]=1,到时候拆分的第二个数据要从a[t-1]开始,也就是i=a[1]=1开始。if(i<5)成立 a[t]=a[2]=1,拆分出的第二个数据为1,s=4,拆分出1之后,s=s-1=3,因为s不等于0,因此s=3需要继续拆分,递归调用函数search(3,3),3代表此时要拆分的数据是3,3代表上上层5即将要拆分第3个数字。
当s=3,t=3,s代表要拆分的数是3,t代表当前准备拆分5的第3个数,首先初始化i=a[t-1]=a[3-1]=a[2]=1,因为按照题目的要求是要按字典序输出,所以拆分顺序肯定是从小到大,从前一个数据开始,现在t=3,前一个拆分的数据是a[t-1]=a[2]=1,到时候拆分的第三个数据要从a[t-1]开始,也就是i=a[2]=1开始。if(i<5)成立 a[t]=a[3]=1,拆分出的第三个数据为1,s=3,拆分出1之后,s=s-1=2,因为s不等于0,因此s=2需要继续拆分,递归调用函数search(2,4),2代表此时要拆分的数据是2,4代表上上上层5即将要拆分第4个数字。
当s=2,t=4,s代表要拆分的数是2,t代表当前准备拆分5的第4个数,首先初始化i=a[t-1]=a[4-1]=a[3]=1,因为按照题目的要求是要按字典序输出,所以拆分顺序肯定是从小到大,从前一个数据开始,现在t=4,前一个拆分的数据是a[t-1]=a[3]=1,到时候拆分的第四个数据要从a[t-1]开始,也就是i=a[3]=1开始。if(i<5)成立 a[t]=a[4]=1,拆分出的第四个数据为1,s=2,拆分出1之后,s=s-1=1,因为s不等于0,因此s=1需要继续拆分,递归调用函数search(1,5),1代表此时要拆分的数据是1,5代表上上上上层5即将要拆分第5个数字。
当s=1,t=5,s代表要拆分的数是1,t代表当前准备拆分5的第5个数,首先初始化i=a[t-1]=a[5-1]=a[4]=1,因为按照题目的要求是要按字典序输出,所以拆分顺序肯定是从小到大,从前一个数据开始,现在t=5,前一个拆分的数据是a[t-1]=a[4]=1,到时候拆分的第五个数据要从a[t-1]开始,也就是i=a[4]=1开始。if(i<5)成立 a[t]=a[5]=1,拆分出的第五个数据为1,s=1,拆分出1之后,s=s-1=0,因为s等于0,因此直接输出a[1]~a[5]的值,出现第一种拆分情况5=1+1+1+1+1。输出之后,s+=i,s=s+1=1,此时s=1,注意,这步很关键。
=====================================================================
当输出第一种拆分组合之后,search(1,5)执行结束,因为此时for循环条件已经不成立了。回退到上一层search(2,4),search(1,5)已经执行结束,s+=i,s=s+1=2,在search(2,4)中继续执行for循环,此时i=2,if(i<5)条件成立,有a[4]=i=2,s=s-i=0,s此时等于0,又得到了一种拆分方案:5=1+1+1+2,此时这个拆分方案只有4个数字,即t=4。
=====================================================================
当输出第二种拆分组合之后,search(2,4)执行结束,因为此时for循环条件已经不成立了,回到上一层search(3,3),search(2,4)已经执行结束—s+=i,s=s+2=2,在search(3,3)中继续执行for循环,s+=i,s=s+i=2+1=3,继续执行for循环,此时i=2,if(i<5)条件成立,有a[3]=i=2,s-=i,s=s-i=3-2=1,继续递归,执行search(1,4),有for(i=a[3]=2;i<=1;i++),观察发现,此时for循环条件不成立,直接结束for循环,即search(1,4)执行结束,
回到search(3,3),s+=i,s=s+i=3,继续执行for循环,此时i=3,if(i<5)条件成立,有a[3]=i=3,s-=i,s=s-i=3-3=0,s此时等于0,得到第三种种拆分方案:5=1+1+3,此时这个拆分方案只有3个数字,即t=3,然后继续执行for循环s+=i,s=s+i=3。
此时search(3,3)执行结束,因为for循环条件已经不成立了,回到上一层, search(3,3)已经执行结束—s+=i,s=s+1=3,在search(4,2)中继续执行for循环,s+=i,s=s+i=3+1=4,继续执行for循环,此时i=2,if(i<5)条件成立,有a[2]=i=2,s-=i,s=s-i=4-2=2,继续递归,执行search(2,3),有for(i=a[3]=2;i<=2;i++),if(i<5)条件成立,a[3]=i=2,s=s-i=2-2=0,输出第四种拆分方案:5=1+2+2,继续执行s=s+i=0+2=2,i++,for循环条件不成立,
回退到上一层,在上一层search(4,2)中,继续执行s=s+i=4,然后i的值变为i=3,那么a[2]=3,s=s-i=4-3=1,继续执行递归函数search(1,3),有for(i=a[2]=3;i<=1;i++) 条件不成立,直接退出,回退到上一层
此时继续执行search(4,2)中的for循环,i++,i=4,a[2]=4,s=0,输出第五种拆分方案:5=1+4
输出之后,执行s+=i,s变为4,i++,i=5,条件不成立,search(4,2)函数执行结束,回退到上一层search(5,1),到search(5,1)之后,因为search(4,2)结束,因此执行s+=i,s=5,继续执行for循环,i++,i=2。此时a[1]=2,s-=i,s=3,s不等于0,继续执行递归search(3,2)
在search(3,2)中,i从2开始,即i=2,a[2]=2,s=s-i=3-2=1,继续递归,执行search(1,3),在search(1,3)中,i从2开始,for循环条件不满足,直接退出,回退到上一层search(3,2),继续执行s+=i,s=s+i=1+2=3,然后继续执行该层的for循环,i++,i=3,a[2]=3,s-=i,s=s-i=3-3=0,输出第六种拆分方案:5=2+3
输出之后,继续执行s+=i,s=s+i=0+3=3,然后继续执行for循环,i++,i=4,不满足条件search(3,2)执行结束 ,继续回到search(5,1)中,继续执行s=s+i=3+2=5,此时i++,i=3,即a[1]=3,s=s-i=2,继续递归执行search(2,2),发现search(2,2)for循环条件不成立,直接退出
回到search(5,1),继续执行s+=i,s=s+i=2+3=5。i++,i=4,a[1]=4,s-=i,s=s-i=5-3=1,s不等于0,继续执行递归函数search(1,2),该函数for循环条件不满足,直接退出
回到search(5,1),继续执行s+=i,s=s+i=2+3=5。i++,i=5,发现if(i<5)不满足,此时i继续++,i=6,search(5,1)中的for循环条件不满足,退出,至此,search(5,1)执行完毕,主函数执行完毕,得到5的拆分方案共有六种,即:
六、总结
练,方能出成绩!分析搜索回溯类的题目:做题,总结,做题,总结…………。
如果各位小伙伴们对此题目有什么疑问,或者想要加入算法学习群互相讨论,可以添加下列微信,欢迎你们,让我们共同成长!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)