算法 - 动态规划

动态规划是一种自底向上的算法,通常用于解决最大、最小等最值问题

能使用动态规划解决的问题,一定具备:

  • 重叠子问题:和暴力搜索不同,需要记录子问题的解,避免重复求解(剪枝)
  • 最优子结构:子问题达到最值,整体才能达到最值,即以小见大
  • 状态转移方程:在每个“状态”做出的“选择”会到达什么“状态”

然后就以合适的顺序填表,穷举所有情况并求最值即可
整体流程:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义

1.凑零钱

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

class Solution {
    public int coinChange(int[] coins, int amount) {
        // dp[i][j]代表用前i个硬币凑出面额j的最少硬币数
        int n = coins.length;
        int[][] dp = new int[n][amount + 1];
        int INF = 9999999;
        // 初始化
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= amount; j++) {
                dp[i][j] = INF;
            }
        }
        for (int i = 0; i < n; i++) {
            dp[i][0] = 0;
        }
        for (int j = 0; j <= amount; j++) {
            if (j >= coins[0] && dp[0][j - coins[0]] > -1) {
                dp[0][j] = dp[0][j - coins[0]] + 1;
            }
        }
        for (int i = 1; i < n; i++) {
            for (int j = 1; j <= amount; j++) {
                // 不用当前硬币
                dp[i][j] = dp[i - 1][j];
                // 用当前硬币
                if (j >= coins[i] && dp[i][j - coins[i]] < INF) {
                    dp[i][j] = Math.min(dp[i][j], dp[i][j - coins[i]] + 1);
                }
            }
        }
        return dp[n - 1][amount] >= INF ? -1 : dp[n - 1][amount];
    }
}

2.最长递增子序列*

  • 明确状态:dp[n] = 以下标n结束的(而非从下标0到下标n的,这种弱绑定),最长序列长度
class Solution {
    public int lengthOfLIS(int[] nums) {
        // dp[i]代表以i结尾的递增子序列的长度
        int n = nums.length;
        int[] dp = new int[n];
        int result = 1;
        dp[0] = 1;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                    result = Math.max(result, dp[i]);
                }
            }
            if (dp[i] == 0) {
                dp[i] = 1;
            }
        }
        return result;
    }
}

3.最长回文子序列*

  • dp[ i ][ j ] = 从下标 i 到下标 j 的最长回文子序列的长度(不包括头尾,即 s[i] != s[j] 时 dp[i][j] 可以不为 0)
  • 每次填一个左斜的列
class Solution {
    public int longestPalindromeSubseq(String s) {
        // dp[i][j]代表s[i~j]的最大回文子序列长度
        int n = s.length();
        int[][] dp = new int[n][n];
        // 初始化
        for (int i = 0; i < n; i++) {
            dp[i][i] = 1;
        }
        for (int i = 0; i < n - 1; i++) {
            dp[i][i + 1] = (s.charAt(i) == s.charAt(i + 1)) ? 2 : 1;
        }
        // 填表,注意方向
        int idx = 2;
        while (idx < n) {
            int x = 0;
            int y = x + idx;
            while (y < n) {
                if (s.charAt(x) == s.charAt(y)) {
                    dp[x][y] = dp[x + 1][y - 1] + 2;
                }
                dp[x][y] = Arrays.stream(new int[]{dp[x + 1][y], dp[x][y - 1], dp[x][y]}).max().getAsInt();
                x++;
                y++;
            }
            idx++;
        }
        return dp[0][n - 1];
    }
}

最长回文子串

  • 填表的时候,每个位置需要查看其左下方向的值,所以填表顺序可以是,先将对角线上的两列初始化,再按列填表
  • dp[ i ][ j ] = s[ i : j + 1] 是否是回文子串
class Solution {
    public String longestPalindrome(String s) {
        // dp[i][j]代表s[i~j]是否回文
        // 快手二面
        int n = s.length();
        int l = 0;
        int r = 0;
        boolean[][] dp = new boolean[n][n];
        // 初始化
        for (int i = 0; i < n; i++) {
            dp[i][i] = true;
        }
        for (int i = 0; i < n - 1; i++) {
            if (s.charAt(i) == s.charAt(i + 1)) {
                l = i;
                r = i + 1;
                dp[i][i + 1] = true;
            }
        }
        // 斜向填表
        int idx = 2;
        while (idx < n) {
            int x = 0;
            int y = idx;
            while (y < n) {
                if (dp[x + 1][y - 1] && s.charAt(x) == s.charAt(y)) {
                    dp[x][y] = true;
                    if (r - l < y - x) {
                        l = x;
                        r = y;
                    }
                }
                x++;
                y++;
            }
            idx++;
        }
        return s.substring(l, r + 1);
    }
}

