动态规划
0-1背包问题 --动态规划,放入n个物体按阶段处理,第n次的结果可以基于第n-1的结果来推导.
最大重量9,物体重量分别为 3,2,4,5
int[n][w]或int[w+1]解决
0-1背包升级版:
一组不同重量、不同价值、不可分割的物品,我们选择将某些物品装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少呢?
先计算单价,先放入单价高的商品
有几个节点的 i 和 cw 是完全相同的,比如 f(2,2,4) 和f(2,2,3)。在背包中物品总重量一样的情况下,f(2,2,4) 这种状态对应的物品总价值更大,我们可以舍弃 f(2,2,3) 这种状态,只需要沿着 f(2,2,4) 这条决策路线继续往下决策就可以。也就是说,对于 (i, cw) 相同的不同状态,那我们只需要保留 cv 值最大的那个,继续递归处理,其他状态不予考虑
动态规划理论
一个模型三个特征
一个模型:分阶段最优解模型
三个特征:
1)最优子结构,问题的最优解包含子问题的最优解;也可以通过子问题的最优解推导出问题的最优解
2)无后效性:在推导后面阶段的结果的时候,不关心当前结果怎么获取到的;某阶段的值确定后,就不受之后阶段的决策影响
3)重复子问题:决策序列达到相同阶段时,会产生重复的状态
解决动态规划:面对动态规划问题的时候能够有章可循,不会束手无策
注意:上面的分阶段最优解模型,一般用回溯算法也可以暴力解决,这个时候我们可以定义状态,每个状态代表一个节点;在递归树中展示,判断是否存在重复子问题,判断是否需要使用动态规划。
解决动态规划的两种思路:
1)状态转移表:如1-0背包问题,分别记录每一阶段后可能的结果,在该结果之上获取新的状态。
2)状态转移方程法:
状态转移方程法有点类似递归的解题思路。我们需要分析,某个问题如何通过子问题来递归求解,也就是所谓的最优子结构。根据最优子结构,写出递归公式,也就是所谓的状态转移方程。有了状态转移方程,代码实现就非常简单了。
1-0背包问题适合采用状态转移表,二维表最短路径问题适合状态转移方程,为什么?
因为二维路径问题中,到了某一点可能有多种方式,此时路径值也会有多个,因为此时到达方式不会影响后面的路径选择,此时就需要抛弃到达该点的非最佳方式,这就是状态转移方程的作用。
而1-0背包问题,到第n个物体时,并不知道此时最佳的方式,因为此时前面物体放入与否会决定后面物体的能否放入,此时并不能做出取舍;只能记录所有第n次的结果,之=这就是状态表的作用,依此退出第n+1次的重量。
动态规划实战
量化字符串的相似度:编辑距离
将一个字符串转化成另一个字符串,需要的最少编辑操作次数(比如增加一个字符、删除一个字符、替换一个字符)。编辑距离越大,说明两个字符串的相似程度越小;相反,编辑距离就越小,说明两个字符串的相似程度越大。
需要将源串,修改为目标串:
先找公共子串,确认目标串中哪些字符在源串中存在。
遍历源串,采取替换删除增加等操作:
公共字符,且位置相等,不处理,指针分别后移
非公共,源存在,目标不存在,源删除,源指针后移
非公共,源不存在,目标存在,源插入,源指针后移
如何查找公共子串
如 源cat 和目标cta,那我们可以认为公共子串是ct,方法为:
两指针分别指向c,比较相等后指针后移,源中a不存在a不是公共子串,源指针后移
t是公共子串
练习:如何采用回溯和动态规划实现求莱文斯坦距离?
编辑距离:源串和目的串都修改会复杂化逻辑;本质上都修改也可以也可以简化为仅仅修改源串的,这样能很大程度简化逻辑。
假设源串和目的串分别有一个指针在尾部,均向前移动,此时源串字符仅仅有三种处理:
删除字符:执行源字符的删除,源字符指针移动,但是两个指针指向的字符不一定相等,所以需要继续递归,编辑次数加一
替换或插入:执行源字符的替换和插入,此时插入或替换后的字符就是目的串指针对应的字符,所以一定会实现两个指针指向的字符不一定相等,所以均移动,编辑次数加一
leetcode解法解释如下,dp[i][j]=k,含义:D[i][j]
表示 word1
的前 i
个字母和 word2
的前 j
个字母之间的编辑距离。
-
问题1:如果 word1[0..i-1] 到 word2[0..j-1] 的变换需要消耗 k 步,那 word1[0..i] 到 word2[0..j] 的变换需要几步呢?
-
答:先使用 k 步,把 word1[0..i-1] 变换到 word2[0..j-1],消耗 k 步。再把 word1[i] 改成 word2[j],就行了。如果 word1[i] == word2[j],什么也不用做,一共消耗 k 步,否则需要修改,一共消耗 k + 1 步。
-
问题2:如果 word1[0..i-1] 到 word2[0..j] 的变换需要消耗 k 步,那 word1[0..i] 到 word2[0..j] 的变换需要消耗几步呢?
-
答:先经过 k 步,把 word1[0..i-1] 变换到 word2[0..j],消耗掉 k 步,再把 word1[i] 删除,这样,word1[0..i] 就完全变成了 word2[0..j] 了。一共 k + 1 步。
-
问题3:如果 word1[0..i] 到 word2[0..j-1] 的变换需要消耗 k 步,那 word1[0..i] 到 word2[0..j] 的变换需要消耗几步呢?
-
答:先经过 k 步,把 word1[0..i] 变换成 word2[0..j-1],消耗掉 k 步,接下来,再插入一个字符 word2[j], word1[0..i] 就完全变成了 word2[0..j] 了。
从上面三个问题来看,word1[0..i] 变换成 word2[0..j] 主要有三种手段,用哪个消耗少,就用哪个。
public class DynamicPragraming {
// 单个背包,有限制重量,从n个item中放入东西,求能放入的最大重量
static int[] item = {3,2,4,7};
static int[] value = {6,4,9,9};
static int n = 4;
static int maxWeight = 7;
static int maxV = -1;
// 背包升级版,质量不超过maxWeight时,求最大价值 -回溯算法
public static void f(int i, int cw, int cv){
if(i == n || cw == maxWeight){
if(cv > maxV){
maxV = cv;
System.out.println(maxV);
}
return;
}
// 第i个不放入
f(i+1, cw, cv);
if(cw + item[i] <= maxWeight){
f(i+1, cw+item[i], cv+value[i] );
}
}
// 背包升级版,质量不超过maxWeight时,求最大价值 -动态规划算法
public static int maxValue(int[] item, int[] value, int maxWeight){
int itemNum = item.length;
// state初始化,初始化为-1,区分可达和不可达
int[][] state = new int[itemNum][maxWeight+1];
for(int i=0; i<itemNum; i++){
for(int j=0; j<maxWeight+1; j++){
state[i][j]=-1;
}
}
state[0][0] = 0;
if(item[0]<=maxWeight){
state[0][item[0]]=value[0];
}
for(int i=1; i<itemNum; i++){
for(int j=0;j<=maxWeight;j++){
// 第i个项目不放入,和i-1的价值相同
state[i][j]=state[i-1][j];
}
// 第i个项目放入,和i-1的价值基础上增加
// for(int weight=0;weight<=maxWeight;weight++){
// if(weight+item[i]<=maxWeight && state[i-1][weight]>=0 ){
// // 新价值大于原本的价值才更新,否则不更新
// if(state[i-1][weight]+value[i] > state[i][weight+item[i]]){
// state[i][weight+item[i]]=state[i-1][weight]+value[i];
// }
// }
// }
for(int weight=maxWeight-item[i];weight>=0;weight--){
// i次必须基于已有结果来存放,小于0(默认的-1)说明结果不可达
if( state[i-1][weight]>=0 ){
int newValue = state[i-1][weight] + value[i];
// 放入后的价值大于原本价值才放入,否则不放入
if(newValue > state[i][weight+item[i]]){
state[i][weight+item[i]]=newValue;
}
}
}
}
// 查找最大值
for(int i=maxWeight; i>=0; i--){
if(state[itemNum-1][i] >0){
return state[itemNum-1][i];
}
}
return 0;
}
public static int findBackPack(int[] item, int maxWeight){
int itemNum = item.length;
// 如果state[i][j]=true;表示取到第i个item时,重量可以为j
boolean[][] state= new boolean[itemNum][maxWeight+1];
// 初始化
state[0][0] = true;
if(item[0] < maxWeight){
state[0][item[0]]=true;
}
for(int i =1; i<itemNum; i++){
for(int j=0; j<=maxWeight; j++){
if(state[i-1][j]){
// 第i个不放入
state[i][j] = true;
// 第i个放入
if(j+item[i]<=maxWeight){
state[i][j+item[i]] = true;
}
}
}
}
for(int i=maxWeight; i>0; i-- ){
if(state[itemNum-1][i]){
return i;
}
}
return 0;
}
public static int findBackPackNew(int[] item, int maxWeight){
int itemNum = item.length;
// 如果int[i][j]=true;表示取到第i个item时,重量可以为j
boolean[] state= new boolean[maxWeight+1];
// 初始化
state[0] = true;
if(item[0] < maxWeight){
state[item[0]]=true;
}
for(int i =1; i<itemNum; i++){
// 仿照上面二维数组的错误写法
// for(int j=0; j<=maxWeight; j++){
// if(state[j]){
// // 存在bug,第一个item(值为3)后,0 3为true,
// // 第二个item(值为2),先是j=0,所以新增2为true 然后在j=2的时候,会再次加2,4为true,此时明显2被加了两次是不正确的
// // 解决方案,就是在上面j的遍历进行递减遍历,因为值肯定是大于零的
// if(j + item[i] <= maxWeight){
// state[j+item[i]] = true;
// System.out.println("o");
// }
// }
// }
// 正确且简介
for(int j=maxWeight-item[i]; j>=0; j--){
if(state[j]){
state[j+item[i]] = true;
}
}
}
for(int i=maxWeight; i>0; i-- ){
if(state[i]){
return i;
}
}
return 0;
}
public static void main(String[] args) {
System.out.println(lwstNew(origin.length-1, dest.length-1));
// lwst(0,0,0);
// getShortestRoad(arr, 0,0,1);
// System.out.println(dynamicShrotestRoad(arr,3,3));
// f(0, 0,0);
// System.out.println(maxValue(item,value,7));
// System.out.println(findBackPackNew(item, 8));
// System.out.println(findBackPackNew(item, 7));
// System.out.println(findBackPackNew(item, 6));
// System.out.println(findBackPack(item, 8));
// System.out.println(findBackPack(item, 7));
// System.out.println(findBackPack(item, 6));
}
static int[][] arr = {
{1,2,3,4},
{3,4,4,1},
{3,6,5,5},
{6,6,2,5}
};
static int[][] road = new int[arr.length][arr.length];
static int minLength = Integer.MAX_VALUE;
// n*n数组,从(0,0)到(n-1,n-1)的最短路径--状态转移方程法
// 因为到达[i,j]有两种方式:[i-1,j]和[i][j-1],
// 如果是从i+1,j过来也可以,但此时因为绕行了,所以肯定不是最佳路径
//所以状态转移方程minRoad[i,j]=min(road[i-1,j],road[i,j-1])+[i,j]
public static int dynamicShrotestRoad(int[][] arr, int row, int col){
if(row >= arr.length || col >= arr.length){
return -1;
}
if(row==0 && col==0){
return arr[0][0];
}else if(row == 0){
return dynamicShrotestRoad(arr, row, col-1) + arr[row][col];
}else if(col == 0){
return dynamicShrotestRoad(arr, row-1, col) + arr[row][col];
}else{
return Math.min(dynamicShrotestRoad(arr, row-1, col)
,dynamicShrotestRoad(arr, row, col-1))
+ arr[row][col];
}
}
// n*n数组,从(0,0)到(n-1,n-1)的最短路径--回溯算法
public static int getShortestRoad(int[][] arr, int row, int col, int value){
if(col == arr.length-1 && row == arr.length-1){
if(value < minLength){
minLength = value;
}
System.out.println(minLength);
}
if(row <= arr.length-2){
getShortestRoad(arr, row+1, col, value+arr[row+1][col]);
}
if(col <= arr.length-2){
getShortestRoad(arr, row, col+1, value+arr[row][col+1]);
}
return 0;
}
static char[] origin = "mitcmu".toCharArray();
static char[] dest = "mtacnu".toCharArray();
static int minEditNum = Integer.MAX_VALUE;
// 回溯法---解决两个字符串的编辑距离
// 倒序比较,o:源字符串长度-1 d:目的字符串长的-1;
// 调用方式:lwst(0,0,0);
public static void lwst(int o, int d, int editNum){
if(o>=origin.length-1 || d>=dest.length-1){
if(editNum < minEditNum){
minEditNum = editNum;
}
minEditNum = minEditNum+Math.abs(o-d);
System.out.println(minEditNum);
return;
}
if(origin[o] == dest[d]){
lwst(o+1, d+1, editNum);
}else{
// 源插入 或者 源替换,此时执行后的结果一定是origin[o] == dest[d]
lwst(o+1, d+1, editNum+1);
// 源删除,此时结果不一定满足origin[o] == dest[d],但是源肯定要前进一位
lwst(o+1, d, editNum+1);
}
}
// 状态转移方程---解决两个字符串的编辑距离,
// 1 使用了缓存,避免重复计算 2 删除的时候有两种,删除源或目的
int[][] result;
public int minDistance1(String origin, String dest) {
if(origin == null || dest == null){
return -1;
}
// 缓存,避免重复计算,row:origin剩余的待处理长度, col:des剩余长度,
// value:最小修改次数,注意,这里存的一定是最优结果
result = new int[origin.length()+1][dest.length()+1];
for(int i=0; i<origin.length()+1; i++){
for(int j=0; j<dest.length()+1; j++){
result[i][j] = Integer.MAX_VALUE;
}
}
return getMin(0, 0, origin, dest);
}
// 此时pointO和pointD之前的完全一致了
private int getMin(int pointO, int pointD, String origin, String dest) {
if(pointO == origin.length() || pointD == dest.length()){
return Math.max(origin.length()-pointO, dest.length()-pointD);
}
int leftO = origin.length() - pointO, leftD = dest.length() - pointD;
// 成立则说明数据已经计算,直接返回,避免重复计算
if(result[leftO][leftD] != Integer.MAX_VALUE){
return result[leftO][leftD];
}
int res = 0;
// 字符相等此时直接移动指针,编辑次数无变化
if(origin.charAt(pointO) == dest.charAt(pointD)){
res = getMin(pointO+1, pointD+1, origin, dest);
}else{
// 执行源字符pointO位置的删除,源指针后移一位,目的指针不动
int deleteO = getMin(pointO+1, pointD, origin, dest) + 1;
// 执行源字符的插入,源在pointO前插入和和pointD相同的字符
int insertD = getMin(pointO, pointD+1, origin, dest) + 1;
// 执行源字符的替换,编辑次数加一,此时一定实现origin[d] == dest[o],所以均移动
int updateOrAdd = getMin(pointO+1, pointD+1, origin, dest) + 1;
res = Math.min(Math.min(deleteO, insertD), updateOrAdd);
}
result[leftO][leftD] = res;
return res;
}
// 第二种动态转移方程实现方式,参考leetcode
public int minDistance2(String origin, String dest) {
if(origin == null || dest == null){
return -1;
}
int[][] dp = new int[origin.length()+1][dest.length()+1];
// 初始化边界
for(int i=0; i<=origin.length(); i++){
//源都移除,保证与dest的一致
dp[i][0] = i;
}
for(int i=0; i<=dest.length(); i++){
//源都插入,保证与dest的一致
dp[0][i] = i;
}
for(int i=1; i<=origin.length(); i++){
for(int j=1; j<=dest.length(); j++){
if(origin.charAt(i-1) == dest.charAt(j-1)){
// 如果相等,则直接指针移动
dp[i][j] = dp[i-1][j-1];
}else{
// i-1,j-1-->i,j 直接替换i与j一致; i,j-1-->i,i处插入j i-1,j-->i,j 删除i位的字符
dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j-1], dp[i][j-1]), dp[i-1][j]);
}
}
}
return dp[origin.length()][dest.length()];
}
}