【算法修炼】动态规划专题四:线性DP、经典买卖股票六大子问题详解、摆动序列、粉刷房子、粉刷栅栏

解码方法(中等)

在这里插入图片描述
在这里插入图片描述
注意一下base case即可。

class Solution {
    public int numDecodings(String s) {
        int n = s.length();
        int[] dp = new int[n + 1];
        dp[0] = 1;  // base case类似于走楼梯
        if (s.charAt(0) != '0') {
            dp[1] = 1;
        }
        for (int i = 2; i <= n; i++) {
            // 新编码可以由当前一位i产生,也可以由当前一位i和i-1位两位共同产生
            int numi = s.charAt(i - 1) - '0';
            int numii = (s.charAt(i - 2) - '0') * 10 + numi;
            if (numi != 0) dp[i] += dp[i - 1];  // 只要一位不等于0就可以统计
            if (numii >= 10 && numii <= 26) dp[i] += dp[i - 2];
        }
        return dp[n];
    }
}

交错字符串(中等)

在这里插入图片描述
注意:s1 = abc s2 = def s3 = abcdef也是满足的,也就是说s1 + s2 或者 s2 + s1都是满足题意的。
双指针为啥不行?
在这里插入图片描述
考虑上面的情况,第一个a匹配之后,不知道是用s1的a,还是用s2的a,这其实是两个分支,并不能由双指针直接确定下来,所以用双指针是不行的。

DFS记忆化搜索

class Solution {
    Boolean[][] table;
    public boolean isInterleave(String s1, String s2, String s3) {
        int n1 = s1.length(), n2 = s2.length(), n3 = s3.length();
        if (n1 + n2 != n3) return false;  // s1 + s2的长度必须=s3
        table = new Boolean[n1 + 1][n2 + 1];
        return dfs(s1, s2, s3, 0, 0, 0);
    
    }
    // i,j,k分别代表遍历s1,s2,s3三个串的下标位置
    boolean dfs(String s1, String s2, String s3, int i, int j, int k) {
        if (k == s3.length()) return true;
        // 注意用的数组是Boolean对象数组
        if (table[i][j] != null) return table[i][j];
        if (i != s1.length() && s1.charAt(i) == s3.charAt(k) && dfs(s1, s2, s3, i + 1, j, k + 1)) {
            table[i + 1][j] = true;
            return true;
        }
        if (j != s2.length() && s2.charAt(j) == s3.charAt(k) && dfs(s1, s2, s3, i, j + 1, k + 1)) {
            table[i][j + 1] = true;
            return true;
        }
        // 到达这里,说明s1 s2都不能匹配s3
        table[i][j] = false;
        return false;
    }
}

用搜索的原因是因为有多种选择方案,加上记忆化是为了加快搜索速度,用到了记忆化搜索,那一定可以转成DP模型。

DP解法

考虑s3的当前字符可以跟s1或者s2匹配,匹配的前提是s1 s2还有剩余长度。

class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int n1 = s1.length(), n2 = s2.length(), n3 = s3.length();
        if (n1 + n2 != n3) return false;
        boolean[][] dp = new boolean[n1 + 1][n2 + 1];
        // 考虑s1 = "" s2 = "a" s3 = "a",也是可以匹配的
        // dp[0][1] = dp[0][1] || dp[0][0]
        // so, base case:dp[0][0] = true
        dp[0][0] = true;
        for (int i = 0; i <= n1; i++) {
            for (int j = 0; j <= n2; j++) {
                // i + j的值就是获取s3的坐标值
                int p = i + j;
                if (i > 0) {
                    if (s1.charAt(i - 1) == s3.charAt(p - 1)) {
                        dp[i][j] = dp[i][j] || dp[i - 1][j];
                    }
                }
                if (j > 0) {
                    if (s2.charAt(j - 1) == s3.charAt(p - 1)) {
                        dp[i][j] = dp[i][j] || dp[i][j - 1];
                    }
                }
            }
        }
        return dp[n1][n2];
    }
}

三个无重复子数组的最大和(困难)

在这里插入图片描述
站在最后一个子数组的角度去思考问题,就会很easy。很多时候,dp问题都是需要站在某一个位置去具体思考问题,看这个位置可以由什么状态转移过来。

