一、基本概念
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
二、基本思想与策略
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
三、适用的情况
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
四、求解的基本步骤
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
图1 动态规划决策过程示意图
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
五、算法实现的说明
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。
使用动态规划求解问题,最重要的就是确定动态规划三要素:
(1)问题的阶段 (2)每个阶段的状态
(3)从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
装配线调度问题
问题描述
- 某汽车工厂有2个装配线,每个装配线有n个装配站(按顺序编号1~n),两个装配线对应的装配站执行相同的功能,但所用的时间可能不同.经过第i条流水线(i=1,2)的第j个装配站所花的时间为Aij。从第i条流水线的第j个装配站移到第j+1个装配站的时间可以忽略,而移到另外一个流水线的下一个装配站则需要一定的时间Tij。
- 汽车进入流水线不需要花时间,出流水线时需要花时间Tin。
- 汽车的装配需要按顺序经过所有装配站。
- 现在已知装配时间Aij和转移时间Tij,要求输出装配一辆汽车所需要的最短时间。
动态规划的算法思想
动态规划与贪心策略类似,将一个问题的解决方案视为一系列决策的结果。不同的是,贪心算法每采用一次贪心选择便做出一个不可撤回的决策,而在动态规划中,还要考察每个最优决策序列中是否包含一个最优决策自序列。使用动态规划时,所求的问题应具有以下两种性质:
所求问题的最优子结构性质是采用动态规划算法的条件之一,这种性质又被称为最优化原理。动态规划方法采用最优化原理来建立用于计算最优解的递归式。所谓最优化原理即不管前面的策略如何,此后的决策必须是基于当前状态(由上一次决策产生)的最优决策。由于对于有些问题的某些递归式来说并不一定能保证最优原则,因此在求解问题时有必要对它进行验证。若不能保持最优原则,则不可应用动态规划方法。在得到最优解的递归式之后,需要执行回溯以构造最优解。当最优决策序列中包含最优决策子序列时,可建立动态规划递归方程,它可以帮助我们高效的解决问题
人们总希望编写一个简单的递归程序来求解动态规划方程。然而,如果不努力的去避免重复计算,递归程序的复杂性将非常可观。如果在递归程序设计中解决了重复计算问题,复杂性将大幅度下降。这种方法的思想是:由程序设置“备忘录”,每计算出一个新的子结构的解时,都保存起来。当遇到一次递归时,判断是否已经计算,如果已经计算,只需取出先前保存的结果即可。动态规划递归方程也可以用迭代方式来求解,这时很自然的避免了重复计算。尽管迭代程序与避免重复计算的递归程序有相同的重复性,但迭代程序不需要附加的递归栈空间,因此将避免重复计算的递归程序更快
装配线调度代码(c语言版)
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
-
- #define LINE 2
- #define N 6
-
- int main()
- {
-
- const int product[LINE][N] = {{7, 9, 3, 4, 8, 4}, {8, 5, 6, 4, 5, 7}};
-
-
- const int transport[LINE][N - 1] = {{2, 3, 1, 3, 4}, {2, 1, 2, 2, 1}};
-
-
- const int e1 = 2;
- const int e2 = 4;
-
-
- const int o1 = 3;
- const int o2 = 2;
-
-
- int final[LINE][N] = {{0}, {0}};
-
-
- int time = 0;
-
-
- int line[LINE][N] = {{0}, {0}};
-
-
- int line_out = -1;
-
-
- int path[N];
- memset(path, -1, sizeof(path));
-
- int i, j;
-
-
-
- final[0][0] = e1 + product[0][0];
- final[1][0] = e2 + product[1][0];
-
- for (j = 1; j < N; j ++) {
- if (final[0][j - 1] + product[0][j] <= final[1][j - 1] + transport[1][j - 1] + product[0][j]) {
- final[0][j] = final[0][j - 1] + product[0][j];
- line[0][j] = 0;
- }else {
- final[0][j] = final[1][j - 1] + transport[1][j - 1] + product[0][j];
- line[0][j] = 1;
- }
-
- if (final[1][j - 1] + product[1][j] <= final[0][j - 1] + transport[0][j - 1] + product[1][j]) {
- final[1][j] = final[1][j - 1] + product[1][j];
- line[1][j] = 1;
- }else {
- final[1][j] = final[0][j - 1] + transport[0][j - 1] + product[1][j];
- line[1][j] = 0;
- }
- }
-
- if (final[0][N - 1] + o1 <= final[1][N - 1] + o2) {
- line_out = 0;
- time = final[0][N - 1] + o1;
- }else {
- line_out = 1;
- time = final[1][N - 1] + o2;
- }
- printf("总共花费的时间是:%d\n", time);
-
-
- for (j = N - 1, i = line_out; j >= 1; j --) {
- i = line[i][j];
- path[j] = i;
- }
-
- for (i = 1; i < N; i ++) {
- printf("line: %d, station: %d\n", path[i] + 1, i);
- }
- printf("line: %d, station: %d\n", line_out + 1, N);
-
- return 0;
- }
最长公共子序列(LCS)
题目描述
- 如果Z是X的一个子序列又是Y的一个最长公共子序列。例如,如果X = {A, B, C, B, D, A, B}, Y = {B, D, C, A, B, A},则{B, C, B, A}是X和Y的一个LCS,序列 {B,D, A,B}也是,因为没有长度为5的或更大公共子序列
思路
(1)梳理状态的变化情况,找到最优子结构:
设X = (x1, x2, x3, ..., xm), Y = (y1, y2, y3...yn)为两个序列,并设Z = (z1, z2, z3,...zk)为X和Y的任意一个LCS
- 如果xm == yn,那么zk == xm == yn,而且Z(k - 1)是X(m-1)和Y(n-1)的一个LCS;
- 如果xm != yn,那么zk != xm,蕴含Z是X(m-1)和Y的一个LCS;
- 如果xm != yn,那么zk != yn,蕴含Z是X和Y(n-1)的一个LCS;
(2)找到一个递归的解:
如果xm == yn,必须找到X(m - 1)和Y(n - 1)的一个LCS。将xm = yn添加到这个LCS上,可以产生X和Y的一个LCS;如果xm != yn,就必须解决两个子问题:找到x(m - 1)和y一个LCS,以及找出X和Y(n-1)的一个LCS,以及找出X和Y(n - 1)的一个LCS,这两个LCS中,较长的就是X和Y的一个LCS。由LCS的最优子结构可得到递归式:
最长公共子序列代码(c语言版)
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
-
- #define N 1002
-
- int num[N][N], b[N][N];
-
- int longestCommonString(char *str1, char *str2, int len1, int len2);
-
- int main()
- {
- int l, len1, len2;
- char str1[N], str2[N];
-
- while (scanf("%s %s", str1, str2) != EOF) {
- len1 = strlen(str1);
- len2 = strlen(str2);
-
- l = longestCommonString(str1, str2, len1, len2);
- printf("lcs is %d\n", l);
- }
-
- return 0;
- }
-
- int longestCommonString(char *str1, char *str2, int len1, int len2)
- {
- int i, j;
-
- for (i = 0; i < len1; i ++) {
- num[i][0] = 0;
- }
- for (j = 0; j < len2; j ++) {
- num[0][i] = 0;
- }
-
- for (i = 1; i <= len1; i ++) {
- for (j = 1; j <= len2; j ++) {
- if (str1[i - 1] == str2[j - 1]) {
- num[i][j] = num[i - 1][j - 1] + 1;
- b[i][j] = 0;
- }else {
- if (num[i - 1][j] >= num[i][j - 1]) {
- num[i][j] = num[i - 1][j];
- b[i][j] = 1;
- }else {
- num[i][j] = num[i][j - 1];
- b[i][j] = -1;
- }
- }
- }
- }
-
- return num[len1][len2];
- }
六、动态规划算法基本框架
1 for(j=1; j<=m; j=j+1) // 第一个阶段 2 xn[j] = 初始值; 3 4 for(i=n-1; i>=1; i=i-1)// 其他n-1个阶段 5 for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式 6 xi[j]=j=max(或min){g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])}; 8 9 t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案10 11 print(x1[j1]);12 13 for(i=2; i<=n-1; i=i+1)15 { 17 t = t-xi-1[ji];18 19 for(j=1; j>=f(i); j=j+1)21 if(t=xi[ji])23 break;25 }
具体例子用一个实际例子来体现动态规划的算法思想——硬币找零问题。
硬币找零问题描述:现存在一堆面值为 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– cent
- s]和一个面值为cents元的硬币,由于 i– cents < i , 其解 coinsUsed[ i– cen
- ts] 已经存在,如果面值为 cents 的硬币满足题意,那么最终解 coinsUsed[
- i ] 则等于 coinsUsed[ i– cents] 再加上 1(即面值为 cents)的这一个硬币。
-
- 下面用代码实现并测试一下:
-
- Java代码
-
- public class CoinsChange {
-
-
-
-
-
-
-
-
-
-
-
-
- public static void makeChange(int[] values, int valueKinds, int money,
-
- int[] coinsUsed) {
-
- coinsUsed[0] = 0;
-
-
- for (int cents = 1; cents <= money; cents++) {
-
-
- int minCoins = cents;
-
-
-
- for (int kind = 0; kind < valueKinds; kind++) {
-
-
-
- if (values[kind] <= cents) {
-
- int temp = coinsUsed[cents - values[kind]] + 1;
-
- if (temp < minCoins) {
-
- minCoins = temp;
- }
- }
- }
-
-
- coinsUsed[cents] = minCoins;
-
- System.out.println("面值为 " + (cents) + " 的最小硬币数 : "
-
- + coinsUsed[cents]);
- }
- }
-
-
- public static void main(String[] args) {
-
-
-
- int[] coinValue = new int[] { 25, 21, 10, 5, 1 };
-
-
-
- int money = 63;
-
- int[] coinsUsed = new int[money + 1];
-
- makeChange(coinValue, coinValue.length, money, coinsUsed);
- }
- }
-
-
- 0/1背包问题的动态规划法求解,前人之述备矣,这里所做的工作,不过是自己根
- 据理解实现了一遍,主要目的还是锻炼思维和编程能力,同时,也是为了增进对
- 动态规划法机制的理解和掌握。
-
- 值得提及的一个问题是,在用 JAVA 实现时, 是按算法模型建模,还是用对象模
- 型建模呢? 如果用算法模型,那么 背包的值、重量就直接存入二个数组里;如果
- 用对象模型,则要对背包以及背包问题进行对象建模。思来想去,还是采用了对
- 象模型,尽管心里感觉算法模型似乎更好一些。有时确实就是这样,对象模型虽
- 然现在很主流,但也不是万能的,采用其它的模型和视角,或许可以得到更好的
- 解法。
-
- 背包建模:
-
- Java代码
-
- public class Knapsack {
-
-
- private int weight;
-
-
-
- private int value;
-
-
-
- public Knapsack(int weight, int value) {
- this.value = value;
- this.weight = weight;
- }
- public int getWeight() {
-
- return weight;
-
- }
-
- public int getValue() {
- return value;
-
- }
-
- public String toString() {
-
- return "[weight: " + weight + " " + "value: " + value + "]";
-
- }
- }
-
-
- import java.util.ArrayList;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- public class KnapsackProblem {
-
-
- private Knapsack[] bags;
-
-
- private int totalWeight;
-
-
-
- private int n;
-
-
-
- private int[][] bestValues;
-
-
- private int bestValue;
-
-
- private ArrayList<Knapsack> bestSolution;
-
- public KnapsackProblem(Knapsack[] bags, int totalWeight) {
- this.bags = bags;
-
- this.totalWeight = totalWeight;
- this.n = bags.length;
-
- if (bestValues == null) {
- bestValues = new int[n+1][totalWeight+1];
-
- }
- }
-
-
-
-
-
- public void solve() {
-
- System.out.println("给定背包:");
- for(Knapsack b: bags) {
-
- System.out.println(b);
-
- }
- System.out.println("给定总承重: " + totalWeight);
-
-
-
- for (int j = 0; j <= totalWeight; j++) {
- for (int i = 0; i <= n; i++) {
- if (i == 0 || j == 0) {
-
- bestValues[i][j] = 0;
-
- }
-
- else
-
- {
-
-
-
-
- if (j < bags[i-1].getWeight()) {
-
- bestValues[i][j] = bestValues[i-1][j];
-
- }
- else
-
- {
-
-
-
-
- int iweight = bags[i-1].getWeight();
-
- int ivalue = bags[i-1].getValue();
-
- bestValues[i][j] =
-
- Math.max(bestValues[i-1][j], ivalue + bestValues[i-1][j-iweight]);
- }
- }
-
- }
-
- }
-
-
-
-
- if (bestSolution == null) {
-
- bestSolution = new ArrayList<Knapsack>();
-
- }
- int tempWeight = totalWeight;
-
- for (int i=n; i >= 1; i--) {
- if (bestValues[i][tempWeight] > bestValues[i-1][tempWeight]) {
-
- bestSolution.add(bags[i-1]);
-
- tempWeight -= bags[i-1].getWeight();
-
- }
- if (tempWeight == 0) { break; }
-
- }
- bestValue = bestValues[n][totalWeight];
-
- }
-
-
-
-
-
-
- public int getBestValue() {
-
- return bestValue;
- }
-
-
-
-
-
- public int[][] getBestValues() {
-
- return bestValues;
- }
-
-
-
-
- public ArrayList<Knapsack> getBestSolution() {
- return bestSolution;
- }
-
-
- }
-
- public class KnapsackTest {
-
- public static void main(String[] args) {
-
- Knapsack[] bags = new Knapsack[] {
- new Knapsack(2,13), new Knapsack(1,10),
-
- new Knapsack(3,24), new Knapsack(2,15),
- new Knapsack(4,28), new Knapsack(5,33),
- new Knapsack(3,20), new Knapsack(1, 8)
- };
- int totalWeight = 10;
-
- KnapsackProblem kp = new KnapsackProblem(bags, totalWeight);
-
- kp.solve();
- System.out.println(" -------- 该背包问题实例的解: --------- ");
-
- System.out.println("最优值:" + kp.getBestValue());
-
- System.out.println("最优解【选取的背包】: ");
-
- System.out.println(kp.getBestSolution());
-
- System.out.println("最优决策矩阵表:");
-
- int[][] bestValues = kp.getBestValues();
-
- for (int i=0; i < bestValues.length; i++) {
-
- for (int j=0; j < bestValues[i].length; j++) {
-
- System.out.printf("%-5d", bestValues[i][j]);
-
- }
- System.out.println();
-
- }
- }
- }