动态规划0-1背包问题-最长公共子序列

动态规划

动态规划算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获得最优解的处理算法。

动态规划可以说是一种分治思想,但是又与分治思想不同,与分治算法不同的是,分治算法是把原问题分解为若干子问题,自顶向下求解各个子问题,然后合并子问题的解,从而得到原问题的解。动态规划也是把原问题分解为若干子问题,然后自底向上,先求解最小的子问题,把结果存储在表格中,再求解大的子问题时,直接从表格中查找子问题的解,这样可以避免重复计算,从而提高算法效率。

在这里需要注意的是,什么问题可以使用动态规划呢?在确定一个问题是否可以使用动态规划时,我们首先要分析的问题是该问题时候具有以下两个性质:

(1) 最优子结构
最优子结构性质是指,问题的最优解包含其子问题的最优解,最优子结构是使用动态规划的最基本条件,如果一个问题不具有最优子结构性质,那么这个问题就不可以使用动态规划来解决。

(2)子问题重叠

子问题重叠是指在求解子问题的过程中,有大量的子问题是重复的,那么只需要求解一次,然后把结果存储在表中,以后在求解相同子问题时可以直接查表,不需要再次求解。需要注意的是子问题重叠不是使用动态规划的必要条件,但是如果一个问题存在子问题重叠更能体现动态规划的优势。

我们通过一个例子来进一步看一下动态规划的使用场景:

背包问题:给定一个确定容量的背包,里面放入物品。物品给定重量和价格,要求达到的目标为装入的背包的总价值最大,并且重要不超过背包的容量,要求装入的物品不能重复。

思路分析

背包问题主要是指给定一个给定容量的背包,若干具有一定价值和重量的物品,如何选择放入背包使物品的 总价值最大。其中又分为0-1背包和完全背包(完全背包指的是,每一种物品都有无限件可以使用)
这里的问题属于0-1背包,即每种物品最多放一个,而无限背包可以转化为0-1背包。

算法的主要思想:
利用动态规划来解决,每次遍历到的第i个物品,根据w【i】 和 v【i】来确定是否需要将该物品放入背包中,即对于给定的n个物品,设v【i】,w【i】分别为第i个物品的价值和重量,C为背包的容量,再令v【i】【j】表示在前i个物品中能够装入容量为 j 的背包中的最大价值。

(1)v[i][0] = v[0][j] =0
(2)当w[i]>j 时,v[i][j] = v[i-1][j]
(3)当j>=w[i]时:v[i][j] = max{v[i-1][j] , v[i-1][j-w[i]]+v[i]}

背包的填表过程;
假如我们的物品是
在这里插入图片描述
背包的填表过程

我们首先把表的第一行第一列的初始值设置为0,(主要是为了以后第i个背包对重量相对应)也可以理解为当背包的容量为0时,能放入的物品都是0个,当物品的重量都为0时,背包重量为0。

0磅-1磅—4磅 分别表示当背包容量为这么大时,能放入的物品是什么
在这里插入图片描述

  1. 假设现在我们只有吉他,这时不管背包容量多大,我们只能放一个吉他,因为吉他只有一把,我们更新表

在这里插入图片描述

  1. 假如有吉他和音响,这个时候这么放置?如果背包只有一磅那么只能放置吉他,我们就需要把上面的1500G,填入到下面的音响对应的表格,如果背包为2磅,也只能放吉他,我们也需要把上面的1500G,填入到下面的音响对应的表格,3如果背包为磅,也只能放吉他,我们也需要把上面的1500G,填入到下面的音响对应的表格,当为4磅的时候,因为音响的重量为4磅,这个时候就需要判断,新加入的物品的总价值比着上面的价值是大是小,如果大就需要更新价值。