最长公共子序列

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        // dp[i][j]为text1[0~i]和text2[0~j]的最长公共子序列的长度
        int m = text1.length();
        int n = text2.length();
        int[][] dp = new int[m + 1][n + 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;
                }
                dp[i][j] = Arrays.stream(new int[]{dp[i][j], dp[i - 1][j], dp[i][j - 1]}).max().getAsInt();
            }
        }
        return dp[m][n];
    }
}

最长公共子串

class Solution:
    def LCS(self , str1 , str2 ):
        # write code here
        m, n = len(str1), len(str2)
        if m == 1:
            return str1 
        elif n == 1:
            return str2 
        
        dp = [[0 for _ in range(n)] for _ in range(m)]
        res, end1idx = 0, 0 
        
        # 初始化
        for i in range(m):
            if str1[i] == str2[0]:
                dp[i][0] = 1
                res = 1
                end1idx = i
        for j in range(n):
            if str2[j] == str1[0]:
                dp[0][j] = 1
                
        # 填表
        for i in range(1, m):
            for j in range(1, n):
                if str1[i] == str2[j]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                    end1idx = i if res < dp[i][j] else end1idx
                    res = max(res, dp[i][j])
                    
        return str1[end1idx - res + 1: end1idx + 1]

4.打家劫舍

一般的动态规划(推荐)

class Solution {
    public int rob(int[] nums) {
        // dp[i]为截止到i的最大金额
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }
        int[] dp = new int[n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < n; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[n - 1];
    }
}

5.打家劫舍II

将输入变成环形,掐头/去尾,取最大值

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }
        return Math.max(rob1(Arrays.copyOfRange(nums, 0, nums.length - 1)), rob1(Arrays.copyOfRange(nums, 1, nums.length)));
    }

    public int rob1(int[] nums) {
        // dp[i]为截止到i的最大金额
        int n = nums.length;
        if (n == 1) {
            return nums[0];
        }
        int[] dp = new int[n];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < n; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[n - 1];
    }
}

6.打家劫舍III *

  • 动态规划+树结构
  • 先分析得出是自底向上,所以后根遍历
  • visited 数组起到动态规划的作用,记录之前统计的节点情况
class Solution {

    Map<TreeNode, Integer> visited = new HashMap<>();  // 存放从该节点开始的最大利润

    public int rob(TreeNode root) {
        return postorder(root);
    }

    private int postorder(TreeNode node) {
        // 递归出口
        if (node == null) {
            return 0;
        }
        // 查询缓存
        if (visited.keySet().contains(node)) {
            return visited.get(node);
        }
        // 自底向上,后根遍历
        int l = postorder(node.left);
        int r = postorder(node.right);
        // 决定是否偷当前节点
        int no = l + r;
        int yes = node.val;
        if (node.left != null) {
            yes += postorder(node.left.left) + postorder(node.left.right);  // 是+而非max!!!
        }
        if (node.right != null) {
            yes += postorder(node.right.left) + postorder(node.right.right);
        }
        visited.put(node, Math.max(yes, no));
        return visited.get(node);
    }
}

7.含冷冻期的股票买卖

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
class Solution {
    public int maxProfit(int[] prices) {
        // dp[i][j][k]:第i天【操作完成之后】,是否持有股票为j,是否在冷冻期为k时的最大利润
        int days = prices.length;
        if (days <= 1) {
            return 0;
        }
        int[][][] dp = new int[days][2][2];
        // 初始化
        dp[0][0][0] = 0;
        dp[0][1][0] = -prices[0];
        dp[0][0][1] = Integer.MIN_VALUE;
        dp[0][1][1] = Integer.MIN_VALUE;
        // 填表
        for (int i = 1; i < days; i++) {
            dp[i][0][0] = Math.max(dp[i - 1][0][0], dp[i - 1][0][1]);  // 不持有也没有冷冻期,说明前一天就不持有
            dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][0][0] - prices[i]);  // 持有且不在冷冻期,可能是之前持有,也可能是今天买入
            dp[i][0][1] = Math.max(dp[i - 1][0][1], dp[i - 1][1][0] + prices[i]);  // 不持有且在冷冻期,可能是昨天或今天卖出
            dp[i][1][1] = Integer.MIN_VALUE;  // 持有且在冷冻期,因为不能持有多个股票,该情况不可能
        }
        return Math.max(dp[days - 1][0][0], dp[days - 1][0][1]);
    }
}

