动态规划是一种自底向上的算法,通常用于解决最大、最小等最值问题。
能使用动态规划解决的问题,一定具备:
- 重叠子问题:和暴力搜索不同,需要记录子问题的解,避免重复求解(剪枝)
- 最优子结构:子问题达到最值,整体才能达到最值,即以小见大
- 状态转移方程:在每个“状态”做出的“选择”会到达什么“状态”
然后就以合适的顺序填表,穷举所有情况并求最值即可
整体流程:明确 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;
}
}