算法设计与分析案例代码总结(二):动态规划

动态规划

 动态规划是一种使多阶段决策过程最优的通用方法。与分治法类似,其思想把求解的问题分成许多阶段或多个子问题,然后按顺序求解各子问题。但动态规划中分解得到的子问题往往不是相互独立的,但不同子问题的数目常常只有多项式级。所以在动态规划中,我们要保留已解决子问题的解,避免大量重复计算,从而提升算法效率。
 以下是各种案例的原理代码总结:

矩阵连乘问题

问题描述

 给定n个矩阵{A1,A2,…,An},其中Ai与Ai+1是可乘的,i=1,2…,n-1。如何确定计算矩阵连乘积的计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。例如,给定三个连乘矩阵{A1,A2,A3}的维数分别是10100,1005和550,采用(A1A2)A3,乘法次数为101005+10550=7500次,而采用A1(A2A3),乘法次数为100550+10100*50=75000次乘法,显然,最好的次序是(A1A2)A3,乘法次数为7500次。

寻找最优子结构

 此问题最难的地方在于找到最优子结构。对乘积A1A2…An的任意加括号方法都会将序列在某个地方分成两部分,也就是最后一次乘法计算的地方,我们将这个位置记为k,也就是说首先计算A1…Ak和Ak+1…An,然后再将这两部分的结果相乘。
最优子结构如下:假设A1A2…An的一个最优加括号把乘积在Ak和Ak+1间分开,则前缀子链A1…Ak的加括号方式必定为A1…Ak的一个最优加括号,后缀子链同理。
一开始并不知道k的确切位置,需要遍历所有位置以保证找到合适的k来分割乘积。

构建辅助表,储存重复子问题

 从第二步的递归式可以发现解的过程中会有很多重叠子问题,可以用一个nXn维的辅助表m[n][n] s[n][n]分别表示最优乘积代价及其分割位置k 。
辅助表s[n][n]可以由2种方法构造,一种是自底向上填表构建,该方法要求按照递增的方式逐步填写子问题的解,也就是先计算长度为2的所有矩阵链的解,然后计算长度3的矩阵链,直到长度n;另一种是自顶向下填表的备忘录法,该方法将表的每个元素初始化为某特殊值(本问题中可以将最优乘积代价设置为一极大值),以表示待计算,在递归的过程中逐个填入遇到的子问题的解。

代码实现
public class MatrixChainMultiplication {
    public void matrixChainMultiplication(int[] arr,int[][] m,int[][] s,int length)
    {
        int n = length-1;
        int l,i,j,k,q=0;
        //m[i][i]只有一个矩阵,所以相乘次数都是0
        for(i=1;i<=n;i++)
        {
            m[i][i]=0;
        }
        //l表示矩阵链的长度
        //l=2时,计算m[i,i+1].i=1,2,...,n-1(长度l=2的链的最小代价)
        for(l=2;l<=n;l++)
        {
            for(i=1;i<=n-l+1;i++)
            {
                j = i+l-1;//i为起始位置,j为长度为l的链的末位
                m[i][j] = 0x7fffffff;
                //k从i到j-1,以k为位置划分
                for(k=i;k<=j-1;k++)
                {
                    q=m[i][k]+m[k+1][j]+arr[i-1]*arr[k]*arr[j];
                    if(q<m[i][j])
                    {
                        m[i][j] = q;
                        s[i][j] = k;
                    }
                }
            }
        }
        System.out.println(m[1][length-1]);
    }
    public void printAnswer(int[][] s,int i,int j)
    {
        if(i==j)
        {
            System.out.print("A"+i);
        }
        else
        {
            System.out.print("(");
            printAnswer(s,i,s[i][j]);
            printAnswer(s,s[i][j]+1,j);
            System.out.print(")");
        }
    }
}
测试实现
public class TestDynamic {

    public static void main(String[] args) {
            TestmatrixChain();
    }

    public static void TestmatrixChain()
    {
        int[] arr = {10,100,5,50};
        int N = arr.length;
        int[][] m =new int[N][N];
        int[][] s =new int[N][N];
        MatrixChainMultiplication mc = new MatrixChainMultiplication();
        mc.matrixChainMultiplication(arr, m , s ,N);
        mc.printAnswer(s,1,N-1);
    }
}
测试结果

