动态规划总结专题一

本文参照labuladong大神的算法小抄,读后笔记如下

一、概述

  1. 穷举所有问题的解法:列出状态转移方程
  2. 化简方法:(1)带备忘录的递归解法 (2)DP table
  3. 遍历方向:
    1. 遍历的过程中,所需的状态必须是已经计算出来的。
    2. 遍历的终点必须是存储结果的那个位置
  • 正向遍历
int[][] dp = new int[m][n];
for(int i = 0;i<m;i++){
    for(int j = 0;j<n;j++){
        //计算dp[i][j]
    }
}
  • 反向遍历
for(int i = m - 1;i >= 0;i--){
    for(int j = n-1;j >= 0;j--){
        //计算dp[i][j]
    }
}
  • 斜向遍历
for(int l = 2;l<=n;l++){
    for(int i = 0;i<=n-l;i++){
        int j = l+i-1;
        //计算dp[i][j]
    }
}

二、入门题目(斐波那契数列)

  • 问题描述:1 1 2 3 5 8 13 21 …求第n个数为多少
  1. 解法一 暴力递归
int fib(int n){
    if(n == 1 || n == 2) return 1;
    return fib(n-1) + fib(n-2);
}
  • 出现的问题:重叠子问题
  1. 解法二 带备忘录的递归解法,自顶向下
int[] memo = new int[n+1];
int fib(int n){
    if(n == 1 || n == 2) memo[n] = 1;
    if(memo[n] != 0) return memo[n];//已经计算过
    memo[n] = fib(n-1) + fib(n-2);
    return memo[n];
}
  • 解决了重叠子问题
  1. 解法三 dp数组的迭代解法(DP table),自底向上
