zoukankan      html  css  js  c++  java
  • 字符串普及题

    大家好,我是普及组选手,我又来写普及组题了


    题意

      你有一个 (map<string,int> a) 需要支持 (3) 种操作:
      每次给定整数 (opt) 和字符串 (s)
      若 (opt=1),则 (a[s]++)
      若 (opt=2),则 (a[s]--)
      若 (opt=3),则输出 (sum_t a[t] imes (t在s中出现的次数))
      强制在线,维护一个 (mask),初始时为 (0),每次操作的 (opt) 异或上 (mask),每次操作 (3) 后,设答案为 (ans),将 (mask) 异或上 (|ans|)
      (n,sum |s| le 10^6)

    题解

      文字游戏啊
      原题 (3) 操作没有加那个括号,然后我看错题了,写了一发交上去考后发现爆 (0)
      此后还要注意 (ans) 可能是负的(因为 (a[s]) 可以减成负数),而 (mask) 要异或上 (ans)绝对值
      
      喷完了,题解不想多说,没看错题的人应该都切了
      显然强制在线是假的,没有加密字符串,所以可以预先对所有输入的字符串建 AC 自动机
      然后在线扫一遍询问,问题就转化成了 AC 自动机上单点加/减 (1)、询问一条路径上每个点在 fail 树上到根的点权和之和
      在 AC 自动机上跑一遍询问串,每走一个点就取其 fail 树上的链上权值和。树链剖分树状数组就行了
      复杂度 (O(nlog n)),然而这个做法利用了没有加密字符串的漏洞

    code
    #include<bits/stdc++.h>
    #define ll long long
    #define N 1000005
    using namespace std;
    int n,op[N],len[N],bg[N],Len;
    char s[N];
    struct edge{int v,nxt;}e[N<<1];
    int hd[N],cnt;
    inline void add(int u, int v){e[++cnt]=(edge){v,hd[u]}, hd[u]=cnt;}
    namespace ACA{
    	int ch[N][27],num[N],tot=1;
    	void ins(char *s, int id){
    		int u=1;
    		for(int i=0; i<len[id]; ++i){
    			int c=s[i]-'a';
    			if(!ch[u][c]) ch[u][c]=++tot;
    			u=ch[u][c];
    		}
    		num[id]=u;
    	}
    	int fail[N];
    	queue<int> Q;
    	void build(){
    		for(int i=0; i<26; ++i) ch[0][i]=1;
    		Q.push(1);
    		while(!Q.empty()){
    			int u=Q.front(); Q.pop();
    			for(int i=0; i<26; ++i)
    				if(!ch[u][i]) ch[u][i]=ch[fail[u]][i];
    				else fail[ch[u][i]]=ch[fail[u]][i], Q.push(ch[u][i]);
    		}
    		for(int i=2; i<=tot; ++i){
    			//cout<<fail[i]<<' '<<i<<endl;
    			add(fail[i],i);
    		}
    	}
    }using namespace ACA;
    int dfn[N],siz[N],idx;
    void dfs(int u){
    	dfn[u]=++idx, siz[u]=1;
    	for(int i=hd[u]; i; i=e[i].nxt) dfs(e[i].v), siz[u]+=siz[e[i].v];
    }
    struct Fenwick{
    	int tr[N];
    	inline int lowbit(int x){return x&-x;}
    	inline void add(int i, int v){
    		for(; i<=idx; i+=lowbit(i)) tr[i]+=v;
    	}
    	inline int query(int i){
    		int ret=0;
    		for(; i>0; i-=lowbit(i)) ret+=tr[i];
    		return ret;
    	}
    }fwk;
    ll ans;
    void query(int l, int r){
    	int u=1;
    	for(int i=l; i<=r; ++i){
    		int c=s[i]-'a';
    		u=ch[u][c];
    		ans+=fwk.query(dfn[u]);
    	}
    	//cout<<ans<<endl;
    }
    char str[N];
    int main(){
    	scanf("%d",&n);
    	for(int i=1; i<=n; ++i){
    		scanf("%lld%s",&op[i],str);
    		len[i]=strlen(str);
    		bg[i]=Len+1;
    		ins(str,i);
    		for(int j=0; j<len[i]; ++j) s[++Len]=str[j];
    	}
    	build();
    	dfs(1);
    	int mask=0;
    	for(int i=1; i<=n; ++i){
    		op[i]^=mask;
    		if(op[i]==1)
    			fwk.add(dfn[num[i]],1),
    			fwk.add(dfn[num[i]]+siz[num[i]],-1);
    		else if(op[i]==2)
    			fwk.add(dfn[num[i]],-1),
    			fwk.add(dfn[num[i]]+siz[num[i]],1);
    		else{
    			ans=0;
    			//cout<<bg[i]<<' '<<len[i]<<endl;
    			query(bg[i],bg[i]+len[i]-1);
    			cout<<ans<<endl;
    			mask^=labs(ans);
    		}
    	}
    	return 0;
    }
    

      
      官方题解比较神仙,是这么说的:

      用二进制分组建 (log n) 个 AC 自动机,记录每个 AC 自动机的 (size)。若有两个 AC 自动机的 (size) 相同就合并。和启发式合并、线段树类似,每个串最多被插入 (log n) 次,所以总时间复杂度是 (O(nlog n))

      这里详细地举个例子:你本来是对对每个 (1,2) 操作给出的 (s) 串建一个 AC 自动机(对于 (2) 操作,建一个终点权值为 (-1) 的 AC 自动机),但插入 (15) 个串之后,前 (8) 个串合并成了一个 AC 自动机,第 (9-12) 个串合并成了一个,第 (13-14) 个串被合并成了一个,第 (15) 个串合并成了一个。不难发现,加入第 (16) 个串时,它会和第 (15) 个串的 AC 自动机合并,再和第 (13-14) 个串的 AC 自动机合并,以此类推,最终第 (1)(16) 个串的 AC 自动机合并成了一个。
      AC 自动机合并的方法跟线段树合并一样。
      然后我们考虑查询。查询时肯定还是要把 (s) 串放到每个 AC 自动机上匹配,累加经过的每个点在 fail 树上到根的点权和之和。
      由于这种方法建出来的 AC 自动机是静态的,你在建树时可以直接 bfs 一遍,把 fail 树上的父亲对儿子的贡献给加到儿子上。这样查询的时候,经过的每个点上已经存好了该点在 fail 树上到根的点权和,无需再用数据结构动态维护。

    • 建 AC 自动机的总复杂度是 (O(|s|log n))
    • 合并两个 AC 自动机的复杂度是 (O(min(size[a],size[b])),用启发式合并的角度考虑,每个串的 AC 自动机最多被合并 (log n) 次,而每个串的 AC 自动机初始大小就是该串长度 (|s|),所以这部分的总复杂度 (O(sum|s|log n))
    • 查询的总复杂度是 (O(sum|s|log n))(要在 (log) 个 AC 自动机上都跑一遍)。

      (sum|s|)(n) 同级,三部分复杂度相加,总复杂度 (O(nlog n))
      这个做法支持加密输入的串 (s),不过加密后好像期望答案就趋近于 (0) 了(因为字符集大小为 (26) 的情况下,随机出的两个串很难有谁包含谁的关系),所以 yww 供题时没加密输入的串,然后就被“离线”做法艹翻了……

    code from 2019gzez01
    #include <bits/stdc++.h>
    #define ll long long 
    using namespace std;
    const int MAXN=1e6+10;
    int val[MAXN];
    long long mark;
    struct acam
    {
        int ch[MAXN][26],f[MAXN],rt[MAXN],sz[MAXN],c[MAXN];
        ll sum[MAXN];
        queue<int> q;
        int tot,cnt;
     
        int insert(char *s,int delta)
        {
            rt[++cnt]=++tot;
            _insert(rt[cnt],s,delta);
            sz[cnt]=1;
            while (cnt>=2&&sz[cnt]==sz[cnt-1])
                {
                    rt[cnt-1]=merge(rt[cnt-1],rt[cnt]);
                    sz[cnt-1]*=2;
                    --cnt;
                }
            get_f(rt[cnt]);
        }
        int _insert(int root,char *s,int delta)
        {
            int now=root,len=strlen(s);
            for (int i=0; i<len; now=ch[now][s[i]-'a'],++i)
                if (!ch[now][s[i]-'a'])
                    ch[now][s[i]-'a']=++tot;
            val[now]+=delta;
        }
        int merge(int x,int y)
        {
            if (!x||!y)
                return x+y;
            for (int i=0; i<26; ++i)
                ch[x][i]=merge(ch[x][i],ch[y][i]);
            val[x]+=val[y];
            return x;
        }
        int get_f(int root)
        {
            int v,u;
            c[0]=0;
            f[root]=0;
            for (int i=0; i<26; ++i)
                if (ch[root][i]) q.push(ch[root][i]),f[ch[root][i]]=root;
            while (!q.empty())
                {
                    v=q.front();
                    q.pop();
                    c[++c[0]]=v;
                    for (int i=0; i<26; ++i)
                        {
                            if (!ch[v][i]) continue;
                            u=f[v];
                            while (u&&!ch[u][i]) u=f[u];
                            if (u)
                                f[ch[v][i]]=ch[u][i];
                            else f[ch[v][i]]=root;
                            q.push(ch[v][i]);
                        }
                }
            for (int i=1; i<=c[0]; ++i)
                {
                    sum[c[i]]=val[c[i]];
                    if (f[c[i]]) sum[c[i]]+=sum[f[c[i]]];
                }
        }
        int get_ans(char *s)
        {
            ll ans=0;
            for (int i=1; i<=cnt; ++i)
                ans+=_get_ans(s,rt[i]);
            printf("%lld
    ",ans);
            mark^=abs(ans);
        }
        ll _get_ans(char *s,int root)
        {
            ll ret=0;
            int len=strlen(s),now=root,p;
            for (int i=0; i<len; ++i)
                {
                    p=s[i]-'a';
                    while (!ch[now][p]&&now)
                        now=f[now];
                    if (now)
                        now=ch[now][p];
                    else
                        now=root;
                    ret+=sum[now];
                }
            return ret;
        }
    } ac;
    int n,m;
    int main()
    {
        ll op;
        int len;
        char s[MAXN];
        scanf("%d",&n);
        mark=0;
        for (int i=1; i<=n; ++i)
            {
                scanf("%d%s",&op,s);
                op=op^mark;
                if (op==1)
                    {
                        ac.insert(s,1);
                    }
                else if (op==2)
                    {
                        ac.insert(s,-1);
                    }
                else
                    {
                        ac.get_ans(s);
                    }
            }
        return 0;
    }
    
  • 相关阅读:
    [转载] 十问 TiDB :关于架构设计的一些思考 TiDB
    blender low poly + unity 3d游戏制作
    d2js + activiti 备忘
    使用ActionFilterAttribute进行重定向注意事项
    一键发布部署vs插件[AntDeploy],让net开发者更幸福
    Docker常用命令
    C# 自然周,月,季度计算。
    .Net Core Web Api使用模型验证验证参数合法性
    WebApi 路由机制剖析
    WebApi路由机制详解
  • 原文地址:https://www.cnblogs.com/scx2015noip-as-php/p/190806b.html
Copyright © 2011-2022 走看看