动态规划1

动态规划
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()];
    }
    

 
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值