0%

动态规划

我觉得动态规划主要有两点:

1. 当前这个点要与不要
2. 由小推向大(也就是所谓子问题),以空间换时间。

01背包

例:有5个物品,w = [2,2,6,5,4],v = [6,3,5,4,6],背包的容量为10。

1. 一个物品一个物品的慢慢放
2. 拿到一个物品到底是放还是不放

当前物品不放,则价值为上一次放的情况;如果放了,则价值为把前i-1个物品放入(当前容量-这个物品容量)的价值加当前物品的价值。显然,谁大要谁。

| | 0|1 | 2 | 3 | 4|5 | 6 |7 | 8 | 9 |10
|–|–|–|–|–|–|–|–|–|–|–|–|–|–|–|–|
|0 |0 |0 |0 | 0 | 0 | 0 |0 | 0 | 0 | 0 |0 |
| 1 | 0 | 0 |6 |6 | 6 | 6 |6 | 6 | 6 | 6 |6 |
| 2 | 0 |0 | ? | | | | | | | | | | | | | | | | |
| 3 | 0 | 0 | | | | | | | | | | | | | | | | | |
| 4 | 0 | 0 | | | | | | | | | | | | | | | | | |
| 5 | 0 | 0 | | | | | | | | | | | | | | | | | |

初始化:i个物品装入容量0的背包中,价值肯定0;0个物品放入容量j的背包,价值也是0 。
dp[1][?]处放第一个物品,放不下,价值是放0物品时得0;放进去价值是6;
dp[2][?]处放第二个物品,放不下,价值和上一行的一样;?处放得下,放,价值为前(2-1)个物品放入(2-2)容量中,得,0+3 = 3;不放,就是上一次的6,;所以?处不放新物品。
以此类推。。。
最后放到第5个物品,便能得到最大价值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  /**
* @param c : 表示背包容量
* @param n : 表示商品个数
* @param w : 表示商品重量
* @param p : 表示商品价值
*
*/
public static int KnapSack(int c,int n,int w[],int p[]){
//c[i][m] 表示前i件物品放入容量为m的背包时的最大价值
int[][] dp = new int[n+1][c+1];

for(int i=0;i<n+1;i++){
dp[i][0] = 0;
}
for(int j=0;j<c+1;j++){
dp[0][j] = 0;
}
for(int i=1;i<n+1;i++){
for(int j=1;j<c+1;j++){
if(w[i-1] <= j){ //放得下,谁大要谁
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w[i-1]]+p[i-1]);

}else{ //放不下,就是上一次的情况
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][c];
}

最长公共子序列

例:给定两个字符串 “abcbdb”和 “acbbabdbb”,返回这两个字符串的最长公共子序列的长度。
1. 从字符串开头慢慢比
2. 当前字符相等了,公共长度则为上一次的加1;不等了,要么不要字符1要么不要字符2,显然,谁的结果长要谁。

| | |”” |a | c | b | b|a | b |d | b| b |
|–|–|–|–|–|–|–|–|–|–|–|–|–|–|
| | |0|1 | 2 | 3 | 4|5 | 6 |7 | 8 | 9 |
|”” |0 |0 |0 |0 | 0 | 0 | 0 |0 | 0 | 0 | 0 |0 |
|a| 1 | 0 | 1 |1 |1 | 1 | 1 |1 | 1 | 1 | 1
|b| 2 | 0 |1 | ? | | | | | | | | | | | | | | | | |
|c| 3 | 0 | 1 | | | | | | | | | | | | | | | | | |
|b| 4 | 0 | 1 | | | | | | | | | | | | | | | | | |
|d| 5 | 0 | 1 | | | | | | | | | | | | | | | | | |
|b| 6 | 0 | 1 | | | | | | | | | | | | | | | | | |

