基础实验2-2.5整数分解为若干项之和
将一个正整数N分解成几个正整数相加,可以有多种分解方法,例如7=6+1,7=5+2,7=5+1+1,…。编程求出正整数N的所有整数分解式子。
输入格式:
每个输入包含一个测试用例,即正整数N (0<N≤30)。
输出格式:
按递增顺序输出N的所有整数分解式子。递增顺序是指:对于两个分解序列N1={n1,n2,⋯}和N2={m1,m2,⋯},若存在i使得n1=m1,⋯,ni=mi,但是ni+1<mi+1,则N1序列必定在N2序列之前输出。每个式子由小到大相加,式子间用分号隔开,且每输出4个式子后换行。
输入样例:7
输出样例1:
7=1+1+1+1+1+1+1;7=1+1+1+1+1+2;7=1+1+1+1+3;7=1+1+1+2+2 7=1+1+1+4;7=1+1+2+3;7=1+1+5;7=1+2+2+2 7=1+2+4;7=1+3+3;7=1+6;7=2+2+3 7=2+5;7=3+4;7=7
算法分析
看到这题第一反应是减而治之。很像全排列问题,先写出第一项,再写出剩下的数的分解项。一起来分析步骤。
首先假设我们要求 $F(4)$, 即写出 $n=4$ 的所有分解项。那么我们可以先写出第 1 位的分解项的所有可能。即 $k_1=1,2,3,4$。对于每个 $k_1$,我们只需求解它的子问题 $F(n-k_1)$ 。
例如:对于 $F=4$ 的情况,第一位 $k_i$ 所有可能的值为 $k_i=1,2,3,4$ 。对于 $k_i=1$,问题被分解成了 $F(4)= 1 + F(3)$ 。递归这个过程,得到一棵如下的树:
由于题目中要求结果序列升序排列,所以我们要去除重复情况,分析后发现:
- 对于重复情况,结果序列会出现一个降序的子序列。即为了不重复,我们要保证 $k_{i-1}<=k_i$ 。
整理一下,可得:处于区间 $[0,k_{i-1}-1]\bigcup(\lfloor\frac n2\rfloor,n)$ 的情况必须舍去,我们只需要区间 $[k_{i-1},\lfloor\frac n2\rfloor]\bigcup\{n\}$ 便可。
对于 $F=4$ ,我们可以看做是 $F(5)=1+F(4)$ ,可得 $k_{i-1}=1$,带入刚才推导出的公式得第 $1$ 位的分解项的所有值为 $i=1,2,4$。
对于 $i=1$ 这种情况,问题被分解成了:$1 + F(n-1) == 1 + F(3) $ 。递归得:
递归公式也不难写出来:
$$F(n)=k_i+F(n-k_i),\ (n>0)$$
相信看到这里很多同学已经能用DFS解出来了,估计核心代码在3、4行左右。但是看到几十层的递归栈······打扰了。我决定再推一下递推式,毕竟性能快好多不是。
下面根据递归式去推导递推。
显然,F(n)的分解式不会超过 $n$ 项,也就是第一次DFS到底时, $\underbrace{1+1+1+···+1}_{n个1}$这种情况,设为序列 $U$。我们从这种情况开始反推。由于公式为 $F(n)=k_i+F(n-k_i)$, 将序列 $U$ 末尾2位代入公式,得 :
$$\begin{cases}k_{n-1}=1, \\n-k_{n-1}==n-1==1\Rightarrow n=2\end{cases}$$
此时再判定有无处于区间 $[k_{n-1},\lfloor\frac n2\rfloor]\bigcup\{n\}$ 的其他 $k_{n-1}$,有的话每次只需要处理比当前 $k_{n-1}$ 大 1 的情况,即如果 $j=\lfloor\frac n2\rfloor-k_{n-1}>0$,进行j次循环分解,每次执行 $n-=k_i+1$ ,遇到逆序情况则退出循环,最后存留的 $n$ 即为最后一位的值。与递归相反,整个递推过程当 $n=30$ 时自然结束递推了。
代码
递归的代码网上都是,我就不传了,这边给出递推优化版本。经测试递归版本耗时400ms,递推版本11ms。
#include <stdio.h>
int main()
{
int n = 0, a[31] = {0};
scanf("%d", &n);
for(int i=0; i<n; i++){
a[i] = 1;
}
int pos = n-1, temp = 0, cnt = 0;
while(pos>=0){
printf("%d=", n);
for(int i=0; i<=pos; i++){
if(i==pos){
printf("%d", a[i]);
}else{
printf("%d+",a[i]);
}
}
if(++cnt%4 == 0){
printf("\n");
cnt=0;
}else{
if(pos!=0) printf(";");
}
if(pos==0) break;
int sum = a[pos] + a[pos-1];
a[pos--] = 0;
temp = a[pos] + 1;
int j = (double)sum/2;
int k = j - a[pos];
while(k-->0){
if(sum-temp < temp) break;
sum -= temp;
a[pos++] = temp;
}
a[pos] = sum;
}
return 0;
}
小结
要培养减而治之和分而治之的思维方式。重剑无锋,大巧不工。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。