题解 P3823 【[NOI2017]蚯蚓排队】

· · 题解

题意

给定n条长度不超过6的蚯蚓,初始各自在一个队伍。m次操作:1.将i号蚯蚓和j号蚯蚓的队伍合并(保证i为队尾,j为队首)。2.将i号蚯蚓和它后面的蚯蚓分离成两个队。3.询问:给定字符串S和正整数k,求f(每个长度为k的子串)的乘积。其中f(S)定义为蚯蚓在其队伍向后延伸k位组成的字符串等于S的蚯蚓个数。(n \leq 210^5,m \leq 510^5,k \leq 50,\sum |s| \leq 10^7,c \leq 10^3),其中c为操作2的数量。

算法分析

模拟+hash 暴力一点考虑,每个队伍维护一条链,合并和分裂时维护(c_{x,i})表示第x号蚯蚓向后延伸i位的字符串hash值并更新长度为i的答案,查询的时候直接枚举子串访问长度为i的答案中hash值相同的。 容易知道这样最坏复杂度是(O(mk^2+\sum |s|)),理论上依然无法通过。

继续分析。 考虑分离操作至多c次,复杂度为(O(ck^2))。 考虑合并操作,因为每次合并其实显然是不需要枚举满的,是否有可能省去一个k的复杂度?合并的终串中每个子串只会被统计一次(分离只有1000次,不影响复杂度),而总共有nk个子串,所以复杂度为(O(mk))。

总复杂度(O(ck^2+mk+\sum |s|))。 注意:我用的hash方式是一个小哈希存邻接表,一个大哈希当成真实值来检验。

AC代码

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int mod=998244353;
const int P=19260817;
const int N=P+10;
const int M=2e5+5;
const int B=17;
int read()
{
  int ret=0,op=1,c=getchar();
  while(c>'9' || c<'0')
    {
      if(c=='-') op=-1;
      c=getchar();
    }
  while(c>='0' && c<='9')
    {
      ret=ret*10+c-'0';
      c=getchar();
    }
  return ret*op;
}
int ecnt=0,head[N];
int n,m;
int nxt[M],lst[M],val[M];//nxt和lst为维护蚯蚓位置的链表
ll powB[100],pow10[100],qwq[100];
char s[10000005];
struct edge
{
  int next,cnt,last;
  ll w;
}e[N];
void add(ll hash,ll q)//哈希表,如果有对于hash这个哈希值有q这个原值则cnt++,否则新开一个点
{
  //printf("add:%lld:%lld\n",hash,q);
  for(int i=head[hash];i;i=e[i].next)
    if(e[i].w==q)
      {
    e[i].cnt++;
    return ;
      }
  e[++ecnt].cnt=1;
  e[ecnt].w=q;
  e[ecnt].next=head[hash];
  head[hash]=ecnt;
}
void dlt(ll hash,ll q)//与add同理
{
  for(int i=head[hash];i;i=e[i].next)
    if(e[i].w==q)
      {
    e[i].cnt--;
    return ;
      }
}
int query(ll hash,ll q)//与add同理
{
  for(int i=head[hash];i;i=e[i].next) if(e[i].w==q) return e[i].cnt;
  return 0;
}
int s1[100],s2[100];
void link(int u,int v)
{
  int l1=0,l2=0;
  ll hsh=0,q=0,haxi=0,Q=0;
  nxt[u]=v;
  lst[v]=u;
  for(int i=u;i&&l1<49;i=lst[i])
    s1[++l1]=val[i],hsh=(hsh+val[i]*powB[l1-1])%P,q=q+val[i]*pow10[l1-1];//将u与u之前最长50的串取出来,注意在s1中的串是反着的
  for(int i=v;i&&l2<49;i=nxt[i]) s2[++l2]=val[i];
  for(int i=l1;i>=1;i--)//s1是反串,最前面的字符在l1的位置,所以倒序枚举
    {
      haxi=0;Q=0;
      for(int j=1;j<=l2&&i+j<=50;j++)
    {
      haxi=(haxi*B+s2[j])%P;
      Q=Q*10+s2[j];
      add((hsh*powB[j]%P+haxi)%P,q*pow10[j]+Q);
    }
      hsh=((hsh-powB[i-1]*s1[i])%P+P)%P;
      q=q-pow10[i-1]*s1[i];
    }
}
void cut(int u,int v)//与link同理
{
  int l1=0,l2=0;
  ll hsh=0,q=0,haxi=0,Q=0;
  nxt[u]=0;
  lst[v]=0;
  for(int i=u;i&&l1<49;i=lst[i])
    s1[++l1]=val[i],hsh=(hsh+val[i]*powB[l1-1])%P,q=q+val[i]*pow10[l1-1];
  for(int i=v;i&&l2<49;i=nxt[i]) s2[++l2]=val[i];
  for(int i=l1;i>=1;i--)
    {
      haxi=0;Q=0;
      for(int j=1;j<=l2&&i+j<=50;j++)
    {
      haxi=(haxi*B+s2[j])%P;
      Q=Q*10+s2[j];
      dlt((hsh*powB[j]%P+haxi)%P,q*pow10[j]+Q);
    }
      hsh=((hsh-powB[i-1]*s1[i])%P+P)%P;
      q=q-pow10[i-1]*s1[i];
    }
}
int main()
{
  n=read(),m=read();
  for(int i=1;i<=n;i++) val[i]=read(),qwq[val[i]]++;
  powB[0]=pow10[0]=1;
  for(int i=1;i<=50;i++) powB[i]=powB[i-1]*B%P,pow10[i]=pow10[i-1]*10;
  int op,x,y;
  while(m--)
    {
      op=read();
      if(op==1)
    {
      x=read(),y=read();
      link(x,y);
    }
      else if(op==2)
    {
      x=read();
      cut(x,nxt[x]);
    }
      else
    {
      int k;
      scanf("%s%d",s+1,&k);
      int len=strlen(s+1);
      ll hsh=0,q=0,ans=1;
      if(k==1)//特别的,如果询问长度为1的串,因为之前我也没有加过,所以在读入的时候开一个桶来统计答案
        {
          for(int i=1;i<=len;i++) ans=ans*qwq[s[i]-'0']%mod;
          printf("%lld\n",ans%mod);
          continue;
        }
      for(int i=1;i<=k;i++)
        {
          hsh=(hsh*B+s[i]-'0')%P;
          q=q*10+s[i]-'0';
        }
      // printf("(%lld)%lld->",hsh,q);
                  ans=query(hsh,q)%mod;
      for(int i=k+1;i<=len;i++)
        {
          hsh=(((hsh-(s[i-k]-'0')*powB[k-1])%P+P)%P*B+s[i]-'0')%P;
          q=((q-(s[i-k]-'0')*pow10[k-1]))*10+s[i]-'0';
          //    printf("(%lld)%lld->",hsh,q);
                ans=ans*query(hsh,q)%mod;
        }
      printf("%lld\n",ans);
    }
    }
  return 0;
}