动态规划详解
动态规划(Dynamic Programming,简称DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。在计算机科学中,动态规划是解决优化问题的一个强大工具。
一、动态规划的基本思想
动态规划的基本思想与分治法类似,也是将待求解的问题分解成若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
二、动态规划的基本要素
-
最优子结构性质:当问题的最优解包含了其子问题的最优解时,就称该问题具有最优子结构。动态规划算法正是利用了这种最优子结构性质,通过子问题的最优解来构造原问题的最优解。
-
无后效性:即某阶段状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响。也就是说,“未来与过去无关”,只与当前的状态有关。
-
子问题的重叠性:动态规划算法将原问题分解为若干子问题,然后逐个求解。这一点与分治法类似。区别在于,子问题之间不是独立的,一个子问题的解在下一阶段决策中可能会被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
三、C语言实现动态规划的基本步骤
-
划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
-
定义状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
-
确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
-
寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
以上就是用C语言实现动态规划的基本步骤。在实际编程中,我们还需要注意数据的存储和访问方式,以及算法的时间复杂度和空间复杂度等问题。
四、C语言动态规划示例——背包问题
背包问题是一类典型的动态规划问题。问题描述如下:有N件物品和一个容量为V的背包。第i件物品的重量是weight[i],得到的价值是value[i]。求解将哪些物品装入背包可使价值总和最大。
以下是一个用C语言实现的背包问题的动态规划解法:
#include <stdio.h>
#include <stdlib.h>
int max(int a, int b) {
return a > b ? a : b;
}
int knapsack(int W, int wt[], int val[], int n) {
int i, w;
int K[n + 1][W + 1];
// Build table K[][] in bottom up manner
for (i = 0; i <= n; i++) {
for (w = 0; w <= W; w++) {
if (i == 0 || w == 0)
K[i][w] = 0;
else if (wt[i - 1] <= w)
K[i][w] = max(val[i - 1] + K[i - 1][w - wt[i - 1]], K[i - 1][w]);
else
K[i][w] = K[i - 1][w];
}
}
return K[n][W];
}
int main() {
int val[] = {60, 100, 120}; // 价值数组
int wt[] = {10, 20, 30}; // 重量数组
int W = 50; // 背包容量
int n = sizeof(val) / sizeof(val[0]); // 物品数量
printf("Maximum value that can be put in a knapsack of capacity %d is %d\n", W, knapsack(W, wt, val, n));
return 0;
}
以上代码通过二维数组K[][]来保存子问题的解,其中K[i][w]表示前i个物品,当前背包容量为w时的最大价值。然后通过两层循环来遍历所有的子问题,并根据状态转移方程来更新K[][]数组的值。最后返回K[n][W]即可得到原问题的解。