之前ACM时期,一开始学习动态规划时,并不懂这个东西究竟是个什么概念。搜索题解,大部分每每也是甩个题目,而后直接列出状态转移方程,紧接着直接来个AC代码,让人云里雾里。尽管作过一些题目,可是每每遇到新的题目便抓瞎了。只会一些作过的题,例如导弹拦截,各类背包。今天借助算法课的做业,仔细的将动态规划剖析一遍。一步一步说明,如何将普通的指数级复杂度的递归算法优化为多项式级复杂度的动态规划算法。算法
$m$ 元钱,投资 $n$ 个项目,效益函数 $f_i(x) $,表示 $i$ 个项目投资 $x$ 元的收益,求如何分配每一个项目的钱数使得总效益最大?
假设投资 $ i $ 个项目,共投资 $ x $ 元的收益状况的全部可能性为 $g_i(x)$ 。显然可得:函数
$$ \begin{cases} g_1(x)=f_1(x_1),\ \ x_1 \leq x .\\ g_2(x)=f_1(x_1)+f_2(x_2),\ \ x_1+x_2 \leq x\\ g_3(x)=f_1(x_1)+f_2(x_2)+f_3(x_3),\ \ x_1+x_2+x_3 \leq x\\ \ \ ...\\ \ \ ...\\ \ \ ...\\ g_i(x)=\sum_{k=1}^if_k(x_k),\ \ \sum_{k=1}^ix_k \leq x\\ \end{cases} $$性能
将 $g_{i-1}(x-x_i)$ 代入 $g_i(x)$ 可得:学习
$$ \begin{cases} g_1(x)=f_1(x_1),\\ g_2(x)=g_1(x-x_2)+f_2(x_2),\\ g_3(x)=g_2(x-x_3)+f_3(x_3),\\ \ \ ...\\ \ \ ...\\ \ \ ...\\ g_i(x)=g_{i-1}(x-x_i)+f_i(x_i)\\ \end{cases} $$测试
整理可得递归式:优化
$$ g_i(x)= \begin{cases} f_1(x),\ \ i=1 .\\ g_{i-1}(x-x_i)+f_i(x_i),\ \ i>1, \sum_{k=1}^ix_k \leq x\\ \end{cases} $$ spa
咱们但愿总收益最大,即求
$$w_i(x)=max\{g_i(x)\}$$
此即递归定义的目标函数 ,注意 $g_i(x)$ 是当前项目投资收益与前面全部投资项目的收益的排列组合。翻译
/** * @Author xwh * @Date 2020/4/13 13:05:53 **/ public class Investment { /* 投资收益函数 */ public static int f[][] = new int[5][10]; /** * 投资i个工厂, 共x元的最大收益 */ public static int g(int i, int x) { // 输出一下当前的计算层级, 方便下个步骤分析复杂度 System.out.println("g " + i + " " + x); int max = 0; if (i == 1) { // 投资第一个工厂的最大收益就是对应函数值 return f[i][x]; } else { // DFS, 根据公式穷举全部收益状况, 并求其中最大值返回 // 当前收益 = 第i个工厂投资j元收益 + 前i-1个工厂投资x-j元的最大收益 for (int j = 0; j <= x; j++) { int temp = f[i][j] + g(i - 1, x - j); if (temp > max) { max = temp; } } } return max; } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int n = 4, m = 6; // 投资函数初始化, f[i][j]表示第i个工厂投资j元的收益 for (int i = 1; i <= n; i++) { for (int j = 0; j <= m; j++) { f[i][j] = scanner.nextInt(); } } System.out.println("搜索树的DFS序列:"); int w = g(4, 6); System.out.printf("向%d个工厂投资%d元的最大收益为:%d元", n, m, w); } }
0 20 50 65 80 85 85
0 20 40 50 55 60 65
0 25 60 85 100 110 115
0 25 40 50 60 65 70
其中第 $i$ 行第 $j$ 列的数字表示第 $i$ 个工厂投资 $j$ 元的收益。code
对于每个 $g_i(x)$ ,递归求解时下一级都为一个组合数。因此复杂度应为:
$$C_{m+n-1}^m= \frac {(m+n-1)!} {(n-1)!m!} = O((1+\epsilon)^{m+n-1})$$
结果是指数级的。咱们知道指数就意味着爆炸。显然这样复杂度的算法在解决实际问题上并无太大的意义。因此咱们必须经过必定的方法将这个复杂度降阶。这种方法就是动态规划。blog
要知道如何进行优化,咱们必须知道问题出在哪里。下面来分析一下递归的性能就行损耗在了哪里。传入测试数据,执行上一节的代码,获得以下结果:
能够看到,计算4个工厂投资6元的收益,共穷举了120种状况。继续分析:
咱们发现 $g_2(j)$ 这种状况被计算了28次。实际上这种状况应该只有 $ g_2(0), g_2(1) \cdots g_2(6)$ 共6种才对。
为何会出现这种状况呢?
对于每一次计算,递归程序都会去尝试一遍更深一层递归的全部排列组合,致使性能很是低。例如,在计算 $g_4(6)$ 时,程序会去计算 $g_3(0) \cdots g_3(6) $, 而对于其中的每一项,都会去计算 $g_2(0) \cdots g_2(6)$,在这些排列组合中找到当前的最优解。
可见,当前递归算法的问题在于:1. 相同的子问题重复计算。 2. 已经得出最优解的问题重复地去穷举次优解。
能用动态规划进行优化的问题,必须知足以下三个原则:
看上去很是抽象,实际上咱们已经证实了当前问题知足这三个性质,下面开始说明。
重叠子问题是一个递归解决方案里包含的子问题虽然不少,但不一样子问题不多。少许的子问题被重复解决不少次。
这是重叠子问题的定义说明,看上去是否是很熟悉。就在上一节,咱们已经发现的递归算法的问题1即是这个意思。咱们重复计算了不少同样的问题,而这种计算其实是能够经过记忆化搜索,经过时间换空间避免的。
一个最优化策略的子策略老是最优的。换句话说,一个最优策略,必然包含子问题的最优策略。
一样,咱们在上一节发现,咱们计算当前最大收益 $g_i(x)$ 时,$ g_{i-1}(j) $ 必然是所有被计算过的,递归发现 $g_{i-2}(k)$ 一样如此。
所谓无后效性原则,指的是这样一种性质:某阶段的状态一旦肯定,则此后过程的演变再也不受此前各状态及决策的影响。
意思就是,对于每个 $g_i(x)$,仅仅与 $g_{i-1}(j)$ 有关,而 $g_{i+1}(k)$ 也仅仅和 $g_i(x)$ 有关。一样的,咱们在计算中,每次的排列组合仅仅与前 $i$ 个工厂的总收益有关。
如今咱们已经证实,当前的投资问题,知足使用动态规划三个原则。那么咱们能够经过动态规划来进行优化。那么说了这么久究竟什么是动态规划呢?
咱们回到以前提到过的两个问题,即:1. 相同的子问题重复计算。 2. 已经得出最优解的问题重复地去穷举次优解。
问题1对应着重叠子问题,问题2对应着最优子结构。那么咱们是否是能够经过必定的方法,避免程序重复地去穷举次优解,以及重复地去计算最优解呢?
既然咱们已经得出,当前最优解只与前一个最优解有关,那咱们只要每次保存下计算的最优解,每次用到直接调用拿到,不须要再深刻递归去计算不就消去以上两个问题了吗?
换句话说,咱们只要找到一个公式,根据公式,利用当前状态的最优解集合不断地穷举下一个状态的最优解,不是能够直接消去递归了吗?
利用最优子结构及重叠子问题性质,改写以前的递归公式,得:
$$ w_i(x)= \begin{cases} f_1(x),\ \ i=1 .\\ max\{g_i(x)\}=f_i(k)+w_{i-1}(x-k),\ \ i>1 \\ \end{cases} $$
咱们成功地将当前最优解与以前计算过的最优解联系了起来,避免了每次去穷举次优解和计算过的最优解。
此即状态转移方程。
public static void dp(int n, int m) { int i, j, k, temp; int w[][] = new int[n + 1][m + 1]; // 计算投资第一个项目的最大收益 for (i = 0; i <= m; i++) { w[1][i] = f[1][i]; } // 投资前i个项目 for (i = 2; i <= n; i++) { // 计算每个g[i][x], 0<=x<=m for (j = 0; j <= m; j++) { // 状态转移, 利用w[i-1][]计算w[i] // g[i][x] == temp == f[i][k] + w[i-1][j-k] // k投资当前项目的钱数, 0<=k<=j for (k = 0; k <= j; k++) { temp = f[i][k] + w[i - 1][j - k]; if (temp > w[i][j]) { // 更新当前的最优解, 给下一个最优解调用 w[i][j] = temp; } } } } System.out.printf("向%d个工厂投资%d元的最大收益为:%d元\n", n, m, w[n][m]); }
我把动态规划理解为进行了次优解剪枝与重复子问题记忆化的穷举算法,经过最优解来穷举最优解。也不知道当初的学者为何会把 $ Dynamic \ Programming $ 翻译成动态规划,太字面了真的很差理解。