【做题纪要】冲刺NOIP逆天题单之字符串篇
幽默题目,冲刺NOIp全是SA和SAM,冲刺NOIp一不小心把p冲没了,成NOI了
甚至有上个题单没有出现的CF评分*3300,幽默
Yet Another LCP Problem
题意
给出一个长度为 \(n\) 的字符串 \(S\) 和 \(q\) 次询问,每次询问给出一个集合 \(A\) 和 \(B\) 求 \(\sum\limits_{i\in A}\sum\limits_{j\in B}\text{LCP}(i,j)\)
逆天冲刺NOIP题单可做题之九(字符串之一)
看到这道题第一想法就是用 \(\text{SA}\),毕竟已经出现求后缀的 \(\text{LCP}\) 等字样了
考虑如何 \(\text{SA}\),先排序然后求 \(\text{height}\) 数组没啥好说的,突然发现这不是我们 [AHOI2013]差异 吗
直接建 \(\text{ST}\) 表,然后跑单调栈似乎就过去了,复杂度 \(O(n \log n)\) 完全可过
首先我们需要知道一个定理
拿到这个定理就很明显了吧这道题,直接挂 \(\text{ST}\) 表就能 \(\text O(1)\) 查询 \(\text{LCP}\) 了
但是如果我们用这个定理直接去暴力枚举肯定寄了,复杂度是 \(\text O(n^2)\) 的,今天你和 \(\text{LCP}\) 只能活一个
我们发现其实就是求几个区间的 \(\min\) 之和,然后随便用单调栈一类的维护一下就过了,参考 [AHOI2013]差异
点击查看代码
namespace Solve{
inline int read(){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=s*10+ch-'0';ch=getchar();}
return s*w;
}
inline void write(const int x){
if(x>=10) write(x/10);
putchar(x%10+'0');
}
inline void print(int x,string s){
if(x<0) putchar('-');
write(x<0?-x:x);
int len=s.size();
for_(i,0,len-1) putchar(s[i]);
}
int n,Q,l[N],k[N];
char s[N];
namespace SA{
int sa[N],rk[N],oldrk[N],oldsa[N],w,cnt[N],key[N];
int height[N];
inline bool cmp(int x,int y,int w){
return (oldrk[x]==oldrk[y])&&(oldrk[x+w]==oldrk[y+w]);
}
inline void Init(char *s){
int n=strlen(s+1),m=127,tot;
for_(i,1,n)
rk[i]=s[i],
++cnt[rk[i]];
for_(i,1,m)
cnt[i]+=cnt[i-1];
_for(i,n,1) sa[cnt[rk[i]]--]=i;
for(int w=1;;w<<=1,m=tot) {
tot=0;
_for(i,n,n-w+1)
oldsa[++tot]=i;
for_(i,1,n)
if(sa[i]>w)
oldsa[++tot]=sa[i]-w;
memset(cnt,0,sizeof(cnt));
for_(i,1,n)
++cnt[key[i]=rk[oldsa[i]]];
for_(i,1,m)
cnt[i]+=cnt[i-1];
_for(i,n,1)
sa[cnt[key[i]]--]=oldsa[i];
memcpy(oldrk+1,rk+1,n*sizeof(int));
tot=0;
for_(i,1,n)
rk[sa[i]]=((cmp(sa[i],sa[i-1],w))?(tot):(++tot));
if(tot==n)
break;
}
}
inline void Init_H(char *s){
int tot=0,n=strlen(s+1);
for_(i,1,n){
if(rk[i]==0) continue;
if(tot) --tot;
while(s[i+tot]==s[sa[rk[i]-1]+tot]) ++tot;
height[rk[i]]=tot;
}
}
}
using namespace SA;
class ST{
public:
int Min[500000][20],Log[N];
inline void init(){
for_(i,1,n) {
Log[i]=log(i)/log(2);
}
for_(i,1,n) {
Min[i][0]=height[i];
}
for_(j,1,Log[n]) {
for_(i,1,n-(1<<j)+1) {
Min[i][j]=min(Min[i][j-1],Min[i+(1<<j-1)][j-1]);
}
}
}
inline int Query(int l,int r){
return min(Min[l][Log[r-l+1]],Min[r-(1<<(Log[r-l+1]))+1][Log[r-l+1]]);
}
}TrEE;
PII a[N];
inline bool Cmp(PII a,PII b){
return (a.first==b.first)?(a.second<b.second):(a.first<b.first);
}
int top,q[N],c[N], Sum,ans,Ans[N];
inline void solve(int m,int M){
int cnt=0;
for_(i,1,M)
a[++cnt]=mp(rk[read()],1);
for_(i,1,m)
a[++cnt]=mp(rk[read()],0);
sort(a+1,a+1+cnt,Cmp);
ans=top=Sum=0;
a[cnt+1]=mp(-1,0);
int r=cnt;
while(a[r].second==1) r--;
_for(i,r,1){
if(i!=r){
int rk=TrEE.Query(a[i].first+1,a[i+1].first),s=a[i+1].second^1;
while(rk<=q[top]&&top){
Sum-=c[top]*q[top],
s+=c[top--];
}
q[++top]=rk,c[top]=s,Sum+=1ll*s*rk;
}
if (a[i].second)
ans+=Sum;
else if(a[i+1].first==a[i].first)
ans+=n-sa[a[i].first]+1;
}
for_(i,2,cnt)
if(a[i].first==a[i-1].first)
swap(a[i],a[i-1]);
top=Sum=0;r=1;
while(a[r].second==1)
r++;
for_(i,r,cnt){
if(i!=r){
int rk=TrEE.Query(a[i-1].first+1,a[i].first),s=a[i-1].second^1;
while((rk<=q[top])&&(top!=0)) {
Sum-=c[top]*q[top];
s+=c[top--];
}
q[++top]=rk,c[top]=s,Sum+=1ll*s*rk;
}
if(a[i].second) ans+=Sum;
}
}
inline void In(){
n=read(),Q=read();
scanf("%s",s+1);
Init(s);
Init_H(s);
TrEE.init();
for_(i,1,Q){
solve(read(),read());
Ans[i]=ans;
}
for_(i,1,Q){
print(Ans[i],"\n");
}
}
}
Minimax
逆天冲刺 NOIP 题单可做题之十(字符串之二)
逆天分讨题,看到就难受
我们要考虑最小化前缀函数且字符串最小,也就是尽可能不要让前缀和后缀相同,同时最小化因此我们考虑分类讨论
-
如果字符串内所有字符都是相同的
直接输出最小的情况即可,因为前后调转不会产生影响
-
如果所有字符串都是不同的
直接输出最小的情况即可,因为前后调转同样不会产生影响
-
当字符串内有字符出现且仅出现了一次
此时我们发现其可以让字典序最小的只出现了一次的字符放在最前面,此时我们可以发现其 \(f\) 为最小
-
如果当前字符串内最小的字符出现了小于一半的次数
我们先把最小值存到第一位和第二位,然后后面按照当前(指放目前的次小值,最小值)的情况用次小值,最小值的顺序放即可
-
如果当前字符串内最小的字符出现了大于一半的次数且只由两种字符形成
我们先用一个最小值存到第一位,然后后面放所有次小值,最后放所有最小值即可
-
最小的字符出现了大于一半的次数而且出现次数大于等于三次
我们先放一个最小值,然后放一个次小的隔开,接下来把所有最小值放完,放一个次次小值,接下来按照字典序排即可
由于代码太长了我压了一下
点击查看代码
inline void In(){
t=read();
while(t--){
int cnt=0;
memset(c,0,sizeof(c));
cin>>s;len=s.size();
for_(i,0,len-1){a[i+1]=(int)(s[i]-'a');c[(int)(s[i]-'a')]+=1;}
sort(a+1,a+len+1);
if(a[1]==a[len]){put(s,'\n');continue;}
int f=1;for_(i,1,len-1){if(a[i]==a[i+1]){f=0;break;}}
if(f){for_(i,1,len)putchar(char(a[i]+'a'));puts("");continue;}
int temp=-1;
for_(i,0,25){if(c[i]==1){temp=i;break;}}
if(temp!=-1){putchar(char(temp+'a'));c[temp]--;for_(i,0,25){for_(j,1,c[i])putchar((char)(i+'a'));}puts("");continue;}
temp=-1;
for_(i,0,25){if(c[i]!=0){temp=i;break;}}
if(c[temp]*2<=len+2){putchar((char)(temp+'a'));putchar((char)(temp+'a'));c[temp]-=2;while(c[temp]>=1){int temp2=-1;for_(i,temp+1,26){if(c[i]!=0){temp2=i;break;}}putchar((char)(temp2+'a'));putchar((char)(temp+'a'));c[temp2]--;c[temp]--;}for_(i,0,25){for_(j,1,c[i]){putchar((char)(i+'a'));}}puts("");continue;}
int temp2=-1;for_(i,temp+1,25){if(c[i]!=0){temp2=i;break;}}
if(c[temp]+c[temp2]==len){putchar((char)(temp+'a'));c[temp]--;for_(i,1,c[temp2]){putchar((char)(temp2+'a'));}for_(i,1,c[temp]){putchar((char)(temp+'a'));}puts("");continue;}
temp2=-1;for_(i,temp+1,26){if(c[i]!=0){temp2=i;break;}}
int temp3=-1;for_(i,temp2+1,25)if(c[i]!=0){temp3=i;break;}putchar((char)(temp+'a'));c[temp]--;putchar((char)(temp2+'a'));c[temp2]--;
for_(i,1,c[temp]){putchar((char)(temp+'a'));}
c[temp]=0;putchar((char)(temp3+'a'));c[temp3]--;for_(i,0,25)for_(j,1,c[i])putchar((char)(i+'a'));puts("");
}
}
CF955D Scissors
思路
首先考虑对于传进来的两个串 \(s_1\) 和 \(s_2\) ,预处理出 \(s_2\) 的每个前缀在 \(s_1\) 中出现的最早位置和 \(s_2\) 每个后缀在 \(s_1\) 中出现的最晚位置
然后直接扫一遍就行了,复杂度 \(\text O(n)\)
代码
轻微压行
点击查看代码
namespace solve{
const int N=5e5+5;
int mod=1e9,base=131;
int n,m,k,ans1,ans2,lm[N],rm[N];
char s[N],t[N];
class Hash{
public:
int Hash[N], poww[N];
inline void Init(char *s) {int len=strlen(s+1);poww[0]=1;for_(i,1,len) {Hash[i]=(Hash[i-1]*base+s[i]-'a'+1) % mod;poww[i]=poww[i-1]*base%mod;}}
inline int get(int l, int r){return ((Hash[r]-Hash[l-1]*poww[r-l+1])%mod+mod)%mod;}
}S,T;
inline bool Judge(){
if (n<(k<<1)||m>(k<<1)) return 0;
S.Init(s),T.Init(t); int pos=k;
for_(i,1,m) lm[i]=n+1;
for_(i,1,min(m,k)){while(pos<=n&&S.get(pos-i+1,pos)!=T.get(1,i)) pos++;if(S.get(k-i+1,k)==T.get(1,i)) pos=k;lm[i]=pos;} pos=n-k+1;
for_(i,1,min(m,k)){ while(pos && S.get(pos,pos+i-1)!=T.get(m-i+1,m)) pos--; if(S.get(n-k+1,n-k+i)==T.get(m-i+1,m)) pos=n-k+1; rm[m-i+1]=pos; }
for_(i,1,n-m+1){ if(S.get(i,i+m-1)==T.get(1,m)){if(k>=i&&n-k+1<=i+m-1) continue;ans1=min(max(1ll,i-k+1),n-k+1-k);ans2=max(k+1,min(n-k+1,i));return 1;}}
for_(i,1,m-1){ if(lm[i]<rm[i+1]&&lm[i]<=n&&rm[i+1]){ans1=lm[i]-k+1;ans2=rm[i+1];return 1;} }
return 0;
}
inline void In() {
srand(time(0));mod+=rand();
base+=rand()%10;read(n,m,k);
FastI>>(s+1)>>(t+1);
if(Judge())
write("Yes\n",ans1," ",ans2);
else
write("No");
}
}
using namespace solve;
CF1037H Security
题意
给定一个字符串 \(s\) 和 \(q\) 次询问,每次询问给出一个 \(l_i,r_i\) 和 \(x_i\),你需要选出一个满足以下条件的字符串 \(str\)。
-
\(str\) 是 \(s[l_i\sim r_i]\) 的子串。
-
\(str > x_i\)。
-
\(str\) 是所有满足以下条件内字符串字典序最小的。
输出 \(str\) 的长度,如果不存在 \(str\) 则输出 \(-1\)。
思路
先把 \(s\) 和所有的询问 \(x_i\) 串都连起来,用小于 \(a\) 的字符连接,连成字符串 \(S\)
枚举 \(T\) 的前缀,判断区间中是否存在一个子串满足以下条件
-
这个子串等于这个前缀加上一个字母
-
加上的字母大于 \(T\) 的前缀的后面一个字符
如果前缀为 \(T\) 本身,那么后面一个字符是极小的。
在 \(\text {SA}\) 上二分出一个区间,满足区间中的后缀与 \(T\) 的 \(\text{LCP}\) 长度等于正在枚举的 \(T\) 的前缀长度 \(len\),记录上次的二分结果
求这个区间中满足 \(sa_i\in [l,r-len]\) 的最小的 \(i\) ,用主席树或者扫描线维护,复杂度 \(\text O(|S| \log |S|)\)
代码
点击查看代码
class SegTree{
public:
int t[N*4],d;
inline void build(int n){
for(d=1;d<n;d<<=1);
memset(t,0x7f,sizeof(t));
}
inline int ask(int l,int r){
int mn=2e9;
for(l=l+d-1,r=r+d+1;l^r^1;l>>=1,r>>=1){
if(l&1^1) mn=min(mn,t[l^1]);
if(r&1) mn=min(mn,t[r^1]);
}
return mn;
}
inline void add(int x,int v){
for(int i=x+d;i;i>>=1)
t[i]=v;
}
}T;
pair<int,int> q[N];
namespace ST_Table{
int val[N],l[N],ans[N],ansl[N],Log[N*2],ST[20][N*2];
inline int askmin(int l,int r){
int k=Log[r-l+1];
return min(ST[k][l],ST[k][r-(1<<k)+1]);
}
}
using namespace ST_Table;
namespace Solve{
class node{
public:
int w,l,r,id,len;
};
vector<node> que[N*2];
inline void ask(int l,int r,int val,int slen,int id){
int w,cnt2=a.rk[val];
for(int j=1<<20;j;j>>=1)
((cnt2+j<=len)&&(askmin(a.rk[val]+1,cnt2+j)>=(min(slen,r-l)+1)))&&(
cnt2+=j
);
_for(i,min(slen,r-l),0){
w=cnt2;
for(int j=1<<20;j;j>>=1)
((w+j<=len)&&(askmin(cnt2+1,w+j)>=i))&&(
w+=j
);
que[cnt2+1].push_back(node{w,l,r-i,id,i});
cnt2=w;
}
}
inline void solve(){
T.build(l[0]);
int tp,id;
_for(i,len,1){
(a.sa[i]<=l[0])&&(T.add(a.sa[i],i),0);
int len1=que[i].size();
for_(j,0,len1-1)
(que[i][j].len>ansl[id=que[i][j].id]&&(tp=T.ask(que[i][j].l,que[i][j].r))<=que[i][j].w)&&(
ans[id]=a.sa[tp],ansl[id]=que[i][j].len,
0
);
}
}
}
using namespace Solve;
inline void In(){
memset(ansl,-1,sizeof(ansl));
int m;
FastI>>stp>>m;
l[0]=strlen(stp);
for_(i,0,l[0]-1)
s[++len]=stp[i];
s[++len]='z'+1;
for_(i,1,m){
read(q[i].first,q[i].second);
FastI>>stp;
l[i]=strlen(stp),val[i]=len+1;
for_(j,0,l[i]-1)
s[++len]=stp[j];
s[++len]='a'-1;
}
a.build(s,len,'z'+1);
for_(i,2,len)
ST[0][i]=a.height[i];
for(int j=1;(1<<j)<=len;j++)
for_(i,1,len-(1<<j)+1)
ST[j][i]=min(ST[j-1][i],ST[j-1][i+(1<<j-1)]);
for_(i,2,len)
Log[i]=Log[i>>1]+1;
for_(i,1,m)
ask(q[i].first,q[i].second,val[i],l[i],i);
solve();
for_(i,1,m){
if(ans[i]){
for_(j,ans[i],ans[i]+ansl[i])
write(char(s[j]));
write("\n");
}
else
write("-1\n");
}
}