子串计数问题

定义

子串的定义:将一个字符串从开头与结尾删去任意字符,形成新的字符串,称为原字符串的子串。
子序列的定义:将一个字符串删去任意字符,形成新的字符串,称为原字符串的子序列。
区别就是:子串在原串中是连续的,子序列不一定。

子串计数问题,一般是给定一个字符串,求满足条件的子串的个数。

一般解题方法

遍历该字符串中的每个字符(for(i=1;i<=n;i++)),不重不漏的计算出以该字符为开头的方案数。答案就是每个字符答案的累加。

关键在于不重不漏的算出包含该字符的方案数

例题1:01序列

链接
解法1:考虑上面的一般解题方法,按照这种思路,我们只需要计算每一位为开头,满足要求的子串个数。对于每个字符,我们记从当前字符开始,恰好出现k个1的最前位置为l,恰好出现k个1的最后位置为r,那么答案就是r-l+1,实际上,l和r是可以通过二分的到的。我们对每个字符都这样进行计算,即可得到答案。注意特判k=0。
复杂度:O(nlogn)
代码

#include <bits/stdc++.h>
#include <unordered_map>
#include <unordered_set>
#define pb push_back
#define debug(x) cerr<<#x<<'='<<x<<'\n' 
#define debugg(x,y) cerr<<#x<<'='<<x<<','<<#y<<'='<<y<<'\n' 
#define FOR(a,b,c) for(int a=(b),a##_end=(c);a<=a##_end;++a)
#define ROF(a,b,c) for(int a=(b),a##_end=(c);a>=a##_end;--a)
#define FASTIO() cin.sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define rvs(s) reverse(s.begin(),s.end())
#define all(s) s.begin(),s.end()
#define sz(s) (int)(s.size())
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 1000010 ,M = 1000010,mod = 998244353,_=0;const double eps = 1e-9;
template<typename T>inline bool chkmin(T &x,const T &y){return y<x?x=y,1:0;}
template<typename T>inline bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template <typename T> void inline read(T &x) {
    int f = 1; x = 0; char s = getchar();
    while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
    while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
    x *= f;
}
int n,m,k;
int sum[N];
char s[N];
signed main()
{
    read(k);
    scanf("%s",s+1);
    n = strlen(s+1);
    if(k==0)
    {
        ll now = 0,ans = 0;
        for(int i=1;i<=n;i++)
        {
            if(s[i]=='0')
                ++now;
            else{
                ans = ans+(now+1)*now/2;
                now = 0;
            }
        }
        ans += (now+1)*now/2;
        cout<<ans;
        return 0;
    }
    for(int i=1;i<=n;i++)
        sum[i] = sum[i-1]+(s[i]=='1');
    ll ans = 0;
    for(int i=1;i<=n;i++)
    {
        int l = i,r = n;
        while(l<r)
        {
            int mid = l+r >>1 ;
            if(sum[mid]-sum[i-1]>=k) r = mid;
            else l = mid+1;
        }
        if(sum[l]-sum[i-1]!=k) break;
        int pos = l;
        l = i,r = n;
        while(l<r)
        {
            int mid = l+r+1 >>1;
            if(sum[mid]-sum[i-1]<=k) l = mid;
            else r = mid-1;
        }
        ans+=l-pos+1;
    }
    cout<<ans;
    return ~~(0 ^ _ ^ 0);
}

解法2: 其实本题不用上面的方法也可以做。对于两个中间‘1’个数恰好为k个的1,我们只需要计算一下这两个1前后0的个数,然后二者相乘即可。因此我们只需枚举两个中间‘1’个数恰好为k个的1,所得答案相加即可。
复杂度O(n)
代码:(写的有点复杂,特判有点多,写的好的话是可以写比较短的)

