知识的学习在于点滴记录,坚持不懈;知识的学习要有深度和广度,不能只流于表面,坐井观天;知识要善于总结,不仅能够理解,更知道如何表达!
动态规划算法思想
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行的解,每一个解都对应于一个值,我们希望找到具有最优值的解。
动态规划算法和分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
但是与分治法不同的是,适用于动态规划算法求解的问题,经分解得到的子问题往往不是互相独立的,即下一个子阶段的求解是建立在上一个子阶段的解的基础上进一步的求解,如果这样的问题采用分治法求解时,有些子问题被重复计算了很多次,效率比较低!
如果能够保存已解决的子问题的结果,而在需要的时候再找出子问题已求得的答案,就可以避免大量重复计算,从而得到多项式的时间算法,这要比用子集树解决同样问题(比如0-1背包)所花费的指数时间要好的多!。
但是这也不是说动态规划就一定比子集树解决问题的效率高,子集树解决问题的时间复杂度虽然是指数级别的 O ( 2 n ) O(2^n) O(2n),但是通过添加适当的剪枝操作,可以大大提高子集树的遍历效率。
我们可以用一个表来记录所有已解的子问题的答案,不管该子问题以后是否被用到,只要它被计算过,就将其计算结果填入表中,这就是动态规划算法的基本思路,虽然具体的问题采用动态规划算法的形式多种多样,但它们都具有相同的填表格式。
动态规划基本步骤
1.找出问题最优解的性质,并刻画其结构特征(找“状态”)
2.递归的定义最优值 (找“状态转移方程”)
3.以自底向上的方式计算出问题的最优值
4.根据计算最优值时得到的信息,构造最优解
核心就是找出问题的“状态”以及”状态转移方程“。
硬币问题
问题描述:有面值1,3,5分的硬币,问达到指定价值,最少需要几个硬币?
状态dp(i)表示i价值需要的最少的硬币数量;状态转换方程:dp(i) = min{dp(i-vj) + 1},其中 i >= vj,i表示价值,vj表示第j个硬币的价值,代码如下:
public static void main(String[] args) {
/**
* dp(i):表示i价值需要的最少的硬币数量
* dp(i) = min{dp(i-vj) + 1}; 其中i-vj>=0,vj表示第j个硬币的面值
*/
int[] ar = {1,3,5};
int val = 11;
int[] dp = new int[val+1];
for (int i = 1; i <= val; i++) {
dp[i] = i; // 先取一个最大值,假设放的都是面额为1的硬币,硬币数量最多
for (int j = 0; j < ar.length; j++) {
if(i >= ar[j] && (dp[i-ar[j]] + 1) < dp[i]){
dp[i] = dp[i-ar[j]] + 1;
}
}
}
System.out.println(Arrays.toString(dp));
System.out.println(dp[val]);
}
最大字段和问题
问题描述:给定n个整数,可能为负数组成的序列a[1],a[2],…,a[n],求该序列如a[i]+a[i+1]+…+a[j]的字段和的最大值,当所给的整数均为负数时定义字段和为0。该问题用动态规划算法求解,状态dp[i]表示以第i个元素结尾的最大字段和,状态转换方程是:d[i] = max{a[i], d[i-1]+a[i]} ,代码如下:
public class DPMaxChildSegmentSum {
public static void main(String[] args) {
int[] ar = {12,-16,8,-5,9,-12,10} ;
int[] dp = new int[ar.length];
int sum = maxSegmentSum(ar, dp);
System.out.println(Arrays.toString(dp));
System.out.println(sum);
}
private static int maxSegmentSum(int[] ar, int[] dp) {
dp[0] = ar[0] >= 0? ar[0] : 0;
int maxSum = dp[0];
for(int i=1; i<ar.length; ++i){
dp[i] = dp[i-1] + ar[i];
if(dp[i] < 0){
dp[i] = 0;
}
if(maxSum < dp[i]){
maxSum = dp[i];
}
}
return maxSum;
}
}
求LIS最长非降子序列问题
该问题采用动态规划算法解决,其状态dp[i]表示以第i个元素结尾的最长非降子序列的长度值,状态转移方程是:dp(i) = max{1, dp(j)+1} 其中j<i 而且 A[j]<=A[i],代码如下:
public class DPLISLength {
public static void main(String[] args) {
String str = "asdfiez";
int[] dp = new int[str.length()];
int length = LIS(str, str.length(), dp);
System.out.println("LISLength:" + length);
}
private static int LIS(String str, int length, int[] dp) {
int maxlen = 0;
for (int i = 0; i < length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if(str.charAt(j) < str.charAt(i)
&& dp[j]+1 > dp[i]){
dp[i] = dp[j] + 1;
}
}
if(maxlen < dp[i]){
maxlen = dp[i];
}
}
return maxlen;
}
}
求LCS最长公共子序列问题
/**
* 描述: 求两个序列的最长公共子序列问题
* 有两个序列
* Xn = {X1, X2, X3, ... , Xn}
* Yn = {Y1, Y2, Y3, ... , Yn}
*
* 以上两个序列的最长公共子序列为
* 1.如果Xn == Yn,那么
* LCS = LCS{[X1...Xn-1], [Y1...Yn-1]} + Xn
*
* 2.如果Xn != Yn,那么
* LCS = LCS{[X1...Xn], [Y1...Yn-1]}
* LCS = LCS{[X1...Xn-1], [Y1...Yn]}
* @param args
*/
public static void main(String[] args) {
String str1 = "abcsafsa";
String str2 = "cafa";
int length = LCS(str1, str2, str1.length(), str2.length());
System.out.println("length1:" + length);
}
/**
* 非递归求解LCS问题
* @param str1
* @param str2
* @param i
* @param j
* @return
*/
private static int LCS(String str1, String str2, int i, int j) {
int maxlen = 0;
// 用该数组记录动态规划问题求解中,重复子问题的解,避免重复计算,提高算法效率
int[][] dp = new int[i+1][j+1];
for(int m=1; m <= i; ++m){
for(int n=1; n <= j; ++n){
if(str1.charAt(m-1) == str2.charAt(n-1)){
dp[m][n] = dp[m-1][n-1] + 1;
} else {
dp[m][n] = Math.max(dp[m-1][n], dp[m][n-1]);
}
if(maxlen < dp[m][n]){
maxlen = dp[m][n];
}
}
}
return maxlen;
}
上面的代码只是打印出最长公共子序列的长度,如果想要打印最长公共子序列的元素,则要记录dp表的走向,然后用回溯法进行元素打印,代码如下:
/**
* 描述: 求两个序列的最长公共子序列问题
* 有两个序列
* Xn = {X1, X2, X3, ... , Xn}
* Yn = {Y1, Y2, Y3, ... , Yn}
*
* 以上两个序列的最长公共子序列为
* 1.如果Xn == Yn,那么
* LCS = LCS{[X1...Xn-1], [Y1...Yn-1]} + Xn
*
* 2.如果Xn != Yn,那么
* LCS = LCS{[X1...Xn], [Y1...Yn-1]}
* LCS = LCS{[X1...Xn-1], [Y1...Yn]}
* @param args
*/
public static void main(String[] args) {
String str1 = "abcsafsa";
String str2 = "cafa";
// 辅助数组,记录LCS元素的走向
int[][] arr = new int[str1.length()+1][str2.length()+1];
int length = LCS(str1, str2, str1.length(), str2.length(), arr);
System.out.println("length1:" + length);
backstrace(str1, str1.length(), str2.length(), arr);
}
/**
* 非递归求解LCS问题
* @param str1
* @param str2
* @param i
* @param j
* @param arr
* @return
*/
private static int LCS(String str1, String str2, int i, int j, int[][] arr) {
int maxlen = 0;
// 用该数组记录动态规划问题求解中,重复子问题的解,避免重复计算,提高算法效率
int[][] dp = new int[i+1][j+1];
for(int m=1; m <= i; ++m){
for(int n=1; n <= j; ++n){
if(str1.charAt(m-1) == str2.charAt(n-1)){
dp[m][n] = dp[m-1][n-1] + 1;
arr[m][n] = 1;
} else {
if(dp[m-1][n] > dp[m][n-1]){
dp[m][n] = dp[m-1][n];
arr[m][n] = 2;
} else {
dp[m][n] = dp[m][n-1];
arr[m][n] = 3;
}
}
if(maxlen < dp[m][n]){
maxlen = dp[m][n];
}
}
}
return maxlen;
}
/**
* 输出最长LCS子序列内容
* @param str1
* @param length
* @param length1
* @param arr
*/
private static void backstrace(String str1, int length,
int length1, int[][] arr) {
if(length <= 0 || length1 <= 0){
return;
}
if(arr[length][length1] == 1){
backstrace(str1, length-1, length1-1, arr);
System.out.print(str1.charAt(length-1) + " ");
} else {
if(arr[length][length1] == 2){
backstrace(str1, length-1, length1, arr);
} else {
backstrace(str1, length, length1-1, arr);
}
}
}
0-1背包问题
/**
* 描述: 动态规划求解0-1背包问题
*
* 先处理上面表中最后一行dp(n, j)
* j < wn 0
* j >= wn vn
* 再处理其它行的情况 dp(i, j)
* 0<= j < wi dp(i+1, j)
* j >= wi max{dp(i+1, j), dp(i+1, j-wi)+vi}
*
* @Author shilei
* @Date 5/1
*/
public class DP01Package {
public static void main(String[] args) {
int[] w = {8,6,4,2,5};
int[] v = {6,4,7,8,6};
int c = 20;
int[][] dp = new int[w.length][c+1];
// 动态规划求解0-1背包问题
func(w, v, c, dp);
// 打印最优解的值和选择的物品
int[] x = new int[w.length];
backtrace(w, v, c, dp, x);
}
/**
* 回溯打印选择的物品
* @param w
* @param v
* @param c
* @param dp
* @param x
*/
private static void backtrace(int[] w, int[] v, int c, int[][] dp, int[] x) {
int bestv = 0;
for(int i=0; i<w.length-1; ++i){
if(dp[i][c] == dp[i+1][c]){
x[i] = 0;
} else {
x[i] = 1;
bestv += v[i];
c -= w[i];
}
}
// 处理第n个物品
if(dp[w.length-1][c] > 0){
bestv += v[w.length-1];
x[w.length-1] = 1;
}
System.out.println(bestv);
System.out.println(Arrays.toString(x));
}
/**
* 求解0-1背包价值最优的物品选择
* @param w
* @param v
* @param c
* @param dp
*/
private static void func(int[] w, int[] v, int c, int[][] dp) {
int n = w.length - 1;
// 先处理dp表中最后一行 dp(n, j)
for(int j=1; j<=c; ++j){
if(j < w[n]){
dp[n][j] = 0;
} else {
dp[n][j] = v[n];
}
}
// 处理其它n-1行的最优解
for(int i=n-1; i>=0; --i){
for(int j=1; j<=c; ++j){
if(j < w[i]){
dp[i][j] = dp[i+1][j];
} else {
dp[i][j] = Math.max(dp[i+1][j], dp[i+1][j-w[i]]+v[i]);
}
}
}
}
}
数字三角形问题
数字三角形问题:一个数字三角宝塔,规定从最顶层走到最底层,每一步可沿垂直向下,左斜线向下,或右斜线向下走,求解从最顶层走到最底层的一条路径,使得沿着该路径所经过的数字的总和最大,输出最大值。
样例输入:
第一行是数塔层数N(1<=N<=100)。
第二行起,从一个数字按数塔图形依次递增,共有N层。
5
13
11 8
12 7 26
6 14 15 8
12 7 13 24 11
样例输出:86
这个问题可以自底向上递归进行求解(但是注意,很多子问题被重复求解,效率不好!),代码如下:
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("输入三角形数组的层数:");
int n = in.nextInt();
System.out.println("输入三角数组:");
int[][] arr = new int[n+1][n+1];
for(int i=1; i<=n; ++i){
for (int j = 1; j <= i; j++) {
arr[i][j] = in.nextInt();
}
}
int max = findMaxPath(arr, 1, 1, n);
System.out.println("max value:" + max);
}
private static int findMaxPath(int[][] arr, int i, int j, int n) {
if(i == n || j == 0){
return arr[i][j];
} else {
return Math.max(Math.max(findMaxPath(arr, i+1, j-1, n), findMaxPath(arr, i+1, j, n))
, findMaxPath(arr, i+1, j+1, n)) + arr[i][j];
}
}
上面的递归存在子问题被重复求解(这不刚好是动态规划能解决的问题的要素之一吗?)这个问题很容易换个角度思考一下,在值最大的数字路径上,也就是最优路径上的每一个数字开始的向下的路径也是该数字到最后一层的最优路径,符号动态规划的第二个基本要素(最优子结构原理),所以用动态规划解决该问题非常合适。
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("输入三角形数组的层数:");
int n = in.nextInt();
System.out.println("输入三角数组:");
int[][] arr = new int[n+1][n+1];
for(int i=1; i<=n; ++i){
for (int j = 1; j <= i; j++) {
arr[i][j] = in.nextInt();
}
}
// dp数组用来记录子问题的解
int[][] dp = new int[n+1][n+1];
int max = findMaxPath(arr, 1, 1, n, dp);
System.out.println("max value:" + max);
}
private static int findMaxPath(int[][] arr, int i, int j, int n, int[][] dp) {
if (i == n || j == 0){
dp[i][j] = arr[i][j];
return dp[i][j];
} else {
if(dp[i][j] > 0) { // 如果子问题的解已经计算过,直接返回
return dp[i][j];
}
dp[i][j] = Math.max(Math.max(findMaxPath(arr, i+1, j-1, n, dp)
, findMaxPath(arr, i+1, j, n, dp))
, findMaxPath(arr, i+1, j+1, n, dp)) + arr[i][j];
return dp[i][j];
}
}
输出结果如下:
输入三角形数组的层数:
5
输入三角数组:
13
11 8
12 7 26
6 14 15 8
12 7 13 24 11
max value:86