本题的难点在于如何找到字典序最小的路径!

class Solution {
    public int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int n = nums.length;
        int[] preSum = new int[n + 1];
        for (int i = 0; i < n; i++) {
            preSum[i + 1] = preSum[i] + nums[i];
        }
        // dp[i][j]:前i个数字,前j个子数组的最大和
        int[][] dp = new int[n + 1][4];
        // dp[k][1] = preSum[k];
        for (int i = k; i <= n; i++) {
            for (int j = 1; j <= 3; j++) {
                // 站在最后一个子数组的角度去考虑
                // 可以是前面已经有j个子数组、也可能是加上现在这个子数组才有j个子数组
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - k][j - 1] + preSum[i] - preSum[i - k]);
            }
        } 
        // 最大和
        // System.out.println(dp[n][3]);
        
        // 求路径
        int[] ans = new int[3];
        int i = n, j = 3, index = 2;
        while (j > 0) {
            // 这里一定要把 = 也排除掉,因为需要选择字典序最小,只有完全的大于,才能保证字典序最小
            if (dp[i - 1][j] >= dp[i - k][j - 1] + preSum[i] - preSum[i - k]) {
                i--;
            } else {
                // 选择当前下标
                i = i - k;
                j--;
                ans[index--] = i;
            }
        }
        return ans;
    }
}

买卖股票的最佳时机(简单)

在这里插入图片描述
对于具体某一天,只要买在它之前的天数里面股票价格最低的,就可以保证这一天的收益最大(当然可能小于0)。

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int min = prices[0];
        int ans = 0;
        for (int i = 1; i < n; i++) {
            ans = Math.max(ans, prices[i] - min);
            min = Math.min(min, prices[i]);
        }
        return ans;
    }
}

买卖股票的最佳时机Ⅱ(中等)

在这里插入图片描述
上面一题要求在整个过程中只能进行一次交易,本题可以进行多次交易,手中最多持有一只股票。

针对每一天,具有两种状态:持有现金、持有股票,在这个基础上进行考虑具体某一天在持有现金、持有股票的情况下的状态转移方程(无非就是从前一天的持有现金、持有股票的两种状态转移过来)

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n + 1][2];
        // dp[i][0/1],第i天,有0\1两种状态
        // 0:持有现金、1:持有股票
        dp[1][0] = 0;
        dp[1][1] = -prices[0];
        // 默认手上的金钱数目=0
        // 所以第一天持有股票后的金钱数目=-prices[0]
        for (int i = 2; i <= n; i++) {
            // 第i天的持有现金可以等于前一天持有的现金
            // 也可以是之前买了股票,然后今天卖出去就是+prices[i]
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
            // 第i天的持有股票时的状态可以等于前一天持有股票的状态
            // 也可以是之前没有买股票,今天买股票的状态
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);
        }
        // 返回最后一天的持有现金,也即最大利益
        return dp[n][0];
    }
}

买卖股票的最佳时机Ⅲ(困难)

在这里插入图片描述
上一题中没有限制交易次数,只是说任意时刻手上只能持有现金或持有股票,本题限制了交易次数:最多完成两笔交易。
在这里插入图片描述
一定要注意,只有买的时候,才会影响k值!对于base case,只需要考虑第一天持有、不持有股票的状态即可(当然需要遍历每一种交易上限次数)

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][][] dp = new int[n + 1][3][3];
        for (int i = 1; i <= n; i++) {
            for (int j = 2; j >= 1; j--) {  // 遍历交易上限次数,不是已经进行的交易次数!!!
                if (i == 1) {
                    // 针对第一天,无论交易次数限制是多少
                    // 只考虑持有、未持有股票的情况
                    dp[i][j][0] = 0;
                    dp[i][j][1] = -prices[i - 1];
                    continue;
                }
                // 当天未持有股票:之前也未持有、之前持有今天卖了
                // 交易次数是说之前买了几次,今天卖并不影响
                dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i - 1]);
                // 当天持有股票:之前也持有、今天才持有(那么之前的交易次数上限=k-1)
                dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1]);
            }
        }
        return dp[n][2][0];
    }
}

买卖股票的最佳时机Ⅳ(困难)