初始化:有一方为空,公共长度都是0;
dp[1][?]处第一个字符的比较结果,a==a,则长度为:上一次(即左上对角线)[0][0]+1 = 1; a != c,不要a(即上一行)则长度为0;不要c(即左一列)则长度为1。所以公共长度为1
dp[2][?]处第二个字符的比较结果,b != a,不要b(即上一行),长度为1;不要a(即左一列)长度为0 。所以公共长度为1 。
以此类推。。。
最后字符比较完,便能得到最长结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int longestCommonSubsequence(String str1, String str2) {
if (str1 == null || str2 == null || str1.length() == 0 || str2.length() == 0)
return 0;
int[][] dp = new int[str1.length() + 1][str2.length() + 1];
for (int i = 1; i <= str1.length(); i++) {
for (int j = 1; j <= str2.length(); j++) {
if (str1.charAt(i - 1) == str2.charAt(j - 1)) //当前相等
dp[i][j] = dp[i - 1][j - 1] + 1; //上一次加这一个
else //不等
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); //谁长要谁
}
}
return dp[str1.length()][str2.length()];
}

编辑距离

例:给你两个单词 word1 = “horse”和word2 = “ros”,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

1. 从字符串开头慢慢比
2. 当前字符相等了,则编辑距离和上一次一样;不等了,则进行如下的三种编辑,显然,谁小要谁。

你可以对一个单词进行如下三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

| | |”” |h | o | r | s | e|
|–|–|–|–|–|–|–|–|–|
| | |0|1 | 2 | 3 | 4|5 | 6 |7 | 8 | 9 |
|”” |0 |0 |1 |2 | 3 | 4 | 5|
|r| 1 | 1 | 1|?
|o| 2 | 2 |1 | | | | |
|s| 3 | 3 | 1 | | | | | |

初始化:一个字符编辑成一个字符串,这个简单。
dp[1][?]处第一个字符的编辑距离,r != h,则进行编辑:不要r(即上一行)(对于另一个字符串而言就是加上h,效果一样)则编辑距离为1+1=2;不要h(即左一列)则编辑距离为1+1=2。替换,编辑距离为上一次加1,即0+1 = 1 。所以编辑距离为1,替换一次就行。
以此类推。。。
最后字符比较完,便能得到最小编辑距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int minDistance(String word1, String word2) {
int n1 = word1.length();
int n2 = word2.length();

int[][] dp = new int[n1 + 1][n2 + 1];
// 第一行
for (int j = 1; j <= n2; j++) dp[0][j] = dp[0][j - 1] + 1;
// 第一列
for (int i = 1; i <= n1; i++) dp[i][0] = dp[i - 1][0] + 1;

for (int i = 1; i <= n1; i++) {
for (int j = 1; j <= n2; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1))
dp[i][j] = dp[i - 1][j - 1];
else{
int min = Math.min(dp[i][j - 1], dp[i - 1][j]); //删或者说加
dp[i][j] = Math.min(dp[i - 1][j - 1], min ) + 1; //替换
}
}
}
return dp[n1][n2];
}

打家劫舍

例:如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。房屋存放金额[1,2,3,1],计算不触动警报装置的情况下 ,能够偷窃到的最高金额。

1. 一间一间慢慢偷
2. 当前房屋到底偷还是不偷。

当前房屋偷了,则价值上上一次的价值加这一次的价值;如果不偷,则价值为上一次的价值。显然,谁大要谁。

1
2
3
4
5
6
7
8
9
10
11
public int rob(int[] nums) {
int len = nums.length;
if(len == 0) return 0;
int[] dp = new int[len + 1];
dp[0] = 0;
dp[1] = nums[0];
for(int i = 2; i <= len; i++) {
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]);
}
return dp[len];
}

零钱兑换

例:给定不同面额的硬币 coins = [1, 2, 5]和一个总金额 amount = 11。计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

1. 一块钱一块钱的慢慢换
2. 当前这个硬币到底要不要。

当前硬币不要,则个数没变化;如果要,则个数变为兑换(要兑换的钱减去当前这个面额)的个数加1。显然,谁小要谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static int coinChange(int[] coins, int amount) {
//dp[i]表示钱数为i时最小硬币数
int dp[] = new int[amount + 1];
//初始化钱数为i时的硬币数
Arrays.fill(dp,amount +1);
dp[0] = 0;
//钱数为0时的最小硬币数也为0
for(int i = 1;i<=amount;i++){
for(int coin : coins){
if(i >= coin){
dp[i] = Math.min(dp[i],dp[i-coin]+1);
}
}
}
return dp[amount]> amount ? -1:dp[amount];
}

------------- Thank you for reading -------------

Title - Artist
0:00