在这里插入图片描述

  1. 继续向下扫描行,当扫描到电脑时,1磅时,2磅时,只能放吉他(就把表格上面的音响中对应的价值填入表格中)。当背包容量为3磅时就需要判断,电脑容量为3价值为2000比上面价值大,就需要更新表格放入电脑,这是背白容量剩余0,不再扫描为3磅时的容量。当为4磅时,首先尝试着把电脑放进去,再看看剩余空间,在剩余的1磅中剩余的最大价值是多少,然后加进去。更新表格如下:
    在这里插入图片描述

总结以上过程,我们可以总结一个公式:

(1)v[i][0] = v[0][j] =0
—表示上面的那张表,第一行第一列初始化为0
(2)当w[i]>j 时,v[i][j] = v[i-1][j]
当我们新增加一个商品i的时候,如果这个商品 i 的容量大于当前背包的容量,那么我们就把上一个格子的价值赋予它就行,例如上面表格中所讲。

(3)当j>=w[i]时:v[i][j] = max{v[i-1][j] , v[i]+v[i-1][j-w[i]]}
当准备加入的新增的商品的容量小于等于当前背包的容量,说明新增的商品是可以尝试往里装的。
这个时候我们可以求一个最大值;
装入的方式:
v[i-1][j] 就是上一个单元格的装入策略最大值(因为有可能装入之后,还不如原先装入的价值多)
v[i]:表示当前商品的价值
v[i-1][j-w[i]] :表示装入i-1个商品到剩余空间j-w[i] 的最大价值。
当j>=w[i]时:v[i][j] = max{v[i-1][j] , v[i]+v[i-1][j-w[i]]} 我们求一个最大值就行

我们可以通过上面的表格来对我们的公式进行验证:
首先:
当 v[1][1] = 1500
①. i = 1,j = 1
②. w[i] = w[1] = 1 >= 1 所以 v[i][j] = max{v[i-1][j] , v[i]+v[i-1][j-w[i]]}
③. v[1][1] = max { v[0][1], v[1] + v[0][1-1] } = max{0. 1500+0} = 1500

再验证一个:

v[3][4] = 1500
①. i = 3,j = 4
②. w[i] = w[3] = 3
4>= 3 所以 v[i][j] = max{v[i-1][j] , v[i]+v[i-1][j-w[i]]}
③. v[3][4] = max { v[2][4], v[3] + v[2][1] } = max{3000. 2000+1500} = 2000+1500

下面使用代码实现一下;

package com.suanfa.dongtaiguihua;

public class dongtaiguihua {
    public static void main(String[] args) {
        
        int[] w = {1,4,3};  //定义数组表示物品的重量
        int[] val = {1500,3000,2000}; //定义数组表示物品的价值
        int m = 4;  //背包的容量
        int n = val.length; //物品的个数
        
        //创建二维数组,v【i】【j】 表示在前i个物品中能够装入容量为j 的背包中的最大价值
        int[][] v = new int[n+1][m+1];
        
        //初始化二维数组,第一行第一列为0
        
        for(int i=0;i < v.length;i++){
            v[i][0] = 0;  
        }
        for(int i=0;i < v[0].length;i++){
            v[0][i] = 0;  
        }
        
        //根据动态规划来处理
        for (int i = 1; i < v.length; i++) {
            for (int j = 1; j < v[1].length; j++) {
                if(w[i-1]>j){
                    v[i][j] = v[i-1][j];
                }else{
                    v[i][j] = Math.max(v[i-1][j], val[i-1]+v[i-1][j-w[i-1]]);
                }
            }
        }
        
                
        //输出v二维矩阵
        for (int i = 0; i < v.length; i++) {
            for (int j = 0; j < v[i].length; j++) {
                System.out.print(v[i][j]+" ");
            }
            System.out.println();
        }
        
    }

}

输出结果:
0 0 0 0 0
0 1500 1500 1500 1500
0 1500 1500 1500 3000
0 1500 1500 2000 3500

