转自:http://haolloyin.blog.51cto.com/1177454/352115/
动态规划的基本思想是将待求解问题分解成若干个子问题,先求解子问题,并将这些子问题的解保存起来,如果以后在求解较大子问题的时候需要用到这些子问题的解,就可以直接取出这些已经计算过的解而免去重复运算。保存子问题的解可以使用填表方式,例如保存在数组中。
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。使用动态规划求解问题,最重要的就是确定动态规划三要素:问题的阶段,每个阶段的状态以及从前一个阶段转化到后一个阶段之间的递推关系。递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
下面用一个实际例子来体现动态规划的算法思想——硬币找零问题。
硬币找零问题描述:现存在一堆面值为 V1、V2、V3…个单位的硬币,问最少需要多少个硬币才能找出总值为 T个单位的零钱?假设这一堆面值分别为 1、2、5、21、25 元,需要找出总值 T 为 63 元的零钱。
很明显,只要拿出 3 个 21 元的硬币就凑够了 63 元了。
基于上述动态规划的思想,我们可以从 1 元开始计算出最少需要几个硬币,然后再求 2 元、3元…每一次求得的结果都保存在一个数组中,以后需要用到时则直接取出即可。那么我们什么时候需要这些子问题的解呢?如何体现出由子问题的解得到较大问题的解呢?
其实,在我们从 1 元开始依次找零时,可以尝试一下当前要找零的面值(这里指 1 元)是否能够被分解成另一个已求解的面值的找零需要的硬币个数再加上这一堆硬币中的某个面值之和,如果这样分解之后最终的硬币数是最少的,那么问题就得到答案了。
单是上面的文字描述太抽象,先假定以下变量:
values[] : 保存每一种硬币的币值的数组
valueKinds :币值不同的硬币种类数量,即values[]数组的大小
money : 需要找零的面值
coinsUsed[] : 保存面值为 i的纸币找零所需的最小硬币数
算法描述:
当求解总面值为 i 的找零最少硬币数 coinsUsed[ i ] 时,将其分解成求解 coinsUsed[ i – cents]和一个面值为 cents 元的硬币,由于 i – cents < i , 其解 coinsUsed[ i – cents] 已经存在,如果面值为 cents 的硬币满足题意,那么最终解 coinsUsed[ i ] 则等于 coinsUsed[ i – cents] 再加上 1(即面值为 cents)的这一个硬币。
算法实现:
<span style="font-size:12px;">package com.dynamic;
/**
* @author sjmei
* @date 2012-10-30
*/
public class CoinsChange {
/**
*
* 硬币找零:动态规划算法
* @param values:保存每一种硬币的币值的数组
*
* @param valueKinds:币值不同的硬币种类数量,即coinValue[]数组的大小
*
* @param money:需要找零的面值
*
* @param coinsUsed:保存面值为i的纸币找零所需的最小硬币数
*/
public static void makeChange(int[] values, int valueKinds, int money,int[] coinsUsed,int[] coinTrack) {
coinsUsed[0] = 0;
int last = 0;
// 对每一分钱都找零,即保存子问题的解以备用,即填表
for (int cents = 1; cents <= money; cents++) {
// 当用最小币值的硬币找零时,所需硬币数量最多
int minCoins = 999;
// 遍历每一种面值的硬币,看是否可作为找零的其中之一
for (int kind = 0; kind < valueKinds; kind++) {
// 若当前面值的硬币小于当前的cents则分解问题并查表
if (values[kind] <= cents) {
int temp = coinsUsed[cents - values[kind]] + 1;
if (temp < minCoins) {
minCoins = temp;
last = kind;
}
}
}
// 保存最小硬币数
coinsUsed[cents] = minCoins;
coinTrack[cents] = values[last];
System.out.print("面值为 :" + (cents) + "的最小硬币数 : "+coinsUsed[cents]);
System.out.print(" 硬币为:");
trackPrint(cents, coinTrack);
System.out.println();
}
}
private static void trackPrint(int m,int[] coinTrack){
if(m==0){
return;
}else {
System.out.print(coinTrack[m]+" ");
trackPrint(m-coinTrack[m], coinTrack);
}
}
public static void main(String[] args) {
// 硬币面值预先已经按降序排列
int[] coinValue = new int[] { 25, 21, 10, 5, 1 };
// 需要找零的面值
int money = 65;
// 保存每一个面值找零所需的最小硬币数,0号单元舍弃不用,所以要多加1
int[] coinsUsed = new int[money+1];
int[] coinTrack = new int[money+1];
for(int i=1;i<=money;i++){
coinsUsed[i] = 0;
coinTrack[i] = 0;
}
makeChange(coinValue, coinValue.length, money, coinsUsed,coinTrack);
}
}
</span>
程序运行结果:
面值为 :1的最小硬币数 : 1 硬币为:1
面值为 :2的最小硬币数 : 2 硬币为:1 1
面值为 :3的最小硬币数 : 3 硬币为:1 1 1
面值为 :4的最小硬币数 : 4 硬币为:1 1 1 1
面值为 :5的最小硬币数 : 1 硬币为:5
面值为 :6的最小硬币数 : 2 硬币为:5 1
面值为 :7的最小硬币数 : 3 硬币为:5 1 1
面值为 :8的最小硬币数 : 4 硬币为:5 1 1 1
面值为 :9的最小硬币数 : 5 硬币为:5 1 1 1 1
面值为 :10的最小硬币数 : 1 硬币为:10
面值为 :11的最小硬币数 : 2 硬币为:10 1
面值为 :12的最小硬币数 : 3 硬币为:10 1 1
面值为 :13的最小硬币数 : 4 硬币为:10 1 1 1
面值为 :14的最小硬币数 : 5 硬币为:10 1 1 1 1
面值为 :15的最小硬币数 : 2 硬币为:10 5
面值为 :16的最小硬币数 : 3 硬币为:10 5 1
面值为 :17的最小硬币数 : 4 硬币为:10 5 1 1
面值为 :18的最小硬币数 : 5 硬币为:10 5 1 1 1
面值为 :19的最小硬币数 : 6 硬币为:10 5 1 1 1 1
面值为 :20的最小硬币数 : 2 硬币为:10 10
面值为 :21的最小硬币数 : 1 硬币为:21
面值为 :22的最小硬币数 : 2 硬币为:21 1
面值为 :23的最小硬币数 : 3 硬币为:21 1 1
面值为 :24的最小硬币数 : 4 硬币为:21 1 1 1
面值为 :25的最小硬币数 : 1 硬币为:25
面值为 :26的最小硬币数 : 2 硬币为:25 1
面值为 :27的最小硬币数 : 3 硬币为:25 1 1
面值为 :28的最小硬币数 : 4 硬币为:25 1 1 1
面值为 :29的最小硬币数 : 5 硬币为:25 1 1 1 1
面值为 :30的最小硬币数 : 2 硬币为:25 5
面值为 :31的最小硬币数 : 2 硬币为:21 10
面值为 :32的最小硬币数 : 3 硬币为:21 10 1
面值为 :33的最小硬币数 : 4 硬币为:21 10 1 1
面值为 :34的最小硬币数 : 5 硬币为:21 10 1 1 1
面值为 :35的最小硬币数 : 2 硬币为:25 10
面值为 :36的最小硬币数 : 3 硬币为:25 10 1
面值为 :37的最小硬币数 : 4 硬币为:25 10 1 1
面值为 :38的最小硬币数 : 5 硬币为:25 10 1 1 1
面值为 :39的最小硬币数 : 6 硬币为:25 10 1 1 1 1
面值为 :40的最小硬币数 : 3 硬币为:25 10 5
面值为 :41的最小硬币数 : 3 硬币为:21 10 10
面值为 :42的最小硬币数 : 2 硬币为:21 21
面值为 :43的最小硬币数 : 3 硬币为:21 21 1
面值为 :44的最小硬币数 : 4 硬币为:21 21 1 1
面值为 :45的最小硬币数 : 3 硬币为:25 10 10
面值为 :46的最小硬币数 : 2 硬币为:25 21
面值为 :47的最小硬币数 : 3 硬币为:25 21 1
面值为 :48的最小硬币数 : 4 硬币为:25 21 1 1
面值为 :49的最小硬币数 : 5 硬币为:25 21 1 1 1
面值为 :50的最小硬币数 : 2 硬币为:25 25
面值为 :51的最小硬币数 : 3 硬币为:25 25 1
面值为 :52的最小硬币数 : 3 硬币为:21 21 10
面值为 :53的最小硬币数 : 4 硬币为:21 21 10 1
面值为 :54的最小硬币数 : 5 硬币为:21 21 10 1 1
面值为 :55的最小硬币数 : 3 硬币为:25 25 5
面值为 :56的最小硬币数 : 3 硬币为:25 21 10
面值为 :57的最小硬币数 : 4 硬币为:25 21 10 1
面值为 :58的最小硬币数 : 5 硬币为:25 21 10 1 1
面值为 :59的最小硬币数 : 6 硬币为:25 21 10 1 1 1
面值为 :60的最小硬币数 : 3 硬币为:25 25 10
面值为 :61的最小硬币数 : 4 硬币为:25 25 10 1
面值为 :62的最小硬币数 : 4 硬币为:21 21 10 10
面值为 :63的最小硬币数 : 3 硬币为:21 21 21
面值为 :64的最小硬币数 : 4 硬币为:21 21 21 1
面值为 :65的最小硬币数 : 4 硬币为:25 25 10 5