股票买卖的最佳时机II *

  • 可以多次交易,但每次只能持有一个股票
  • 状态DP,加入当前是否已持有股票的状态
class Solution {
    public int maxProfit(int[] prices) {
        // dp[i][j]表示第i天操作完成后,是否持有股票为j时的利润
        int n = prices.length;
        int[][] dp = new int[n][2];
        // 初始化
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        // 填表
        for (int i = 1; i < n; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);  // 第i天结束后不持有股票,可能是第i-1天就不持有,也可能是第i天卖的
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);  // 第i天结束后持有股票,可能是第i-1天持有,也可能是第i天买的
        }
        return dp[n - 1][0];  // 最后不能持有股票
    }
}

股票买卖的最佳时机III

  • 和上面类似,但限制最多只能交易两次
  • 需要额外加入两个状态
class Solution {
    public int maxProfit(int[] prices) {
        // dp[i][j][k]表示第i天操作完成后,是否持有股票为j,已经完成交易次数为k的最大利润
        int n = prices.length;
        int[][][] dp = new int[n][2][3];
        final int MIN_VALUE = -1000000;
        // 初始化
        dp[0][0][0] = 0;
        dp[0][1][0] = -prices[0];
        dp[0][0][1] = MIN_VALUE;
        dp[0][1][1] = MIN_VALUE;
        dp[0][0][2] = MIN_VALUE;
        dp[0][1][2] = MIN_VALUE;
        // 填表
        for (int i = 1; i < n; i++) {
            dp[i][0][0] = dp[i - 1][0][0];
            dp[i][1][0] = Math.max(dp[i - 1][1][0], dp[i - 1][0][0] - prices[i]);
            dp[i][0][1] = Math.max(dp[i - 1][0][1], dp[i - 1][1][0] + prices[i]);
            dp[i][1][1] = Math.max(dp[i - 1][1][1], dp[i - 1][0][1] - prices[i]);
            dp[i][0][2] = Math.max(dp[i - 1][0][2], dp[i - 1][1][1] + prices[i]);
            dp[i][1][2] = MIN_VALUE;
        }
        return Arrays.stream(new int[]{dp[n - 1][0][0], dp[n - 1][0][1], dp[n - 1][0][2]}).max().getAsInt();  // 最后不能持有股票
    }
}

8.编辑距离

问题复杂,题解简介。关键是找到DP数组的定义。

  • 问题1:如果 word1[0…i-1] 到 word2[0…j-1] 的变换需要消耗 k 步,那 word1[0…i] 到 word2[0…j] 的变换需要几步呢?
  • 答:先使用 k 步,把 word1[0…i-1] 变换到 word2[0…j-1],消耗 k 步。再把 word1[i] 改成 word2[j],就行了。如果 word1[i] == word2[j],什么也不用做,一共消耗 k 步,否则需要修改,一共消耗 k + 1 步。
  • 问题2:如果 word1[0…i-1] 到 word2[0…j] 的变换需要消耗 k 步,那 word1[0…i] 到 word2[0…j] 的变换需要消耗几步呢?
  • 答:先经过 k 步,把 word1[0…i-1] 变换到 word2[0…j],消耗掉 k 步,再把 word1[i] 删除,这样,word1[0…i] 就完全变成了 word2[0…j] 了。一共 k + 1 步。
  • 问题3:如果 word1[0…i] 到 word2[0…j-1] 的变换需要消耗 k 步,那 word1[0…i] 到 word2[0…j] 的变换需要消耗几步呢?
  • 答:先经过 k 步,把 word1[0…i] 变换成 word2[0…j-1],消耗掉 k 步,接下来,再插入一个字符 word2[j], word1[0…i] 就完全变成了 word2[0…j] 了。
