任何递推的数学公式都可以直接翻译为递归的算法,于是递归的 斐波那契数计算方法是这样的:
return fib(n-1)+fib(n-2); |
但是这样导致了大量的重复计算,造成时间和空间的极大浪费,如下图所示
把递归的算法写成非递归的算法,子问题的答案记录在一个表里,这种技巧就是动态规划。
在计算斐波那契数的时候我们只需要把F(n)和F(n+1)保留到计算出F(n+2)就可以了,不需要一直保存。改进后的算法如下:
动态规划常用于在序贯决策过程中寻求最优方案,简单地说就是依照某一个决策链如果最终状态是最优的,那么依照同样的决策链最终状态的前一个状态毕然也是最优的。比如说在下图所示的有向无环图中我们找到红色线路所示的关键路径,它是从A到G的最短路径=>它是从A到F1的最短路径=>它是从A到E2的最短路径=>......它是从A到C3的最短路径。
《编程之美》上有一道买书问题:说哈利波特这本书一共有五卷,每卷都是8欧元,如果读者一次购买不同的两卷可扣除5%的折扣,三卷10%,四卷20%,五卷25%。现在我要买很多本书,应该怎么组合才最省钱?
我们用F(Y1,Y2,Y3,Y4,Y5)表示这五卷书分别Yi本时的最少花销。由于购买2本卷一其余只购1本和购买2本卷二其余只购1本的最少花销是一样的,即F(2,1,1,1,1)=F(1,2,1,1,1)=F(1,1,2,1,1)=......。我们用F(2,1,1,1,1)来代表这一组方案的“最小表示”,即在一个最小表示F(Y1,Y2,Y3,Y4,Y5)中满足Y1>=Y2>=Y3>=Y4>=Y5。
用动态规划我们可以建立状态转移方程:
F(Y1,Y2,Y3,Y4,Y5)
=0 if(Y1=Y2=Y3=Y4=Y5=0)
=min{
40*0.75+F(Y1-1,Y2-1,Y3-1,Y4-1,Y5-1) , if(Y5>=1)
32*0.8+F(Y1-1,Y2-1,Y3-1,Y4-1,Y5) , if(Y4>=1)
24*0.9+F(Y1-1,Y2-1,Y3-1,Y4,Y5) , if(Y3>=1)
16*0.95+F(Y1-1,Y2-1,Y3,Y4,Y5) , if(Y2>=1)
8+F(Y1-1,Y2,Y3,Y4,Y5) , if(Y1>=1)
}
状态转移之后得到的F(Y1-1,Y2-1,Y3-1,Y4-1,Y5)等可能不是“最小表示”,要把它转化为对应的“最小表示”。
上代码:
016 | void BubbleSort( int *arr, int len); |
017 | float min( float n1, float n2); |
018 | void copy(NODE buy, int Y1, int Y2, int Y3, int Y4, int Y5); |
019 | float foo( int Y1, int Y2, int Y3, int Y4, int Y5,NODE oldnode); |
023 | int c1=0,c2=0,c3=0,c4=0,c5=0; |
024 | NODE head=(NODE) malloc ( sizeof ( struct buy)); |
026 | copy(head,c1,c2,c3,c4,c5); |
027 | printf ( "Input copies you want buy for each book:\n" ); |
028 | scanf ( "%d%d%d%d%d" ,&c1,&c2,&c3,&c4,&c5); |
029 | float money=foo(c1,c2,c3,c4,c5,head); |
030 | printf ( "购买这批书的最少花销为:%.2f元\n" ,money); |
031 | printf ( "每一步的购书策略为:\n" ); |
037 | printf ( "%-8d%-8d%-8d%-8d%-8d\n" ,head->Y1,head->Y2,head->Y3,head->Y4,head->Y5); |
038 | } while (head->next!=NULL); |
040 | void BubbleSort( int *arr, int len) |
056 | float min( float n1, float n2) |
060 | void copy(NODE buy, int Y1, int Y2, int Y3, int Y4, int Y5) |
068 | float foo( int Y1, int Y2, int Y3, int Y4, int Y5,NODE oldnode) |
070 | if (Y1==0&&Y2==0&&Y3==0&&Y4==0&&Y5==0) |
079 | float f1=INT_MAX*1.0f,f2=INT_MAX*1.0f,f3=INT_MAX*1.0f,f4=INT_MAX*1.0f,f5=INT_MAX*1.0f; |
080 | NODE newnode1=(NODE) malloc ( sizeof ( struct buy)); |
082 | copy(newnode1,1,0,0,0,0); |
083 | NODE newnode2,newnode3,newnode4,newnode5; |
084 | int t1[5]={arr[0]-1,arr[1],arr[2],arr[3],arr[4]}; |
085 | f1=8+foo(arr[0]-1,arr[1],arr[2],arr[3],arr[4],newnode1); |
088 | newnode2=(NODE) malloc ( sizeof ( struct buy)); |
090 | copy(newnode2,1,1,0,0,0); |
091 | f2=2*8*0.95+foo(arr[0]-1,arr[1]-1,arr[2],arr[3],arr[4],newnode2); |
094 | newnode3=(NODE) malloc ( sizeof ( struct buy)); |
096 | copy(newnode3,1,1,1,0,0); |
097 | f3=3*8*0.9+foo(arr[0]-1,arr[1]-1,arr[2]-1,arr[3],arr[4],newnode3); |
100 | newnode4=(NODE) malloc ( sizeof ( struct buy)); |
102 | copy(newnode4,1,1,1,1,0); |
103 | f4=4*8*0.8+foo(arr[0]-1,arr[1]-1,arr[2]-1,arr[3]-1,arr[4],newnode4); |
106 | newnode5=(NODE) malloc ( sizeof ( struct buy)); |
108 | copy(newnode5,1,1,1,1,1); |
109 | f5=5*8*0.75+foo(arr[0]-1,arr[1]-1,arr[2]-1,arr[3]-1,arr[4]-1,newnode5); |
114 | float res=min(min(min(f1,f2),min(f3,f4)),f5); |
116 | oldnode->next=newnode1; |
118 | oldnode->next=newnode2; |
120 | oldnode->next=newnode3; |
122 | oldnode->next=newnode4; |
124 | oldnode->next=newnode5; |
动态规划只给出最优结果,不给出最优方案是没有多大意义的,即我们还需要保留中间每一步的最优决策方案。由于每一步都有很多种可能的状态,所以这一点实现起来比较麻烦。在我的代码中用了一个结构体struct buy来保存每一步的购书组合方式。刚开始的时候我的结构体是这样定义的struct buy{struct buy* next,int *pay}*NODE; 企图用pay去指向一个五元的数组,但问题是在函数foo中给newnode的pay赋值是都只是浅复制,每次当foo退出时pay中保留的值就丢失了,导致我在main中打印最优购书组合时总是输出一些很大的随机数。就为检查出这个错误折腾了一天。C语言的指针大部分情况下还是挺好的,使用它就“自觉”地避免了大量数据的复制,但是当你想进行真正的数组复制时它却给你来“浅复制”,这次是个教训,以后注意。