动态规划总结
主要是记住几种非常普遍的题型就可以了: 背包、股票、子序列,分割、一维二维问题等,凡是局部最优能够推广到全局最优的情况就可以使用状态转移方程来处理。
1、子数组的最大累加和问题
解题分析:首先子数组的最大累加和问题可以采用很多种方法,一个是动态规划,一个是特殊的移动策略,一个是采用分治的手段(将一个数组分为三部分,一个是完全在左侧,一个是完全在右侧,一个是既有在左侧的部分,也有在右侧的部分)
class Solution {
public:
/**
* max sum of the subarray
* @param arr int整型vector the array
* @return int整型
*/
int maxsumofSubarray(vector<int>& arr) {
// write code here
//这里可以采用分治,也可以采用有一个记录项的方式,还可以采用动态规划来处理问题
int right = 0,sum = 0,tmp = 0;
for(;right<arr.size();++right)
{
tmp += arr[right];
if(tmp>0)
{
sum = max(sum,tmp);
}
else
{
tmp = 0;
}
}
return sum;
}
};
2、最长无重复子数组
给定一个数组arr,返回arr的最长无重复元素子数组的长度,无重复指的是所有数字都不相同。子数组是连续的,比如[1,2,3,4,5]的子数组有[1,2],[2,3,4]等等,但是[1,3,4]不是子数组
解题分析:这个题目真是让我印象深刻啊,主要是要采用滑动窗口和哈希存储记录的方式来处理问题,滑动窗口维护的是两个指针,left 和 right 维护的是这样的区间:在这个区间之内,不存在重复的字符,然后在这样的一个前提之下,右侧的 right 需要向下一个位置进行扩张,然后在原来的区间上引入了一个新的区间,这个时候就要判断两种情况:(1)当前引入的新的字符和原来序列里的字符重复,这个时候就要移动左侧的指针直至再一次没有重复的字符 (2)如果之前出现过新的字符,但是其对应的位置是在 left 左侧而没有出现在目前的区间之内,或者根本就没有出现过新的字符,那么我们就记录当前的最长无重复子数组,推往下一个阶段。同时在这个过程中,我们明白需要频繁的查询对应的字符最新的位置,所以为了更快速的查询和插入,我们引入哈辛map来进行存储和更新信息。
class Solution {
public:
/**
*
* @param arr int整型vector the array
* @return int整型
*/
int maxLength(vector<int>& arr) {
// write code here
//滑动窗口加哈希存储
if(arr.size()<=1) return arr.size();
int left = 0, right = 0, sum = 1;//都从左边开始
unordered_map<int,int>record;
while(right<arr.size())
{
auto it = record.find(arr[right]);
if(it==record.end()||it->second<left)
{
sum = max(sum,right-left+1);
}
else
{
left = it->second+1;
}
record[arr[right]] = right;//这一步插入其实会影响到之前的it的值
++right;
}
return sum;
}
};
3、最长公共子串
给定两个字符串str1和str2,输出两个字符串的最长公共子串 题目保证str1和str2的最长公共子串存在且唯一
解题分析:这道题就是简单的二维情况下的动态规划,两个字符串,拆分为子问题,i,j 位置的下一步拆分,主要就是要考虑初始化的条件,这里我就不再赘述了,直接给出代码。对了这里需要注意的是,这道题求的是子串而不是子序列。
class Solution {
public:
/**
* longest common substring
* @param str1 string字符串 the string
* @param str2 string字符串 the string
* @return string字符串
*/
string LCS(string str1, string str2) {
// write code here
int size1 = str1.length();
int size2 = str2.length();
int index = 0;
int maxlength = 0;
map<char, vector<int>> maps;
for (int cnt1 = 0; cnt1 < size1; cnt1++)
maps[str1[cnt1]].push_back(cnt1);
for(int cnt2 = 0; cnt2 < size2; cnt2++)
{
if (0 == maps.count(str2[cnt2]))continue;
for(auto& cnt1 : maps[str2[cnt2]])
{
int cnt = 1;
for(; ((cnt + cnt1) < size1) && ((cnt + cnt2) < size2); cnt++)
if (str1[cnt1 + cnt] != str2[cnt2 + cnt]) break;
if (cnt > maxlength)
{
index = cnt1;
maxlength = cnt;
}
}
}
return str1.substr(index, maxlength);
}
};
4、最长回文子串
对于一个字符串,请设计一个高效算法,计算其中最长回文子串的长度。给定字符串A以及它的长度n,请返回最长回文子串的长度。
解题分析:这里是有多种方法处理的:参考 leetcode 上的官方资料,可以知道:(1)动态规划(2)中心扩展 (3)Manacher 算法
class Solution {
public:
pair<int, int> expandAroundCenter(const string& s, int left, int right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
--left;
++right;
}
return {left + 1, right - 1};
}
string longestPalindrome(string s) {
int start = 0, end = 0;
for (int i = 0; i < s.size(); ++i) {
auto [left1, right1] = expandAroundCenter(s, i, i);
auto [left2, right2] = expandAroundCenter(s, i, i + 1);
if (right1 - left1 > end - start) {
start = left1;
end = right1;
}
if (right2 - left2 > end - start) {
start = left2;
end = right2;
}
}
return s.substr(start, end - start + 1);
}
};
5、最长上升子序列
给定数组arr,设长度为n,输出arr的最长递增子序列。(如果有多个答案,请输出其中字典序最小的)
解题分析:这种类型的题目一般都是采用动态规划的方式来做的,尤其是最长上升子序列的题目,一般都是会卡你时间复杂度,所以我们一般遇到这个题目,都必须要想到二分法加速的情况,当然本题还有一点特殊,它不是输出长度,而是要把序列打印出来。
class Solution {
public:
/**
* retrun the longest increasing subsequence
* @param arr int整型vector the array
* @return int整型vector
*/
vector<int> LIS(vector<int>& arr) {
if(arr.size() < 2) return arr;
vector<int> dp(arr.size(), 1);
vector<int> maxEnd(1, arr[0]);
for(int i = 1; i < arr.size(); i++) {
if(arr[i] > maxEnd.back()) { // in inc seq
dp[i] = maxEnd.size()+1;
maxEnd.push_back(arr[i]);
} else { // ai < maxEnd
auto pos = std::lower_bound(maxEnd.begin(), maxEnd.end(), arr[i]);
int idx = pos - maxEnd.begin();
maxEnd[idx] = arr[i];
dp[i] = idx + 1;
}
}
int len = maxEnd.size();
vector<int> vres(len);
for(int i = dp.size()-1; i >= 0; --i) {
if(dp[i] == len) {
vres[len-1] = arr[i];
--len;
}
}
return vres;
}
};
6、7 最小路径和,求路径
解题分析:忽略
8、最大正方形
解题分析:典型的二维动态规划问题,在这里主要是设置[i,j]为正方形的右下角,看看它的边长应该满足什么样的条件。
class Solution {
public:
/**
* 最大正方形
* @param matrix char字符型vector<vector<>>
* @return int整型
*/
int solve(vector<vector<char> >& matrix) {
// write code here
if(matrix.size()==0||matrix[0].size()==0) return 0;
int m = matrix.size(), n = matrix[0].size(),max_side = 0;
vector<vector<int>>dp(m+1,vector<int>(n+1,0));
for(int i = 1;i<=m;++i)
{
for(int j = 1;j<=n;++j)
{
if(matrix[i-1][j-1]=='1')
{
dp[i][j] = min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
max_side = max(max_side,dp[i][j]);
}
}
}
return pow(max_side,2);
}
};
9、字符串排列
解题分析:这里当然也可以使用动态规划来处理,但是一般涉及到排列的问题,主要是采用回溯的方法更加简单,当然这里一定要注意去重和剪枝的问题。
class Solution {
public:
vector<string> Permutation(string str) {
//明显的可以采取动态规划或者回溯来处理这个问题
vector<string>ans;
unordered_set<string>record;
back_tracking(ans,record,str,0);
sort(ans.begin(), ans.end());
return ans;
}
void back_tracking(vector<string>&ans,unordered_set<string>&record,string &str,int level)
{
if(level==str.length())
{
auto it = record.find(str);
if(it==record.end())
{
record.insert(str);
ans.push_back(str);
}
return;
}
else
{
for(int i = level;i<str.length();++i)
{
swap(str[level],str[i]);
back_tracking(ans,record, str, level+1);
swap(str[level],str[i]);
}
}
}
};
10、股票问题和背包问题
解题分析:题库中没有,但是我们需要更加深入的理解,这些内容我是放在我的leetcode的专栏进行详细的分析和拓展。