在这里插入图片描述
和上一题一样,区别在于,最大交易上限次数为k,那就修改下对k的值即可。

class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        int[][][] dp = new int[n + 1][k + 1][2];
        for (int i = 1; i <= n; i++) {
            for (int j = k; j >= 1; j--) {
                // 特判第一天
                if (i == 1) {
                    // 无论交易上限是多少,只要未持有股票,利润=0
                    dp[i][j][0] = 0;
                    // 持有股票的话,就需要花钱买
                    dp[i][j][1] = -prices[i - 1];
                    continue;
                }
                // 未持有股票:之前未持有、之前持有今天才卖
                dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i - 1]);
                // 持有股票:之前持有、之前未持有今天才买(会影响交易上限)
                dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1]); 
            }
        }
        // 返回的结果一定是考虑所有天数、最大的交易上限次数、并且手上不持股
        return dp[n][k][0];
    }
}

最佳买卖股票时机含冷冻期(中等)

在这里插入图片描述
冷冻期只影响买入,不影响卖出,你可以买入后立马卖出;但不能卖出后立马买入。

所以,需要改变的地方在于买入股票时的状态:如果是今天买入股票,那么就必须要在两天前卖出,这样到今天才能够买入。

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] dp = new int[n + 1][2];
        // dp[i][0/1],第i天不持有股票\持有股票
        for (int i = 1; i <= n; i++) {
            if (i == 1) {
                dp[i][0] = 0;
                dp[i][1] = -prices[i - 1];
                continue;
            }
            // 只是卖出股票后,不能第二天马上买入股票
            // 但是可以买入股票后,立马卖出
            if (i == 2) {  // 特判第二天
                dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
                // 如果是今天持有,那么可能是之前买入,也可以是今天买入
                dp[i][1] = Math.max(dp[i - 1][1], -prices[i - 1]);
                continue;
            }
            // 今天不持有股票,可能是之前就没有持有,也可能是今天才卖
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
            // 今天持有股票,可能是之前就持有,也可能是今天才持有
            // 如果是今天才持有,由于存在冷冻期,必须要在两天前卖出后才能在今天购入
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1]); 
        }
        return dp[n][0];
    }
}

买卖股票的最佳时机含手续费(中等)

在这里插入图片描述
手续费在股票买入、卖出的过程中,只算一次,为方便考虑,可以把手续费算到卖出时,这样只需要更改部分代码即可。

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int n = prices.length;
        int[][] dp = new int[n + 1][2];
        // 把手续费算到卖出股票的时候
        for (int i = 1; i <= n; i++) {
            if (i == 1) {
                // 未持有股票
                dp[i][0] = 0;
                // 持有股票
                dp[i][1] = -prices[i - 1];
                continue;
            }
            // 当前未持有股票:之前就未持有、之前持有今天卖掉
            // 卖掉需要算一次手续费
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1] - fee);
            // 当前持有股票:之前就持有、之前未持有今天才买入
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);
        }
        return dp[n][0];
    }
}

戳气球(困难)

在这里插入图片描述
直接在nums数组上求值是困难的,不妨考虑添加一个个气球进去,看怎样添加能使得硬币数最多。

DFS记忆化搜索

class Solution {
    int[] score;
    int[][] table;
    public int maxCoins(int[] nums) {
        int n = nums.length;
        score = new int[n + 2];
        for (int i = 0; i < n; i++) {
            score[i + 1] = nums[i];
        }
        // 为方便计算,将左右端点的值设置为1
        score[0] = 1;
        score[n + 1] = 1;
        table = new int[n + 2][n + 2];
        for (int i = 0; i < n + 2; i++) {
            Arrays.fill(table[i], -1);
        }
        return dfs(0, n + 1);
    }
    int dfs(int left, int right) {
    	// 区间已经不足以插入一个气球,只能返回0
        if (left >= right - 1) return 0;
        if (table[left][right] != -1) return table[left][right];
        // 枚举可能的添加气球的位置,左右端点不能添加
        for (int i = left + 1; i < right; i++) {
            int sum = score[left] * score[i] * score[right];
            sum += dfs(left, i) + dfs(i, right);
            table[left][right] = Math.max(table[left][right], sum);
        }
        return table[left][right];
    }
}

能用DFS记忆化搜索,肯定可以转化为DP问题:

