这个题首先还是想到枚举回溯,对a[v]排序及时终止不合适的进行优化搜索,但是可想而知的在最后几个测试例超时。这是我写的代码。
/* ID: jzzlee1 PROG:money LANG:C++ */ #include<iostream> #include<fstream> #include<cstdio> using namespace std; //ifstream cin("money.in"); //ofstream cout("money.out"); long long ans; int main() { int v,n,i,j,temp,a[25],b[25],d[25]={0},sum[25]={0}; cin>>v>>n; for(i=0;i!=v;++i) cin>>a[i]; //a[i]=25-i; for(i=0;i!=v;++i) for(j=v-1;j!=i;--j) if(a[j]<a[j-1]) { temp=a[j]; a[j]=a[j-1]; a[j-1]=temp; } for(i=0;i!=v;++i) b[i]=n/a[i]; for(d[24]=0;v>=25?d[24]<=b[24]:d[24]==0;++d[24]) { sum[24]=a[24]*d[24]; for(d[23]=0;v>=24?d[23]<=b[23]:d[23]==0;++d[23]) { sum[23]=sum[24]+a[23]*d[23]; if(sum[23]>n) break; for(d[22]=0;v>=23?d[22]<=b[22]:d[22]==0;++d[22]) { sum[22]=sum[23]+a[22]*d[22]; if(sum[22]>n) break; for(d[21]=0;v>=22?d[21]<=b[21]:d[21]==0;++d[21]) { sum[21]=sum[22]+a[21]*d[21]; if(sum[21]>n) break; for(d[20]=0;v>=21?d[20]<=b[20]:d[20]==0;++d[20]) { sum[20]=sum[21]+a[20]*d[20]; if(sum[20]>n) break; for(d[19]=0;v>=20?d[19]<=b[19]:d[19]==0;++d[19]) { sum[19]=sum[20]+a[19]*d[19]; if(sum[19]>n) break; for(d[18]=0;v>=19?d[18]<=b[18]:d[18]==0;++d[18]) { sum[18]=sum[19]+a[18]*d[18]; if(sum[18]>n) break; for(d[17]=0;v>=18?d[17]<=b[17]:d[17]==0;++d[17]) { sum[17]=sum[18]+a[17]*d[17]; if(sum[17]>n) break; for(d[16]=0;v>=17?d[16]<=b[16]:d[16]==0;++d[16]) { sum[16]=sum[17]+a[16]*d[16]; if(sum[16]>n) break; for(d[15]=0;v>=16?d[15]<=b[15]:d[15]==0;++d[15]) { sum[15]=sum[16]+a[15]*d[15]; if(sum[15]>n) break; for(d[14]=0;v>=15?d[14]<=b[14]:d[14]==0;++d[14]) { sum[14]=sum[15]+a[14]*d[14]; if(sum[14]>n) break; for(d[13]=0;v>=14?d[13]<=b[13]:d[13]==0;++d[13]) { sum[13]=sum[14]+a[13]*d[13]; if(sum[13]>n) break; for(d[12]=0;v>=13?d[12]<=b[12]:d[12]==0;++d[12]) { sum[12]=sum[13]+a[12]*d[12]; if(sum[12]>n) break; for(d[11]=0;v>=12?d[11]<=b[11]:d[11]==0;++d[11]) { sum[11]=sum[12]+a[11]*d[11]; if(sum[11]>n) break; for(d[10]=0;v>=11?d[10]<=b[10]:d[10]==0;++d[10]) { sum[10]=sum[11]+a[10]*d[10]; if(sum[10]>n) break; for(d[9]=0;v>=10?d[9]<=b[9]:d[9]==0;++d[9]) { sum[9]=sum[10]+a[9]*d[9]; if(sum[9]>n) break; for(d[8]=0;v>=9?d[8]<=b[8]:d[8]==0;++d[8]) { sum[8]=sum[9]+a[8]*d[8]; if(sum[8]>n) break; for(d[7]=0;v>=8?d[7]<=b[7]:d[7]==0;++d[7]) { sum[7]=sum[8]+a[7]*d[7]; if(sum[7]>n) break; for(d[6]=0;v>=7?d[6]<=b[6]:d[6]==0;++d[6]) { sum[6]=sum[7]+a[6]*d[6]; if(sum[6]>n) break; for(d[5]=0;v>=6?d[5]<=b[5]:d[5]==0;++d[5]) { sum[5]=sum[6]+a[5]*d[5]; if(sum[5]>n) break; for(d[4]=0;v>=5?d[4]<=b[4]:d[4]==0;++d[4]) { sum[4]=sum[5]+a[4]*d[4]; if(sum[4]>n) break; for(d[3]=0;v>=4?d[3]<=b[3]:d[3]==0;++d[3]) { sum[3]=sum[4]+a[3]*d[3]; if(sum[3]>n) break; for(d[2]=0;v>=3?d[2]<=b[2]:d[2]==0;++d[2]) { sum[2]=sum[3]+a[2]*d[2]; if(sum[2]>n) break; for(d[1]=0;v>=2?d[1]<=b[1]:d[1]==0;++d[1]) { sum[1]=sum[2]+a[1]*d[1]; if(sum[1]>n) break; //for(d[0]=0;v>=1?d[0]<=b[0]:d[0]==0;++d[0]) //{ if((n-sum[1])%a[0]==0) ++ans; //else if(sum[1]+a[0]*d[0]>n) //break; //}//0 }//1 }//2 }//3 }//4 }//5 }//6 }//7 }//8 }//9 }//10 }//11 }//12 }//13 }//14 }//15 }//16 }//17 }//18 }//19 }//20 }//21 }//22 }//23 }//24 cout<<ans<<endl; return 0; }
后来想是要动态规划,自己想了一下,无甚所得,做dp也三五个题了,还是一点不开窍啊,看来欲速则不达,需要补充一下dp的理论知识了。看了一下别人的题解,http://sdjl.me/index.php/archives/97,三种思路(原文为java代码,我用C++改写,另外,此题够阴,空间复杂度O(v*n)时运行错,只有空间复杂度O(n)时才可以):
首先我会引导读者如何去思考这道题的动规方法,通过一个时间效率为O(v*n*n)、空间效率为O(V*n)的简单方法,让读者理解程序的正确性。
然后我将改变一下思考的角度,介绍一个时间效率为O(v*n)、空间效率为O(v*n)的方法,让读者注意到对于同样的方法当考虑角度稍有变化时如何影响到算法的效率。
最后我将介绍一种时间效率为O(v*n)、空间效率为O(n)的方法,指出在动态规划中一种减少空间使用量的常用方法,这对参加编程比赛是很有用的。
注:dp[i][j]表示在前i种硬币中,拿出总值为j元的方案数
方法1:
还是先模拟过程,然后从过程的最后一步考虑起。注意,这里所说的“模拟”,就是在告诉读者如何去思考这个问题。
可以这样来模拟:有一排桌子,共有v个,每张桌子上面都有一堆拿不完的硬币,但是每张桌子上的硬币都是相同面额的,我从第一张桌子顺序走到最后一张桌子,每路过一张桌子的时候我可以拿取这张桌子上面的任意个硬币,直到路过所有桌子后我手中的硬币总额必须为n元。
考虑最后一步:如果我在第i张桌子上拿了m个硬币,然后就不再在其它的桌子上拿硬币了,且我手中的钱正好是n元,那么最后一步就是“在第i张桌子上面拿取了m个硬币”
在最后一步中,我可以拿0个或多个硬币,这就是最后一步的选择,那么选择拿取m个硬币后剩下的子问题就是从前i-1张桌子上拿取j-m×a[i]钱有多少种方法,其中a[i]是第i张桌子上硬币的面额。
因为每种选择都是独立可行的方法,因此有动规方程: dp[i][j] = Sum(dp[i - 1][j - m * a[i]]) {m = 0、1、2、……、k},k是最多可以拿取的个数。
注意到,这里问题数就是dp变量的个数,等于v * n,而选择数最坏情况下为n,因此最坏情况下时间效率为O(v*n*n)。
code 1:
/* ID: jzzlee1 PROG:money LANG:C++ */ #include<iostream> #include<fstream> using namespace std; //ifstream cin("money.in"); //ofstream cout("money.out"); unsigned long long dp[25][10001]; int main() { int v,n,a[30]; cin>>v>>n; int i,j; for(i=0;i!=v;++i) cin>>a[i]; for(j=0;j<=n;++j) if(j%a[0]==0) dp[0][j]=1; for(i=0;i!=v;++i) dp[i][0]=1; for(i=1;i!=v;++i) for(j=1;j<=n;++j) { int d=0; while(d*a[i]<=j) { dp[i][j]+=dp[i-1][j-d*a[i]]; ++d; } } cout<<dp[v-1][n]<<endl; return 0; }
方法2:
如果我们在模拟过程中稍微做一点变动就会发现一种效率更高的算法,如下:
在方法一的模拟过程中,对于每一步的描述可以表达如下“在第i张桌子上我应该拿取多少个硬币?”,现在改为“在第i张桌子上我是否应该再拿取一个硬币?(如果不拿,那就走向下一张桌子)”
此时思考的角度就从“拿多少个(选择数为O(n))”到“拿与不拿(选择数为O(1))”,可见选择数变少了,但是子问题发生了变化。
方法1的子问题可以表达如下“在前i-1张桌子上拿取总额为j-m*a[i]的方法数”,而方法2的子问题变为“当再拿取一个硬币时,在前i张桌子上拿取总额为j – a[i]的方法数”与“不再拿硬币时,在前i张桌子上拿取总额为j的方法数”,至于“最优子结构”问题读者自己证明。
因此可得如下动规方程:dp[i][j] = dp[i][j-a[i]] + dp[i-1][j],dp[i][j-a[i]]是再拿一个的情况,dp[i-1][j]是不再拿走向下一张桌子的情况。 (提示:设在第i张桌子上拿取了m个硬币,当m>0时, 所有的方法都被dp[i][j-a[i]]包含了,因此当走向下一张桌子时仅需要考虑m=0的情况。)
可见子问题数没变而选择数减少了一个数量级,因此时间效率提高到O(v*n)
见 code2
/* ID: jzzlee1 PROG:money LANG:C++ */ #include<iostream> #include<fstream> using namespace std; //ifstream cin("money.in"); //ofstream cout("money.out"); unsigned long long dp[25][10001]; int main() { int v,n,a[30]; cin>>v>>n; int i,j; for(i=0;i!=v;++i) cin>>a[i]; for(j=1;j<=n;++j) if(j%a[0]==0) dp[0][j]=1; for(i=0;i!=v;++i) dp[i][0]=1; for(i=1;i!=v;++i) for(j=1;j<=n;++j) { if (j - a[i] >= 0) { dp[i][j] = dp[i - 1][j] + dp[i][j - a[i]]; } else { dp[i][j] = dp[i - 1][j]; } } cout<<dp[v-1][n]<<endl; return 0; }
方法3:
注意到方法2中的动规方程:dp[i][j] = dp[i][j-a[i]] + dp[i-1][j]
我们在求dp[i][*]时仅会用到dp[i-1][*],而不会用到dp[i-2][*],dp[i-3][*]等等。
这就表示,任何时刻我们都可以仅用两个数组来保存dp的值,而不用v个,公式就可以简化为: dp_2[j] = dp_2[j-a[i]] + dp_1[j]。
且在求dp_2[j]时,dp_2[j]的值可以是任意值而不会影响到dp_2[j]的正确性(因为它的值是由dp_2[j-a[i]]与dp_1[j]决定的),那么我们就可以用dp_2[j]的来保存dp_1[j]的值,公式可以改为: dp_2[j] = dp_2[j-a[i]] + dp_2[j]。
注意,当计算dp_2[j] = dp_2[j-a[i]] + dp_2[j]时,等号左边的dp_2[j]表示“前i张桌子拿取j元的方案数”,而等号右边的dp_2[j]表示“前i-1张桌子拿取j元的方案数”。
这就只需要用一个大小为O(n)的dp数组了。空间效率从O(v*n)提高到了O(n)。
见 code3
/* ID: jzzlee1 PROG:money LANG:C++ */ //#include<iostream> #include<fstream> using namespace std; ifstream cin("money.in"); ofstream cout("money.out"); unsigned long long dp[10001]; int main() { int v,n,a[30]; cin>>v>>n; int i,j; for(i=0;i!=v;++i) cin>>a[i]; for(j=0;j<=n;++j) if(j%a[0]==0) dp[j]=1; for(i=1;i!=v;++i) for(j=1;j<=n;++j) { if (j - a[i] >= 0) { dp[j] = dp[j] + dp[j - a[i]]; } } cout<<dp[n]<<endl; return 0; }
还有一种分析,
…………………………………………(usaco分析)…………………………………………
We use dynamic programming to count the number of ways to make n cents with the given coins. If we denote the value of the kth coin by c_k, then the recurrence is:
nway(n, k) = no. of ways to make n cents with the first k types of coins
nway(n, k) = nway(n, k-1) + nway(n-c_k, k)
This just says the number of ways to make n cents with the first k coins is the number of ways to make n cents using the first k-1 coins (i.e., without using the kth coin) plus the number of ways to make n-c_k cents using the first k coins. For the second set of ways, we then add the kth coin to arrive at a total of n cents.
We keep track of the number of ways to total "n" cents in "nway", updating the array as we read the value of each coin.
代码:
#include <iostream> #include<fstream> using namespace std; //ifstream cin("money.in"); //ofstream cout("money.out"); long long nway[10001]; int main() { int i, j, n, v, c; cin>>v>>n; nway[0] = 1; for(i=0; i<v; i++) { cin>>c; for(j=c;j<=n;j++) nway[j]+=nway[j-c]; } cout<<nway[n]<<endl; return 0; }
对于动态规划,真的需要做点什么了,先把dd牛的背包九讲好好看下。