class Solution {
    public int minDistance(String word1, String word2) {
        // dp[i][j]代表word1的前i个字符到word2的前j个字符的编辑距离
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m + 1][n + 1];
        // 初始化
        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++) {
                int insertCost = dp[i][j - 1] + 1;
                int deleteCost = dp[i - 1][j] + 1;
                int updateCost = dp[i - 1][j - 1] + (word1.charAt(i - 1) == word2.charAt(j - 1) ? 0 : 1);
                dp[i][j] = Arrays.stream(new int[]{insertCost, deleteCost, updateCost}).min().getAsInt();
            }
        }
        return dp[m][n];
    }
}

8.01背包问题

  • 算法设计课上的例题。物品只能选择装入/不装入,所以是01背包。
  • 01背包的问题形式:凑够目标和target ——(能否)凑够target,凑target有几种方式
# dp[i][j] 代表对于物品i,背包容量为j时能承载的最大价值
for i in range(num_items):
	for j in range(capacity):
		dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + val[i])  # 选择装物品i或者不装物品i(伪码,未做防止越界的处理
return dp[-1][-1]

9.分割等和子集(01背包问题)

抽象成可装载重量为 sum / 2 的背包,每个物品的重量为 nums[i],在 sum/2 的前提下,尽量往里装最多的数字,如果恰好能为sum / 2则满足题意

class Solution {
    public boolean canPartition(int[] nums) {
        // 先求和
        int sum = Arrays.stream(nums).sum();
        if ((sum & 1) == 1) {
            return false;
        }
        int target = sum / 2;
        int n = nums.length;
        // 每个元素是有限的,dp[i][j]代表能否用前i个元素凑出和j,可以短路
        boolean[][] dp = new boolean[n][target + 1];
        // 初始化
        for (int i = 0; i < n; i++) {
            dp[i][0] = true;
        }
        for (int j = 1; j <= target; j++) {
            dp[0][j] = (j == nums[0]);
        }
        // 初始化短路
        if (dp[0][target]) {
            return true;
        }
        // 填表
        for (int i = 1; i < n; i++) {
            for (int j = 1; j <= target; j++) {
                // 不用当前元素
                dp[i][j] = dp[i - 1][j];
                // 用当前元素
                if (j - nums[i] >= 0) {
                    dp[i][j] |= dp[i - 1][j - nums[i]];
                }
                // 完成,看当前是否短路
                if (j == target && dp[i][j]) {
                    return true;
                }
            }
        }
        return dp[n - 1][target];
    }
}

可以进一步优化空间:因为每行在填写时只使用上一行dp,所以dp只保留一行即可

10.凑硬币II(完全背包问题)

  • 处理背包问题一定要注意dp数组的定义,不要少定义下标
  • 完全背包问题的每种物品数量无限
  • 这道题同样可以对dp数组进行空间优化
class Solution {
    public int change(int amount, int[] coins) {
        // dp[i][j]代表用前i个硬币凑出面额j的组合数
        int n = coins.length;
        int[][] dp = new int[n][amount + 1];
        // 初始化
        for (int i = 0; i < n; i++) {
            dp[i][0] = 1;
        }
        for (int j = 0; j <= amount; j++) {
            if (j >= coins[0] && dp[0][j - coins[0]] == 1) {
                dp[0][j] = 1;
            }
        }
        // 填表
        for (int i = 1; i < n; i++) {
            for (int j = 1; j <= amount; j++) {
                // 不用当前硬币
                dp[i][j] = dp[i - 1][j];
                // 用当前硬币
                if (j >= coins[i] && dp[i][j - coins[i]] > 0) {
                    dp[i][j] += dp[i][j - coins[i]];
                }
            }
        }
        return dp[n - 1][amount];
    }
}

11.目标和(01背包问题)

给你一个整数数组 nums 和一个整数 target 。 向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个
表达式 : 例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式
“+2-1” 。 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

