目录
最长无重复子串333
记录每种字符最近一次出现的位置
dp[i]表示以第i元素结尾的最长无重复子串,则
dp[i]=min(dp[i-1]+1,i-last_pos); //其中last_pos为第i元素上次出现的位置
class Solution {
public:
int lengthOfLongestSubstring(string s) {
vector<int>character_pos(256,-1);
//dp[i] 以第i个字符结尾的最长长度
vector<int>dp(s.size(),0);
int ret=0;
for(int i=0;i<s.size();++i){
if(i==0) dp[i]=1;
else{
int last_pos = character_pos[s[i]];
//该字符尚未出现过
if(last_pos == -1){
dp[i]=dp[i-1]+1;
}else{
dp[i]=min(dp[i-1]+1,i-last_pos);
}
}
character_pos[s[i]]=i;
if(dp[i]>ret)ret=dp[i];
}
return ret;
}
};
最长有效括号序列333
class Solution {
public:
int longestValidParentheses(string s) {
int m =s.size();
if(m<=1) return 0;
int longest = 0;
vector<int> dp(m,0);
int left=0;
for(int i=0; i< m;++i){
//由于当前左括号数目>0 故当前i位置以及i-1位置一定能匹配到
if(s[i] == ')' && left>0){
dp[i]=dp[i-1]+2;
if(i>dp[i]){
dp[i]+=dp[i-dp[i]];
}
--left;
}else if(s[i]=='('){
++left;
}
longest = max(longest,dp[i]);
}
return longest;
}
};
最长递增子序列333
Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.
最优解法
###状态定义###
数组tail,tail[k]代表长度为k+1的递增子序列中末尾元素最小的那个子序列的尾元素。
易得:tail数组是递增数组,因此对于一个新元素a,可利用二分找到其在tail中最后一个小于a的位置tail[j],则tail[j]与a可形成一个{… tail[j],a}的长度为j+2的新的递增子序列,由于tail[j+1]代表的长度为j+2的递增子序列的尾元素为tail[j+1],根据tail定义,有状态更新方程:
tail[j+1]=min{tail[j+1],a};
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size() <=1 ) return nums.size();
//tail[i]记录长度为i+1的递增序列中末尾元素最小的那个序列的末尾元素值
//则tail[0]...tail[i] 为递增的,因为如果tail[k] > tail[k+1] ,则tail[k+1]为末尾元素形成的k+2长度的递增序列中的第k+1个元素完全可以替换tail[k].
//或者说长度为k+1的递增序列{a}必然包含一个长度为k的递增序列{b},则{b}的末尾值必然<{a}的末尾值,从而必然使得tail[k]<tail[k+1]
std::vector<int>tails;
tails.push_back(nums[0]);
for(int i=0; i<nums.size();++i){
//当前数字比最长序列的末尾元素要大,则形成一个新的tail.size()+1长度的递增序列
if(nums[i] > tails.back()){
tails.push_back(nums[i]);
}else{
//找到第一个末尾值比 nums[i] 大的递增序列{a},假设长度为k,其末尾元素值为tail[k].
//则nums[i] 比长度为[k-1]递增序列的末尾元素值tail[k-1]大,即nums[i]与tail[k-1]可形成一个长度为k的递增序列{b}。
//此时长度为k的递增序列{a},{b}的末尾元素值分别为tail[k],nums[i]
//根据定义,更新tail[k]= min(tail[k],nums[i]);
//此处利用二分查找法找到第一个大于等于nums[i]的位置
auto it=lower_bound(tails.begin(),tails.end(),nums[i]);
if(it!=tails.end()){
*it=min(*it,nums[i]);
}
}
}
return tails.size();
}
};
俄罗斯套娃
此题是最长递增子序列的变种。思路如下:
由于信封长和宽均需要严格大于被套的信封。
先将原信封按照长度由小到达排序
原序列:[1,3] [3,2] [3,5] [3,6] [4,8]
排序后:[1,3] [3,2] [3,5] [3,6] [4,8]
此时我们应该得到宽度的某个最长递增子序列。
但是由于长度相同的信封,在这个递增子序列中只能出现一次。因此为了使得长度相同的信封在宽度递增子序列中最多只会被选取一次。
可以将同长度信封的宽度按照逆序排序。即:
[1,3] [3,6] [3,5] [3,2] [4,8]
这样由于长度相同的信封的宽度是逆序排序,则求宽度递增子序列时,同长度信封肯定最多只会
被选到一次。
最后再按照一维LIS求解方法求宽度最长递增子序列。
int maxEnvelopes(vector<vector<int>>& envelopes) {
if(envelopes.empty()) return 0;
auto cmp=[](vector<int>&a,vector<int>&b){
if(a[0]<b[0]) return true; //长度小的信封排在前面
else if(a[0]==b[0]){
return a[1]>b[1]; //同长度信封按照宽度逆序排序
}else{
return false;
}
};
sort(envelopes.begin(),envelopes.end(),cmp);
//按照LIS思路求信封宽度的最长递增子序列
vector<int>tails;
tails.push_back(envelopes[0][1]);
for(auto &e:envelopes){
if(e[1]>tails.back()){
tails.push_back(e[1]);
}else{
auto it=lower_bound(tails.begin(),tails.end(),e[1]);
*it=min(*it,e[1]);
}
}
return tails.size();
}
长度为3的递增序列
思路与LIS相同,只是最后判断最大长度是否>=3
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
//
vector<int>tail ;//tail[k] 表示长度为K+1且尾元素最小的那个递增序列。
//故tail[0] tail[1]...tail[k]是递增的
for(int i=0;i<nums.size();i++){
if(tail.empty())tail.push_back(nums[i]);
else{
if(nums[i]>tail.back()){
//nums[i] 可与tail的最后一个代表的递增子序列形成新的递增子序列
tail.push_back(nums[i]);
}else if(nums[i]<tail[0]){
tail[0]=nums[i];
}else if(tail.size()>=2 && nums[i]>tail[0]&&nums[i]<tail[1]){
tail[1]=nums[i];
}
}
if(tail.size()>=3)return true;
}
return false;
}
};
最长波动序列
int wiggleMaxLength(vector<int>& nums) {
if(nums.size() <=1) return nums.size();
if(nums.size() == 2){
if(nums[0] == nums[1]) return 1;
else return 2;
}
//dp_p[i] 最多到nums[i],且最后两个元素为递增所能形成的最长波动序列
vector<int>dp_p(nums.size(),0);
//dp_n[i] 最多到nums[i],且最后两个元素为递减所能形成的最长波动序列
vector<int>dp_n(nums.size(),0);
dp_p[0] = dp_n[0] =1;
for(int i=1; i<nums.size();++i){
if(nums[i]>nums[i-1]){
//此时最后两个元素能形成一个向上的波动
dp_p[i] = dp_n[i-1]+1;
dp_n[i] = dp_n[i-1];
}else if(nums[i]<nums[i-1]){
//此时最后两个元素能形成一个向下的波动
dp_p[i] = dp_p[i-1];
dp_n[i] = dp_p[i-1]+1;
}else{
dp_p[i]=dp_p[i-1];
dp_n[i]=dp_n[i-1];
}
}
int ret=0;
for(int i=0;i<nums.size();++i){
ret =max(ret,dp_p[i]);
ret = max(ret,dp_n[i]);
}
return ret;
}
最大连续子数组和333
dp[i]表示以第num[i]元素结尾的连续子数组的最大和
则 dp[i]=max{dp[i-1]+num[i],num[i]};
则最终结果为dp数组的最大值
class Solution {
public:
int maxSubArray(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){
dp[i] = max(nums[i],nums[i]+dp[i-1]);
}
return *max_element(dp.begin(),dp.end());
}
};
5.乘积最大连续子数组
与4.类似,但是乘法负数乘以负数可以为正数,负数乘以正数为负数,所以存在最大最小数翻转,所以同时记录max_dp[i],min_dp[i]。分别表示以num[i]结尾的子数组最大乘积与最小乘积。
此外特殊情况:当num[i]=0, max_dp[i] min_dp[i] 都是0
<1>num[i]≠0
max_dp[i]=max{max_dp[i-1]*num[i] , min_dp[i-1]*num[i]};
min_dp[i]=min{max_dp[i-1]*num[i] , min_dp[i-1]*num[i]};
<2>num[i]==0
max_dp[i]=min_dp[i]=0;
class Solution {
public:
int maxProduct(vector<int>& nums) {
if(nums.size() == 0) return 0;
if(nums.size() == 1) return nums[0];
vector<int> dp_min(nums.size(),0);
vector<int> dp_max(nums.size(),0);
dp_min[0] = dp_max[0] = nums[0];
for(int i=1; i<nums.size();++i){
if(nums[i] == 0){
dp_min[i]=dp_max[i]=0;
}else{
dp_max[i] = max(nums[i],max(nums[i]*dp_max[i-1],nums[i]*dp_min[i-1]));
dp_min[i] = min(nums[i],min(nums[i]*dp_max[i-1],nums[i]*dp_min[i-1]));
}
}
return *max_element(dp_max.begin(),dp_max.end());
}
};
单词分割333
dp[i]表示以i结尾字符串能否分割。依次截取当前以i字符结尾的字符串末尾n个字符,若substr(i,n)单词
存在,且DP[i-n]=DP[j]可分割,则DP[i]可分割
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
set<string>dicts;
for(auto &i:wordDict){
dicts.insert(i);
}
int dp[s.size()+1];
dp[0]=1;
for(int i = 1;i<s.size()+1;++i){
dp[i]=0;
for(int j=i;j>=0;--j){
if(dp[j]==1){
string curr = s.substr(j,i-j);
if(dicts.count(curr)>0){
dp[i]=1;
break;
}
}
}
}
return dp[s.size()];
}
};
单词分割2
对于这种不仅仅是求方案数或者是否可行,而是需要具体的方案输出。一般采用回溯方式。
本题可以用map进行备忘录记录,避免子问题的重复求解。
class Solution {
set<string>dict;
map<string,vector<string>>memo;
public:
vector<string> wordBreak(string s, vector<string>& wordDict) {
if(dict.size() == 0){
for(auto &s:wordDict){
dict.insert(s);
}
}
if(memo.count(s)){
return memo[s];
}
vector<string>ret;
if(dict.count(s)){
ret.push_back(s);
}
for(int i =1;i<s.size();++i){
string tail = s.substr(i,s.size());
if(dict.count(tail)){
auto prevs = wordBreak(s.substr(0,i),wordDict);
if(prevs.size()){
for(auto&p:prevs){
ret.push_back(p+" "+tail);
}
}
}
}
memo[s] = ret;
return ret;
}
};
扭曲字符串
也可以用递归+备忘录方式
class Solution {
map<string,bool>cache;
bool _isScramble(string& s1,string& s2){
//缓存结果
auto it = cache.find(s1+"_"+s2);
if(it!= cache.end()){
return it->second;
}
//特殊情况
if(s1 == s2) return true;
if(s1.size() != s2.size()) return false;
if(s1.size() == 1){
if(s1 == s2) return true;
else{
return false;
}
}
//递归每种可能的划分
for(int i = 1;i<s1.size();++i){
auto l = (s1.substr(0,i));
auto r = (s1.substr(i,s1.size()));
auto l1 = (s2.substr(0,l.size()));
auto r1 = (s2.substr(l.size(),s2.size()));
auto l2 = (s2.substr(0,r.size()));
auto r2 = (s2.substr(r.size(),s2.size()));
if(_isScramble(l,l1)&&_isScramble(r,r1)) {
cache[s1+"_"+s2] = true;
return true;
}
if(_isScramble(l,r2)&&_isScramble(r,l2)){
cache[s1+"_"+s2] = true;
return true;
}
}
cache[s1+"_"+s2] = false;
return false;
}
public:
bool isScramble(string s1, string s2) {
if(s1.size() != s2.size()) return false;
if(s1.size() == 0 || s2.size() == 0) return false;
if(s1 == s2){
return true;
}
return _isScramble(s1,s2);
}
};
青蛙跳
也可以用递归+备忘录方式
class Solution {
map<int,int>stone_map;
map<unsigned long long,bool>memo;
//当前站在第i个石头上,上次通过K步调到这里
bool cross(int i,vector<int>&stones,int k){
if(i==stones.size()-1) return true;
unsigned long long key=((unsigned long long)i<<32)+k;
auto it=memo.find(key);
if(it != memo.end()) return it->second;
//当前能跳的步数是 k-1 k k+1 目标石头是 stones[i]+step
vector<int> steps={k-1,k,k+1};
if(i==0) steps={1};//第0个石头上只能跳一步
for(auto s:steps){
if(s>0){
auto it=stone_map.find(s+stones[i]);
if(it != stone_map.end()){
//能找到下一个距离的石头
if( cross(it->second,stones,s)){
memo[k]=true;
return true;
}
}
}
}
memo[key]=false;
return false;
}
public:
bool canCross(vector<int>& stones) {
for(int i=0;i<stones.size();++i){
stone_map[stones[i]]=i;
}
return cross(0,stones,1);
}
};
最短回文子串
问题可以转化为求给定字符串S从0开始,最长的是回文的前缀。设该回文前缀为S’
则S=S’+R
对S反转,Reverse(S)=Reverse(S’+R)=Reverse®+Reverse(S‘)=Reverse®+S’,此处注意回文前缀S‘的反转为自身
另SS=S+“#“+S‘=S‘+R+“#“+Reverse®+S’
则问题可以转换为求SS的最长公共前后缀长度,此时借鉴求KMP next数组方法
参考 KMP算法分析
求得SS的最长公共前后缀。其本质是一个dp过程
令dp[i]表示字符串S中以第i字符结尾的子串S(0,i)的最长公共前后缀长度。dp[0]=0;
则k=dp[i-1]代表如下,S(0,i-1)中,首尾两段S(0,k-1)与S(i-k,i-1)完全相同,
此时如果Ak==Ai,则DP[i]=DP[i-1]+1;
[A0,A1…Ak-1] Ak …[Ai-k…Ai-1] Ai
如果Ak不等于Ai,意味着dp[i]长度小于k,假设DP[i]=x,x<k。则S(0,i)中首尾S(0,x-1) S(i-x-1,i)完全相同。
[(A0,A1…Ax-2 Ax-1)…Ak-1] Ak …[Ai-k…(Ai-x-1…Ai-1] Ai) ,有Ax-1==Ai,写为
[(A0,A1…Ax-2) Ai…Ak-1] Ak …[Ai-k…(Ai-x-1…Ai-1] Ai)
以上左右两段方括号内容完全相同,因此对于A0~Ax-2这x-1元素,与A0~Ak-1的末尾x-1相同,也就是A0~Ax-2长度实际
是S(0,k-1)的最长公共前后缀dp[k-1],因此x=dp[k-1]+1。反过来,如果S(dp[k-1])==Ai,则dp[i]=dp[k-1]+1,否则递归迭代,继续在左区间查找
int maxCommonHeadTail(string s){
if(s.size()<=1) return 0;
vector<int>dp(s.size(),0); //dp[i] 表示第i个字符结尾的字符串S(0,i)的最大公共前后缀长度
for(int i=1;i<dp.size();++i){
int maxLen=dp[i-1];//S(0,i-1)最长公共前后缀长度为maxLen
while(1){
if(s[maxLen]==s[i]){
dp[i]=maxLen+1;
}else{
if(maxLen<=0){
dp[i]=0;
break;
}
maxLen=dp[maxLen-1];//AmaxLen与Ai不相同,继续在S(0,maxLen-1)里搜索
}
}
}
}
string shortestPalindrome(string s) {
if(s.size()<=1) return s;
auto s1=s;
reverse(s1.begin(),s1.end());
auto s2=s+"-"+s1;
int len=maxCommonHeadTail(s2);
auto remain=s.substr(len,s.size());
reverse(remain.begin(),remain.end());
return remain+s;
}