欧拉计划里一道经典算法题

题目是这样的(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?

你可能感兴趣的:(dp,欧拉工程)