int fib(int n){
    int[] dp = new int[n+1];
    dp[1] = dp[2] = 1;
    for(int i = 3;i<=n;i++){
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}
  1. 解法四 细节优化
  • 斐波那契数列的当前状态之只和之前的两个状态有关,只要存储之前的两个状态就行了。
int fib(int n){
    if(n == 1 || n == 2) return 1;
    int prev = 1,curr = 1;
    for(int i = 3;i <= n;i++){
        int temp = curr;
        curr = curr + prev;
        prev = temp;
    }
    return curr;
}

三、凑零钱问题

  • 问题描述:有k种面值的硬币,面值分别为c1,c2,…,ck,每种硬币数量不限,问最少需要几枚硬币凑出总金额amount,如果不可能凑出返回-1。
  • 例如:k = 3,面值为1,2,5,总金额为amount = 11。则11 = 5 + 5 + 1。
  • 解题步骤:
  1. 解法一 暴力解法:穷举所有可能结果
//设要凑出金额n,需要dp[n]个硬币凑出该金额
int coinSelect(int[] coins,int amount){
    return dp(amount,coins,amount);
}
int dp(int n,int[] coins,int amount){
    //base case
    if(n == 0) return 0;
    if(n < 0) return -1;//无解
    int res = Integer.MAX_VALUE; //表示需要硬币的数量
    for(int coin:coins){
        int subProblem = dp(n-coin,coins,amount);
        if(subProblem != -1) res = Math.min(res,subProblem + 1);
    }
    return res == Integer.MAX_VALUE ? -1 : res;
}
  1. 解法二 带备忘录的递归(自顶向下,n~0)
public int coinSelect(int[] coins,int amount){
    int[] memo = new int[amount+1];
    Arrays.fill(memo,0);
    return dp(amount,coins,amount,memo);
}
public int dp(int n,int[] coins,int amount,int[] memo){
    if(n == 0) return 0;
    if(n < 0) return -1;
    if(memo[n] != 0) return memo[n];
    int res = Integer.MAX_VALUE;
    for(int coin : coins){
        int subProblem = dp(n-coin,coins,amount,memo);
        if(subProblem != -1) res = Math.min(res,subProblem+1);
    }
    memo[n] = (res == Integer.MAX_VALUE ? -1 : res);
    return memo[n];
}
  1. 解法三 dp数组的迭代解法(自底向上,0~n)
public int coinSelect(int[] coins,int amount){
    int[] dp = new int[amount + 1];
    Arrays.fill(dp,amount + 1);
    for(int i = 1;i < dp.length;i++){
        for(int coin : coins){
            //子问题无解
            if(i - coin < 0) continue;
            dp[i] = Math.min(dp[i],dp[i-coin] + 1);
        }
    }
    return (dp[amount] == amount + 1) ? -1 : dp[amount];
}

四、子序列问题

一、最长递增子序列(LIS) : 一维dp数组
  • 通用技巧:数学归纳思想
  • 问题描述:找出一个无序数组的最长上升子序列的长度。
    • 例如:nums = [10,9,2,5,3,7,101,18] 最长子序列为[2,3,7,101],长度为4。
  • 定义:dp[i]表示以nums[i]这个数结尾的最长递增子序列的长度。
public int lengthOfLIS(int[] nums){
    int[] dp = new int[nums.length];
    Arrays.fill(dp,1);//初始化为1,因为子序列最少也要包含自己
    for(int i = 0;i<dp.length;i++){
        //求第i个数的最长递增子序列的长度
        for(int j = 0;j<i;j++){
            if(nums[j] < nums[i]){
                dp[i] = Math.max(dp[i],dp[j]+1);
            }
        }
    }

    int res = 0;
    for(int i = 0;i < dp.length;i++){
        res = Math.max(res,dp[i]);
    }
    return res;
}
二、最长公共子序列(LCS) : 二维dp数组
  • 问题描述:找出两个字符串的LCS长度
    • 例如:str1 = “abcde”,str2 = “ace”. 最长公共子序列是"ace",长度为3。
  • 问题分析:
    1. 定义 dp[i][j]的含义为:对于s1[1…i]和s2[1…j],它们的LCS长度为dp[i][j]。
    2. 定义base case:索引为0的行和列表示空串,dp[0][…]和dp[…][0]都初始化为0。索引从1开始,索引为0表示空串,如dp[0][3]表示""和"bab"的LCS。
    3. 状态转移方程(最难的一步,但套路差不多):
      • 用两个指针i和j从后往前遍历s1和s2,如果s1[i] == s2[j],那么这个字符一定在lcs中;否则,s1[i]和s2[j]这两个字符至少有一个不在lcs中,需要丢弃一个。
  1. 解法一 递归暴力
public int longestCommonSubsequence(String str1,String str2){
    return dp(str1.length()-1,str2.length()-1,str1,str2);
}
public int dp(int i,int j,String str1,String str2){
    if(i == -1 || j == -1) return 0;
    if(str1.charAt(i) == str2.charAt(j)) //找到一个lcs元素,继续向前找
        return dp(i-1,j-1,str1,str2)+1;
    else{
        //谁能让lcs最长,就听谁的
        return Math.max(dp(i-1,j,str1,str2),dp(i,j-1,str1,str2));
    }
}
  1. 解法二 DP table
public int LCS(String str1,String str2){
    int m = str1.length(),n = str2.length();
    int[][] dp = new int[n+1][m+1];
    for(int i = 1;i<=m;i++){
        for(int j = 1;j<=n;j++){
            if(text1.charAt(i-1) == text2.charAt(j-1)) {
                dp[i][j] = dp[i-1][j-1] + 1;
            }else{
                dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
            }
        }
    }
    return dp[m][n];
}
  • 疑难解答:
    s1[i]和s2[j]不相等且都不在lcs中的情况,
dp[i][j] = Math.max(dp[i-1][j],dp[i]][j-1],dp[i-1][j-1]);

可以把dp[i-1][j-1]省略,因为dp[i-1][j-1]永远是三者最小的。

三、编辑距离:二维dp数组
  • 问题描述:
    • 将字符串s1转换成s2所使用的最少操作数,操作为插入、删除和替换一个字符。
    • s1 = “horse”,s2 = “ros”,最少的操作为3,即h->r,删r,删e
  • base case : i走完s1或j走完s2,可以直接返回另一个字符串剩下的长度。
  1. 解法一 暴力递归
public int minDistance(String str1,String str2){
    return dp(str1.length()-1,str2.length()-1,str1,str2);
}
public int dp(int i,int j,String str1,String str2){
    //base case
    if(i == -1) return j + 1;
    if(j == -1) return i + 1;
    if(str1.charAt(i) == str2.charAt(j)){
        return dp(i-1,j-1);
    }else{
        return min(
            dp(i-1,j)+1,//删除
            dp(i,j-1)+1//插入
            dp(i-1,j-1)+1//替换
        );
    }
}
public int min(int a,int b,int c){
    return Math.min(a,Math.min(b,c));
}
  1. 解法二 带备忘录的递归
  2. 解法三 DP table
  • base case:
    • dp[…][0]和dp[0][…]为i和j的下标,因为""和str1的编辑距离为str1.length()
