子串子序列
本人最近被一系列子串子序列递增最长连续 个数 和 之类的问题逼疯,特此整理
基础判断
首先要知道判断是否是子串还是子序列
判断子串
子串比较简单,因为是连续的
判断子序列
子序列其实也很简单,不用想的复杂,贪心就好
越早匹配到越好,只要匹配到一个就可以往后面走 leetcode392
class Solution {
public boolean isSubsequence(String s, String t) {
int n = s.length(), m = t.length();
int i = 0, j = 0;
while (i < n && j < m) {
if (s.charAt(i) == t.charAt(j)) {
i++;
}
j++;
}
return i == n;
}
}
//时间复杂度:O(n+m) 空间复杂度:O(1)
题目
题目的话其实一般都是考察子序列的,因为子序列难度会大一些,子串实际上没什么意义。
单个字符串中的判断
求出所有递增子序列
关于这种找出所有的排列组合的问题,真的二进制十分十分重要,而且其实实现起来也十分的简单,就是要熟练掌握。
核心思想是:set一共有n个元素,转换为n位二进制,0表示该元素没有取,1表示该元素取了,然后判断的范围就是从0-2n,然后将每个数字 不断右移跟1比较或者是跟 i左移多少位 来比较,可以判断是否是1,从而判断该元素是否获取,然后就可以拿到所有的情况
这题的难度是如何来消除重复!
- 基本想法是找出每一个递增子序列然后来判断是否重复,是否重复其实有很多方法,如果最傻的循环判断也不是不行,但是这里的解法是自定义了一个哈希来判断是否重复
- 但是这题最好的方法应该是dfs+剪枝,而且十分巧妙如何去重
- 如果向前位置的元素≥上一个元素 则需要dfs加入这个元素的情况
- 那么对于做dfs不加入这个元素的情况呢
- 2 2 都取
- 2取 2不取 (重复了,所以如果当前元素跟前一个元素一样了话,那当前位置就不考虑不取了,因为会出现重复
- 2不取 2取
- 2不取 2不取
class Solution {
public:
vector<int> temp;
vector<vector<int>> ans;
void dfs(int cur, int last, vector<int>& nums) {
if (cur == nums.size()) {
if (temp.size() >= 2) {
ans.push_back(temp);
}
return;
}
if (nums[cur] >= last) {
temp.push_back(nums[cur]);
dfs(cur + 1, nums[cur], nums);
temp.pop_back();
}
if (nums[cur] != last) {
dfs(cur + 1, last, nums);
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
dfs(0, INT_MIN, nums);
return ans;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/increasing-subsequences/solution/di-zeng-zi-xu-lie-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
求出 所有不重复的子序列的个数
这题有一种仿佛看题很简单的感觉,因为是求所有子序列,那全部dfs就好了,还不用判断递增,但是需要判断是否重复,(这个时间复杂度应该很夸张)仔细想一下,这肯定不可能是求出所有的满足条件的子序列了,因为数字会很大,只会是求个数,要求取余。
- 求个数的话,还是会想到dp,因为该位置的满足条件的序列一定跟前一个位置有关。
- 不考虑重复的话,很简单dp[i]=2*dp[i-1],dp[i]表示0-i元素可以组成的子序列的数目,就是num[i]加或者num[i]不加,因此是两倍
- 但如果考虑重复的话,需要好好思考的一下的。比如现在要插入元素b,那么如果之前就出现过元素b的话,之前b没插入,和现在b插入就会重复(这一部分有点乱,需要灵光一现领悟的那种
- 首先是 2*dp[i-1],即所有的情况都算进去了,包括每个位置上面取或者不取的
- 然后要减掉重复的元素,什么重复呢,就是上次没放这次放了,那跟上次放了这次没放会有重复,因此要减掉重复的部分,上次放一共有多少情况呢,那是dp[last[s[k]]-1]种情况
class Solution {
public int distinctSubseqII(String S) {
int MOD = 1_000_000_007;
int N = S.length();
int[] dp = new int[N+1];
dp[0] = 1;
int[] last = new int[26];
Arrays.fill(last, -1);
for (int i = 0; i < N; ++i) {
int x = S.charAt(i) - 'a';
dp[i+1] = dp[i] * 2 % MOD;
if (last[x] >= 0)
dp[i+1] -= dp[last[x]];
dp[i+1] %= MOD;
last[x] = i;
}
dp[N]--;
if (dp[N] < 0) dp[N] += MOD;
return dp[N];
}
}
作者:LeetCode
链接:https://leetcode-cn.com/problems/distinct-subsequences-ii/solution/bu-tong-de-zi-xu-lie-ii-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
最长递增子序列的长度是?
这就是经典dp,也是基础dp,dp[i]表示以元素i结尾的递增子序列的最长长度,因此其实是双重循环来判断的,dp[i]实际上是 由前面所有满足元素大小关系的 最长dp[j]+1; leetcode300
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n= nums.size();
if(n==0){
return 0;
}
//dp[i]表示 0-i中最长的递增子序列
vector<int> dp(n);
for(int i=0;i<n;i++){
dp[i]=1;
for(int j=0;j<i;j++)
{
if(nums[i]>nums[j]){
dp[i]=max(dp[i],dp[j]+1);
}
}
}
return *max_element(dp.begin(),dp.end());
}
};
其实该算法有时间复杂度更小一点的方法:贪心+二分查找,不过总是记不住怎么做罢了。面试和笔试的时候写出dp的基本就好了。
考虑一个简单的贪心,如果我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
在这一题目的基础上,有两道变形,其实道理都是一样的,只是需要暂时存的内容会有所变化
最长递增子序列的个数是?
因为此时需要记录最长递增子序列的个数,之前dp[i]只保存了最长递增子序列的长度,因此对应还需要一个cnt的数组来保存个数,同时注意更新的时候,即长度发生变化的时候,cnt也要对应发生变化。
- 如果dp[j]+1>dp[i] 则i位置的最长长度应为dp[j]+1,且个数应该跟cnt[i]相同
- 如果dp[j]+1==dp[i],则i位置的最长长度还是dp[i],但是个数的话是两者相加cnt[i]+ cnt[j]
- 每个位置dp[i]判断完后,再跟maxlen进行判断,决定是否更新ans
int findNumberOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n,1);
vector<int> cnt(n,1);
int maxlen=1;
int ans =1;
for(int i=1;i<n;i++)
{
int tmp=0;
for(int j=i-1;j>=0;j--)
{
if(nums[i]>nums[j])
{
if(dp[j]+1>dp[i])
{
dp[i]=dp[j]+1;
cnt[i]=cnt[j];
}else if(dp[j]+1==dp[i])
{
cnt[i]+=cnt[j];
}
}
}
if(dp[i]>maxlen)
{
maxlen = dp[i];
ans=cnt[i];
}else if(dp[i]==maxlen)
{
ans+=cnt[i];
}
}
return ans;
}
最长递增子序列是?
这题我印象中是有原题的,但是一直没能搜到,也是在面试中遇到过的题目,当时太傻了没有做出来,但其实还是很简单的,核心思想是不变的,转移方程也是一样的,只是需要再多一个变量来存放当前的数组是什么样子的,vector来暂存就好了,如果dp[i]=dp[j]+1,那么vector[i]=vector[j]+元素i,所以其实还是在leetcode300的基础上面来做的
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
vector<int> lengthOfLIS(vector<int>& nums) {
int n= nums.size();
if(n==0){
return vector<int>();
}
//dp[i]表示 0-i中最长的递增子序列
vector<int> dp(n);
vector<vector<int>> cur_vector(n);
for(int i=0;i<n;i++)
{
cur_vector[i].push_back(nums[i]);
}
cout<<"1"<<endl;
for(int i=0;i<n;i++){
dp[i]=1;
for(int j=0;j<i;j++)
{
if(nums[i]>nums[j]){
if(dp[j]+1>dp[i])
{
dp[i]=dp[j]+1;
cur_vector[i]=cur_vector[j];
cur_vector[i].push_back(nums[i]);
}
}
}
}
cout<<"2"<<endl;
int index=0;
int maxx=0;
for(int i=0;i<n;i++)
{
if(dp[i]>maxx)
{
maxx=dp[i];
index=i;
}
}
cout<<"3"<<endl;
cout<<index<<endl;
return cur_vector[index];
}
};
int main()
{
vector<int> nums{10,9,2,5,3,7,101,18};
vector<int> ans;
cout<<"4"<<endl;
ans = Solution().lengthOfLIS(nums);
cout<<"5"<<endl;
for(int i=0;i<ans.size();i++)
{
cout<<ans[i]<<" ";
}
return 0;
}
最长和谐子序列
做这种子序列的题目最怕的就是思维定式,总感觉需要dp来做,但是如果题目有所变化的话,要学会如何进行转型,和谐子序列的意思是这个序列当中 最大与最小的元素相差不多于1,如果这题还想着dfs来找子序列的话,时间复杂度太高了。。当然也可以做,虽然有相当枚举的方法,但是看评论之后发现,这道题目其实就是求相邻元素个数罢了
有双指针的解法,也有哈希的解法
public class Solution {
public int findLHS(int[] nums) {
HashMap < Integer, Integer > map = new HashMap < > ();
int res = 0;
for (int num: nums) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
for (int key: map.keySet()) {
if (map.containsKey(key + 1))
res = Math.max(res, map.get(key) + map.get(key + 1));
}
return res;
}
}
作者:LeetCode
链接:https://leetcode-cn.com/problems/longest-harmonious-subsequence/solution/zui-chang-he-xie-zi-xu-lie-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
递增的三元子序列
求是否存在递增的长度为3的子序列,dfs笨方法做当然是可以的。但是这题很巧妙的方法是题解中有的,主要是贪心,small的值尽可能的小,然后再存一个mid值,如果存在一个值比mid和small都大的话,则说明存在这样子的三元子序列
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
int small=INT_MAX,mid=INT_MAX;
for(int n:nums){
if(n<=small){
small=n;
}else if(n<=mid){
mid=n;
}else if(n>=mid){
return true;
break;
}
}
return false;
}
};
多个字符串的判断
这主要是两个字符串之间进行转换,比如删除增加替换。然后最小的多少,那就是二维dp
或者是求公共子序列的问题,这也是常常会遇到的题目
最长公共子序列
https://leetcode-cn.com/problems/qJnOS7/
经典题目了真的是,经典二维dp
- dp[i][j]表示 s 0→i-1, t 0→j-1字符匹配的最长公共子序列的长度
- 如果s[i-1]==t[j-1], 那么就可以把该位置算进去 dp[i][j]=dp[i-1][j-1]+1
- 如果不同的话,取最大的 dp[i][j]=max(dp[i-1][j],dp[i][j-1])
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
char c1 = text1.charAt(i - 1);
for (int j = 1; j <= n; j++) {
char c2 = text2.charAt(j - 1);
if (c1 == c2) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
}
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/qJnOS7/solution/zui-chang-gong-gong-zi-xu-lie-by-leetcod-ugg7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
判断子序列的数目,t在s的子序列中出现的次数
https://leetcode-cn.com/problems/21dk04/
或者说是判断一个指定字符串在 源字符串中可能出现的次数
依旧是动态规划最主要是状态如何来建立 dp[i][j]表示 target[j:t-1]字符串在source[i:s-1]的子序列中出现的次数
- j为t时,则target为空,在dp[i][t]=1; 当i为s是,则source为空所以 dp[s][j]=0;
- 当s[i]=t[j],
- 如果他两匹配则,target这个位置可以匹或者不匹
- 如果不匹配,则target这个位置只能是不匹
class Solution {
public int numDistinct(String s, String t) {
int m = s.length(), n = t.length();
if (m < n) {
return 0;
}
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
dp[i][n] = 1;
}
for (int i = m - 1; i >= 0; i--) {
char sChar = s.charAt(i);
for (int j = n - 1; j >= 0; j--) {
char tChar = t.charAt(j);
if (sChar == tChar) {
dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j];
} else {
dp[i][j] = dp[i + 1][j];
}
}
}
return dp[0][0];
}
}
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/21dk04/solution/zi-xu-lie-de-shu-mu-by-leetcode-solution-l8v1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
——————————————————————————
以后遇到类似的问题,再来及时归纳整理 2021.11.1