运行结果

背包问题

01背包问题

问题描述:

 有n 种不同的物品,每个物品有两个属性,size 体积,value 价值,现在给一个容量为 w 的背包,问最多可带走多少价值的物品。

 例:编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,每件物品数量只有一个,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和?

问题分析:

 假设现在又一个总容量为8的背包,有四个物品,体积分别为2,3,4,5,价值分别为3,4,5,6,如何让背包里装入的物品具有最大的价值总和?对于这四个物品而言,只有装或者不装两种情况。如果不装,那当前背包的剩余总重量和当前背包价值跟上一个装入物品获得的结果相同。如果装入,那么装入后背包的价值就相当于装入前背包减去当前物品的重量的背包的状态再加上当前物品的价值。最后一句话可能有点难以理解,接下来我们来看一张表。
 假设有这么一张表,也就是二维数组flag[][],横轴表示背包容量,纵轴表示物品编号(为了使数组的下标和物品以及背包的编号对应,我们都从0开始),flag[i][j]就表示在背包容量为j的情况下,装0,1,…,i,物品所获得的最大价值。
动态规划表
 我们现在开始看着这个表装物品。首先,横竖第一行因为背包容量或者物品编号都是0,所以他们都是0。从flag[1][1]开始,背包容量为1,物品编号为1,体积是2,装不下,为0,往右,也就是背包容量为2时,这时1物品可以装下了,这时我们面临两个抉择,装或者不装,我们要获得最大的价值,当然是装了!后面的跟前面的原理相同(因为只有一个物品)。
 然后我们来到物品编号为2的这一行,背包容量为1时,1物品和2物品都装不下,容量为2时,只装的下1,所以我们填入3。当背包容量为3时,诶,可以装2物品了,这时我们就面临了一个装或者不装的抉择:1、不装,那么他的价值就跟只装1的价值相同,也就是它上面那一个。2、装,那么我们就装入2(这时背包状态为空,1并没有被装入),这时我们就使背包容量减去2物品的体积,也就是flag[2][当前背包容量-物品2的价值],即flag[2][0],这时,我们需要判断,flag[1][0]+value[2],和flag[1][3],哪个大(因为我们要获得最大价值,flag[1][0]就存储着除去物品2的时候背包的最大价值),选出最大值存储在表中。
 后面的原理跟前面完全相同,当表填完时,右下角的值就代表背包能装的最大价值。

背包问题的回溯

 如果我们不单单需要背包所能装入的最大价值还需要知道哪些物品被装入了背包呢?这就涉及到了背包问题的回溯。其实,这个问题很简单,从上面的原理分析我们可以知道,在动态规划表中,如果一个物品被装入,那么它在表中对应位置的值肯定和头上的不同,还是拿上面的表来说。
动态规划表
 这时我们要从右下角开始看,最右下角为10,他头上的是9,说明4物品被装入,然后我们减去4物品的体积,也就是5,看3物品是否被装,也就是来到了flag[3][3],发现他和flag[2][3]相同,也就是3未被装,所以我们向上走一格,判断2是否被装,发现他跟上面那一格不同,所以2被装,然后我们减去2的体积3,来到了flag[1][0]跟上面一格相同,也就是1没有被装,然后我们回到flag[0][0],结束。也就是被装入的物品编号是2,4;

完全背包问题

问题描述:

 例:有编号分别为a,b,c,d的四件物品,它们的重量分别是2,3,4,7,它们的价值分别是1,3,5,9,每件物品数量无限个,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和?

求解方法

 一、完全背包问题可以用贪心算法求解,算出每个物品的单位价值,对他们进行排序,先装单位价值最大的物品。
 二、完全背包问题和01背包问题的不同之处在于物品如果被装入之后仍然可以继续装入,所以就不能回到flag[i-1][j-weight[i]]了应该回到flag[i][j-weight[i]]判断,同时第一行的初始化的出示化也不能都为3了,也应该改变。