就是我们上面需要求解的表
但是这只是求出了一个最佳的方案,并没有说明该方案放置的是哪几个商品,没有说明商品的路径
我们需要增加一个二维矩阵用来存储商品中存放的路径,就是存放的是第几个商品。
代码如下:

package com.suanfa.dongtaiguihua;

public class dongtaiguihua {
    public static void main(String[] args) {
        
        int[] w = {1,4,3};  //定义数组表示物品的重量
        int[] val = {1500,3000,2000}; //定义数组表示物品的价值
        int m = 4;  //背包的容量
        int n = val.length; //物品的个数
        
        
        //创建二维数组,v【i】【j】 表示在前i个物品中能够装入容量为j 的背包中的最大价值
        int[][] v = new int[n+1][m+1];
        
        //为了记录放入商品的情况,我们定义一个二维数组
        int[][] path = new int[n+1][m+1];
        
        
        //初始化二维数组,第一行第一列为0
        for(int i=0;i < v.length;i++){
            v[i][0] = 0;  
        }
        for(int i=0;i < v[0].length;i++){
            v[0][i] = 0;  
        }
        
        //根据动态规划来处理
        for (int i = 1; i < v.length; i++) {
            for (int j = 1; j < v[1].length; j++) {
                if(w[i-1]>j){
                    v[i][j] = v[i-1][j];
                }else{
                    //v[i][j] = Math.max(v[i-1][j], val[i-1]+v[i-1][j-w[i-1]]);
                    
                    if(v[i-1][j] < val[i-1]+v[i-1][j-w[i-1]]){
                        v[i][j] = val[i-1]+v[i-1][j-w[i-1]];
                        //把当前情况记录到path
                        path[i][j] = 1;
                    }else{
                        v[i][j] = v[i-1][j];
                    }
                    
                    
                }
            }
        }
                
        //输出v二维矩阵
        for (int i = 0; i < v.length; i++) {
            for (int j = 0; j < v[i].length; j++) {
                System.out.print(v[i][j]+" ");
            }
            System.out.println();
        }
        
        //输出路径矩阵
                for (int i = 0; i < path.length; i++) {
                    for (int j = 0; j < path[i].length; j++) {
                        System.out.print(path[i][j]+" ");
                    }
                    System.out.println();
                }
        
        System.out.println("=====================");
        //输出放入了哪些商品
        int i = path.length - 1;
        int j = path[0].length - 1;
        while(i > 0 && j > 0){
            if(path[i][j] == 1){
                System.out.printf("第%d个商品放入到背包\n",i);
                j -= w[i-1];
            }
            i--;
        }  
    }

}

求解最长公共子序列

我们再看另外一个经典的例子----求解最长公共子序列

给定两个序列,例如X={A,B,C,B,A,D,B} Y={B,C,B,A,A,C} name最长公共子序列是B C B A
(主要公共子序列,是按照下标递增的顺序来排序的)

如何找到最长公共子序列呢。如果使用穷举法,那么时间复杂度那么时间复杂度将是指数级的增长。这是我们避之不及的时间复杂度。
所以,在这里,我们可以使用动态规划来进行求解

我们需要先分析该问题是否具有最优子结构的性质,经过分析我们会发现,最长公共子序列问题具有最优子结构性质,我们可以使用自底向上的方法来进行求解。

(1)首先我们需要确定合适的数据结构,我们可以采用一个二维数组c[][] 来记录最长公共子序列的长度,二维数组b[][] 来记录最长公共子序列的来源,定义这个数组的作用就是当求解数组完毕,我们可以使用这个数组倒退求解出我们所求的最长公共子序列问题。
(2)定义两个字符数组

    private static String s1 = "ABCBADB";
    private static String s2 = "BCBAAC";
    private static char[] arr1 = s1.toCharArray();
    private static char[] arr2 = s2.toCharArray();

假设我们要求的是以上两个字符数组

算法步骤

(1)我们首先需要初始化数组,初始第一行第一列的数组为0 ,当然这里我们使用Java语言编写,数组默认为0,不需要进行初始化的步骤。

