1. 动态规划
动态规划(dynamic programming)是一种动态地求解问题的解的算法,其核心在于动态二字,就是问题的最优解依赖于子问题的最优解,因此要求原问题就要知道子问题的解,还要知道原问题和子问题之间的关系,也就是递推方程。尽管动态规划听起来很容易用递归实现,但是实际上往往我们使用的是迭代的方法,因为递归过程有很多重复的子问题,造成时间复杂度过高。
递推方程是核心,动态规划问题最重要的就是找出正确的递推方程。
子问题划分是首要,如果你的子问题都没定义明白,那就不要想得到递推方程了。
哈哈,上面两句话是模仿政治书里的语言。写完一读,嘿,还挺是那么回事,自夸一波。
2. 矩阵链相乘问题
给定矩阵序列:
M
0
M
1
M
2
M
3
.
.
.
M
n
−
1
M_0M_1M_2M_3...M_{n-1}
M0M1M2M3...Mn−1
和矩阵的大小:
m
0
m
1
m
2
m
3
.
.
.
m
n
m_0m_1m_2m_3...m_n
m0m1m2m3...mn
其中矩阵Mk的大小为mk*mk+1
求这些矩阵做乘法运算时需要的最小运算次数时的运算顺序。
矩阵链相乘的子问题是:counter[i][j]
,表示矩阵序列MiMi+1Mi+2…Mj相乘的最少次数。
初值:i等于j时,只有一个矩阵,counter[i][j]=0
;
递推方程:
c
o
u
n
t
e
r
[
i
]
[
j
]
=
m
i
n
(
c
o
u
n
t
e
r
[
i
]
[
k
]
+
c
o
u
n
t
e
r
[
k
+
1
]
[
j
]
+
m
i
∗
m
k
+
1
∗
m
j
+
1
∣
k
∈
[
i
,
j
−
1
]
)
counter[i][j] = min(counter[i][k] + counter[k+1][j] + m_i*m_{k+1}*m_{j+1} | k\in [i, j-1])
counter[i][j]=min(counter[i][k]+counter[k+1][j]+mi∗mk+1∗mj+1∣k∈[i,j−1])
这些都有了就可以实现了:
// 写一下动态规划的矩阵乘法问题
public String dp(int[] matrixSize, String matrix){
long[][] count = new long[matrix.length()][matrix.length()];
int[][] partition = new int[matrix.length()][matrix.length()];
//初始化
for (int i = 0; i < count.length; i++) {
for (int j = 0; j < count.length; j++) {
if (i == j) count[i][j] = 0;
if(i == j-1) count[i][j] = matrixSize[i]*matrixSize[j]*matrixSize[j+1];
}
}
for (int k = 2; k < count.length; k++) {//遍历矩阵序列长度(为了便于计算,-1)
for (int i = 0; i < count.length - k; i++) {//遍历矩阵序列开头
long min_ = Long.MAX_VALUE;
int par = -1;
for (int j = i; j < i+k; j++) {//遍历划分位置
long c = count[i][j] + count[j+1][i+k] + matrixSize[i]*matrixSize[j+1]*matrixSize[i+k+1];
if(c < min_){
min_ = c;
par = j;
}
}
count[i][i+k] = min_;
partition[i][i+k] = par;
}
}
return fun(matrix, partition, 0, matrix.length()-1, 0);
}
public String fun(String matrix, int[][] partition, int left, int right, int offset){
if(right - left < 2) return matrix;
int par = partition[left][right];
String lstr = matrix.substring(left-offset, par+1-offset);
String rsrt = matrix.substring(par+1-offset);
String l = fun(lstr, partition, left, par, offset);
String r = fun(rsrt, partition, par+1, right, par+1);
if (l.length() > 1) l = "(" + l + ")";
if (r.length() > 1) r = "(" + r + ")";
return l + r;
}
3. 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/edit-distance
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
子问题: 假设word1长为m,word2长为n。定义oper[i][j]
为子串word1[0:i]
转换成word2[0:j]
所需要的最小操作数。
初值: 当i=0时,j从0变到n,表示word1的子串只有第一个字符,变成word2的子串。如果word2子串中有字符与word1第一个字符相同的,操作数就等于子串长度减1,否则就等于子串长度;当j=0时同理。
递推方程:
if(word1.charAt(i) == word2.charAt(j)){
oper[i][j] = Math.min(oper[i-1][j-1], Math.min(oper[i-1][j]+1, oper[i][j-1]+1));
}else{
oper[i][j] = Math.min(oper[i-1][j-1],Math.min(oper[i-1][j], oper[i][j-1])) + 1;
}
最终的实现:
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
if(m == 0) return n;
if(n == 0) return m;
int[][] oper = new int[m][n];
//初始化
int flag = 0;//一旦出现相同的字符,flag就变成1
for(int i = 0; i < m; i++){
if(word1.charAt(i) == word2.charAt(0)){
flag = 1;
oper[i][0] = i+1 - flag;
} else oper[i][0] = i+1 - flag;
}
//注意这里的初始化稍稍复杂一点点
//如果出现过相同的字符,那么操作数就会比子串长度少一
flag = 1 - oper[0][0];
for(int j = 1; j < n; j++){
if(word2.charAt(j) == word1.charAt(0)){
flag = 1;
oper[0][j] = j+1 - flag;
}else{
oper[0][j] = j+1 - flag;
}
}
//
for(int i = 1; i < m; i++){
for(int j = 1; j < n; j++){
if(word1.charAt(i) == word2.charAt(j)){
oper[i][j] = Math.min(oper[i-1][j-1], Math.min(oper[i-1][j]+1, oper[i][j-1]+1));
}else{
oper[i][j] = Math.min(oper[i-1][j-1],Math.min(oper[i-1][j], oper[i][j-1])) + 1;
}
}
}
return oper[m-1][n-1];
}
}
这是一道力扣的难题,可以看到其实还是不难的。
4. 递归实现?
因为这里的子问题划分和原问题具有相同的性质,因此我们想到了递归,但是递归可以吗?
上面是力扣的一道中等题,其实算是简单题。
如果用递归代码很简单:
class Solution {
public int uniquePaths(int m, int n) {
if(m < 1 || n < 1) return 0;
if(m == 1 && n == 1) return 1;
return uniquePaths(m, n-1) + uniquePaths(m-1, n);
}
}
作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/unique-paths/solution/kong-jian-huan-shi-jian-sha-ye-bu-shuo-liao-by-fan/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
但是很不幸,超时了,因为什么呢?
从上图可以看到,递归的时候有大量的重复子问题需要计算,耗费了时间。
因此在使用动态规划时,一般都是用数组将前面子问题的解保存起来,然后再进行更大子问题的计算。
通过的代码:
class Solution {
public int uniquePaths(int m, int n) {
int[][] count = new int[m+1][n+1];
for(int i = 1; i < n+1; i++) count[1][i] = 1;
for(int i = 1; i< m+1; i++) count[i][1] = 1;
for(int i = 2; i <= m; i++){
for(int j = 2; j <= n; j++){
count[i][j] = count[i-1][j] + count[i][j-1];
}
}
return count[m][n];
}
}
作者:fang-wen-chu
链接:https://leetcode-cn.com/problems/unique-paths/solution/kong-jian-huan-shi-jian-sha-ye-bu-shuo-liao-by-fan/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
动态规划先介绍到这里。