public int minDistance(String str1,String str2){
    int m = str1.length(),n = str2.length();
    int[][] dp = new int[m+1][n+1];
    //base case
    for(int i = 1;i<=m;i++){
        dp[i][0] = i;
    }
    for(int j = 1;j<=n;j++){
        dp[0][j] = j;
    }
    for(int i = 1;i<=m;i++){
        for(int j = 1;j<=n;j++){
            if(str1.charAt(i) == str2.charAt(j))
                dp[i][j] = dp[i-1][j-1];
            else{
                dp[i][j] = min(
                    dp[i-1][j] + 1;//删除
                    dp[i][j-1] + 1;//插入
                    dp[i-1][j-1] + 1;//替换
                )
            }
        }
    }
    return dp[m][n];
}
public int min(int a,int b,int c){
    return Math.min(a,Math.min(b,c));
}
四、最长回文子序列
  • 问题描述:求一个字符串最长回文子序列
  • 定义:在子串s[i…j]中,最长回文子序列的长度为dp[i…j]。
  • 问题分析:
    • 如果我们想求dp[i][j],假设知道子问题dp[i+1][j-1]的结果,那么只需比较s[i]和s[j]两个字符,如果相等,则结果加2,不等则分别加入到s[i+1…j-1]中。
  • 状态转移方程
if(s[i] == s[j]){
    dp[i][j] = dp[i+1][j-1] + 2;
}else{
    dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1]);
}
  • base case:
    1. 只有一个字符:dp[i][j] = 1(i == j)
    2. i < j,所以对于i>j的位置不存在子序列,初始化为0。
  • 确定遍历顺序
    • 想求dp[i][j]需要知道dp[i+1][j-1],dp[i+1][j],dp[i][j-1],只能斜着遍历或者反着遍历
public int longestPalindromeSubseq(String str){
    int n = str.length();
    int[][] dp = new int[n][n];
    // i == j时初始化为1
    for(int i = 0;i<n;i++){
        dp[i][i] = 1;
    }
    //反着遍历
    for(int i = n-1;i>=0;i--){
        for(int j = i+1;j<n;j++){
            if(str.charAt(i) == str.charAt(j))
                dp[i][j] = dp[i+1][j-1] + 2;
            else{
                dp[i][j] = Math.max(dp[i+1][j],dp[i][j-1]);
            }
        }
    }
    return dp[0][n-1];
}

五、高楼扔鸡蛋问题

  • 问题描述:
    • N层楼,K个鸡蛋,在最坏情况下(即指若从第一层开始扔鸡蛋,则在第K层碎),算出最少尝试次数,找到鸡蛋恰好摔不碎的那层楼。
  • 问题分析:
    1. 状态转移:
      • 在第i层楼扔了鸡蛋后,可能出现两种情况:鸡蛋碎了和鸡蛋没碎。
      • 如果鸡蛋碎了,那么鸡蛋个数K-1,搜索楼层区间从[1…N]变为[1…i-1]共i-1层楼;
      • 如果鸡蛋没碎,那么鸡蛋个数K不变,搜索楼层区间从[1…N]变为[i+1…N]共N-i层楼。
    • 解惑:鸡蛋没碎情况下,楼层区间变为[i+1…N],此处可不包括i,因为已经包含了。因为楼层数可以为0(这种情况不需要扔鸡蛋),向上递归后,第i层楼相当于第0层,可以被取到。
    1. base case : 当楼层数N等于0时,不需要扔鸡蛋;当鸡蛋数为1时,只能线性扫描所有楼层。
  1. 解法一
public int superEggDrop(int k,int n){
    int[][] memo = new int[k+1][n+1];
    return dp(k,n,memo);
}
public int dp(int k,int n,int[][] memo){
    //base case
    if(k == 1) return n;
    if(n == 0) return 0;

    if(memo[k][n] != 0) return memo[k][n];
    int res = Integer.MAX_VALUE;
    //穷举所有可能选择
    for(int i = 1;i<=n;i++){//假设在每层楼都尝试扔鸡蛋
        res = Math.min(res,Math.max(
            dp(k-1,i-1),//碎了
            dp(k,n-i)//没碎
        ) + 1);
    }
    memo[k][n] = res;
    return res;
}
  1. 解法二 二分法优化
  • 基本思路:

    • dp[k-1,i-1]和dp[k,n-i]这两个函数,固定k和n,看成关于i的函数,前者是单增,后者单减,所以就是求最大值中的最小值(交点)。
  • 说明:此处用闫式二分优化有待考究… ?

