厨师和字符串
题目描述
大厨有一个长度为 N 的仅包含小写字符的字符串S,他同时准备了一个包含了所有S的子串的列表工。
现在他向你询问了Q个询问,第i个询问形如,你需要,你需要在L中选择恰好个相同的字符串。答案可能很大,输出模
+7后的结果。
输入格式
输入数据的第一行包含一个整数 T,表示数据组数。对于每组测试数据,第一行包含两个整数 N,Q。接下来的一行,包含一个长度为 N 的字符串 S。接下来的Q行,每行包含一个整数 表示一组询问。
输出格式
对于每组询问,输出一行表示对应的答案。
输入范围
题目分析
1.相同的子字符串的数量最多仅为N,因为最小的子字符串为单个字符,单个字符最多有N个,即使所有的字符都相同,也仅有N个,所以可以把问题直接缩小到
2.根据题目上写的取模,说明这道题的答案会及其大,不能通过枚举求解.正确方针是求出相同子字符串的数量,再用组合数,求出个相同的答案.例如此时有7个子字符串相同,
为2时,等于C(7,2),
为3时,等于C(7,3),以此类推.
3.子字符串可能各不相同,所以要分别统计每个子字符串相同的数量.
一种方法是把所有的子字符串记录下来,然后快排一遍,所有的子字符串数量是n(n+1)/2,空间复杂度是O(n^2),时间复杂度是O(n^2log(n)),原题目限制很小,这样可能能跑的过去,作者没试,只是云,TLE了我不背锅(
第二种方法所耗的空间和时间都挺小的.不考虑组合数算法的情况下空间O(n),时间O(n^2)
我们试想一下,要子字符串相同,例如存在一个等于
.此时将
字符串倒转,一定有一个位置的z函数大于等于
(想一想为什么是大于等于而不是等于)
因此我们可以通过这种方式来找相同的子字符串.枚举结尾,然后将字符串倒转进行z函数操作.
有两点要注意,第一点,计算完z函数后,要前缀和的从后往前统计相同子字符串长度.
原因是如果某一串跟串头有k个相同的,那么一定代表(1~k-1)都是相同的,都要统计进去.
第二点,要记录一个vis数组,维护索引为i时,它的z函数最大值为多少.在之后统计相同子字符串长度时,要大于vis[i],因为小于等于vis[i]的说明已经跟以前某个字符串匹配过,计算过了,不能再统计.
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll ans[5005],k[5005],c[5005][5005],vis[5005],z[5005];
const ll mod=1e9+7;
void calc(string&s){
int n=s.size();
for (int i=1,l=0,r=0;i<n;i++){
if (i<=r&&z[i-l]<r-i+1){
z[i]=z[i-l];
}
else{
z[i]=max(0,r-i+1);
while (z[i]<n&&s[i+z[i]]==s[z[i]]) ++z[i];
}
if (i+z[i]-1>r) l=i,r=i+z[i]-1;
if (vis[n-i]<z[i]){
k[z[i]]++;
vis[n-i]=z[i];
}
}
}
void solve(){
memset(ans,0,sizeof ans);
memset(vis,0,sizeof vis);
int n,q;
cin>>n>>q;
string str;
cin>>str;
for (int i=n;i>=1;i--){
memset(k,0,sizeof k);
string s=str.substr(0,i);
reverse(s.begin(),s.end());
calc(s);
k[i]=1;
for (int p=i;p>vis[i];p--){
k[p]+=k[p+1];
for (int j=1;j<=k[p];j++){
if (k[p]>=j) ans[j]=(ans[j]+c[k[p]][j])%mod;
}
}
}
for (int i=1;i<=q;i++){
int x;
cin>>x;
if (x<=n) cout<<ans[x]<<"\n";
else cout<<0<<"\n";
}
}
int main(){
for (int i=0;i<=5000;i++) c[i][0]=1;
for (int i=1;i<=5000;i++){
for (int j=1;j<=i;j++){
c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
}
}
int t;
cin>>t;
while (t--){
solve();
}
return 0;
}