题目是这样的(pj euler 76):
任意一个数字,例如5,可以写成
5=1+1+1+1+1
5=1+1+1+2
5=1+1+3
5=1+4
5=1+2+2
5=2+3
一共是六种写法(不重复),那么100又多少种写法?
此题不同于高中的排列组合,插空法做出来的是重复的结果,这里要求不重复的,所以想想真是没主义。
在project euler之前有一道题是这样的:有面值1,2,5,10,20,50,100,200若干,求给定钱数的凑法。其实这道题和上面的题一样,改改上面的题的意思就是有面值1-99的钞票若干,求100块钱的凑法(用这个方式来说好解释)。
最暴利的算法就是99重循环了,别想解!当然递归也是可以的,不过消耗大量内存,速度也不行(而且递归关系也不好找)。那么好的方法是是什么呢?
#include <stdio.h>
int main(void)
{
int a[101];
int i, j;
a[0] = 1;
for(i=1;i<=100;i++)
a[i] = 0;
for(i=1;i<100;i++)
{
for(j=i;j<=100;j++)
a[j]+=a[j-i];
}
printf ("%d\n",a[100]);
return 0;
}
这是一个典型的DP问题。DP问题的关键就是寻找Subproblems,并从第一个subproblem一直求到最后一个。再次推荐某大神翻译的文章http://hawstein.com/posts/dp-novice-to-advanced.html,个人觉得非常值得学习,但是看完这篇文章后我总是希望通过找dp数组前一个数与后一个数的关系来寻求状态转移方程,有时候的确可以看出来,但这里好像不太行,因为这里是个二维的状态转移过程,方程中是有两个参数的。那么方程是什么呢?
D(w,c) = D(w,c-1) + D(w-c,c)
式中w代表所要凑的值,c代表可以使用的最大值(最大面值的钞票),D(w,c)就代表使用最大面积为c的钞票凑取w这个钱数的方法总数。而这个方程就可以解释为“用面值最大为c的钞票凑w的方法数等于用面值最大为c-1的钞票凑w的方法总数加上用面值最大为c的钞票凑w并且至少有一张面值为c的钞票的方法总数”,D(w-c,c)可以理解成在凑w时至少出现一张面值为c的钞票。这样就可以保证不重复。方法就是这个方法了。
写成代码便是以上,i代表可以使用的最大钞票,j代表值,a[j] = a[j] + a[j-i]就是转移方程了(在两次循环中i不一样,代表c-1,c),最后即可输出结果,为190596291。
写了这么半天,感觉有点晕,本来是想总结一下寻找状态转移方程的方法,好像无能了。有一个方法是画一个表,表示两个维度(首先你得会判断维度),总之还是需要多学习的。在Sanjoy的"Algorithms"里专门有一章谈Dynamic Programming,写的比较深,目前我还在磕,作为一个20年没接受过任何计算机教育的人来说... no zuo no die?