文章目录
什么是动态规划算法
- 总体思想
(1)动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题
(2)经分解得到的子问题往往不是互相独立的,有些子问题被重复计算多次
(3)如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法(备忘录法)
(4)图解:
- 使用动态规划来求解的问题需要具备的基本要素包括:
(1)重复子问题
- 递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次,这种性质称为子问题的重叠性质
- 动态规划算法,对每一个子问题只解一次,而后将其解保存在一个表格中,当再次需要解此子问题时,只是简单地用常数时间查看一下结果
- 通常不同的子问题个数随问题的大小呈多项式增长,用动态规划算法只需要多项式时间,从而获得较高的解题效率
(2)最优子结构
- 一个问题的最优解包含着其子问题的最优解,这种性质称为最优子结构性质
- 分析问题的最优子结构性质:首先假设由问题的最优解导出的子问题的解不是最优的,然后再设法说明在这个假设下可构造出比原问题最优解更好的解,从而导致矛盾
- 利用问题的最优子结构性质,以自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解
- 最优子结构是一个问题能用动态规划算法求解的前提
-
动态规划算法与分治算法的异同点:
(1)动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题
(2)分治算法经分解得到的子问题往往是独立的
(3)动态规划算法经分解得到的子问题往往不是独立的,有些子问题被重复计算多次 -
动态规划求解的基本步骤:
(1)找出最优解的性质,并刻划其结构特征
(2)递归地定义最优值
(3)以自底向上的方式计算出最优值
(4)根据计算最优值时得到的信息,构造最优解
数字三角形
有一只小兔子站在一片三角形的胡萝卜地的入口,如右图所示,图中的数字表示每一个坑中胡萝卜的数量,小兔子每次只能跳到左下角或者右下角的坑中,请问小兔子怎么跳才能得到最多数量的胡萝卜?
经典递归解法
如果我们采用暴力法来解决这个问题,很明显从第一层开始,每次都有两个选择,左下角和右下角,也就是n层的话,有 2 n − 1 2^{n-1} 2n−1 条;路径,T(n) = 2 n 2^n 2n。
大致代码实现思路:
- 把上面的三角形放到一个二维数组中,没有放数的地方都为0
- 我们从二位数组的
a [i][j]
开始,最多的胡萝卜数是不是为a[i][j]
加上以右下角为起点和左下角为起点路径中的胡萝卜最大数的路径,循坏递归,因为每个点都有两条路径选择,每次选择路径中胡萝卜最多的路径。 - 递归的结束条件是
i = n+1
时结束,因为我们有n层,到n+1层的时候自然就结束。
实现代码:
public class Demo {
public static void main(String[] args) {
int[][] a = {{1},{3,2},{4,10,1},{4,3,2,20}};
System.out.println(solve(a,0,0));
}
public static int solve(int[][] a,int i,int j){
//第 n+1 层结束 ===》从0层开始计算 ,那么 i = n 时结束
if (i == a.length){
return 0;
}
return a[i][j]+ Math.max(solve(a,i+1,j),solve(a,i+1,j+1));
}
}
备忘录法
-
详情见文章:【算法】备忘录法(记忆化搜索)
-
上面递归时候,我们
solve(a,2,1)
被重复计算过两次,随着层数的增加,我们重复计算的子问题也会增加,为了避免重复计算子问题,我们就需要用到备忘录法,就是利用一个二维数组记录每次子问题计算的值,每次需要计算子问题时,先判断数组中是否计算过保存了,有的话直接用数组中结果,没有就计算并把结果保存到数组中。
-
代码实现:
public class Demo {
public static void main(String[] args) {
int[][] a = {{1},{3,2},{4,10,1},{4,3,2,20}};
System.out.println(solve(a,0,0,new int[a.length][a.length]));
}
public static int solve(int[][] a,int i,int j,int[][] p){
//第 n+1 层结束 ===》从0层开始计算 ,那么 i = n 时结束
if (i == a.length){
return 0;
}
if (p[i][j] == 0) {
p[i][j] = a[i][j] + Math.max(solve(a, i + 1, j, p), solve(a, i + 1, j + 1, p));
}
return p[i][j];
}
}
动态规划法
思路一
-
p[i][j]
表示(i, j)的达到最后一层的最大路径和,那么p[i][j]的最优解包含了子问题p[i+1][j]
或p[i+1][j+1]
的最优解 -
状态转移方程(递归方程):
-
图解:
-
我们最终结果是
p[0][0]
-
动态规划法又叫填表法,填完上面那张表我们的结果就出来了
-
实现代码:
public class Demo {
public static void main(String[] args) {
int[][] a = {{1},{3,2},{4,10,1},{4,3,2,20}};
System.out.println(solve(a));
}
public static int solve(int[][] a){
int[][] p = a.clone();
//最后一层的数不需要修改 ,从倒数第二次开始
for (int i = a.length -2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
p[i][j] = a[i][j] + Math.max(p[i+1][j],p[i+1][j+1]);
}
}
return p[0][0];
}
}
- 通过代码可知时间复杂度O(n) = N 2 N^2 N2
思路二
-
p[i][j]
表示从(1,1)到达(i, j) 的最大路径和,那么p[i][j]
的最优解包含了子问题p[i-1][j-1]
或p[i-1][j]
的最优解 -
状态转移方程(递归方程):
-
思路一就是从表的最后一层开始填,思路二是表的第一层开始填:
最长公共子序列
LCS,Longest Common Subsequence
- 什么是子序列?子序列和子串的区别?
(1) 串中任意个连续的字符组成的子序列称为该串的子串。
(2) 某个序列的子序列是从最初序列通过去除某些元素但不破坏余下元素的相对位置(在前或在后)而形成的新序列。
-
公共子序列
给定2个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列 -
最长公共子序列
(1)给定2个序列X={x1,…,xm}和Y={y1,…,yn},要求找出X和Y的一个最长公共子序列
(2)例如:已知X = {A, G, C, G, T, A, G},Y = {G, T, C, A, G, A},求序列X和序列Y的最长公共子序列,有以下四种:
穷举搜索
- 对X的每一个子序列,检查它是否也是Y的子序列,即是否为X和Y的公共子序列
- 在检查过程中遴选出最长的公共子序列
- X的所有子序列都检查过后即可求出X和Y的最长公共子序列
- X的一个子序列相应于下标序列{1,2,…,m}的一个子序列,故X共有 2 m 2^m 2m个不同子序列
- 穷举搜索法需要指数时间 T ( n ) = n ∗ 2 m T(n)=n*2^m T(n)=n∗2m
备忘录法
- 我们在递归算法的基础上,添加一个数组,记录已经计算过的值。
- 递归表达式:
c[i,j]表示X序列[1…i]和Y序列[1…j]的最长公共子序列的长度,我们是从最后一个元素开始比较
- i=0或者j=0,表示X和Y中有空序列,空序列和任何序列的公共子序列长度都为0
- i>0,j>0下有两种情况,X和Y的最后一个元素
X
i
X_i
Xi和
Y
j
Y_j
Yj
- 相等 X i = Y j X_i=Y_j Xi=Yj,最后一个元素匹配成功,我们最长公共子序列肯定有最后一个元素,长度大于等于1,那么我们c[i,j]=c[i-1,j-1]+1,把最后一个的长度加上,继续比较前面的。
- 不相等 X i ≠ Y j X_i\ne Y_j Xi=Yj,去除 X i X_i Xi或者 Y j Y_j Yj,c[i,j]=Max(c[i-1,j],c[i,j-1])
- 表达式:
c [ i , j ] = { 0 , i = 0 , j = 0 c [ i − 1 , j − 1 ] + 1 , x i = y j M a x ( c [ i − 1 , j ] , c [ i , j − 1 ] ) , x i ≠ y j (4.2) c[i,j]=\begin{cases} 0 , i=0,j=0\\ c[i-1,j-1]+1 ,x_i= y_j \\ Max(c[i-1,j],c[i,j-1]),x_i\ne y_j \\ \end{cases} \tag{4.2} c[i,j]=⎩⎪⎨⎪⎧0,i=0,j=0c[i−1,j−1]+1,xi=yjMax(c[i−1,j],c[i,j−1]),xi=yj(4.2)
- 具体实例:
设置X序列长度为7,Y序列长度为6,每次都是 X i ≠ Y j X_i\ne Y_j Xi=Yj,递归树如下
- 通过上面递归树,有重复子问题,可以用到备忘录法,以及时间复杂度: T(n)=O(m*n)
- 代码实现:
public class Blog {
public static void main(String[] args) {
//数组从1开始,舍弃0
char[] x= {' ','A','C','D','E','D','C'};
char[] y= {' ','A','B','C','D','C'};
System.out.println(LCS(x, y, x.length-1, y.length-1, new int[x.length][y.length]));
}
/**
*
* @param x 序列X
* @param y 序列Y
* @param i X下标
* @param j Y下标
* @param c 备忘录表
* @return 最长子序列的长度
*/
public static int LCS(char[] x,char[] y,int i,int j,int[][] c){
if (i == 0||j == 0){
return 0;
}
if (c[i][j] == 0){
if (x[i] == y[j]){
c[i][j] = LCS(x,y,i-1,j-1,c)+1;
}else {
c[i][j] = Math.max(LCS(x,y,i-1,j,c),LCS(x,y,i,j-1,c));
}
}
return c[i][j];
}
}
动态规划
动态规划求解其实就是填一张表,很巧妙的用到了上面的公式:
c
[
i
,
j
]
=
{
0
,
i
=
0
,
j
=
0
c
[
i
−
1
,
j
−
1
]
+
1
,
x
i
=
y
j
M
a
x
(
c
[
i
−
1
,
j
]
,
c
[
i
,
j
−
1
]
)
,
x
i
≠
y
j
(4.2)
c[i,j]=\begin{cases} 0 , i=0,j=0\\ c[i-1,j-1]+1 ,x_i= y_j \\ Max(c[i-1,j],c[i,j-1]),x_i\ne y_j \\ \end{cases} \tag{4.2}
c[i,j]=⎩⎪⎨⎪⎧0,i=0,j=0c[i−1,j−1]+1,xi=yjMax(c[i−1,j],c[i,j−1]),xi=yj(4.2)
假设:X = {A, B, C, B, D, A, B},Y = {B, D, C, A, B, A},下面就是我们需要填的表格:
- 公式的第一条
0
,
i
=
0
,
j
=
0
0 , i=0,j=0
0,i=0,j=0,也就是有0所在的行和列都为0
- 公式第二条和第三条: x i = y j 时 , c [ i ] [ j ] = c [ i − 1 , j − 1 ] + 1 x_i= y_j时,c[i][j]=c[i-1,j-1]+1 xi=yj时,c[i][j]=c[i−1,j−1]+1。 x i ≠ y j 时 , c [ i ] [ j ] = M a x ( c [ i − 1 , j ] , c [ i , j − 1 ] ) x_i\ne y_j时,c[i][j]=Max(c[i-1,j],c[i,j-1]) xi=yj时,c[i][j]=Max(c[i−1,j],c[i,j−1])
接下来的表格该怎么填?
(1)找到对应的
X
i
和
Y
j
X_i和Y_j
Xi和Yj,看是否相等,代入公式即可。
(2)相等情况:
x
i
=
y
j
时
,
c
[
i
]
[
j
]
=
c
[
i
−
1
,
j
−
1
]
+
1
x_i= y_j时,c[i][j]=c[i-1,j-1]+1
xi=yj时,c[i][j]=c[i−1,j−1]+1,该方格填入的值就是左上方方格的值+1
(3)不相等情况:
x
i
≠
y
j
时
,
c
[
i
]
[
j
]
=
M
a
x
(
c
[
i
−
1
,
j
]
,
c
[
i
,
j
−
1
]
)
x_i\ne y_j时,c[i][j]=Max(c[i-1,j],c[i,j-1])
xi=yj时,c[i][j]=Max(c[i−1,j],c[i,j−1]),该方格填入的值为上门方格和左边方格中的最大值。
-
最长子序列的长度是表格的最右下角的值。
-
代码实现:
public class Blog {
//测试:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String line1 = scanner.nextLine();
String line2 = scanner.nextLine();
char[] chars1 = line1.toCharArray();
char[] chars2 = line2.toCharArray();
System.out.println(lcsLength(chars1, chars2));
}
}
public static int lcsLength(char[] x,char[] y){
int m = x.length;
int n = y.length;
//要填的表
int[][] p = new int[m+1][n+1];
//表的第1行和第一列全部为0
for (int i = 0; i <= m; i++) {
p[i][0]=0;
}
for (int i = 0; i <= n; i++) {
p[0][i]=0;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (x[i-1]==y[j-1]){
p[i][j] = p[i-1][j-1]+1;
}else {
p[i][j] = Math.max(p[i-1][j],p[i][j-1]);
}
}
}
return p[m][n];
}
}
- 构造最长公共子序列:
我们可以定义一个数组来判断表格中的数据来自那个方向,是从左上角,还是左边,还是右边。
定义了方向怎么求解,看下面举例的表格,斜线下的数字表示方向,1表示↘,2表示 ↓,3表示→。我们从表格的最后一个元素开始遍历,最后一个元素4/2
,2表示数据来自上面,找到上面的元素4/1
,1表示来自左上角,且是因为
X
i
=
Y
j
X_i=Y_j
Xi=Yj,也就是最长的公共子序列包含这个元素,继续遍历直到 i 和 j 等于0,然后输出方向为1的元素就是我们的最长公共子序列。
代码实现:
package test.gaolang.blog3;
import java.util.List;
import java.util.Scanner;
/**
* @author DELLHL
*/
public class Blog {
//测试:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String line1 = scanner.nextLine();
String line2 = scanner.nextLine();
char[] chars1 = line1.toCharArray();
char[] chars2 = line2.toCharArray();
int[][] b = lcsLength(chars1, chars2, new int[chars1.length + 1][chars2.length + 1]);
printlcs(chars1.length ,chars2.length ,chars1,b);
System.out.println();
}
}
public static int[][] lcsLength(char[] x,char[] y,int[][] b){
int m = x.length;
int n = y.length;
int[][] p = new int[m+1][n+1];
for (int i = 0; i <= n; i++) {
p[0][i] = 0;
}
for (int i = 0; i <= m; i++) {
p[i][0] = 0;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (x[i-1]==y[j-1]){
b[i][j] = 1;
p[i][j] = p[i-1][j-1]+1;
}else if (p[i-1][j]>=p[i][j-1]){
b[i][j] = 2;
p[i][j] = p[i-1][j];
}else {
b[i][j] = 3;
p[i][j] = p[i][j-1];
}
}
}
return b;
}
public static void printlcs(int i,int j, char[]a,int [][]drection) {
if(i==0||j==0) {
return;
}
if(drection[i][j]==1) {
//下面两句代码的位置不能调换,调换就相当于逆序输出,全部递归完后,从第一个开始输出
printlcs(i-1,j-1,a,drection);
System.out.print(a[i-1]);
}
else if(drection[i][j]==2) {
printlcs(i-1,j,a,drection);
}else {
printlcs(i,j-1,a,drection);
}
}
}
改进
- 如果只需要计算最长公共子序列的长度,则算法的空间需求可大大减少。在计算
p[i][j]
时,只用到数组p的第i
行和第i-1
行。用2行的数组空间就可以计算出最长公共子序列的长度,可将空间需求减至O(min(m,n)) - 这里利用滚动数组的原理,为什么上面的改进可行,首先,方格取值在上面,左边,左上角,两行足够了,循环覆盖,最大长度是最后一个元素。
- 代码实现:
public class Blog {
//测试:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String line1 = scanner.nextLine();
String line2 = scanner.nextLine();
char[] chars1 = line1.toCharArray();
char[] chars2 = line2.toCharArray();
System.out.println(lcsLength(chars1, chars2));
}
}
public static int lcsLength(char[] x,char[] y){
int m = x.length;
int n = y.length;
//要填的表
int[][] p = new int[2][Math.min(m,n)+1];
//表的第1行和第一列全部为0
for (int i = 0; i <= 1; i++) {
p[i][0]=0;
}
for (int i = 0; i < Math.min(m,n); i++) {
p[0][i]=0;
}
int flag = 0;
for (int i = 1; i <= Math.max(m,n); i++) {
flag = 1 - flag;
for (int j = 1; j <= Math.min(m,n); j++) {
if (x[i-1]==y[j-1]){
p[flag][j] = p[1-flag][j-1]+1;
}else {
p[flag][j] = Math.max(p[1-flag][j],p[flag][j-1]);
}
}
}
return p[flag][Math.min(m,n)];
}
}
最大子段和
- 问题描述:
- 给定n个整数(可能是负数)组成的序列a[1], a[2], a[3], …, a[n],求该序列的子段和,例如a[i]+a[i+1]+…+a[j]的最大值。
- 当所给的整数均为负数时定义子段和为0,依此定义,所求的最优值为:max{0, a[i]+a[i+1]+…+a[j]}, 1<=i<=j<=n
- 例如,当(a[1],a[2],a[3],a[4],a[5],a[6])=(-2,11,-4,13,-5,-2)时,最大子段和为20,即 20 = 11 + (-4) + 13。
- 动态规划求解:
- 若记b[j]=max(a[i]+a[i+1]+…+a[j]),b[j]表示以a[j]作为最后一个元素的最大子段和,其中1<=i<=j,并且1<=j<=n,则所求的最大子段和为max{b[j]},1<=j<=n
- 由b[j]的定义可易知,当b[j-1]>0时b[j]=b[j-1]+a[j],否则b[j]=a[j]。故b[j]的动态规划递归式为:
b [ j ] = m a x ( b [ j − 1 ] + a [ j ] , a [ j ] ) , 1 < = j < = n b[j]=max(b[j-1]+a[j], a[j]),1<=j<=n b[j]=max(b[j−1]+a[j],a[j]),1<=j<=n - 举例:
代码实现:
public class Test {
public static void main(String[] args) {
int[] a = {-2,11,-4,13,-5,-2};
System.out.println(solve(a));
}
public static int solve(int[] a){
int[] p = new int[a.length];
//以第一个结尾
p[0] = a[0];
int max = p[0];
for (int i = 1; i < a.length; i++) {
//从第二个开始
if (p[i-1] > 0){
p[i] = a[i] +p[i-1];
}else {
p[i] = a[i];
}
if (p[i] > max){
max = p[i];
}
}
return max;
}
}
最长公共子串
两个字符串,输出最长公共子串的长度。
暴力法
算法思路:
两个字符串,以第一个字符串为基准,第二个字符跟它进行比较,例如"ABCDEF"
和"BDEF"
找最长公共子序列。
找第一个字符串中第一个字符A
是否在"BDEF"
中,如果存在,继续比较两个字符串中A
字符后面的字符,相等接着比较下一个,记录好长度,循环第一个字符串中的字符与第二字符串比较,最后就可以求出所有的公共子串的长度,选择最大的输出。
public class Main {
//公共子串
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//多组输入测试:
while (scanner.hasNext()){
String line = scanner.nextLine();
String line2 = scanner.nextLine();
char[] chars = line.toCharArray();
char[] chars2 = line2.toCharArray();
//记录最大长度
int max = 0;
//两个字符串,只要有一个为空,我们的长度为0,直接输出
if (chars.length==0||chars2.length==0){
System.out.println(0);
}else {
//以第一个字符
for (int i = 0; i < chars.length; i++) {
for (int j = 0; j < chars2.length; j++) {
int m = i;
int k = j;
int len = 0;
while(m < chars.length&&k <chars2.length&&chars[m]==chars2[k]){
len++;
m++;
k++;
}
if (len > max){
max = len;
}
}
}
System.out.println(max);
}
}
}
}
动态规划
类似前面最长公共子序列的求法,填一张表,第0行和第0列为为0,遇到
X
i
=
Y
j
X_i=Y_j
Xi=Yj的方格的值等于左上角方格加1,不相等为0,记录一下表格中的最大值为我们的最长公共子串的长度。
代码:
public class Test2 {
public static void main(String[] args) {
String x = "acbcbcef";
String y = "abcbced";
System.out.println(LCS(x, y));
}
public static int LCS(String x,String y){
char[] a = x.toCharArray();
char[] b = y.toCharArray();
int m = a.length;
int n = b.length;
int max = 0;
int[][] p = new int[m+1][n+1];
for (int i = 0; i <= m; i++) {
p[i][0] = 0;
}
for (int i = 0; i <= n; i++) {
p[0][n] = 0;
}
for (int i = 1; i < m+1; i++) {
for (int j = 1; j < n+1; j++) {
if (a[i-1]==b[j-1]){
p[i][j] = p[i-1][j-1]+1;
if (p[i][j] > max){
max = p[i][j];
}
}else {
p[i][j] = 0;
}
}
}
return max;
}
}
如果要把这个公共子串输出,我可以在上面算法的基础上记录一下最大值的横坐标或者纵坐标。
public class Test2 {
public static void main(String[] args) {
String x = "acbcbcef";
String y = "abcbced";
System.out.println(LCS(x, y));
}
public static int LCS(String x,String y){
char[] a = x.toCharArray();
char[] b = y.toCharArray();
int m = a.length;
int n = b.length;
int max = 0;
int index = 0;
int[][] p = new int[m+1][n+1];
for (int i = 0; i <= m; i++) {
p[i][0] = 0;
}
for (int i = 0; i <= n; i++) {
p[0][n] = 0;
}
for (int i = 1; i < m+1; i++) {
for (int j = 1; j < n+1; j++) {
if (a[i-1]==b[j-1]){
p[i][j] = p[i-1][j-1]+1;
if (p[i][j] > max){
max = p[i][j];
index = i;
}
}else {
p[i][j] = 0;
}
}
}
for (int i = max; i > 0; i--) {
System.out.print(a[index-i]);
}
System.out.println();
return max;
}
}
矩阵连乘
- 问题描述:
-
给定n个矩阵 A 1 , A 2 , . . . , A n {{A_1,A_2,...,A_n}} A1,A2,...,An ,其中 A i A_i Ai与 A i + 1 A_{i+1} Ai+1 是可乘的, i = 1 , 2 , . . . , n − 1 i=1,2,...,n-1 i=1,2,...,n−1
-
考察n个矩阵的连乘积 A 1 , A 2 , . . . , A n A_1,A_2,...,A_n A1,A2,...,An
-
矩阵乘法满足结合律——》计算矩阵的连乘可以有许多不同的计算次序——》计算次序可以用加括号的方式来确定
-
若一个矩阵连乘积的计算次序完全确定(该连乘积已完全加括号)——》可依此次序反复调用2个矩阵相乘的标准算法计算出矩阵连乘积
-
如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少
- 为什么会有这个问题?
我们来看一下面一个问题:
设有四个矩阵A, B, C, D ,它们的维数分别是: A = 50 × 10 , B = 10 × 40 , C = 40 × 30 , D = 30 × 5 A=50×10,B=10×40,C=40×30,D=30×5 A=50×10,B=10×40,C=40×30,D=30×5
这四个矩阵连乘,有多少种计算次序:五种
每一种次序进行乘法的次数:
- ( A ( ( B C ) D ) ) (A((BC)D)) (A((BC)D)):最先计算BC,然后BC的结果与D相乘,最后A:10×30×40+10×5×30+50×5×10=16000
- ( A ( B ( C D ) ) ) (A(B(CD))) (A(B(CD))):10500次
- ( ( A B ) ( C D ) ) ((AB)(CD)) ((AB)(CD)):36000次
- ( ( ( A B ) C ) D ) (((AB)C)D) (((AB)C)D):87500次
- ( ( A ( B C ) D ) ((A(BC)D) ((A(BC)D):34500次
我们可以看到四个矩阵连乘,顺序不一样,计算乘法的次数也不一样,我们需要找出计算乘法最少的顺序。
动态规划
- 将矩阵连乘积
A
1
,
A
2
,
.
.
.
,
A
n
{{A_1,A_2,...,A_n}}
A1,A2,...,An 简记为
A[i:j]
, i ≤ j i≤j i≤j - 考察计算
A[i:j]
的最优计算次序:设这个计算次序在矩阵 A k A_k Ak和 A k + 1 A_{k+1} Ak+1之间将矩阵链断开, i ≤ k < j i≤k<j i≤k<j,则其相应完全加括号方式为 ( A i A i + 1 . . . A k ) ( A k + 1 A k + 2 . . . A j ) (A_iA_{i+1}...A_k)(A_{k+1}A_{k+2}...A_j) (AiAi+1...Ak)(Ak+1Ak+2...Aj) - 计算量:
A[i:k]
的计算量加上A[k+1:j]
的计算量,再加上A[i:k]
和A[k+1:j]
相乘的计算量(三部分组成) - 分析最优解的结构
- 特征:计算
A[i:j]
的最优次序所包含的计算矩阵子链A[i:k]
和A[k+1:j]
的次序也是最优的 - 矩阵连乘计算次序问题的最优解包含着其子问题的最优解(最优子结构)
- 问题的最优子结构性质是该问题可用动态规划算法求解的显著特征
- 建立递归关系
- 计算 A [ i : j ] , 1 ≤ i ≤ j ≤ n A[i:j],1≤i≤j≤n A[i:j],1≤i≤j≤n,所需要的最少数乘次数 m [ i , j ] m[i,j] m[i,j],则原问题的最优值为 m [ 1 , n ] m[1,n] m[1,n]
- 当 i = j 时 , A [ i : j ] = A i , 也 就 是 只 有 一 个 矩 阵 , 因 此 , m [ i , i ] = 0 , i = 1 , 2 , … , n i=j时,A[i:j]=Ai,也就是只有一个矩阵,因此,m[i,i]=0,i=1,2,…,n i=j时,A[i:j]=Ai,也就是只有一个矩阵,因此,m[i,i]=0,i=1,2,…,n
- 当 i < j 时 , m [ i , j ] = m [ i , k ] + m [ k + 1 , j ] + p i − 1 p k p j i<j时,m[i,j] = m[i,k] + m[k+1,j]+p_{i-1}p_kp_j i<j时,m[i,j]=m[i,k]+m[k+1,j]+pi−1pkpj
-
p
i
−
1
p
k
p
j
p_{i-1}p_kp_j
pi−1pkpj是什么意思?原来的矩阵从k分成两个部分,那么我们原问题乘法次数=前半部分的乘积次数+后半部分的次数+前后两个部分的计算完后两个矩阵的次数(三部分组成)。
p
i
−
1
p
k
p
j
p_{i-1}p_kp_j
pi−1pkpj是
A[i:k]
和A[k+1:j]
相乘的计算量 -
A
i
A_i
Ai的维数为
p
i
−
1
p
i
p_{i-1}p_i
pi−1pi
可以递归地定义m[i,j]为:
- 根据上面公式,动态规划相当于填一张表: i > j i>j i>j的部分不需要填。
7. 代码实现
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextInt()){
int n = scanner.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = scanner.nextInt();
}
System.out.println(matrixChain(a, new int[a.length-1][a.length-1]));
}
}
/**
* 矩阵连乘
* @param p 连乘矩阵
* @param m 最小解
*/
public static int matrixChain(int[] p,int[][] m){
//矩阵个数
int n = p.length-1;
//i=j的情况 m[i][i]:自有一个矩阵,没有乘法
for (int i = 0; i < n; i++) {
m[i][i] = 0;
}
//i < j的情况
for (int i = 1; i < n; i++) {
int k = i;
for (int j = 0; j < n-i; j++) {
//从j分开 m[j][j](=0)和m[j+1][k] ,合并的乘法次数:p[j]xp[j+1]与p[j+1]xp[k+1]
m[j][k] = m[j+1][k]+ p[j]*p[k+1]*p[j+1];
//m[j][k] 从j+1开始分开,到k-1结束,选出乘法次数最少的
for (int l = j+1; l < k; l++) {
int t = m[j][l]+m[l+1][k] +p[j]*p[k+1]*p[l+1];
if (t < m[j][k]){
m[j][k] = t;
}
}
k++;
}
}
//m[0][n-1]:第一个矩阵到第n个矩阵连乘的最少次数
return m[0][n-1];
}
}
- 构造最优解代码:‘
import java.util.Scanner;
/**
* @author DELLHL
*/
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextInt()){
int n = scanner.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = scanner.nextInt();
}
int[][] ints = matrixChain(a, new int[a.length - 1][a.length - 1], new int[a.length - 1][a.length - 1]);
/* for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-1; j++) {
System.out.print(ints[i][j]);
}
System.out.println();
}*/
traceback(ints,0,n-2);
}
}
/**
* 矩阵连乘
* @param p 连乘矩阵
* @param m 最小解
* @param s 记录分割的值
*/
public static int[][] matrixChain(int[] p,int[][] m,int[][] s){
//矩阵个数
int n = p.length-1;
//i=j的情况 m[i][i]:自有一个矩阵,没有乘法
for (int i = 0; i < n; i++) {
m[i][i] = 0;
s[i][i] = 0;
}
//i < j的情况
for (int i = 1; i < n; i++) {
int k = i;
for (int j = 0; j < n-i; j++) {
//从j分开 m[j][j](=0)和m[j+1][k] ,合并的乘法次数:p[j]xp[j+1]与p[j+1]xp[k+1]
m[j][k] = m[j+1][k]+ p[j]*p[k+1]*p[j+1];
s[j][k] = j;
//m[j][k] 从j+1开始分开,到k-1结束
for (int l = j+1; l < k; l++) {
int t = m[j][l]+m[l+1][k] +p[j]*p[k+1]*p[l+1];
if (t < m[j][k]){
m[j][k] = t;
s[j][k] = l;
}
}
k++;
}
}
return s;
}
public static void traceback(int[][] s,int i,int j){
//只有一个矩阵 ==》结束
if (i == j){
return;
}
//[i,j]是从s[i][j]分开的,写成两个部分
traceback(s,i,s[i][j]);
traceback(s,s[i][j]+1,j);
//因为我们i是从0开始的,所以 i和j输出时都需要加1,分开也是从0开始,比如s[i][j]=i表示分成[i][i]和[i+1][j]
System.out.println("A["+(i+1)+":"+(s[i][j]+1)+"] * A["+(s[i][j]+2)+":"+(j+1)+"]");
}
}
最长递增子序列
- 问题描述:
LIS(Longest Increasing Subsequence,最长递增子序列):给出一个序列a1,a2,a3,a4,a5,a6,a7…an,求它的一个子序列(设为s1,s2,…sn),使得这个子序列满足这样的性质,s1<s2<s3<…<sn,并且这个子序列的长度最长。 - 例题:
- 【任务】输出这个最长子序列的长度。
- 【样例输入】 1 7 3 5 9 4 8
- 【样例输出】 长度为4【序列为1 3 5 9或1 3 4 8等】
-
动态规划解题思路:
设dp[i]
是在a[i]
为单调递增子序列最后一个元素时,所得最长单调递增子序列的长度为
d p [ i ] = { 1 , i = 1 M a x ( d p [ j ] ) + 1 , a [ j ] < a [ i ] 且 1 < = j < i (1.1) dp[i]=\begin{cases} 1 , i=1\\ Max(dp[j])+1,a[j]<a[i]且1<=j<i\\ \end{cases} \tag{1.1} dp[i]={1,i=1Max(dp[j])+1,a[j]<a[i]且1<=j<i(1.1)
解释一个公式:
1)单调调递增子序列最后一个元素最第1个的时候,长度只能为1
2)最后一个元素不为1时,长度等于前面的元素值中小于当前元素值且对应dp数组最大的元素a[j]
, d p [ i ] = d p [ j ] + 1 dp[i]=dp[j]+1 dp[i]=dp[j]+1 -
具体例题:输出这个
{ 1, 7, 3, 5 ,9, 4 ,8}
最长子序列的长度:
填表:
1)设置dp
数组的初始值为1
2)i=1
时dp[1]=1
2)后面的值=
比当前a[i]
小的中dp
最大的+1
,LIS为dp数组中最大的值。
-
代码实现:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//多组测试
while (scanner.hasNextInt()){
//序列长度
int n = scanner.nextInt();
//序列
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = scanner.nextInt();
}
System.out.println(LIS(a));
}
}
public static int LIS(int[] a){
int[] dp = new int[a.length];
int max = 1;
for (int i = 0; i < a.length; i++) {
//初始化为 1
dp[i] = 1;
//以 i结尾,最长递增序列长度
for (int j = 0; j < i; j++) {
if (a[j] < a[i]){
if (dp[j] >= dp[i]){
dp[i] = dp[j]+1;
}
}
}
if (dp[i] > max){
max = dp[i];
}
}
return max;
}
}
测试:
7 #输入
1 7 3 5 9 4 8 #输入
4 #输出
- 构造一条最长递增子序列代码:
定义一个索引数组,方便查找前一个数的下标。
import java.util.Scanner;
import java.util.Stack;
public class Main3 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextInt()){
int n = scanner.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = scanner.nextInt();
}
LIS(a);
}
}
public static int LIS(int[] a){
int[] dp = new int[a.length];
int[] pre = new int[a.length];
int max_index = 1;
int max = 1;
pre[0] = -1;
for (int i = 0; i < a.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (a[j] < a[i]){
if (dp[j] >= dp[i]){
dp[i] = dp[j]+1;
pre[i] = j;
}
}
}
if (dp[i] > max){
max = dp[i];
max_index = i;
}
}
Stack<Integer> stack = new Stack<>();
//递增子序列最长时 最后一个元素为max_index
int index = max_index;
while (index >= 0){
//入栈
stack.push(a[index]);
//前一个下标
index = pre[index];
}
int size = stack.size();
for (int i = 0; i < size; i++) {
//一一出栈
System.out.print(stack.pop()+" ");
}
System.out.println();
return max;
}
}
测试:
7 #输入
1 7 3 5 9 4 8 #输入
1 3 5 9 #输出
0-1背包问题
- 问题描述:
- 给定n种物品(每种物品只有一件)和一个背包:物品i的重量是wi,其价值为vi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
- 对于每种物品,只有两种选择:装(1)或者不装(0),不允许装物品的一部分
- 动态规划求解:
-
n=5, c=10, w={2, 2, 6, 5, 4}, v={6, 3, 5, 4, 6}
-
选中1,2,5三件物品,最高价值15,总重8
-
填表,物品的种类为表行,背包容量+1为行,从后面填到前面,
j
表示背包容量。 -
最后一行:
- 前面的行
- 最大值为右上角
- 代码实现:
public class Knapsack {
public static void main(String[] args) {
//浪费数组的第一个
int[] w = {0,2, 2, 6, 5, 4};
int[] v = {0,6, 3, 5, 4, 6};
System.out.println(fun(5, 10, v, w));
}
public static int fun(int n,int c,int[] v,int[] w){
int[][] m = new int[n+1][c+1];
//防止数组越界
int jMax = Math.min(w[n]-1,c);
//Step1:填最后一行
//j<w[n] ==>m[n][j]=0
for (int j = 0; j <= jMax; j++) {
m[n][j] = 0;
}
//j>=w[n] ==>m[n][j]=v[n]
for (int j = w[n]; j <= c; j++) {
m[n][j] = v[n];
}
//Step2: 从倒数第二行往前面填
for (int i = n-1; i > 1; i--) {
jMax = Math.min(w[i]-1,c);
for (int j = 0; j <= jMax; j++) {
m[i][j] = m[i+1][j];
}
for (int j = w[i]; j <= c; j++) {
m[i][j] = Math.max(m[i+1][j],m[i+1][j-w[i]]+v[i]);
}
}
//第一行最后一个元素
m[1][c] = m[2][c];
if (c >= w[1]){
m[1][c] = Math.max(m[1][c],m[2][c-w[1]]+v[1]);
}
return m[1][c];
}
}
- 代码实现,知道选择了哪些物品使得价值最大,选择了物品用1表示,没选择用0表示:
//根据填的表格推断
public static void traceback(int[][] m,int n,int c,int[] w){
int[] x = new int[n+1];
for (int i = 1; i < n; i++) {
//没有选择
if (m[i][c] == m[i+1][c] ){
x[i] = 0;
}else {
x[i] = 1;
c -= w[i];
}
}
x[n] = (m[n][c]>0) ? 1:0;
for (int i =1 ; i < x.length; i++) {
System.out.print(x[i]+" ");
}
System.out.println();
}