JOJ 1026 The staircases

题目描述:有N块砖头,摆成阶梯状,要求阶梯高度必须严格递增。给定N,求所有不同的摆法总数。

原题目见:http://acm.jlu.edu.cn/joj/showproblem.php?pid=1026

方法一:
N块砖头至少摆出两阶,因此第一阶的砖头数目最大为floor((n-1)/2)。设f(i,j)表示i块砖头
摆成的阶梯中第一阶摆了j块砖头的所有摆法总数。那么,给定砖头总数N,所要求的答案就是
f(N,1)+f(N,2)+...+f(N,floor((n-1)/2))................(*)。
设i块砖头第一阶为j块砖头的某种摆法为j,n1,n2,...,nm。其中ni表示第i+1阶的砖头数。则有
j<n1<n2<...<nm,且有j+n1+n2+...+nm=i,其中1<=j<=floor((i-1)/2),m>=1,将其移项,得:
n1+n2+...+nm=i-j
而这个式子就表示i-j块砖头第一阶为n1块砖头的一种摆法。其中:
j+1<=n1<=floor((i-j-1)/2),因此i块砖头的问题可以转化为i-j块砖头的问题。于是:
f(i,j)=f(i-j,j+1)+f(i-j,j+2)+...+f(i-j,floor((i-j-1)/2)。
给定N块砖头,所有不同的摆法就是式(*),在(*)中:
f(N,1)=f(N-1,2)+f(N-1,3)+...+f(N-1,floor((N-1-1)/2);
f(N,2)=f(N-2,3)+f(N-2,4)+...+f(N-2,floor((N-2-1)/2);
......
在上述的分析中,是假定砖头至少要被分成两堆的,对于本来只分为两堆的情形,经状态转化后
就会把它作为0计算,比如:
f(8,3)=f(5,4)=0,但是8可以分为3,5这样的情形的。所以需要把漏掉的这一组解加上去。
代码为:

  1. #include <stdio.h>
  2. int main() {
  3.     double f[501][501]={0},s;
  4.     int i,j,k,n;
  5.     for(i=3;i<=500;i++)
  6.         for(j=1;j<=(i-1)/2;j++)
  7.         f[i][j]=1;
  8.     for(i=3;i<=500;i++)
  9.         for(j=1;j<=(i-1)/2;j++) {
  10.         for(k=j+1;k<=(i-j-1)/2;k++)
  11.             f[i][j]+=f[i-j][k];
  12.     }
  13.     while(scanf("%d",&n),n) {
  14.         s=0;
  15.         for(i=1;i<=(n-1)/2;i++) s+=f[n][i];
  16.         printf("%.0f/n",s);
  17.     }
  18.     return 0;
  19. }

解法二:生成函数
题目的实质就是把N分成一些分部量不同的数的和的整数拆分问题。借助于生成函数,对于这个问题,
就是说对于一个数t,1<=t<=N,t或者不出现,或者只出现一次。所以,生成函数为:
G(x)=(1+x)(1+x^2)(1+x^3)...(1+x^n)
那么,G(x)的展开式中x^n的系数就是各个分部量不同的整数拆分问题。其中要减掉1*x^n的这种拆分
情况。也就是最终结果的x^n的系数减1就是本题要求的答案。
代码如下:

  1. #include <stdio.h>
  2. double ans[510]={1,1};
  3. int main() {
  4.     int i,j,t=1;
  5.     // 1+x^i
  6.     for(i=2;i<=500;i++) { 
  7.         for(j=t;j>=0;j--) {
  8.             if(j>500 || i+j>500) continue;
  9.             ans[i+j]+=ans[j];
  10.         }
  11.         t=i+t;
  12.     }
  13.     while(scanf("%d",&t),t) {
  14.         printf("%.0lf/n",ans[t]-1);
  15.     }
  16.     return 0;
  17. }

上述生成函数的代码可以写的更高效一些:

 

  1. #include <stdio.h> 
  2. double ans[510]={1,1}; 
  3. int main() { 
  4.     int i,j; 
  5.     // 1+x^i 
  6.     for(i=2;i<=500;i++) {  
  7.         for(j=500;j>=0;j--) { 
  8.             if(i+j<=500) ans[i+j]+=ans[j];
  9.         } 
  10.     } 
  11.     while(scanf("%d",&i),i) { 
  12.         printf("%.0lf/n",ans[i]-1); 
  13.     } 
  14.     return 0; 

上面同样是生成函数的思想,但两段实现代码效率差别是非常巨大的,第一段代码提交成绩为0.05秒,第二段

代码为0.00秒。只是一个非常小的改动何以效率差别会如此巨大呢?

仔细分析,原因非常明确。第一段代码中,自己的for循环是这样写的:

  1.     for(i=2;i<=500;i++) { 
  2.         for(j=t;j>=0;j--) {
  3.             if(j>500 || i+j>500) continue;
  4.             ans[i+j]+=ans[j];
  5.         }
  6.         t=i+t;
  7.     }
外层for循环表示乘以(1+x^i),内层for循环用来遍历已经得到的多项式的每一项,里面有个变量t,自己的初衷是用它
来表示当前已经得到的多项式的最高次数。而事实上,乘到1+x^32的时候,最高次数就已经是1+2+3+...+32=528了。
所以,从1+x^32起,即外层循环运行到i为32之后,程序的绝大部分时间都花在了执行内层循环中的那个continue语句
上面,而且那个执行continue语句的条件判断自己写成了那么低效的方式。每次循环,都还要执行一次外层循环中的
毫无意义的t=i+t这一步更新操作。由于自己非常糟糕的代码实现,把一个O(n*n)的算法变成了O(n*n*n),降到了
和动态规划一样的时间复杂度。
 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值