动态规划-最长
Abstract
base - 重复子问题、状态转移
// 509. 斐波那契数 -- 重复子问题 f(20) = f(19) + f(18) , f(19)拆解又需要重新解f(18), 用memo/dp数组来记住字问题的解,需要求解一个子问题时,先去看看适否以及有人解过了。
// 自顶向下(递归返回)、自低向上(循环迭代)、空间约简(只记录前两个状态)
int fib(int n);
// 322. 零钱兑换 -- 状态转移-暴力递归(自顶向下), for循环(自底向上)
// k=1, 2, 5 求amount=11, 可拆解为三个子问题取min, min(a=10, a=9, a=6), 这三个方案+1硬币都能变成原文题的一个方案 (最优子结构 <- 子问题独立 <-硬币数量没有限制)
// dp(n) 凑出amount为n的最少硬币数
int coinChange(vector<int>& coins, int amount);
advanced - dp 数组初值设置
// 931. 下降路径最小和 -- dp数组-从上到下从左到右,dp[i][j]初值设置为不能取到的最小值10001;
// 最后一行选一个最小值
int minFallingPathSum(vector<vector<int>>& matrix);
// 300.最长递增子序列 - dp[i] 以nums[i]结尾的,拥有最大的长度 递增子序列 的长度
动态规划 - Dynamic Programming, 常见于最值问题解题。
核心是穷举:穷举所有子问题,由子问题 递推 原问题题。需要将原问题拆解为子问题,子问题需要满足相互独立,暴力递归所有子问题的解-> 求最优 ->递推 ->得到原问题得到解
聪明的穷举:动态规划存在重叠子问题,如果暴力求解效率会很低,所以 在穷举所有可能解的时候,可以使用DP table记录已求可能解,避免可能解重复计算。 f ( 19 ) = f ( 18 ) + f ( 17 ) f(19) = f(18) + f(17) f(19)=f(18)+f(17) 自底向上递推可以不用多次求 f ( 18 ) f(18) f(18)
DP三要素:1.状态转移:如何从大变小(由小的信息 + 有效判断条件)得出大的结论、2.边界条件、3.dp数组的定义和填充
典型题目:数组-子序列(连续/不连续)、字符串-子序列(不连续),子串(连续)
Tips:
- 最优子问题 应理解为 对所有子问题的解 求最值。要保证原问题的解 必须 包含在所有子问题中。各个班最高成绩 可以推 全校最高成绩; 各个班最大成绩差 不能推 全校最大成绩差(最大成绩差可会出现在不同的班级)
- 状态转移方程 是在穷举,DP table 是在聪明的穷举
- 子问题相互独立理解:每个科目考最高分,如果每个科目的成绩不相互独立,那其实每个科目都各自求一个最高分,最后无法由各个科目最高分得出总分,因为该状态不可达。
- 一维dp,dp[i] --以item[i]结尾的,有效的,最长XXX的长度
1. DP-Base: 重复子问题、状态转移
509.斐波那契数
509.斐波那契-带你了解重叠子问题, 其没有求最值,严格来说不是动态规划
509.斐波那契 -(通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:
F
(
0
)
=
0
,
F
(
1
)
=
1
F(0) = 0,F(1) = 1
F(0)=0,F(1)=1,
F
(
n
)
=
F
(
n
−
1
)
+
F
(
n
−
2
)
,其中
n
>
1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
F(n)=F(n−1)+F(n−2),其中n>1。给定 n ,请计算 F(n) 。
- 自顶向下(递归返回)、自低向上(循环迭代)、空间约简(只记录前两个状态)
class Solution {
vector<int> _memo;
public:
// 509. 斐波那契数 -- 重复子问题 f(20) = f(19) + f(18) , f(19)拆解又需要重新解f(18), 用memo/dp数组来记住字问题的解,需要求解一个子问题时,先去看看适否以及有人解过了。
// - 自低向上(循环迭代) + 空间约简(只记录前两个状态)
int fib(int n) {
if (n < 2) {
return n;
}
int fi_1 = 1;
int fi_2 = 0;
int res = 0;
for (int i = 2; i < n+1; i++) {
res = fi_1 + fi_2;
fi_2 = fi_1;
fi_1 = res;
}
return res;
}
// 自低向上(循环迭代) + DP tabel
int fib_2(int n) {
_memo.resize(n+1);
if (n < 1) {
return n;
}
_memo[1] = 1;
for (int i = 2; i < n+1; i++) {
_memo[i] = _memo[i-1] + _memo[i-2];
}
return _memo[n];
}
// 自顶向下(递归返回) + DP tabel
int fib_1(int n) {
_memo.resize(n+1);
return helper_1(n);
}
int helper_1(int k) {
if (k <= 1) {
return k;
}
if (_memo[k] != 0) {
return _memo[k];
}
_memo[k] = helper(k-1) + helper(k-2);
return _memo[k];
}
// 自顶向下(递归返回) + 暴力穷举,子问题重复计算
int fib_0(int n) {
return helper(n);
}
int helper_0(int n) {
if (n < 2) {
return n;
}
return helper(n-1) + helper(n-2);
}
};
322.零钱兑换
322.零钱兑换 – 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。
class Solution {
public:
//322. 零钱兑换 -- 状态转移-暴力递归(自顶向下), for循环(自底向上)
//k=1, 2, 5 求amount=11, 可拆解为三个子问题取min, min(a=10, a=9, a=6), 这三个方案+1硬币都能变成原文题的一个方案 (最优子结构 <- 子问题独立 <-硬币数量没有限制)
// dp(n) 凑出amount为n的最少硬币数
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1, amount+1); // dp[i] amount = i 需要的最少硬币数, 很烦
dp[0] = 0; // base case 总是很头疼的。
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
// cout << i << ", " << coin << "," << endl;
if (i - coin >= 0 && dp[i - coin] != amount+1) {
// cout << i << ", " << coin << "," << dp[i-coin] << endl;
dp[i] = min(dp[i], dp[i-coin]+1);
}
}
}
return dp[amount] != amount+1 ? dp[amount]: -1;
}
};
2. DP-Advanced: DP数组初值设置
931. 下降路径最小和 - 数
931.下降路径最小和 – 给你一个 n x n
的 方形 整数数组 matrix
,请你找出并返回通过 matrix
的下降路径 的 最小和 。 下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col)
的下一个元素应当是 (row + 1, col - 1)
、(row + 1, col)
或者 (row + 1, col + 1)
。
class Solution {
public:
// 迭代求解,自底向上
int minFallingPathSum(vector<vector<int>>& matrix) {
int n = matrix.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
copy(matrix[0].begin(), matrix[0].end(), dp[0].begin()); // base case
for (int i = 1; i < n; i++) {
for (int j = 0 ; j < n; j++) {
int min_pre = 10001; // 单元 100 * 行数100 累计最大值
int offsets[3][2] = {{-1, -1,}, {-1, 0}, {-1, 1}};
for (auto& offset:offsets) {
int pre_i = i + offset[0], pre_j = j + offset[1];
if (pre_i < 0 || pre_i >= n || pre_j < 0 || pre_j >= n) {
continue;
}
min_pre = min(min_pre, dp[pre_i][pre_j]);
}
dp[i][j] = min_pre + matrix[i][j]; // 前路最短 + 走到[i][j]的增量
// cout << "i: " << i << ", j: " << j << ", dp: " << dp[i][j] << endl;
}
}
int res = 10001;
for (int j = 0; j < n; j++) {
res = min(res, dp[n-1][j]);
}
return res;
// int res = 10001;
// for (int j = 0; j < _n; j++) {
// res = min(res, helper(_m-1, j, matrix));
// }
// return res;
}
// 递归求解 自顶向下
int helper(int row, int col, vector<vector<int>>& matrix) {
// cout << row << ", " << col << endl;
if (row < 0 || col < 0 || row > _m - 1 || col > _n -1 ) {
// cout << "a: "<< row << ", " << col << endl;
return 10001; // 取不到的位置,返回无法娶到的值
}
if (row == 0) {
// cout << "b: "<< row << ", " << col << endl;
return matrix[row][col];
}
int tmp = 10001;
int offsets[3][2] = {{-1, -1}, {-1, 0}, {-1, 1}};
for (auto& offset : offsets) {
int pre_row = row + offset[0];
int pre_col = col + offset[1];
tmp = min(tmp, helper(pre_row, pre_col, matrix) + matrix[row][col]);
// cout << "c: "<< row << ", " << col << "," << tmp << endl;
}
return tmp;
}
};
300. 最长递增子序列 - 长度
300.最长递增子序 - [longest-increasing-subsequence] 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
class Solution {
public:
// 数组无序,nums[i] = 8, 可以接在nums[i-1] = 2后,也能接在nums[i-2] = 6 后面,明显 nums[i-2] = 6 能够构成的递增子序列潜力大一些,因为无序,所以我们要遍历一下 nums[i] 前面的所有潜在候选对象。
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 1);
int res = 1;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if(nums[i] > nums[j]) { // nume[i]接 在每个现成的递增子序列后,从有望构成新递增子序列里,找出最大的
dp[i] = max(dp[i], dp[j] + 1);
}
}
res = max(res, dp[i]);
}
return res;
}
};
53. 最大子数组和 - 数
53.最大子数组和 - 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。
dp[i] = max(nums[i], nums[i] + dp[i-1]), nums[i]
- 自成一派:说明nums[i] > nums[i] + dp[i-1] , 即 dp[i-1] < 0, nums[i-1]成份被舍弃了,这些成分只有副作用,不要也罢,nums[i]开启新征程,往下去找一找
- 建立连结:说明nums[i] < nums[i] + dp[i-1], 即nums[i]<0,
如果nums[i]不太小(nums[i] + dp[i-1]>0), 还是有希望numi[i+1]能够打平numi[i]带来的副作用,
如果nums[i] 太小了(nums[i] + dp[i-1]>0), 在nume[i+1] 决策时,会直接把numi[i]成分舍弃。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 0); // dp[i] 以 dp[i] 结尾的连续子数组的最大和
dp[0] = nums[0];
int res = dp[0];
for (int i = 1; i < n; i++) {
if (nums[i] + dp[i-1] > nums[i]) {
dp[i] = dp[i-1] + nums[i];
} else {
dp[i] = nums[i];
}
// cout << dp[i-1] << ", ";
res = max(res, dp[i]);
}
// cout << dp[n-1] << endl;
return res;
}
};
1143. 最长公共子序列 - 长度
1143.最长公共子序列 – 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
(两个字符串,二维dp 才是标配嘛)
dp[i][j] 表示s1[0]-s1[i] 于s2[0]-s2[j] 的最长公共子序序列,更新形式于512题类似,只不过初值和方向不大一样,初值为第0行第0列均为0,方向由上到下,由左到右。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.size(), n = text2.size();
vector<vector<int>> dp(m+1, vector<int>(n+1, 0)); // dp[i][j] t1[0-i] t2[0-j] 公共子序列的长度;
for (int i = 1; i < m + 1; i++) {
for (int j = 1; j < n + 1; j++) {
if (text1[i-1] == text2[j-1]) { // 对角线传输,s_1[i-1] s_2[j-1]同为公共子序列一部分
dp[i][j] = dp[i-1][j-1] + 1;
} else { // 边传输,s_1[i-1] s_2[j-1]其一 or none 为公共子序列的一部分
// dp[i-1][j] dp[i][j-1] 其一是通过对角线操作来的话,那么s_1[i-1] s_2[j-1] 其一是公共子序列的一部分
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
};
// 最长公共子序 - 回溯法去找是谁
def longestCommonSubsequence(self, text1, text2):
l1, l2 = len(text1), len(text2)
dp = [[0] * (l2 + 1) for _ in range(l1 + 1)]
for i in range(1, l1 + 1):
for j in range(1, l2 + 1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# return dp[-1][-1]
# 回溯 只有加
res_str=[""]*dp[-1][-1]
i,j=l1,l2
while(i>0):
if dp[i][j]>dp[i-1][j]: # 比上面的大,不是来上面
if dp[i][j]>dp[i][j-1]: # 比左边的大,不是来自左边
res_str[dp[i][j]-1]=text1[i-1] # 来自对角线操作
else: # 没有左边大,来自左边 横坐标操作
i+=1 # i 需要先+1,最后的-1会抵消
j-=1 # 没有左边大,来自左边,纵坐标操作
i-=1
print(res_str)
—20240118—
3.
32. 最长有效括号–子串
题目:给定一个只包含 ‘(’ 和 ‘)’ 的字符串s,找出最长的包含有效括号的子串的长度。
套用:最长回文子串 + 改造 NO!!!!
dp[i][j] 更新
dp[i+1][j-1] == True and dp[i] == “(” and dp[j] == “)”
dp[i][j-1] == Flase and dp[i] == “(” and dp[j] ==“)”
状态转移不像 最长回文子串dp[i+1][j-1] -> dp[i][j] 中间子串仅需考虑是/否 回文,s[i] s[j] 单独加一个都不会影响中间的回文状态
dp[i+1][j-1] = False and dp[i] == “(” and dp[j] == “)” 但是dp[i][j] 的状态更新不一致
“( ( )”
“( )( )”
# 1.s[i]==")" and s[i-1]=="(" dp[i] = dp[i-2]+2
# 2.s[i]==")" and s[i-1]==")" dp[i] = dp[i-1] + dp[i-dp[i-1]-2]+2 下标的合理性
# ((xx)) dp[i-1] 也有效的情况下
def longestParentheses(self, s: str) -> int:
"""
@brief 当s[i] == ")", 考虑所有会新增闭合情况
dp[i] = dp[i-1] + 2 <= s[i] == ")" and s[i-1] == "("
dp[i] = dp[k-1] + dp[i-1] + 2 <= s[i] == ")" and s[k] == "(" and s[i-1] == ")"
s[i] == ")" 可能新增闭合括号, 附加s[k] == "(" 可以形成有效括号
s[i-1] == ")" 可能是有效括号的一部分
如果 dp[i-1]表示的有效括号,不包含 s[k] "(", 就明确会新增闭合括号
@note 注意边界条件
"""
n = len(s)
dp = [0] * n
res = 0
for i in range(1, n):
if s[i] == ")" and s[i-1] == "(":
dp[i] = dp[i-2] + 2
elif s[i] == ")" and s[i-1] == ")":
k = i - 1 - dp[i-1]
if k >= 0 and s[k] == "(":
dp[i] = dp[i-1] + 2
if k - 1 >= 0:
dp[i] += dp[k - 1]
res = max(res, dp[i])
return res
2.维度d
32. 最长回文子串–是什么
状态转移:dp[i][j] = s[i]==s[j] and dp[i+1][j-1]
边界条件:主对角线,副对角线
dp数组含义:dp[i][j] 表示s[i]-s[j]子串是否是回文子串
dp数组更新方向:状态转移决定了dp初始化元素(对角线)和更新的方向(由下向上,由对角线往右)
(独自一个人也,也可以二维dp, 强!二维dp 遍历所有子串,子串左右扩展)
def longestPalindrome(self, s):
n = len(s)
if n < 2: # 空字符和单个字符直接输出
return s
dp = [[False] * n for _ in range(n)]
for i in range(n):
dp[i][i] = True
res_str, res_len = s[0], 1 # 两个字符的的时候,不相等时结果为单个字符
for i in range(n-2,-1,-1):
for j in range(i+1, n):
if j == i+1: # 次对角线上的元素单独判断。
if s[i] == s[j]:
dp[i][j] = True
if j-i+1 > res_len:
res_str = s[i:j+1]
res_len = j - i + 1
else:
if s[i] == s[j] and dp[i+1][j-1]:
dp[i][j] = True
if j-i+1 > res_len:
res_str = s[i:j+1]
res_len = j - i + 1
return res_str
中心扩展法:
class Solution {
public:
string palindrome(string s, int left, int right) {
int n = s.length();
while(left >= 0 && right < n && s[left] == s[right]) {
left -= 1;
right += 1;
}
// std::cout << s << "," << left << "," << right << "," << endl;
return s.substr(left+1, right - 1 - left); // substr 位置和长度
}
string longestPalindrome(string s) {
int n = s.length();
string res = "";
for (int i = 0; i < n; i++) {
// std::cout << "i1-" << i << endl;
string s1 = palindrome(s, i, i);
// std::cout << "i2-" << i << endl;
string s2 = palindrome(s, i, i+1);
// std::cout << "i3-" << i << "," << s1 << "," << s2 << endl;
res = res.length() > s1.length() ? res : s1;
res = res.length() > s2.length() ? res : s2;
}
return res;
}
};
512. 最长回文子序列–长度
和上题的基本思路一样,不过dp数组表示的含义变为
dp[i][j] 表示s[i]-s[j]子串中回文序列的长度
# if s[i]==s[j]:dp[i][j] = dp[i+1][j-i]+2,
# if s[i]!=s[j]:dp[i][j] = max(dp[i+1][j],dp[i][j-1])
def longestPalindromeSubseq(self, s):
n = len(s)
if n < 2:
return n
dp = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for i in range(n-2, -1, -1):
for j in range(i+1,n):
if s[j] == s[i]:
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
return dp[0][n-1]
128. 最长连续序列–长度
给定一个未排序的整数数组,找出最长连续序列的长度。要求算法的时间复杂度为 O(n)。
数组本身无序,连续序列有序,只要能找到连续序列的开头,就能确定序列长度,迭代更新最长长度就可以了。
def longestConsecutive(self, nums):
if not nums:
return 0
res = 1
nums_set = set(nums)
for val in nums_set:
if val - 1 not in nums_set: # val 为连续序列的开头
count = 1
num = val
while(num + 1 in nums_set):
count += 1
num = num + 1
res = max(res, count)
return res
14. 最长公共前缀-字符串
暴力法:纵向扫描
先验:最长公共前缀不回比最短的字符串长,所以先求出最短的长度。
def longestCommonPrefix(self, strs):
"""
:type strs: List[str]
:rtype: str
"""
n = len(strs)
if n == 0:
return ""
min_l = float("INF")
for string in strs:
min_l = min(len(string), min_l)
i = 0
con_pre = ""
while(i < min_l):
con_pre = strs[0][:i+1]
for string in strs:
if string[:i+1] != con_pre:
return con_pre[:i]
i += 1
return con_pre
剑指offer-48 最长不含重复字符串的子字符串-长度
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
移动窗口,保证窗口内的字符无重复,如果有重复就缩小窗口。
def lengthOfLongestSubstring(self, s):
"""
:type s: str
:rtype: int
"""
win = {}
left, right = 0, 0
n = len(s)
res = 0
while (right < n):
c1 = s[right]
if win.get(c1):
win[c1] += 1
else:
win[c1] = 1
right += 1
while(win[c1]>1):
c2 = s[left]
win[c2] -=1
left += 1
res = max(res, right - left)
return res