class Solution {
    public int maxCoins(int[] nums) {
        int n = nums.length;
        int[][] dp = new int[n + 2][n + 2];
        // dp[0][n + 1]
        int[] score = new int[n + 2];
        for (int i = 0; i < n; i++) {
            score[i + 1] = nums[i];
        }
        score[0] = 1;
        score[n + 1] = 1;
        for (int i = n - 1; i >= 0; i--) {  // 枚举左端点
            for (int j = i + 2; j <= n + 1; j++) {  // 枚举右端点
                for (int k = i + 1; k < j; k++) {  // 枚举放气球的位置
                    int sum = score[i] * score[j] * score[k];
                    sum += dp[i][k] + dp[k][j];
                    dp[i][j] = Math.max(dp[i][j], sum);
                }
            }
        }
        return dp[0][n + 1];
    }
}

摆动序列(中等)

在这里插入图片描述
dp[i][0/1],考虑前 i 个数,0代表当前差值为正数,1代表当前差值为负数,最终答案:max(dp[n][0], dp[n][1])

考虑差值的变换,如果当前差为正数,要求上一个差必须为负数;如果当前差为负数,要求上一个差必须为正数。同时,要注意只有一个数的情况,它们的长度都=1。

class Solution {
    public int wiggleMaxLength(int[] nums) {
        // 仅有一个元素或者含两个不等元素的序列也视作摆动序列
        int n = nums.length;
        int[][] dp = new int[n + 1][2];
        for (int i = 0; i < n + 1; i++) {
            // 单独一个元素就可以视作摆动序列
            Arrays.fill(dp[i], 1);
        }
        // dp[i][0/1]:0,本次差为正数;1,本次差为负数
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j < i; j++) {
                if (nums[j - 1] > nums[i - 1]) {
                    // 本次的差为负数,那么上一次的差必须为正数
                    dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1);
                }
                if (nums[j - 1] < nums[i - 1]) {
                    // 本次的差为正数,那么上一次的差必须为负数
                    dp[i][0] = Math.max(dp[i][0], dp[j][1] + 1);
                }
            }
        }
        // 看最后的差是正数,还是负数的长度最大
        return Math.max(dp[n][0], dp[n][1]);
    }
}

粉刷房子(中等)

在这里插入图片描述
考虑每个房间,有3种颜色选择,dp[i][0/1/2],第 i 号房间选择 0、1、2颜色,所需的最小花费,最后答案:min(dp[n][0], dp[n][1], dp[n][2])

考虑当前某个房间,如果选择颜色0,则前一个房间只能选择1、2颜色;如果选择颜色1,则前一个房间只能选择0、2颜色;如果选择颜色2,则前一个房间只能选择0、1颜色。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        int[][] cost = new int[][] {
                {17,2,17},
                {16,16,5},
                {14,3,19}
        };
        // 房子个数
        int n = cost.length;
        int[][] dp = new int[n + 1][3];
        for (int i = 0; i < n + 1; i++) Arrays.fill(dp[i], 0x3f3f3f3f);
        // dp[i][j],第i号房子,粉刷颜色为colors[j]的最小花费
        // 第一个房间的粉刷方案是确定的
        dp[1][0] = cost[0][0];
        dp[1][1] = cost[0][1];
        dp[1][2] = cost[0][2];
        // 第一个房间无需考虑
        for (int i = 2; i <= n; i++) {  // 枚举房间号
            // 当前房间刷颜色0,则要求前面的房间不能刷颜色0
            dp[i][0] = Math.min(dp[i][0], dp[i - 1][1] + cost[i - 1][0]);
            dp[i][0] = Math.min(dp[i][0], dp[i - 1][2] + cost[i - 1][0]);

            dp[i][1] = Math.min(dp[i][1], dp[i - 1][0] + cost[i - 1][1]);
            dp[i][1] = Math.min(dp[i][1], dp[i - 1][2] + cost[i - 1][1]);

            dp[i][2] = Math.min(dp[i][2], dp[i - 1][0] + cost[i - 1][2]);
            dp[i][2] = Math.min(dp[i][2], dp[i - 1][1] + cost[i - 1][2]);
        }
        System.out.println(Math.min(Math.min(dp[n][0], dp[n][1]), dp[n][2]));
    }
}