(2) 当i=1时:arr1【0】与arr2【j-1】 依次比较,即A 与BCBAAC中元素分别比较一次。
注意:如果字符相等,c[i][j] 取左上角数值加 1 ,并记录最优值的来源是b[i][j] = 1 。
如果字符不相等,取左侧和上面数值中的最大值,如果左侧和上面数值相等,默认取左侧数值,,如果c[i][j] 的值来源于左侧 则b[i][j] = 2,如果值来源于上面,则b[i][j] = 3

例如:j = 1 时 A与字符串BACDBA中的B相比较,不相等,则取左侧和上面的最大值,但是都为0 所以默认取左侧的0 b数组中记录最优来源于左侧b[1][1] = 2
图解如下:
c数组如下:

在这里插入图片描述

b数组如下:

在这里插入图片描述

所有数组比较完毕,相应数组如下
c数组如下:

公共子序列长度数组=
0 0 0 0 0 0 0
0 0 1 1 1 1 1
0 1 1 1 1 2 2
0 1 1 2 2 2 2
0 1 2 2 2 2 3
0 1 2 2 3 3 3
0 1 2 2 3 3 4
0 1 2 2 3 4 4

b数组如下

公共子序列长度来源数组=
0 0 0 0 0 0 0
0 2 1 2 2 2 1
0 1 2 2 2 1 2
0 3 2 1 2 2 2
0 3 1 2 2 2 1
0 3 3 2 1 2 2
0 3 1 2 3 2 1
0 1 3 2 3 1 2

完整Java代码如下:


package com.suanfa.dongtaiguihua;

import java.beans.beancontext.BeanContext;

/**
 * 动态规划,求解最长的公共子序列
 * @author Administrator
 *
 */
public class sameString {
	
	private static String s1 = "ABCADAB";
	private static String s2 = "BACDBA";
	private static char[] arr1 = s1.toCharArray();
	private static char[] arr2 = s2.toCharArray();
	public static void main(String[] args) {
		//使用二维数组c[][] 来记录最长公共子序列的长度
		//使用二维数组b[][] 来记录最长公共子序列的长度的来源于哪一个字符数组,便于倒退求公共子序列
		int[][] c = new int[s1.length()+1][s2.length()+1];
		int[][] b = new int[s1.length()+1][s2.length()+1];

		int len1 = s1.length();
		int len2 = s2.length();
		
		for (int i = 1; i <= len1; i++) {
			for (int j = 1; j <= len2; j++) {
				
				if(arr1[i-1] == arr2[j-1]){
					c[i][j] = c[i-1][j-1] + 1;
					b[i][j] = 1;
				}else{
					if(c[i][j-1] >= c[i-1][j]){
						c[i][j] = c[i][j-1];
						b[i][j] = 2;
					}else{
						c[i][j] = c[i-1][j];
						b[i][j] = 3;
					}
					
				}
				
			}
		}
		
		System.out.println("==============公共子序列长度数组===============");
		print(c);
		System.out.println("==============公共子序列长度来源数组===============");
		print(b);

		System.out.println("最长公共子序列是");
		printArr(len1,len2,b);
	} 
	

	
	//输出最优解
	public static void printArr(int i,int j,int[][] b){
		
		if(i == 0 || j == 0)
			return;
		if(b[i][j] == 1){
			printArr(i-1,j-1,b);
			System.out.print(arr1[i-1]+ " ");
		}else{
			if(b[i][j] == 2){
				printArr(i,j-1,b);
			}else{
				printArr(i-1,j,b);
			}
		}
	}


	public static void print(int[][] arrs){
		for (int i = 0; i < arrs.length; i++) {
			for (int j = 0; j < arrs[0].length; j++) {
				System.out.print(arrs[i][j]+"  ");
			}
			System.out.println();
		}
	}
	
	
	
	

}


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值