动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。在面试笔试中动态规划也是经常作为考题出现,其中较为简单的DP题目我们应该有百分之百的把握顺利解决才可以。
动态规划定义
动态规划实际上是一类题目的总称,并不是指某个固定的算法。动态规划的意义就是通过采用递推(或者分而治之)的策略,通过解决大问题的子问题从而解决整体的做法。动态规划的核心思想是巧妙的将问题拆分成多个子问题,通过计算子问题而得到整体问题的解。而子问题又可以拆分成更多的子问题,从而用类似递推迭代的方法解决要求的问题。
动态规划的解题核心
动态规划的解题核心主要分为两步:
- 第一步:状态的定义
- 第二步:状态转移方程的定义
在这里,我们为了避免混淆用“状态”这个词来替代“问题”这个词。“问题”表示的含义类似:题目、要求解的内容、题干中的疑问句这样的概念。状态表示我们在求解问题之中对问题的分析转化。
第一步:状态的定义
有的问题过于抽象,或者过于啰嗦干扰我们解题的思路,我们要做的就是将题干中的问题进行转化(换一种说法,含义不变)。转化成一系列同类问题的某个解的情况,比如说:
题目:求一个数列中最大连续子序列的和。
我们要将这个原问题转化为:
状态定义:Fk是第k项前的最大序列和,求F1~FN中最大值。
通过换一种表述方式,我们清晰的发现了解决问题的思路,如何求出F1~FN中的最大值是解决原问题的关键部分。上述将原问题转化成另一种表述方式的过程叫做:状态的定义。这样的状态定义给出了一种类似通解的思路,把一个原来毫无头绪的问题转换成了可以求解的问题。
第二步:状态转移方程的定义
在进行了状态的定义后,自然而然的想到去求解F1~FN中最大值。这也是状态定义的作用,让我们把一个总体的问题转化成一系列问题,而第二步:状态转移方程的定义则告诉我们如何去求解一个问题,对于上述已经转换成一系列问题我们要关注的点就在于:如何能够用前一项或者前几项的信息得到下一项,这种从最优子状态转换为下一个最优状态的思路就是动态规划的核心。
对于上面的例子题目来说,状态转移方程的定义应该是:Fk=max{Fk-1+Ak,Ak}
Fk是前k项的和,Ak是第k项的值
仔细思考一番,我们能够得到这样的结论,对于前k个项的最大子序列和是前k-1项的最大子序列和Fk与第k项的和、或者第k项两者中较大的。如果大家还是不能理解这个原理建议用演算纸自己计算一番,这里就不过多赘述了。这种状态转移的思路就是DP的核心。
动态规划的应用场景
看过了如何去使用动态规划,那么随之而来的问题就在于:既然动态规划不是某个固定的算法,而是一种策略思路,那么什么时候应该去使用什么时候不能用呢?答案很简短:
对于一个可拆分问题中存在可以由前若干项计算当前项的问题可以由动态规划计算。
能用动态规划计算的问题存在一种递推的连带关系。
例题1: 数塔取数问题
一个高度为N的由正整数组成的三角形,从上走到下,求经过的数字和的最大值。
每次只能走到下一层相邻的数上,例如从第3层的6向下走,只能走到第4层的2或9上。该三角形第n层有n个数字,例如:
第一层有一个数字:5
第二层有两个数字:8 4
第三层有三个数字:3 6 9
第四层有四个数字:7 2 9 5
最优方案是:5 + 8 + 6 + 9 = 28
注意:上面应该是排列成一个三角形的样子不是竖向对应的,排版问题没有显示成三角形。
状态定义: Fi,j是第i行j列项最大取数和,求第n行Fn,m(0 < m < n)中最大值。
状态转移方程:Fi,j = max{Fi-1,j-1,Fi-1,j}+Ai,j
题解:
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); long max = 0; int[][] dp = new int[n][n]; dp[0][0] = in.nextInt(); for (int i = 1; i < n; i++) { for (int j = 0; j <= i; j++) { int num = in.nextInt(); if (j == 0) dp[i][j] = dp[i - 1][j] + num; else dp[i][j] = Math.max(dp[i - 1][j - 1], dp[i - 1][j]) + num; max = Math.max(dp[i][j], max); } } System.out.println(max); }}
例题2:编辑距离
编辑距离,又称Levenshtein距离(也叫做Edit Distance),是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。
例如将kitten一字转成sitting: sitten (k->s) sittin (e->i) sitting (->g) 所以kitten和sitting的编辑距离是3。俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。 给出两个字符串a,b,求a和b的编辑距离。状态定义:Fi,j表示第一个字符串的前i个字母和第二个字符串的前j个字母需要编辑的次数,求Fn,m,n和m分别是两个字符串的长度。
状态转移方程:
当Fi,j-1=Fi-1,j时,Fi,j=Fi,j-1; 当Fi,j-1!=Fi-1,j时,Fi,j=min{Fi-1,j-1,Fi,j-1,Fi-1,j}+1.题解:
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); String aStr = in.nextLine(); String bStr = in.nextLine(); int aLen = aStr.length(); int bLen = bStr.length(); int[][] dp = new int[aLen + 1][bLen + 1]; for (int i = 0; i < aLen + 1; i++) dp[i][0] = i; for (int i = 0; i < bLen + 1; i++) dp[0][i] = i; for (int i = 1; i < aLen + 1; i++) for (int j = 1; j < bLen + 1; j++) if (aStr.charAt(i - 1) == bStr.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1]; else dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1; System.out.println(dp[aLen][bLen]); }}
例题3:矩阵取数问题
一个N*N矩阵中有不同的正整数,经过这个格子,就能获得相应价值的奖励,从左上走到右下,只能向下向右走,求能够获得的最大价值。例如:3 * 3的方格。
1 3 3
2 1 3 2 2 1能够获得的最大价值为:11。
题解:
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); int[] dp = new int[n + 1]; int[] readArr = new int[n + 1]; for (int i = 0; i < n; i++) { for (int k = 1; k < n + 1; k++) readArr[k] = in.nextInt(); for (int j = 1; j < n + 1; j++) dp[j] = Math.max(dp[j], dp[j - 1]) + readArr[j]; } System.out.println(dp[n]); }}
例题4:背包问题
在N件物品取出若干件放在容量为W的背包里,每件物品的体积为W1,W2……Wn(Wi为整数),与之相对应的价值为P1,P2……Pn(Pi为整数)。求背包能够容纳的最大价值。
题解:
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); int v = in.nextInt(); int[] dp = new int[v + 1]; int[] price = new int[n + 1]; int[] weight = new int[n + 1]; long max = 0; for (int i = 1; i < n + 1; i++) { weight[i] = in.nextInt(); price[i] = in.nextInt(); } for (int i = 1; i < n + 1; i++) for (int j = v; j > 0; j--) if (j - weight[i] >= 0) dp[j] = Math.max(dp[j], dp[j - weight[i]] + price[i]); else dp[j] = dp[j]; for (int i = 0; i < v + 1; i++) max = max > dp[i] ? max : dp[i]; System.out.println(max); }}
例题5:最长递增子序列
给出长度为N的数组,找出这个数组的最长递增子序列。
(递增子序列是指,子序列的元素是递增的) 例如:5 1 6 8 2 4 5 10,最长递增子序列是1 2 4 5 10。题解:
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); int[] L = new int[n]; for (int j = 0; j < n; j++) L[j] = in.nextInt(); double[] B = new double[n + 1]; B[0] = Integer.MIN_VALUE; B[1] = L[0]; int Len = 1; int p, r, m; for (int i = 1; i < n; i++) { p = 0; r = Len; while (p <= r) { m = (p + r) / 2; if (B[m] < L[i]) p = m + 1; else r = m - 1; } B[p] = L[i]; if (p > Len) Len++; } System.out.println(Len); }}
例题6:最大子段和
N个整数组成的序列a[1],a[2],a[3],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的连续子段和的最大值。
当所给的整数均为负数时和为0。 例如:-2,11,-4,13,-5,-2,和最大的子段为:11,-4,13。和为20。题解:
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); long max = 0, subMax = 0; for (int i = 0; i < n; i++) { subMax += in.nextInt(); if (max < subMax) max = subMax; if (subMax < 0) subMax = 0; } System.out.println(max); }}
例题7:最长公共子序列Lcs
给出两个字符串A B,求A与B的最长公共子序列(子序列不要求是连续的)。
比如两个串为:abcicba
abdkscabab是两个串的子序列,abc也是,abca也是,其中abca是这两个字符串最长的子序列。
题解:
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); String aStr = in.nextLine(); String bStr = in.nextLine(); int aLen = aStr.length(); int bLen = bStr.length(); int[][] dp = new int[aLen + 1][bLen + 1]; for (int i = 1; i < aLen + 1; i++) for (int j = 1; j < bLen + 1; j++) if (dp[i - 1][j] == dp[i][j - 1] && aStr.charAt(i - 1) == bStr.charAt(j - 1) && dp[i - 1][j] == dp[i - 1][j - 1]) dp[i][j] = dp[i - 1][j] + 1; else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); int max = dp[aLen][bLen]; StringBuilder sb = new StringBuilder(); while (max > 0) { if (dp[aLen - 1][bLen] == dp[aLen][bLen - 1] && dp[aLen - 1][bLen] + 1 == dp[aLen][bLen]) { sb.append(aStr.charAt(aLen - 1)); max--; aLen--; bLen--; } else { if (dp[aLen][bLen - 1] == dp[aLen][bLen]) bLen--; else aLen--; } } System.out.println(sb.reverse().toString()); }}
例题8:正整数分组
将一堆正整数分为2组,要求2组的和相差最小。
例如:1 2 3 4 5,将1 2 4分为1组,3 5分为1组,两组和相差1,是所有方案中相差最少的。题解:
import java.util.Scanner;public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n = in.nextInt(); long sum = 0, max = 0; int[] nums = new int[n]; for (int i = 0; i < n; i++) { nums[i] = in.nextInt(); sum += nums[i]; } int[] dp = new int[(int) (sum / 2 + 1)]; for (int i = 0; i < n; i++) for (int j = (int) (sum / 2); j > 0; j--) if (j >= nums[i]) dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]); for (int i = 1; i < sum / 2 + 1; i++) max = max > dp[i] ? max : dp[i]; System.out.println(Math.abs((sum - max) - max)); }}