动态规划(三)-序列型动态规划

一.序列型动态规划概述

1.序列型动态规划特点

  • 给定一个序列
  • 动态规划方程f[i]中的下标i表示前i个元素a[0]、a[1]、…a[i-1]的某种性质
    • 坐标型的f[i]表示以ai为结尾的某种性质
  • 初始化中,f[0]表示空序列的性质
    • 坐标型动态规划的初始条件f[0]就是指以a0为结尾的子序列的性质

2.例1(LintCode 516 Paint House I)

  • 题意
    有一排N栋房子,每栋房子要漆成3种颜色中的一种:红、蓝、绿,任何两栋相邻的房子不能漆成同样的颜色,第i栋房子染成红色、蓝色、绿色的花费分别是cost[i][0],cost[i][1],cost[i][2],问最少需要花多少钱油漆这些房子

  • 例子

    • 输入:N=3,Cost=[[14,2,11],[11,14,5],[14,3,10]]
    • 输出:10(第0栋房子蓝色,第1栋房子绿色,第2栋房子蓝色,2+5+3=10)
  • 可以直接上暴力…不过暴力的时间复杂度是3的n次方,数据量大的话多半凉凉,所以我们还是来考虑下用dp思想怎么做这道题吧

A.确定状态
  • 最优策略就是花费最小的策略
    • 最后一步:最优策略中房子N-1一定是染成了红、蓝、绿中的一种,我们先不管是哪一种,但是相邻两栋房子不能漆成一样颜色
    • 如果最优策略房子N-1是红,房子N-2只能是蓝或绿
    • 所以如果最优策略房子N-1是蓝,房子N-2只能是红或绿
    • 所以如果最优策略房子N-1是绿,房子N-2只能是红或蓝
    • 貌似有点复杂了…
    • 如果直接套用以前思路,记录油漆前N栋房子的最小花费,根据以前的套路,也需要记录油漆前N-1栋房子的最小花费,但是,前N-1栋房子的最小花费的最优策略中,不知道房子N-2是什么颜色,所以有可能和房子N-1撞色…
    • 我们知道N-1房子最少花100,但是不知道房子N-1是什么颜色,如果是蓝色,N-2也是蓝色。。。那么答案就是错的…简单的来说就是以前的套路是不管颜色,仅仅是求的最少的花费而已
      在这里插入图片描述
  • 尽然如此,那么就只有记录下颜色了,不知道房子N-2是什么颜色,就把它记录下来!
  • 分别记录油漆前N-1栋房子并且房子N-2是红色、蓝色、绿色的最小花费

在这里插入图片描述

  • 求油漆前N栋房子并且房子N-1是红色、蓝色、绿色的最小花费
  • 需要知道油漆前N-1栋房子并且房子N-2是红色、蓝色、绿色的最小花费
  • 子问题
  • 状态:设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别是f[i][0],f[i][1],f[i][2] (从0开始的)
B.转移方程
  • 设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别为f[i][0],f[i][1],f[i][2]
    在这里插入图片描述
C.初始条件和边界情况
  • 设油漆前i栋房子并且房子i-1是红色、蓝色、绿色的最小花费分别为f[i][0],f[i][1],f[i][2]
  • 初始条件:f[0][0]=f[0][1]=f[0][2]=0(即不油漆任何房子的花费)
  • 无边界情况
D.计算顺序

在这里插入图片描述

E.代码
public int minCost(int[][] costs) {
        if(costs==null||costs.length==0){
            return 0;
        }
        int result = 0x7fffffff;
        int n=costs.length;
        int m=costs[0].length;
        int [][] dp = new int[n+1][m];
        dp[0][0]=0;
        dp[0][1]=0;
        dp[0][2]=0;
        for(int i=1;i<=n;i++){
            dp[i][0]=Math.min(dp[i-1][1],dp[i-1][2])+costs[i-1][0];
            dp[i][1]=Math.min(dp[i-1][0],dp[i-1][2])+costs[i-1][1];
            dp[i][2]=Math.min(dp[i-1][1],dp[i-1][0])+costs[i-1][2];
        }
        
        for(int i=0;i<m;i++){
            result=Math.min(dp[n][i],result);
        }
        return result;
    }
  • 这里数组dp又是只和上一行有关…所以我们可以用滚动数组优化
 public int minCost(int[][] costs) {
        if(costs==null||costs.length==0){
            return 0;
        }
        int result = 0x7fffffff;
        int n=costs.length;
        int m=costs[0].length;
        int [][] dp = new int[2][m];
        int news,olds;
        dp[0][0]=0;
        dp[0][1]=0;
        dp[0][2]=0;
        news=1;
        olds=0;
        for(int i=1;i<=n;i++){
            dp[news][0]=Math.min(dp[olds][1],dp[olds][2])+costs[i-1][0];
            dp[news][1]=Math.min(dp[olds][0],dp[olds][2])+costs[i-1][1];
            dp[news][2]=Math.min(dp[olds][1],dp[olds][0])+costs[i-1][2];
            news = news^olds;
            olds = news^olds;
            news = news^olds;
        }
        
        for(int i=0;i<m;i++){
            result=Math.min(dp[olds][i],result);
        }
        return result;
    }