public int superEggDrop(int k,int n){
    int[][] memo = new int[k+1][n+1];
    return dp(k,n,memo);
}
public int dp(int k,int n,int[][] memo){
    if(k == 1) return n;
    if(n == 0) return 0;
    if(memo[k][n] != 0) return memo[k][n];
    int res = Integer.MAX_VALUE;
    //二分法
    int l = 1,r = n;
    while(l<=r){
        int mid = l + r >> 1;
        int broken = dp(k-1,mid-1,memo);
        int not_broken = dp(k,n-mid,memo);
        if(not_broken > broken){
            l = mid + 1;
            res = Math.min(res,not_broken + 1);
        }else{
            r = mid - 1;
            res = Math.min(res,broken + 1);
        }
    }
    memo[k][n] = res;
    return res;
}

六、股票买卖问题

一、问题概述
  • 问题描述:
    • 一个数组的第i个元素是一支给定的股票在第i天的价格,最多完成k笔交易(在再次购买前出售掉之前的股票),计算所能获取的最大利润。
    • 例如:nums = [3,2,6,5,0,3],k = 2 2买,3卖,5买,6卖,利润为7
  • 问题分析:
    1. 定义:dp[n-1][k][0]表示在第n-1(天数从0开始)天,最多进行了k次交易,且现在手上没有持有股票。
    2. base case :
      1. dp[-1][k][0] = 0;//i=-1还未开始,利润为0
      2. dp[-1][k][1] = Integer.MIN_VALUE;//没开始时,不可能持有股票
      3. dp[i][0][0] = 0;//k=0表示不允许进行交易,利润为0
      4. dp[i][0][1] = Integer.MIN_VALUE;//不允许交易时,不可能持有股票
      dp[-1][k][0] = dp[i][0][0] = 0;
      dp[-1][k][1] = dp[i][0][1] = Integer.MIN_VALUE;
      
    3. 状态转移方程
      1. dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1] + prices[i]);//今天没有持有股票,有两种可能:昨天没有持有股票,今天也没有;昨天持有股票,今天卖掉了。
      2. dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0] - prices[i]);//今天持有股票,有两种可能:昨天就持有股票,今天继续持有;昨天没有股票,今天购买了。
      dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1] + prices[i]);
      dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0] - prices[i]);
      
二、买卖股票的最佳时机:只允许完成一笔交易
  • 问题描述:
    • 一个数组的第i个元素是一支给定股票第i天的价格,只允许完成一笔交易(即买入和卖出一支股票一次),求所能获取的最大利润。
    • 例如:prices = [7,1,5,3,6,4],2买,5卖,利润为5
  1. 解法一 暴力法非动态规划
public int maxProfit(int[] prices){
    int maxPro = 0;
    int min = Integer.MAX_VALUE;
    for(int i = 0;i<prices.length;i++){
        if(prices[i] < min){
            min = prices[i];
        }else{
            maxPro = Math.max(maxPro,prices[i] - min);
        }
    }
    return maxPro;
}
  1. 解法二 动态规划
int maxProfit(int[] prices){
    int n = prices.length;
    for(int i = 0;i<n;i++){
        if(i - 1 == -1){
            dp[i][0] = 0;
            dp[i][1] = -prices[i];
            continue;
        }
        dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
    }
    return dp[n-1][0];
}
  1. 解法三 使用一维变量
  • 空间复杂度降为O(1):新状态只和相邻的一个状态相关
int maxProfit(int[] prices){
    int n = prices.length;
    int dpi0 = 0,dpi1 = Integer.MIN_VALUE;
    for(int i = 0;i<n;i++){
        //dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
        dpi0 = Math.max(dpi0,dpi1 + prices[i]);
        //dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
        dpi1 = Math.max(dpi1,-prices[i]);
    }
    return dpi0;
}
三、买卖股票的最佳时机II:不限制交易次数
  • 问题描述:一个数组的第i个元素是一支给定股票第i天的价格,不限制交易次数,求最大利润。
  1. 解法一 暴力法
  • 可以简单的理解为当天的股票卖出还可以当天买入,即只要今天的股票比昨天高就可以卖出。