#include <bits/stdc++.h>
#include <unordered_map>
#include <unordered_set>
#define pb push_back
#define debug(x) cerr<<#x<<'='<<x<<'\n' 
#define debugg(x,y) cerr<<#x<<'='<<x<<','<<#y<<'='<<y<<'\n' 
#define FOR(a,b,c) for(int a=(b),a##_end=(c);a<=a##_end;++a)
#define ROF(a,b,c) for(int a=(b),a##_end=(c);a>=a##_end;--a)
#define FASTIO() cin.sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define rvs(s) reverse(s.begin(),s.end())
#define all(s) s.begin(),s.end()
#define sz(s) (int)(s.size())
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 1000010 ,M = 1000010,mod = 998244353;const double eps = 1e-9;
template<typename T>inline bool chkmin(T &x,const T &y){return y<x?x=y,1:0;}
template<typename T>inline bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template <typename T> void inline read(T &x) {
    int f = 1; x = 0; char s = getchar();
    while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
    while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
    x *= f;
}
int n,m,k;
int a[N];
char s[N];
int last[N],ne[N];
signed main()
{
    read(k);
    scanf("%s",s+1);
    n = strlen(s+1);
    ll ans = 0;
    if(k==0)
    {
        ll now = 0;
        for(int i=1;i<=n;i++)
        {
            if(s[i]=='0')
                ++now;
            else{
                ans = ans+(now+1)*now/2;
                now = 0;
            }
        }
        ans += (now+1)*now/2;
        cout<<ans;
        return 0;
    }
    for(int i=n,j=n+1;i>=1;i--)
    {
        ne[i] = j;
        if(s[i]=='1') j = i;
    }
    for(int i=1,j=0;i<=n;i++)
    {
        last[i] = j;
        if(s[i]=='1') j = i;
    }
    int be = -1,ed = -1,now = 0;
    for(int i=1;i<=n;i++)
    {
        if(s[i]=='1'){
            ++now;
            if(be==-1) be = i;
            if(now==k){
                ed = i;
                break;
            }
        }
    }
    if(ed==-1) {
        puts("0");
        return 0;
    }
    for(int i = be,j=ed;j<=n;)
    {
        ans = ans+(i-last[i])*(ne[j]-j);
        i = ne[i],j = ne[j];
    }
    cout<<ans;
    return 0;
}

我们主要讲第一种解法的应用,第二种解法的复杂度虽然低,但是写起来可能需要注意很多边界条件,出错的概率较大

题目2:R

链接
题解:继续使用上述一般解题方法的思路,我们依次考虑每个字符为开头的答案。发现本题比上一题还要简单,我们只要二分出从当前字母开始,字母R个数大于等于k的最前位置l,预处理出当前字母后面最接近的字母P的位置为r。答案即为max(0,r-l+1)
代码

#include <bits/stdc++.h>
#include<ext/rope>
#define pb push_back
#define debug(x) cerr<<#x<<'='<<x<<'\n'
#define debugg(x,y) cerr<<#x<<'='<<x<<','<<#y<<'='<<y<<'\n' 
#define FOR(a,b,c) for(int a=(b),a##_end=(c);a<=a##_end;++a)
#define ROF(a,b,c) for(int a=(b),a##_end=(c);a>=a##_end;--a)
#define FASTIO() cin.sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define int long long
using namespace std;
using namespace __gnu_cxx;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 200010 ,M = 1000010,mod = 998244353;const double eps = 1e-9;
ll qmi(ll,ll);
template<typename T>inline bool chkmin(T &x,const T &y){return y<x?x=y,1:0;}
template<typename T>inline bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template <typename T> void inline read(T &x) {
    int f = 1; x = 0; char s = getchar();
    while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
    while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
    x *= f;
}
char s[N];
int p[N];
ll sum[N];
int ne[N];
int n,k;
signed main()
{
	cin>>n>>k;
	scanf("%s",s+1);
	p[n+1] = n+1;
	for(int i=n;i>=1;i--)
	{
		p[i] = p[i+1];
		if(s[i]=='P')
			p[i] = i;
	}
	ll ans = 0;
	for(int i=1;i<=n;i++)
	{
		sum[i] = sum[i-1];
		if(s[i]=='R') ++sum[i];
	}
	for(int i=1;i<=n;i++)
	{
		if(sum[n]-sum[i-1]<k) {
			ne[i] = 0;
			break;
		}
		int l = i,r = n;
		while(l<r)
		{
			int mid = l+r>>1;
			if(sum[mid]-sum[i-1]>=k) r = mid;
			else l = mid+1;
		}
		ne[i] = l;
	}
	for(int i=1;i<=n;i++)
	{
		if(ne[i]==0) break;
		int maxd = p[i]-1;
		ans+=max(0ll,maxd-ne[i]+1);
	}
	cout<<ans;
    return 0;
}

ll qmi(ll a,ll b) {ll res=1;a%=mod;  for(;b;b>>=1){if(b&1)res=res*a%mod;a=a*a%mod;}return res;}

题目3:子串分值和

链接

题解:前两题计算的是符合要求的子串的个数,这题要求每个子串的权值和。其实方法类似。我们继续使用上述一般解题方法的思路,计算每个字符为开头的答案。如果进行暴力,肯定会t。但是注意到,每个子串的答案都是1-26(最多26个字符),也就是说,固定第一个字符,右边界慢慢变大,答案必然递增,且最多上升26次。
不难想到,我们可以预处理出每个位置的下一个(‘a’-‘z’)出现的位置,看不懂这句话可以看这段代码