class Solution(object):
    def findTargetSumWays(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        # dp[i][j]:前i个数字加减 得到总和j 的方法数目
        # dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]
        """!!!!数组的列范围 要开到[-sum(nums), sum(nums)]而非[-target, target]!!!!"""
        # 特判
        summary = sum(nums)
        if summary < target:
            return 0

        n = len(nums)
        dp = [[0 for _ in range(2 * summary + 1)] for _ in range(n)]  # 包含负数,下标target处为0

        # 初始化首行
        for j in range(2 * summary + 1):
            if nums[0] == 0:
                dp[0][summary] = 2
                break
            else:
                real_val = j - summary
                if nums[0] == real_val or -nums[0] == real_val:
                    dp[0][j] = 1

        # 填表
        for i in range(1, n):
            for j in range(2 * summary + 1):
                minus = j - nums[i]
                plus = j + nums[i]
                dp[i][j] += dp[i - 1][minus] if minus >= 0 else dp[i - 1][0]
                dp[i][j] += dp[i - 1][plus] if plus <= 2 * summary else dp[i - 1][0]

        return dp[-1][summary + target]

12.n个骰子的点数

  • 把n个骰子扔在地上,所有骰子朝上一面的点数之和为s
  • 输入n,打印出s的所有可能的值出现的概率
class Solution {
    public double[] dicesProbability(int n) {
        int maxVal = n * 6;
        double[][] dp = new double[n + 1][maxVal + 1]; // dp[i][j]代表用i个扔出j的概率
        // 初始化首行
        for (int i = 1; i <= 6; i++) {
            dp[1][i] = 1.0 / 6;
        }
        // 填表
        for (int i = 2; i <= n; i++) {
            for (int j = i; j <= 6 * i; j++) {
                for (int k = 1; k <= 6; k++) {  // 当前可能扔出1~6
                    if (j - k >= i - 1) {
                        dp[i][j] += dp[i - 1][j - k] / 6;
                    }
                }
            }
        }
        return Arrays.copyOfRange(dp[n], n, maxVal + 1);
    }
}

13.前 n 个数字二进制中 1 的个数

class Solution {
    public int[] countBits(int n) {
        int[] result = new int[n + 1];
        result[0] = 0;
        if (n == 0) {
            return result;
        }
        result[1] = 1;
        for (int i = 2; i <= n; i++) {
            if (i % 2 == 1) {
                result[i] = result[i - 1] + 1;  // 奇数,相当于result[i - 1]再加1
            } else {
                result[i] = result[i / 2];  // 偶数,相当于result[i / 2]左移一位
            }
        }
        return result;
    }
}

14.旅行的最低票价

在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。

火车票有 三种不同的销售方式 :
一张 为期一天 的通行证售价为 costs[0] 美元;
一张 为期七天 的通行证售价为 costs[1] 美元;
一张 为期三十天 的通行证售价为 costs[2] 美元。
通行证允许数天无限制的旅行。 例如,如果我们在第 2 天获得一张 为期 7 天 的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。

返回 你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费 。

class Solution {
    public int mincostTickets(int[] days, int[] costs) {
        // 定义dp[i]为结束第i天旅行的最小花费,dp[i]单调不减
        int lastDay = days[days.length - 1];
        Set<Integer> daysSet = Arrays.stream(days).boxed().collect(Collectors.toSet());
        int[] dp = new int[lastDay + 1];
        for (int i = 1; i <= lastDay; i++) {
            if (daysSet.contains(i)) {
                // 当天需要旅行,选择买哪种票【因为dp[i]单调不减,所以不去检查中间情况,例如i - 6, i - 18...】
                int a = dp[i - 1] + costs[0];
                int b = (i - 7 >= 0) ? (dp[i - 7] + costs[1]) : costs[1];
                int c = (i - 30 >= 0) ? (dp[i - 30] + costs[2]) : costs[2];
                dp[i] = Arrays.stream(new int[]{a, b, c}).min().getAsInt();
            } else {
                // 当天不需要旅行
                dp[i] = dp[i - 1];
            }
        }
        return dp[lastDay];
    }
}

15.构建乘积数组

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

  • 双指针+DP
class Solution {
    public int[] constructArr(int[] a) {
        if (a.length == 0) {
            return a;
        }
        int[] l = new int[a.length];  // l[i]代表左侧从idx=0累乘到idx=i-1
        int[] r = new int[a.length];
        l[0] = 1;
        r[a.length - 1] = 1;
        for (int i = 1; i < a.length; i++) {
            l[i] = l[i - 1] * a[i - 1];
        }
        for (int j = a.length - 2; j >= 0; j--) {
            r[j] = r[j + 1] * a[j + 1];
        }
        // 综合结果 res[i] = l[i] * r[i]
        for (int k = 0; k < a.length; k++) {
            l[k] = l[k] * r[k];
        }
        return l;
    }
}

16.包含.*的正则表达式匹配

class Solution {
    public boolean isMatch(String s, String p) {
        // 状态设计:dp[i][j]表示s的前i个字符和p的前j个字符是否匹配
        int m = s.length();
        int n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];
        for (int i = 0; i <= m ; i++) {
            for (int j = 0; j <= n; j++) {
                // 特例:空模式
                if (j == 0) {
                    dp[i][j] = (i == 0);
                    continue;
                }
                if ((i >= 1) && (s.charAt(i - 1) == p.charAt(j - 1) || p.charAt(j - 1) == '.')) {  // 1.当前位置可以直接匹配
                    dp[i][j] = dp[i - 1][j - 1];
                } else if (p.charAt(j - 1) == '*') {  // 3.当前位置不能直接匹配,且为*,需要查看前面的匹配情况
                    // 将x*纳入匹配
                    if (i >= 1 && j >= 2 && (p.charAt(j - 2) == '.' || (p.charAt(j - 2) == s.charAt(i - 1)))) {
                        dp[i][j] = dp[i - 1][j];
                    }
                    // 不将x*纳入匹配
                    if (j >= 2) {
                        dp[i][j] |= dp[i][j - 2];
                    }
                } else {  // 4.当前位置不能直接匹配,且不为*,匹配失败
                    dp[i][j] = false;
                }
            }
        }
        return dp[m][n];
    }
}