public int maxProfit(int[] prices){
    int maxPro = 0;
    for(int i = 1;i<prices.length;i++){
        int temp = prices[i]-prices[i-1];
        if(temp > 0) maxPro += temp;
    }
    return maxPro;
}
  1. 解法二 动态规划,与上题只有细微差别
int maxProfit(int[] prices){
    int n = prices.length;
    for(int i = 0;i<n;i++){
        if(i - 1 == -1){
            dp[i][0] = 0;
            dp[i][1] = -prices[i];
            continue;
        }
        dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
    }
    return dp[n-1][0];
}
  1. 解法三 使用一维变量
int maxProfit(int[] prices){
    int n = prices.length;
    int dpi0 = 0,dpi1 = Integer.MIN_VALUE;
    for(int i = 0;i<n;i++){
        int temp = dp10;
        dpi0 = Math.max(dpi0,dpi1 + prices[i]);
        dpi1 = Math.max(dpi1,temp - prices[i]);
    }
    return dpi0;
}
四、买卖股票最佳时机含冷冻期:即每次卖出股票后要等一天才能继续交易
  • 问题描述:一个数组的第i个元素是一支给定股票第i天的价格,不限制交易次数,含冷冻期一天,求最大利润。
  1. 解法一
public int maxProfit(int[] prices) {
    int n = prices.length;
    if(n == 0) return 0;
    int[][] dp = new int[n][2];
    for(int i = 0;i<n;i++){
        if(i == 0){
            dp[i][0] = 0;
            dp[i][1] = -prices[i];
            continue;
        }
        if(i == 1){
            dp[i][0] = dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
            dp[i][1] = Math.max(dp[i-1][1],-prices[i]);
            continue;
        }
        dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i-1][1],dp[i-2][0] - prices[i]);
    }
    return dp[n-1][0];
}
  1. 解法二 使用一维的空间
public int maxProfit(int[] prices){
    int n = prices.length;
    int dpi0 = 0,dpi1 = Integer.MIN_VALUE;
    int dppre0 = 0;//代表dp[i-2][0]
    for(int i = 0;i<n;i++){
        int temp = dpi0;
        dpi0 = Math.max(dpi0,dpi1 + prices[i]);
        dpi1 = Math.max(dpi1,dppre0 - prices[i]);
        dppre0 = temp;
    }
    return dpi0;
}
五、买卖股票最佳时机含手续费:每笔交易需要支付手续费
  • 问题描述:一个数组的第i个元素是一支给定股票第i天的价格,不限制交易次数,每笔交易都需要手续费,求最大利润。
  1. 解法一
public int maxProfit(int[] prices, int fee) {
    int n = prices.length;
    if(n == 0) return 0;
    int[][] dp = new int[n][2];
    for(int i = 0;i<n;i++){
        if(i == 0){
            dp[i][0] = 0;
            dp[i][1] = -prices[i]-fee;
            continue;
        }
        dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]);
        dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0] - prices[i] - fee);
    }
    return dp[n-1][0];
}
  1. 解法二 一维变量
public int maxProfit(int[] prices,int fee){
    int n = prices.length;
    int dpi0 = 0,dpi1 = Integer.MIN_VALUE;
    for(int i = 0;i<n;i++){
        int temp = dpi0;
        dpi0 = Math.max(dpi0,dpi1 + prices[i]);
        dpi1 = Math.max(dpi1,temp - prices[i] - fee);
    }
    return dpi0;
}
六、买卖股票的最佳时机II:只允许完成两笔交易
  • 问题描述:一个数组的第i个元素是一支给定股票第i天的价格,最多完成两笔交易,求最大利润。
  1. 解法一 动态规划
//dp[i][k][0]是由dp[i-1]的各个数值求出的,而dp[i-1]中各个数字已经算出
public int maxProfit(int[] prices) {
    int maxk = 2;
    int n = prices.length;
    int[][][] dp = new int[n][maxk+1][2];
    for(int i = 0;i<n;i++){
        for(int k = maxk;k>=1;k--){ //k从1开始递增也正确
            if(i == 0){
                dp[0][k][0] = 0;
                dp[0][k][1] = -prices[i];
                continue;
            }
            dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0] - prices[i]);
        }
    }
    return dp[n-1][maxk][0];
}
  1. 解法二 一维变量优化