memset(a,-1,sizeof a);
for(int i=n;i>=1;i--)
    {
        a[s[i]-'a'] = i;
        for(int j=0;j<26;j++)
            ne[i][j] = a[j];
    }

当我们计算时,对于每一个字符,假设其位置是i。我们只需要把ne[i] 中不是-1的值取出并且排序,只有当右边界的下标等于ne[i]中的值时答案才会上升。
复杂度O(26n)

#include <bits/stdc++.h>
#include <unordered_map>
#include <unordered_set>
#define pb push_back
#define debug(x) cerr<<#x<<'='<<x<<'\n' 
#define debugg(x,y) cerr<<#x<<'='<<x<<','<<#y<<'='<<y<<'\n' 
#define FOR(a,b,c) for(int a=(b),a##_end=(c);a<=a##_end;++a)
#define ROF(a,b,c) for(int a=(b),a##_end=(c);a>=a##_end;--a)
#define FASTIO() cin.sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define rvs(s) reverse(s.begin(),s.end())
#define all(s) s.begin(),s.end()
#define sz(s) (int)(s.size())
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 1000010 ,M = 1000010,mod = 998244353,_=0;const double eps = 1e-9;
template<typename T>inline bool chkmin(T &x,const T &y){return y<x?x=y,1:0;}
template<typename T>inline bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template <typename T> void inline read(T &x) {
    int f = 1; x = 0; char s = getchar();
    while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
    while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
    x *= f;
}
int n,m,k;
int a[26];
char s[N];
int ne[N][26];
signed main()
{
    memset(a,-1,sizeof a);
    scanf("%s",s+1);
    n = strlen(s+1);
    for(int i=n;i>=1;i--)
    {
        a[s[i]-'a'] = i;
        for(int j=0;j<26;j++)
            ne[i][j] = a[j];
    }
    ll ans =0 ;
    for(int i=1;i<=n;i++)
    {
        vector<int> v;
        for(int j=0;j<26;j++)
        {
            if(ne[i][j]!=-1)
                v.pb(ne[i][j]);
        }
        sort(v.begin(),v.end());
        int cnt = 0;
        
        for(int j=0;j+1<v.size();j++)
        {
            ++cnt;
            ans+=1ll*cnt*(v[j+1]-v[j]);
            // if(i==1) cout<<cnt<<' '<<v[j+1]-v[j]<<endl;
        }
        if(v.size())
            ans+=1ll*(++cnt)*(n-v.back()+1);
        // cout<<"i="<<i<<' '<<ans<<endl;
    }
    cout<<ans;
    return ~~(0 ^ _ ^ 0);
}

题目4:小y的序列

链接
题解:本题与第1题类似(不过是从字符串变成了数组)。我们直接计算每个元素为开头的答案,即固定i。随着j的增加,max(a[i],a[i+1],…,a[j])的值不断增加,min(a[i],a[i+1],…,a[j])不断减小。我们需要max(a[i],a[i+1],…,a[j])-min(a[i],a[i+1],…,a[j])=k。 可以直接二分出左右边界l和r,答案即为r-l+1
注:查询区间的最大和最小值可以使用st表或者线段树(这里使用st表)

#include <bits/stdc++.h>
using namespace std;

const int N = 1000010, M = 21;

int n, m,k;
int w[N];
int f[N][M];
int g[N][M];
int Log2[N];
void init()
{
    for (int j = 0; j < M; j ++ )
        for (int i = 1; i + (1 << j) - 1 <= n; i ++ )
            if (!j) f[i][j] =g[i][j]= w[i];
            else{
                f[i][j] = max(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);
                g[i][j] = min(g[i][j - 1], g[i + (1 << j - 1)][j - 1]);
            } 
    
    for (int i = 2; i <= n; ++i)
    	Log2[i] = Log2[i / 2] + 1;

}

int querymax(int l, int r)
{
    int len = r - l + 1;
    int k = Log2[len]/Log2[2];

    return max(f[l][k], f[r - (1 << k) + 1][k]);
}
int querymin(int l, int r)
{
    int len = r - l + 1;
    int k = Log2[len]/Log2[2];

    return min(g[l][k], g[r - (1 << k) + 1][k]);
}
int main()
{
    scanf("%d%d", &n,&k);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);

    init();
    long long ans = 0;
    for(int i=1;i<=n;i++)
    {
        int l = i,r = n;
        while(l<r)
        {
            int mid = l+r+1>>1;
            if(querymax(i,mid)-querymin(i,mid)<=k) l = mid;
            else r=mid-1;
        }
        int ma = querymax(i,l),mi = querymin(i,l);
        if(ma-mi==k)
        {
            int low =i,high = n;
            while(low<high)
            {
                int mid = low+high>>1;
                if(querymax(i,mid)-querymin(i,mid)>=k) high = mid;
                else low = mid+1;
            }
            ans+=l-low+1;
        }
            
    }
    cout<<ans<<'\n';
    return 0;
}

