首页 > 技术文章 > 动态规划问题

ze7777 2017-09-09 13:42 原文

动态规划基本思路:

0. 应用场景:1. 后一状态依赖前一状态的值;2. 无后效性,即后面的阶段不会对前一阶段有影响

1. 建立2维表格,先根据题目要求,填空

2. 根据表格,推导状态转移方程,从后往前推dp[i]=f(dp[i-1]),即当前值需要前一阶段的值作为基础

3. 写程序时,根据状态转移方程,从dp[1]用for循环从前往后计算,dp[1]的前一阶段是dp[0],要以要先设置初始值,即dp[0]=0

 

问题1: 凑硬币。现在拥有面值为1元、3元、5元的硬币若干;问:如何用最少数量的硬币凑出11元?

分析:

我们先假设一个函数 d(i) 来表示需要凑出 i 的总价值需要的最少硬币数量。

  1. 当 i = 0 时,很显然我们可以知道 d(0) = 0。因为不要凑钱了嘛,当然也不需要任何硬币了。注意这是很重要的一步,其后所有的结果都从这一步延伸开来
  2. 当 i = 1 时,因为我们有 1 元的硬币,所以直接在第 1 步的基础上,加上 1 个 1 元硬币,得出 d(1) = 1
  3. 当 i = 2 时,因为我们并没有 2 元的硬币,所以只能拿 1 元的硬币来凑。在第 2 步的基础上,加上 1 个 1 元硬币,得出 d(2) = 2
  4. 当 i = 3 时,我们可以在第 3 步的基础上加上 1 个 1 元硬币,得到 3 这个结果。但其实我们有 3 元硬币,所以这一步的最优结果不是建立在第 3 步的结果上得来的,而是应该建立在第 1 步上,加上 1 个 3 元硬币,得到 d(3) = 1
  5. ...

可以看出,除了第 1 步这个看似基本的公理外,其他往后的结果都是建立在它之前得到的某一步的最优解上,加上 1 个硬币得到。得出:

状态转化方程:dp[i]=Math.min(dp[i], dp[i-coin[j]]+1)

 1 public static void main(String[] args) {
 2         
 3         int[] coins={1,3,5};
 4         int money=12;
 5         System.out.println(CalculateCoin(money,coins));
 6     }
 7     
 8     /*
 9      * dp[i]表示money=i时,需要的最小硬币数量
10      * 状态转化方程:dp[i]=Math.min(dp[i], dp[i-coin[j]]+1)
11      */
12     public static int CalculateCoin(int k,int[] coins){
13         
14         int[] dp=new int[k+1];
15         dp[0]=0;
16         
17         for(int i=1;i<dp.length;i++){
18             dp[i]=Integer.MAX_VALUE;
19             for(int j=0;j<coins.length;j++){
20                 if(coins[j]<=i){
21                     dp[i]=Math.min(dp[i], dp[i-coins[j]]+1);
22                 }
23             }
24         }
25         
26         return dp[k];
27     }

 

问题2: 背包问题具体例子:假设现有容量10kg的背包,另外有3个物品,分别为a1,a2,a3。物品a1重量为3kg,价值为4;物品a2重量为4kg,价值为5;物品a3重量为5kg,价值为6。将哪些物品放入背包可使得背包中的总价值最大?

分析:

先将原始问题一般化,欲求背包能够获得的总价值,即欲求前i个物体放入容量为m(kg)背包的最大价值dp[i][m]——使用一个数组来存储最大价值,当m取10,i取3时,即原始问题了。而前i个物体放入容量为m的背包,又可以转化成前(i-1)个物体放入背包的问题。下面使用数学表达式描述它们两者之间的具体关系。

  表达式中各个符号的具体含义。

  w[i] :  第i个物体的重量;

  v[i] : 第i个物体的价值;

  d[i][j] :前i个物体放入容量为j的背包的最大价值;

