字符串普及题

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


题意

  你有一个 (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;
}
原文地址:https://www.cnblogs.com/scx2015noip-as-php/p/190806b.html