问题5:子串的最大差

链接
题解:注意到答案等于 所有子串的最大值-左右子串的最小值。因此我们只需要单独计算最大值的和 以及 最小值的和 即可。
下以最大值为例:
对于每个位置的值,设其位置为i,我们计算a[i] 作为最大值的子串的个数(答案显然>=1,因为自身也是子串)。不难想到,只要预处理出a[i] 左边第一个比a[i]大的数的位置l,a[i]右边第一个比a[i]大的数的位置r ,l++,r–后,在l-r这个区间内,只要是包含位置i的子区间即都是符合要求的,区间个数为(i-l+1)*(r-i+1)。
至于预处理,这是单调栈的操作,只需要进行四次单调栈即可。(实际上可以只用两次,因为找a[i]右边第一个比a[i] 大的数a[j] 等价于 找a[j]左边第一个比a[j]小的数a[i])

其实这样子计算还是会重复,当有两个相同的数字比较接近时,这二者会为共同的区间贡献答案,那么这个贡献就被重复计算了。因此实际计算过程中,我们要记录每个a[i]对应的r,当遇见下一个a[i]时,l应该要和r+1取min,这样就能保证不重复了。
代码

#include <bits/stdc++.h>
#include <unordered_map>
#include <unordered_set>
#define pb push_back
#define debug(x) cerr<<#x<<'='<<x<<'\n' 
#define debugg(x,y) cerr<<#x<<'='<<x<<','<<#y<<'='<<y<<'\n' 
#define FOR(a,b,c) for(int a=(b),a##_end=(c);a<=a##_end;++a)
#define ROF(a,b,c) for(int a=(b),a##_end=(c);a>=a##_end;--a)
#define FASTIO() cin.sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr)
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#define rvs(s) reverse(s.begin(),s.end())
#define all(s) s.begin(),s.end()
#define sz(s) (int)(s.size())
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 500010 ,M = 1000010,mod = 998244353;const double eps = 1e-9;
template<typename T>inline bool chkmin(T &x,const T &y){return y<x?x=y,1:0;}
template<typename T>inline bool chkmax(T &x,const T &y){return x<y?x=y,1:0;}
template <typename T> void inline read(T &x) {
    int f = 1; x = 0; char s = getchar();
    while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
    while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
    x *= f;
}
int n,m,k;
int a[N];
int mzuo[N],myou[N];
int mazuo[N],mayou[N];
int s[N],tt;
signed main()
{
    read(n);
    FOR(i,1,n) read(a[i]);
    tt = 0;
    for(int i=1;i<=n;i++)
    {
        while(a[i]<=a[s[tt]]&&tt>0)tt--;
        if(tt==0) mzuo[i] = 0;
        else mzuo[i] = s[tt];
        s[++tt] = i;
    }
    
    tt = 0;
    for(int i=n;i>=1;i--)
    {
        while(a[i]<=a[s[tt]]&&tt>0) tt--;
        if(tt==0) myou[i] = n+1;
        else myou[i] = s[tt];
        s[++tt] = i;
    }
    tt = 0;
    for(int i=1;i<=n;i++)
    {
        while(a[i]>=a[s[tt]]&&tt>0)tt--;
        if(tt==0) mazuo[i] = 0;
        else mazuo[i] = s[tt];
        s[++tt] = i;
    }
    
    tt = 0;
    for(int i=n;i>=1;i--)
    {
        while(a[i]>=a[s[tt]]&&tt>0) tt--;
        if(tt==0) mayou[i] = n+1;
        else mayou[i] = s[tt];
        s[++tt] = i;
    }
    ll ans = 0;
    unordered_map<int,int> mp,have;
    for(int i=1;i<=n;i++)
    {
        int l = mzuo[i]+1,r = myou[i]-1; 
        l = max(l,have[a[i]]+1);
        mp[a[i]] = r;have[a[i]] = i;
        ans-=1ll*a[i]*(1ll*(i-l+1)*(r-i+1));
    }
    mp.clear();have.clear();
    for(int i=1;i<=n;i++)
    {
        int l = mazuo[i]+1,r = mayou[i]-1;
        if(mp.count(a[i])&&mp[a[i]]>=i){
            l = have[a[i]]+1;
        }
        mp[a[i]] = r;have[a[i]] = i;
        ans+=1ll*a[i]*(1ll*(i-l+1)*(r-i+1));
    }
    cout<<ans<<' ';
    return 0;
}



  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值