17.最长有效括号序列

添加链接描述

给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度

  • 注意两个合法序列并排的情况
  • 二维DP做法(时间复杂度O(n3),超时)
    • 情况1:一个大括号把之前的合法串包围了
    • 情况2:两个合法串并排
class Solution {
    public int longestValidParentheses(String s) {
        // dp[i][j]代表从i到j是否是合法的
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        // 初始化
        int result = 0;
        for (int i = 0; i < n - 1; i++) {
            if (s.charAt(i) == '(' && s.charAt(i + 1) == ')') {
                dp[i][i + 1] = true;
                result = 2;
            }
        }
        for (int i = 2; i < n; i++) {
            int x = 0;
            int y = i;
            while (y < n) {
                // 情况1:大括号包围
                dp[x][y] |= dp[x + 1][y - 1] && s.charAt(x) == '(' && s.charAt(y) == ')';
                // 情况2:搜索有无dp[x][j]和dp[j + 1][y]同时成立
                for (int j = x + 1; j <= y - 2; j = j + 2) {
                    if (dp[x][j] && dp[j + 1][y]) {
                        dp[x][y] = true;
                        break;
                    }
                }
                if (dp[x][y]) {
                    result = Math.max(y - x + 1, result);
                }
                x++;
                y++;
            }
        }
        return result;
    }
}
  • 一维DP做法:dp[i] 代表包含 s[i] 的最长合法括号长度
    • 情况0:当前元素为(,一定不能组成合法序列
    • 情况1:当前元素为),前一个元素为(,直接合并,并加上 dp[i - 2] 的合法长度
    • 情况2:当前元素为),前一个元素也为),检验前面是否有(能与当前位置闭合,并加上 dp[i - dp[i - 1] - 2] 的合法长度
    • 情况3:当前元素为),没有合法匹配
class Solution {
    public int longestValidParentheses(String s) {
        // 一维dp,dp[i]代表包含s[i]的最长有效括号长度
        int n = s.length();
        if (n < 2) {
            return 0;
        }
        int[] dp = new int[n];
        dp[0] = 0;
        int result = 0;
        for (int i = 1; i < n; i++) {
            if (s.charAt(i) == '(') {
                dp[i] = 0;
            } else {
                if (s.charAt(i - 1) == '(') {  // 1.左右括号立刻合并,尝试两个合法串并排
                    dp[i] = 2 + (i >= 2 ? dp[i - 2] : 0);
                } else if (dp[i - 1] > 0 && (i - dp[i - 1] - 1) >= 0 && s.charAt(i - dp[i - 1] - 1) == '(') {  // 2.没有立刻合并:大括号包围,尝试合法串并排
                    dp[i] = dp[i - 1] + 2 + (i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] : 0);
                } else {  // 3.没有合法匹配
                    dp[i] = 0;
                }
            }
            result = Math.max(dp[i], result);
        }
        return result;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值