粉刷房子Ⅱ

在这里插入图片描述
差别在于:颜色种类不固定,是 k 种颜色,那其实和上一题没区别,还是需要枚举 k 种颜色。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        int[][] cost = new int[][] {
                {1,5,3},
                {2,9,4}
        };
        // 房子个数
        int n = cost.length;
        // 颜色种类
        int k = cost[0].length;
        int[][] dp = new int[n + 1][k];
        for (int i = 0; i < n + 1; i++) Arrays.fill(dp[i], 0x3f3f3f3f);
        // dp[i][j],第i号房子,粉刷颜色为colors[j]的最小花费
        // 第一个房间可以粉刷任意颜色
        for (int i = 0; i < k; i++) {
            dp[1][i] = cost[0][i];
        }
        for (int i = 2; i <= n; i++) {  // 枚举房间号
            for (int j = 0; j < k; j++) {  // 枚举当前房间的颜色
                for (int l = 0; l < k; l++) {  // 枚举上一间房间的颜色
                    if (l == j) continue;  // 不能和上一个房子选择的颜色一致
                    dp[i][j] = Math.min(dp[i][j], dp[i - 1][l] + cost[i - 1][j]);
                }
            }
        }
         int ans = Integer.MAX_VALUE;
        for (int i = 0; i < k; i++) {
            ans = Math.min(ans, dp[n][i]);
        }
        System.out.println(ans);
    }
}

粉刷栅栏

在这里插入图片描述
给出一种,跟粉刷房子类似的解决思路,但不是本题的最优解。

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        // n个栅栏
        int n = 3;
        // k种颜色
        int k = 2;
        int[][][] dp = new int[n + 1][k + 1][3];
        // dp[i][j][k],k代表当前有几个相同的颜色,k=0没有相同/1和前面一个相同
        // 对于第一个栅栏,不同颜色的涂法都只有1种,累加的种类数=k种
        for (int i = 1; i <= k; i++) dp[1][i][0] = 1;
        for (int i = 2; i <= n; i++) {  // 枚举当前栅栏
            for (int j = 1; j <= k; j++) {  // 枚举当前栅栏所选颜色
                for (int l = 1; l <= k; l++) {  // 枚举前一个栅栏所选颜色
                    if (j == l) {
                        // 如果当前颜色和上一个颜色相同
                        dp[i][j][1] = dp[i - 1][l][0];
                    } else {
                        // 当前颜色和上一个颜色不同
                        dp[i][j][0] += dp[i - 1][l][0] + dp[i - 1][l][1];
                    }
                }
            }
        }
        int sum = 0;
        for (int i = 1; i <= k; i++) {
            sum += dp[n][i][0] + dp[n][i][1];
        }
        System.out.println(sum);
    }
}

最优解
dp[i],表示粉刷前 i 个栅栏的方案数,有 k 个颜色,那么dp[1] = k,dp[2] = k * k,因为第二个栅栏可以选择与第一个栅栏相同的颜色,也可以不同,k个颜色每个颜色又有k个选择,所以为k * k。关键是dp[3]之后的怎么推?

考虑:当前栅栏颜色 与 上一个栅栏颜色相同,那么就需要考虑上上个栅栏的颜色,必须要与后面栅栏的颜色不同,不同的话也就有 k - 1种选择,即:dp[i - 2] * (k - 1)

当前栅栏颜色 与 上一个栅栏颜色不同,那么只用考虑上一个栅栏的颜色即可,只需要与上一个栅栏的颜色不同,那么就有k - 1种选择,即:dp[i - 1] * (k - 1)

综合上述两种情况:dp[i] = dp[i - 2] * (k - 1) + dp[i - 1] * (k - 1)

import java.util.*;
import java.io.*;

public class Main {
    static BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(System.out));
    static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    public static void main(String[] args) throws IOException {
        // n个栅栏
        int n = 3;
        // k种颜色
        int k = 2;
        int[] dp = new int[n + 1];
        // dp[i]前i个栅栏的方案数
        dp[1] = k;
        dp[2] = k * k;
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 2] * (k - 1) + dp[i -1] * (k - 1);
        }
        System.out.println(dp[n]);
    }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@u@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值