01背包和完全背包的代码实现
public class PackageProblem {
    public void findMax01(int[] weight,int[] value, int[][] flag,int pw,int[] item) //01背包问题
    {
        //weight:物品的体积或重量;  value:物品的价值;  flag:动态规划表;   pw:背包大总重量或体积;  item:最优解情况
        for(int i = 1;i <= weight.length-1;i++){
            for(int j = 1;j <= pw; j++){
                if(j < weight[i]) //判断当前物品是否能被装下
                    flag[i][j] = flag[i-1][j];
                else
                    flag[i][j] = max(flag[i-1][j],flag[i-1][j-weight[i]]+value[i]); //两种情况,装或者不装,取最大值
            }
        }
        findWhat(weight.length-1,pw,weight,value,flag,item); //找出被装的物品编号,并存储在item数组中
    }
    public void findWhat(int i, int j,int[] weight,int[] value,int[][] flag,int[] item) {				//最优解情况
        if (i > 0) { //判断是否在表中
            if (flag[i][j] == flag[i - 1][j]) { //判断当前物品是否被装(动态规划表中是否和i的上一位相同)
                item[i] = 0;
                findWhat(i - 1, j,weight,value,flag,item); //去到上一行重新判断
            }
            else if (j - weight[i] >= 0 && flag[i][j] == flag[i - 1][j - weight[i]] + value[i]) { //判断当前物品是否被装
                item[i] = 1; //物品被装,相应数组位置值变为1
                findWhat(i - 1, j - weight[i],weight,value,flag,item); //返回上一行重新判断,知道i=0结束
            }
        }
    }

    public void findMaxFull(int[] weight,int[] value, int[][] flag,int pw,int[] item) //完全背包问题
    {
        //weight:物品的体积或重量;  value:物品的价值;  flag:动态规划表;   pw:背包大总重量或体积;  item:最优解情况
        for(int i = 1; i <= pw; i++)
        {
            flag[1][i] = (i < weight[1])?0:((i/weight[1])*value[1]);//与01背包不同
        }
        for(int i = 2;i <= weight.length-1;i++){
            for(int j = 1;j <= pw; j++){
                if(j < weight[i])
                    flag[i][j] = flag[i-1][j];
                else
                    flag[i][j] = max(flag[i-1][j],flag[i][j-weight[i]]+value[i]); //与01背包不同,并没有去到上一行,因为可以重复装
            }
        }
        findWhatFull(weight.length-1,pw,weight,value,flag,item);
    }

    public void findWhatFull(int i, int j,int[] weight,int[] value,int[][] flag,int[] item) {				//最优解情况
        if (i > 0) {
            if (flag[i][j] == flag[i - 1][j]) {
                //此处不能给item[i]赋值0,不然会覆盖。
                findWhatFull(i - 1, j,weight,value,flag,item);
            }
            else if (j - weight[i] >= 0 && flag[i][j] == flag[i][j - weight[i]] + value[i]) {
                item[i]++; //计算当前被装物品被装的数目
                findWhatFull(i, j - weight[i],weight,value,flag,item);//在当前行继续查询
            }
        }
    }

    private int max(int a,int b)
    {
        if(a>=b) return a;
        else return b;
    }
}




public static void TestPackage()
    {
        int[] weight = {0,3,4,6,8,10};
        int[] value = {0,3,4,5,6,1}; //第一个元素为0为了让位置和物品编号对应
        int n = weight.length;
        int pw = 23;
        int[][] flag = new int[n][pw+1];
        int[] item = new int[n];
        int s=0;
        PackageProblem pc = new PackageProblem();
//        pc.findMax01(weight,value,flag,pw,item); //测试01背包问题
        pc.findMaxFull(weight,value,flag,pw,item); //测试完全背包问题
        System.out.println(flag[n-1][pw]);
        System.out.print("被装入背包的物品编号为(括号内表示放入物品的数目):");
        while (s < n)
        {
            if(item[s]!=0) System.out.print(s+1+"("+item[s]+")");
            s++;
        }
        System.out.print("\n");
        for(int i = 0; i <weight.length;i++){
            for(int j =0;j <= pw;j++)
            {
                System.out.print(flag[i][j]+" ");
            }
            System.out.print("\n");
        }
    }
测试结果

01背包问题测试结果
完全背包问题测试结果

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值