public maxProfit(int[] prices){
    int dpi10 = 0,dpi11 = -prices[0];
    int dpi20 = 0,dpi21 = -prices[0];
    for(int price : prices){
        int temp = dpi10;
        dpi10 = Math.max(dpi10,dpi11 + price);
        dpi11 = Math.max(dpi11,-price);
        dpi20 = Math.max(dpi20,dpi21 + price);
        dpi21 = Math.max(dpi21,temp - price);
    }
    return dpi20;
}
七、买卖股票最佳时机III:最多进行k笔交易
  • 问题描述:一个数组的第i个元素是一支给定股票第i天的价格,最多完成k笔交易,求最大利润。
  1. 解法一
//注意:当max_k超过n/2是就退化为了不限制交易次数n
public int maxProfit(int max_k,int[] prices){
    int n = prices.length;
    if(max_k > n/2){
        return maxProfit_any_k(prices);
    }
    int[][][] dp = new int[n][max_k+1][2];
    for(int i = 0;i<n;i++){
        for(int k = max_k;k>=1;k--){
            if(i == 0){
                dp[0][k][0] = 0;
                dp[0][k][1] = -prices[i];
                continue;
            }
            dp[i][k][0] = Math.max(dp[i-1][k][0],dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = Math.max(dp[i-1][k][1],dp[i-1][k-1][0] - prices[i]);
        }
    }
    return dp[n-1][max_k][0];
}
public int maxProfit_any_k(int[] prices){
    int n = prices.length;
    int maxPro = 0;
    int min = Integer.MAX_VALUE;
    for(int i = 1;i<n;i++){
        int temp = prices[i] - prices[i-1];
        if(temp > 0) maxPro += temp;
    }
    return maxPro;
}

七、打家劫舍问题

一、House RobberI
  • 问题描述:
    -给定一个代表每个房屋存放金额的数组,如果两间相邻的房屋在同一晚上被窃贼闯入,系统会自动报警,求在不触动警报的情况下,能够偷窃到的最高金额。
    • 例如:nums = [2,7,9,3,1] 选择2,9,1,最高金额为12
  • 问题分析:
    1. 解决动态规划问题就是找状态和选择
      • 状态 :房子的索引,start,用dp数组表示
      • 选择 :当前房子抢或不抢
    2. dp定义为当前抢到金额
    3. base case : 当走过最后一件房子后,抢到的金额为0
    4. 状态转移方程
  1. 解法一 暴力解法
public int rob(int[] nums){
    return dp(nums,0);
}
public int dp(int[] nums,int start){
    if(start >= nums.length) return 0;
    int res = Math.max(
        dp(nums,start + 1),
        nums[start] + dp(nums,start + 2);
    )return res;
}
  1. 解法二 带备忘录的递归
public int rob(int[] nums){
    int[] memo = new int[nums.length];
    Arrays.fill(memo,-1);
    return dp(nums,0,memo);
}
public int dp(int[] nums,int start,int[] memo){
    if(start >= nums.length) return 0;
    if(memo[start] != -1) return memo[start];
    int res = Math.max(dp(nums,start + 1,memo),
                            nums[start] + dp(nums,start + 2,memo));
    memo[start] = res;
    return res;
}
  1. 解法三 DP table
  • 定义:dp[i] = x;表示从第i间房子开始抢劫,最多能抢到的钱为x
  • base case : dp[n] = 0;
public int rob(int[] nums){
    int n = nums.length;
    int[] dp = new int[n+2];
    for(int i = n-1;i>=0;i--){
        dp[i] = Math.max(dp[i+1],nums[i] + dp[i+2]);
    }
    return dp[0];
}
  1. 解法四 一维变量优化
int rob(int[] nums){
    int n = nums.length;
    int dpi1 = 0,dpi2 = 0;
    int dpi = 0;//记录dp[i]
    for(int i = n-1;i>=0;i--){
        dpi = Math.max(dpi1,dpi2 + nums[i]);
        dpi2 = dpi1;
        dpi1 = dpi;
    }
    return dpi;
}
二、House RobberII
  • 问题描述:给定一个代表每个房屋存放金额的数组,这些房屋不是一排,而是围成一个圈,如果两间相邻的房屋在同一晚上被窃贼闯入,系统会自动报警,求在不触动警报的情况下,能够偷窃到的最高金额。
    • 比如:nums = [2,3,2] ,返回3
  • 问题分析:首尾房屋有三种情况
    1. 都不被抢
    2. 第一间房子被抢,最后一间不被抢
    3. 第一间房子不被抢,最后一间被抢
  • 注意:根据最优决策,只需比较情况二和情况三即可
public int rob(int[] nums){
    int n = nums.length;
    if(n == 1) return nums[0];
    return Math.max(robRange(nums,0,n-2),robRange(nums,1,n-1));
}
public int robRange(int[] nums,int start,int end){
    int n = nums.length;
    int[] dp = new int[n+2];
    for(int i = end;i>=start;i--){
        dp[i] = Math.max(dp[i+1],dp[i+2] + nums[i]);
    }
    return dp[start];
}
三、House RobberIII
  • 问题描述:给定一个代表每个房屋存放金额的数组,这些房屋不是一排,也不是围成一个圈,而是一棵二叉树,只有一个入口,如果两间相邻的房屋在同一晚上被窃贼闯入,系统会自动报警,求在不触动警报的情况下,能够偷窃到的最高金额。
  • 做抢与不抢的选择
  1. 解法一
Map<TreeNode,Integer> memo = new HashMap<>();
public int rob(TreeNode root){
    if(root == null) return 0;
    if(memo.containsKey(root)){
        return memo.get(root);
    }
    int do_it = root.val
        + (root.left == null ? 0 : rob(root.left.left) + rob(root.left.right))
        + (root.right == null ? 0 : rob(root.right.left) + rob(root.right.right));
    int not_do = rob(root.left) + rob(root.right);
    int res = Math.max(do_it,not_do);
    memo.put(root,res);
    return res;
}
  1. 解法二
public int rob(TreeNode root){
    int[] res = dp(root);
    return Math.max(res[0],res[1]);
}
/*
返回一个大小为2的数组arr,arr[0]表示不抢root得到的最大钱数
arr[1]表示抢root得到的最大钱数
*/
public int dp(TreeNode root){
    if(root == null){
        return new int[]{0,0};
    }
    int[] left = dp(root.left);
    int[] right = dp(root.right);
    //抢root,下家不能抢
    int rob = root.val + left[0] + right[0];
    //不抢root,下家可抢可不抢
    int not_rob = Math.max(left[0],left[1])
            +Math.max(right[0],right[1]);

    return new int[]{rob,not_rob};
}

八、四键键盘

  • 问题描述:
    • A: 屏幕上打印’A’,Ctrl-A:选中整个屏幕,Ctrl-C:复制选中区域到缓冲区,Ctrl-V:将缓冲区内容输出到上次输入的结束位置,并显示在屏幕上。现在可以按键N次,求屏幕最多可以显示几个’A’。
    • 例如:N=7,最多输出9个’A’。A,A,A,Ctrl A,Ctrl C,Ctrl V,Ctrl V
思路一
  • 寻找状态和选择
    • 选择 : A,CA,CC,CV
    • 状态 : n剩余的按键次数,a_num当前屏幕上字符A的数量,copy剪切板中字符A的数量 dp存储
    • base case : 当剩余次数n为0时,a_num为答案
    • 将选择用状态转移表示:
      • A: dp(n-1,a_num+1,copy)
      • CV: dp(n-1,a_num+copy,copy)
      • CA,CC(全选和复制必然是联合使用): dp(n-2,a_num,a_num)
        (此法超时)
public int maxA(int n){
    return dp(n,0,0);
}
public int dp(int n,int a_num,int copy){
    if(n <= 0) return a_num;
    return Math.max(
        dp(n-1,a_num+1,copy),
        Math.max(
            dp(n-1,a_num+copy,copy),
            dp(n-1,a_num,a_num);
        )
    );
}
思路二
  • 状态:敲击次数n
    • 最后一次按键要么是A,要么是CV
  • 定义:dp[i]表示i次操作后最多能显示多少个A
  • 状态表示 :
    • 按A键:dp[i] = dp[i-1]+1
    • 按CV键:最优操作一定是CA,CC,然后若干CV,用一个变量j作为若干CV的起点的前一个位置,i之前的2个操作就是CA,CC,即j表示CC的位置
public int maxA(int n){
    int[] dp = new int[n + 1];
    dp[0] = 0;
    for(int i = 1;i<=n;i++){
        dp[i] = dp[i-1] + 1;
        for(int j = 2;j<i;j++){
            dp[i] = Math.max(dp[i],dp[j-2] * (i-j+1));
        }
    }
    return dp[n];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值