适用问题
常用于寻找符合某种条件的子序列的最优解。
思路
枚举数组长度为1,2…时的解,找出递推关系式,注意找当前状态与之前状态的联系,即可求解。
子序列最优解是一个值或者最优子序列(如最长回文子串),dp[i]表示输入数组长度为i时的最优解。
类型
(1) 对子序列顺序做限制,比如连续子序列的最大和/最大乘积,子序列元素必须是间隔1的元素等。
用dp[i]表示包含当前元素num[i]的局部最优解,最后从整个dp数组找全局最优。
House Robber: 给定数组表示抢劫每个房子的收益,限制抢劫的房子不能相邻,要求抢劫的最大收益是多少。
Example:
Input: [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.
[2,1,1,2] -->2, + 2=4
//对于限定子序列顺序的问题(如连续子序列,子序列元素不能相邻),直接用dp[i]表示表示待求解的状态是不可行的,
//因为当前的最优解之前的最优解并没有直接的联系,当前的最优解可能需要重新组合之前的序列。这是应该给dp加限定条件,
//用dp[i]表示包含当前元素nums[i]的最优解,也就是局部最优,最后全局最优从整个dp中找。
//类似题目:最大和子序列。
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.empty()) return 0;
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
for (int i = 1; i < nums.size(); i++)
{
if(i>=2) dp[i] = dp[i-2];
if(i>=3) dp[i] = max(dp[i], dp[i-3]);
dp[i] += nums[i];
}
int res = dp[0];
for (int i = 1; i < dp.size(); i++) res = max(res, dp[i]);
return res;
}
};
5. Longest Palindromic Substring 最长回文字串:
Example 1:
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.
这次dp[i]不是包含以当前元素为结尾的子序列,而是包含以当前元素为中心的子序列。(循环次数相当于减半,如果使用一概元素为结尾
会time limited)
class Solution {
public:
string longestPalindrome(string s) {
if (s.empty()) return "";
vector<string> dp(s.size(), "");
for (int i = 0; i < s.size(); i++)
{
dp[i] = longest(s, i);
}
string res = "";
for (int i = 0; i < dp.size(); i++) res = res.length() < dp[i].length() ? dp[i] : res;
return res;
}
string longest(const string &s, int i)
{
//考虑到以该元素为中心的偶数长度子串和奇数长度子串。定义左右指针真相两边搜索
int left = i, right = i + 1;
string res = "";
while (left >= 0 && right < s.length() && s[left] == s[right])
{
left--;
right++;
}
//由于left--,right++,所以长度为r-l-1
if (right - left - 1 > 0) res = s.substr(left + 1, right - left - 1);
left = i; right = i;
while (left >= 0 && right < s.length() && s[left] == s[right])
{
left--;
right++;
}
//由于left--,right++,所以长度为r-l-1
if (right - left - 1 > res.length()) res = s.substr(left + 1, right - left - 1);
return res;
}
};
同类型:
最大和子序列 https://leetcode.com/problems/maximum-subarray/
Example:
Input: [-2,1,-3,4,-1,2,1,-5,4],
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.
//dp[i]表示一当前元素结尾的最大和序列。地推:d[i]=max(d[i-1]+num[i], num[i])
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
if (n == 0)return 0;
vector<int> dp(n, 0);
for (int i = 0; i < n; i++)
{
if (i >= 1) dp[i] = max(0, dp[i - 1]);
dp[i] += nums[i];
}
int res = dp[0];
for (int i = 0; i < n; i++) res = max(res, dp[i]);
return res;
}
};
最大乘积子序列 https://leetcode.com/problems/maximum-product-subarray/
(2) 对数组每个元素都是选或不选,如零钱找零,求最少找零数。
需要设置n+1长度的dp数组。
322. Coin Change:
Example 1:
Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1
class Solution {
//容易找到递推关系式: dp[i] = min(dp[i], dp[i-j] + 1), j为零钱面值,i为总钱数。
//将dp初始化为n+1,那么无法找零的dp[i]就是n+1;
public:
int coinChange(vector<int>& coins, int amount) {
if (coins.empty()) return -1;
vector<int> dp(amount + 1, amount+1);
dp[0] = 0;
for (int i = 1; i <= amount; i++)
{
for (int j = 0; j < coins.size(); j++)
{
if (i >= coins[j]) dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
return dp[amount] > amount?-1 : dp[amount];
}
};
(3)输入是 二维数组,dp数组也要设置成二维
64. Minimum Path Sum
Input:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
Output: 7
Explanation: Because the path 1→3→1→1→1 minimizes the sum.
class Solution {
//典型的二维dp问题,递推关系式:d(i,j) = num(i,j) + min{d(i-1,j), d(i,j-1)}
public:
int minPathSum(vector<vector<int>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int m = grid.size(), n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n, INT_MAX));
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
if (i == 0 && j == 0)
{
dp[i][j] = grid[i][j];//因为前面的INT_MAX这里单独处理一下dp[0]0],不大美观。。。
continue;
}
if (i > 0) dp[i][j] = min(dp[i][j], dp[i - 1][j]);
if (j > 0) dp[i][j] = min(dp[i][j], dp[i][j - 1]);
dp[i][j] += grid[i][j];
}
}
return dp.back().back();
}
};
(4)最长非降子序列(不需要连续)
找出最长非降子序列的长度:
[5,3,4,8,6,7] -->
output: 4 ([3,4,6,7])
//dp[i] = max(1, dp[j]+1) if j<=i-1 && num[i]>=num[j]
class Solution {
public:
int core(vector<int> nums)
{
if (nums.empty()) return 0;
int n = nums.size();
vector<int> dp(n, 0);
for (int i = 0; i < n; i++)
{
dp[i] = 1;
if (i >= 1)
{
for (int j = 1; j <= i; j++)
{
if (nums[i] >= nums[i - j]) dp[i] = max(dp[i], dp[i - j] + 1);
}
}
}
return dp.back();
}
};
把字符串切分后能否使是所有字串都能在字典中找到。这个题表面看起来不是求最优子序列的极值,但是在每个位置都有切分不切分两种可能,有点类似于零钱找零,所以我们建立dp数组,表示字符串长度为i时是否可行。
Example 3:
Input: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
Output: false
//先零字符串长度为1,2,3.。找出递归关系,dp[i]表示当前为之前的字符串额能否符合条件,那么递推关系
//为 d[i] = has(str[0:i+1]) || has(str[j+1:i+1]), d[j]>0 && 0<= j
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
if (s.empty()) return true;
if (wordDict.empty()) return false;
unordered_set<string> words(wordDict.begin(), wordDict.end());
int n = s.length();
vector<bool> dp(n, false);
for (int i = 0; i < n; i++)
{
if (words.find(s.substr(0, i + 1)) != words.end())
{
dp[i] = true;
continue;
}
for (int j = i-1; j>=0; j--)
{
if (dp[j] && words.find(s.substr(j + 1, i - j)) != words.end())
{
dp[i] = true;
break;
}
}
}
return dp.back();
}
};
'A' -> 1
'B' -> 2
...
'Z' -> 26
问给定数字字符串有多少种解码结果:
Input: "226"
Output: 3
Explanation: It could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).
思路:每一位置上元素,如果单独解码,当前元素和上一元素的解码组合形成一种解码方式,所以解码数d[i]等于d[i-1];如果和上一位形成两位数解码,那么元素和上二元素的解码组合形成一种解码方式,所以
d[i]+=d[i-2]。当然单独解码时当前位不能是’0‘,两位数解码不能大于’26‘。
class Solution {
public:
int numDecodings(string s) {
int n2 = 0, n1 = 1, n = 0;
for (int i = 0; i < s.length(); i++)
{
if (s[i] != '0') n += n1;
if (i > 0 && (s[i - 1] == '1' || (s[i - 1] == '2'&&s[i] <= '6'))) n += n2;
n2 = n1;
n1 = n;
if (i < s.length() - 1) n = 0;
}
return n;
}
};