对物体个数及背包重量进行递推,列出一个表格,当逐步推出表中每个值的大小,那个最大价值就求出来了。

推导过程中,要横向计算:

先推导i=1,即只放入第一件物品时,容量为j的背包的最大价值;

再推导i=2,即放入第二件物品时,容量为j的背包的最大价值;

再推导i=3,即放入第三件物品时,容量为j的背包的最大价值

 

状态转移方程:

1. 如果物品可以放入背包,即w[i]<=j,dp[i][j]=Math.max(dp[i][j-1], dp[i-1][j-w[i]]+v[i])

  解释:dp[i-1][j-w[i]]+v[i] 表示第i件物品物品放入背包的价值,即(放入前一个物品(i-1) 背包容量为j-w[i]时的价值)+v[i]

  然后再和dp[i][j-1]进行比较,即放入第i件物品背包容量为j-1时的价值,取其大者。

2. 如果物品重量超过背包容量,即w[i]>j,dp[i][j]=dp[i][j-1]。

 

 1 public static void main(String[] args) {
 2         int[] w = {3,4,5}; //物品重量  
 3         int[] v = {4,4,6}; //物品价值  
 4         int m=10;
 5         int n=w.length;
 6         System.out.println(MaxValue(w,v,m,n));
 7         System.out.println(MaxValue1(w,v,m,n));
 8         System.out.println(MaxValue2(w,v,m,n));
 9        
10     }
11     
12     /*
13      * 01背包问题
14      * 
15      * 状态转移方程:
16      * 1. 如果物品可以放入,即w[i]<=j:
17      * dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])
18      * 
19      * 2. 物品无法放入背包,即w[i]>j:
20      * dp[i][j]=dp[i-1][j]
21      * 
22      */
23     public static int MaxValue(int[] w, int[] v, int m, int n){
24         int[][] dp=new int[n+1][m+1];
25         for(int i=0;i<dp.length;i++){
26             dp[i][0]=0;
27         }
28         
29         for(int j=0;j<dp[0].length;j++){
30             dp[0][j]=0;
31         }
32         
33         for(int i=1;i<dp.length;i++){
34             for(int j=1;j<dp[0].length;j++){
35                 if(w[i-1]<=j)
36                     dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-w[i-1]]+v[i-1]);
37                 else
38                     dp[i][j]=dp[i-1][j];
39             }
40         }
41         
42         return dp[n][m];
43     }
44     
45     /*
46      * 01背包内存优化
47      * 因为当前阶段值dp值,只依赖于上一阶段的dp值
48      * 所以可以用一位数组,内圈for循环从后往前计算即可
49     */
50     public static int MaxValue1(int[] w,int[] v,int m,int n){
51         
52         int[] dp=new int[m+1];
53         
54         for(int i=0;i<dp.length;i++)
55             dp[i]=0;
56         
57         for(int i=0;i<n;i++){
58             for(int j=dp.length-1;j>=w[i];j--){
59                 dp[j]=Math.max(dp[j],dp[j-w[i]]+v[i]);
60             }
61         }
62         
63         return dp[m];
64     }
65     
66     /*
67      * 完全背包问题,即物品可以重复使用
68      * 把01背包的内存优化版,内圈for循环从前往后计算即可
69     */
70     public static int MaxValue2(int[] w,int[] v,int m,int n){
71         
72         int[] dp=new int[m+1];
73         
74         for(int i=0;i<dp.length;i++)
75             dp[i]=0;
76         
77         for(int i=0;i<n;i++){
78             for(int j=w[i];j<dp.length;j++){
79                 dp[j]=Math.max(dp[j],dp[j-w[i]]+v[i]);
80                 
81             }
82         }
83         
84         return dp[m];
85     }
86     

 

内存优化:

每一次dp[i][j]改变的值只与dp[i-1][j]或dp[i][j-1]有关,dp[i-1][j]是前一次i循环保存下来的值。因此,可以将dp缩减成一维数组,从而达到优化空间的目的,状态转移方程转换为 dp[j]= max{ dp[j], dp[j-w(i)]+v[i] }