3.例2(LintCode 516 Paint House II)

  • 题意:
    有一排N栋房子,每栋房子要漆成K种颜色中的一种,任何两栋相邻的房子不能漆成同样的颜色,房子i染成第j种颜色的花费是cost[i][j],问最少需要花多少钱油漆这些房子
  • 例子
    • 输入:N=3,K=3,cost=[[14,2,11],[11,14,5],[14,3,10]]
    • 输出:10(房子0蓝色,房子1绿色,房子2蓝色,2+5+3=10)
A.确定状态
  • 这题和Paint House类似,只是颜色种类变成了K种
  • 动态规划思路和Paint House一样,需要记录油漆前i栋房子并且房子i-1是颜色1,颜色2,…,颜色K的最小花费:f[i][1]、f[i][2]、…、f[i][k]
B.转移方程
  • 设油漆前i栋房子并且房子i-1是颜色1,颜色2,…颜色K的最小花费分别为f[i][1]、f[i][2]、…、f[i][K]

  • f[i][1]=min{f[i-1][2]+cost[i-1][1],f[i-1][3]+cost[i-1][1],…,f[i-1][K]+cost[i-1][1]}

  • f[i][2]=min{f[i-1][1]+cost[i-1][2],f[i-1][3]+cost[i-1][2],…,f[i-1][K]+cost[i-1][2]}

  • f[i][K]=min{f[i-1][1]+cost[i-1][K],f[i-1][2]+cost[i-1][K],…,f[i-1][K-1]+cost[i-1][K]}

  • 设油漆前i栋房子并且房子i-1是颜色1,颜色2,…颜色K的最小花费分别为f[i][1]、f[i][2]、…、f[i][K]

在这里插入图片描述

  • 设油漆前i栋房子并且房子i-1是颜色1,颜色2,…颜色K的最小花费分别为f[i][1]、f[i][2]、…、f[i][K]

  • f[i][j]=min(k!=j){f[i-1][k]}+cost[i-1][j]

  • 直接计算的时间复杂度(计算步数):i从0到N,j从1到K,k从1到K,O(NK²)

C.代码
public int minCostII(int[][] costs) {
        if(costs==null||costs.length==0){
            return 0;
        }
        int n = costs.length;
        int m = costs[0].length;
        int [] [] dp = new int[n+1][m];
        int result = Integer.MAX_VALUE;
        for(int i=1;i<=n;i++){
            for(int j=0;j<m;j++){
                dp[i][j]=Integer.MAX_VALUE;
                for(int k=0;k<m;k++){
                    if(j!=k){
                       dp[i][j]=Math.min(dp[i-1][k]+costs[i-1][j],dp[i][j]);
                    }    
                }
            }
        }
        
        for(int i=0;i<m;i++){
            result = Math.min(dp[n][i],result);
        }
        
        return result;
    }
  • 能不能再快点?上面代码在LIntcode上提交时间才超越百分之8的人
D.动态规划常见优化
  • f[i][j]=[min(k!=j){f[i-1][k]}]+cost[i-1][j]
  • 每次需要求f[i-1][1],…,f[i-1][K]中除了一个元素之外其他元素的最小值,会产生很多重复
    在这里插入图片描述
  • 只要不是除开的是最低值,最低值永远都是它,除开它的最低值就是次小值

在这里插入图片描述

  • f[i][j]=min(k!=j){f[i-1][k]}+cost[i-1][j]
  • 记录下f[i-1][1],…,f[i-1][K]中的最小值和次小值分别是哪个
  • 加入最小值时f[i-1][a],次小值是f[i-1][b],则对于j=1,2,3,…,a-1,a+1,…,K,f[i][j]=f[i-1][a]+cost[i-1][j],f[i][a]=f[i-1][b]+cost[i-1][a]
  • 时间复杂度降为O(NK)