并且,状态转移方程,每一次推导dp[i][j]是通过dp[i-1][j-w(i)]来推导的,所以一维数组中j的扫描顺序应该从大到小,否者前一次循环保存下来的值将会被修改,从而造成错误。

 

 

完全背包问题:即第i 件物品可以重复使用

分析:把01背包的内存优化版,内圈for循环从前往后计算即可

 

问题3: 字符串最短编辑距离(字符串最想相似度)

编辑距离,是指两个字串之间,由一个字符串转成另一个字符串所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

分析:

假设word1和word2分别为:michaelab和michaelxy,dis[i][j]作为word1和word2之间的编辑距离,即word1[i]和word2[j]的编辑距离。

dis[0][0]表示word1和word2都为空的时候,此时他们的编辑距离为0。

dis[0][j]就是word1为空,word2长度为j的情况,此时他们的编辑距离为j,也就是从空,添加j个字符转换成word2的最小编辑距离为j;同理dis[i][0]就是,word1长度为i,word2为空时,word1需要删除i个字符才能转换成空,所以转换成word2的最小编辑距离为i。

 

状态转移方程:

1. 如果word1.charAt(i)==word2.charAt(j):

  dp[i][j]=dp[i-1][j-1]

2. 如果char1!=char2:

  insert=1+dp[i][j-1]; 

  delete=1+dp[i-1][j];

  replace=1+dp[i-1][j-1]; 

  dp[i][j]=Math.min(Math.min(insert,delete), replace);

 

假设word1[i]和word2[j] (此处i = j) 分别为:michaelab和michaelxy

显然如果word1.charAt(i)==word2.charAt(j), 那么dis[i][j] = dis[i-1][j-1]。

 

如果word1.charAt(i)!=word2.charAt(j),那么:

1. 添加:也就是在michaelab后面添加一个y,那么word1就变成了michaelaby,此时

dis[i][j] = 1 + dis[i][j-1];

上式中,1代表刚刚的添加操作,添加操作后,word1变成michaelaby,word2为michaelxy。dis[i][j-1]代表从word[i]转换成word[j-1]的最小它是指word1[i]和word2[j]的编辑距离,也就是michaelab转换成michaelx的最小它是指word1[i]和word2[j]的编辑距离,由于两个字符串尾部的y==y,所以只需要将michaelab变成michaelx就可以了,而他们之间的最小它是指word1[i]和word2[j]的编辑距离就是dis[i][j-1]。

2. 删除:也就是将michaelab后面的b删除,那么word1就变成了michaela,此时

dis[i][j] = 1 + dis[i-1][j];

上式中,1代表刚刚的删除操作,删除操作后,word1变成michaela,word2为michaelxy。dis[i-1][j]代表从word[i-1]转换成word[j]的最小它是指word1[i]和word2[j]的编辑距离,也就是michaela转换成michaelxy的最小它是指word1[i]和word2[j]的编辑距离,所以只需要将michaela变成michaelxy就可以了,而他们之间的最小它是指word1[i]和word2[j]的编辑距离就是dis[i-1][j]。

3. 替换:也就是将michaelab后面的b替换成y,那么word1就变成了michaelay,此时

dis[i][j] = 1 + dis[i-1][j-1];

上式中,1代表刚刚的替换操作,替换操作后,word1变成michaelay,word2为michaelxy。dis[i-1][j-1]代表从word[i-1]转换成word[j-1]的最小它是指word1[i]和word2[j]的编辑距离,也即是michaelay转换成michaelxy的最小它是指word1[i]和word2[j]的编辑距离,由于两个字符串尾部的y==y,所以只需要将michaela变成michaelx就可以了,而他们之间的最小它是指word1[i]和word2[j]的编辑距离就是dis[i-1][j-1]。

最后,dp[i][j]=min{插入,删除,替换}

代码如下:

 1 public static void main(String[] args) {
 2         String a="abc";
 3         String b="zabc";
 4         System.out.println(MinDistance(a,b));
 5         
 6     }
 7     
 8     /*
 9      * 最小编辑距离问题
10      * 
11      * 状态转移方程:
12      * 
13      * 1. 如果char1=char2:
14      * dp[i][j]=dp[i-1][j-1]
15      * 
16      * 2. 如果char1!=char2:
17      * insert=1+dp[i][j-1]; 
18      * delete=1+dp[i-1][j];
19      * replace=1+dp[i-1][j-1]; 
20      * dp[i][j]=Math.min(Math.min(insert,delete), replace);
21      * 
22      */
23     public static int MinDistance(String s1, String s2){
24         
25         int len1=s1.length();
26         int len2=s2.length();
27         
28         int dp[][]=new int[len1+1][len2+1];
29         for(int i=0;i<dp.length;i++){
30             dp[i][0]=i;
31         }
32         for(int j=0;j<dp[0].length;j++){
33             dp[0][j]=j;
34         }
35         
36         for(int i=1;i<dp.length;i++){
37             char ci=s1.charAt(i-1);
38             for(int j=1;j<dp[0].length;j++){
39                 char cj=s2.charAt(j-1);
40                 if(ci==cj)
41                     dp[i][j]=dp[i-1][j-1];
42                 else{
43                     int insert=1+dp[i][j-1];
44                     int delete=1+dp[i-1][j];
45                     int replace=1+dp[i-1][j-1];
46                     dp[i][j]=Math.min(Math.min(insert,delete), replace);
47                 }
48                 
49             }
50         }
51         
52         return dp[len1][len2];
53     }
54     

 

问题4: 字符串最长公共子序列和公共子串

分析:

什么是最长公共子序列呢?举个简单的例子吧,如:有两个随机数列,1 2 3 4 5 6 和 3 4 5 8 9,则它们的最长公共子序列便是:3 4 5。字符串同理。

最长公共子串最长公共子序列的区别为:子串是串的一个连续的部分,子序列则是从不改变序列的顺序,而从序列中去掉任意的元素而获得新的序列;也就是说,子串中字符的位置必须是连续的,子序列则可以不必连续。

word1="abzfc"

word2="zabcd"

 

dp[i][j]表示word1[0,i]和word2[0,j]的字符串的公共最长子串

最长公共子串:

状态转移方程:

1. 如果word1[i]==word2[j],即两个字符相同,那么

  dp[i][j]=dp[i-1][j-1]+1;

2. 如果word1[i]!=word2[j],即两个字符不同,那么当前dp[i][j]要清零。

  dp[i][j]=0;

 1     /*
 2      * 状态转移方程:
 3      * 1. word1[i]==word2[j]:
 4      *     dp[i][j]=dp[i-1][j-1]+1;             
 5      * 
 6      * 2. word1[i]!=word2[j]:
 7      *     dp[i][j]=0;
 8      * 
 9      */
10     public static int MaxSubstring(String s1, String s2){
11         int len1=s1.length();
12         int len2=s2.length();
13         int dp[][]=new int[len1+1][len2+1];
14         
15         for(int i=0;i<dp.length;i++)
16             dp[i][0]=0;
17 
18         for(int j=0;j<dp[0].length;j++)
19             dp[0][j]=0;
20         
21         int maxLen=0;
22         for(int i=1;i<dp.length;i++){
23             char ci=s1.charAt(i-1);
24             for(int j=1;j<dp[0].length;j++){
25                 char cj=s2.charAt(j-1);
26                 if(ci==cj){
27                     dp[i][j]=dp[i-1][j-1]+1;
28                     maxLen=Math.max(maxLen, dp[i][j]);
29                 }
30                 else{
31                     dp[i][j]=0;
32                 }
33             }
34         }
35         
36         return maxLen;
37         
38     }

 

推荐阅读