E.优化后代码
public int minCostII(int[][] costs) {
        if(costs==null||costs.length==0){
            return 0;
        }
        int n = costs.length;
        int m = costs[0].length;
        int [] [] dp = new int[n][m];
        int result = Integer.MAX_VALUE;
        int min1,min2,j1,j2;
        //我这里dp代表表的是涂第0栋房屋,第0栋涂颜色i所花费最少的价格,从0就开始了
        for(int i=0;i<m;i++){
            dp[0][i]=costs[0][i];
        }
        j1=0;
        j2=0;
        for(int i=1;i<n;i++){
            min1 = Integer.MAX_VALUE;
            min2 = Integer.MAX_VALUE;
            for(int j=0;j<m;j++){
               //更新最小值和最小值下标
               if(dp[i-1][j]<min1){
                   min2=min1;
                   j2=j1;
                   min1=dp[i-1][j];
                   j1=j;
               }else{
                   //更新次小值和次小值下标
                   if(dp[i-1][j]<min2){
                       min2=dp[i-1][j];
                       j2=j;
                   }
               }
            }
            for(int j=0;j<m;j++){
                //当前颜色和上个颜色不一样,直接取最小值;一样就取次小值
                if(j!=j1){
                    dp[i][j]=dp[i-1][j1]+costs[i][j];
                }else{
                    dp[i][j]=dp[i-1][j2]+costs[i][j];
                }
            }
        }
        
        for(int i=0;i<m;i++){
            result = Math.min(dp[n-1][i],result);
        }
        
        return result;
    }

4.例3(LintCode 392 House Robber)

  • 题意
  • 有一排N栋房子(0-N-1),房子i里有A[i]个金币,一个窃贼想选择一些房子偷金币,但是不能偷任何挨着的两家邻居,否则会被警察逮住,问最多能偷多少金币
  • 例子
    • 输入A={3,8,4}
    • 输出:8(只能偷第二家的金币)
A.确定状态
  • 最后一步:窃贼的最优策略中,有可能偷或者不偷最后一栋房子N-1
  • 情况1:不偷房子N-1(简单,最优策略就是前N-1栋房子的最优策略)
  • 情况2:偷房子N-1(仍然需要知道在前N-1栋房子中最多能偷多少金币,但是,需要保证不偷第N-2栋房子)
  • 如何知道在不偷房子N-2的前提下,在前N-1栋房子中最多能偷多少金币呢?
    • 用f[i][0]表示不偷房子i-1前提下,前i栋房子中最多能偷多少金币
    • 用f[i][1]表示偷房子i-1前提下,前i栋房子中最多能偷多少金币
B.转移方程
  • 设f[i][0]表示不偷房子i-1前提下,前i栋房子中最多能偷多少金币
  • 设f[i][1]表示偷房子i-1前提下,前i栋房子中最多能偷多少金币
  • f[i][0]=max(f[i-1][0],f[i-1][1]),因为不偷房子i-1,所以房子i-2可以选择偷或不偷
  • f[i][1]=f[i-1][0]+A[i-1],偷房子i-1,房子i-2必须不偷
  • 简化
  • 在不偷房子i-1前提下,前i栋房子中最多能偷多少金币
    • 其实这就是前i-1栋房子最多能偷多少金币
  • 所以我们可以简化前面的表示
  • 设f[i]为窃贼在前i栋房子最多偷多少金币
C.初始条件和边界情况
  • 设f[i]为窃贼在前i栋房子最多偷多少金币
  • f[i]=max{f[i-1],f[i-2]+A[i-1]}
  • 初始条件
    • f[0]=0(没有房子,偷0枚金币)
    • f[1]=A[0]
    • f[2]=max{A[0],A[1]}
  • 序列型动态规划好处…初始条件简单
D.计算顺序
  • 初始化f[0]
  • 计算f[1]、f[2]、…、f[n]
  • 答案f[n]
  • 时间复杂度O(N),空间复杂度O(1)[可以开个长度为2的数组]
  • 可以用递归解,但是需要记忆化,开个数组
E.代码
 public long houseRobber(int[] A) {
         if(A==null||A.length==0){
              return 0;
          }
          long [] dp = new long[A.length+1];
          dp[0]=0;
          dp[1]=A[0];
          for(int i=2;i<=A.length;i++){
              dp[i]=Math.max(dp[i-1],dp[i-2]+A[i-1]);
          }
          return dp[A.length];
    }

5.例4(LintCode 534 House Robber II)

  • 题意
    有一圈N栋房子,房子i-1里有A[i]个金币,一个窃贼想选择一些房子偷金币,但是不能偷任何挨着的两家邻居,否则会被警察逮住,最多偷多少金币
  • 例子
    • 输入:A={3,8,4}
    • 输出:8(只偷房子1的金币)
A.思考
  • 这题和House Robber非常类似,只是房子现在排成一个圈
  • 于是房子0和房子N-1成了邻居,不能同时偷(用上道题做法,房子0偷没偷没有记录)
  • 要么没偷房子0,要么没偷房子N-1,我们可以枚举窃贼是没有偷房子0还是没有偷房子N-1
  • 情况1:没偷房子0(删掉0),最优策略就是窃贼对于房子1-N-1的最优策略->化为House Robber
  • 情况2:没偷房子N-1,最优策略就是窃贼对于房子0-N-2的最优策略->化为House Robber
  • 圈情况比序列复杂,但是,通过对于房子0和房子N-1不能同时偷原理,进行分情况处理,经过处理,变成序列情况,问题迎刃而解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值