ICPC World Final 2019 G First of Her Name :广义SAM / 离线ACAM / 树上SA

交题链接:First of Her Name

题意:

给出n个人,他们每个人的名字都是之前某个人的名字在最前边加上一个字母得到。比如2号的名字是 A C AC AC,3号在2号前边加一个字母 K K K,这样3号的名字是 K A C KAC KAC
之后给出k次询问,每次给出一个字符串 T T T,询问名字前缀是 T T T的有几个人。

题解1:

把每个人名字反过来看,所有人的名字就是一个Trie。要求的是以某个T(需要把输入的T给做一次reverse)为后缀的串个数。因此考虑在Trie上建出SAM,然后答案就是T所在自动机节点的后缀树子树大小
那么由Trie建SAM有若干假算法:如在trie上dfs,然后插入SAM。

正确做法是:在trie上做bfs,这样保证每次插入的点都是第一次出现,而且是按照长度递增的顺序插入,因此复杂度是均摊掉的。只需要每次记录trie上的每个点对应于sam上的每个点,节点的分裂操作并不会改变trie点与sam点的对应关系,因为trie每个点对应的都是“前缀串”,而“前缀串”是他所在节点能够表示的最长串,因此分裂操作只要让原来的老点表示长的那部分,对应关系就不变,可以类比于单串sam的每个前缀所在节点是不变的。

总感觉国内应该每个人都会的trie上sam,结果final上却没人秒这个题。

Code:


// Created by calabash_boy on 19-4-5.
//wf2019 first of her name
//build sam using trie
#include
using namespace std;
const int maxn = 1e6+100;
typedef long long ll;
struct Suffix_Automaton{
    int nxt[maxn*2][26],fa[maxn*2],l[maxn*2];
    int last,cnt;
    vector<int> E[maxn*2];
    int Num[maxn*2];
    Suffix_Automaton(){ clear(); }
    void clear(){
        last =cnt=1;
        fa[1]=l[1]=0;
        memset(nxt[1],0,sizeof nxt[1]);
    }
    int add(int pre,int c,int num){
        last = pre;
        int p = last;
        int np = ++cnt;
        Num[np] = num;
        memset(nxt[cnt],0,sizeof nxt[cnt]);
        l[np] = l[p]+1;last = np;
        while (p&&!nxt[p][c])nxt[p][c] = np,p = fa[p];
        if (!p)fa[np]=1;
        else{
            int q = nxt[p][c];
            if (l[q]==l[p]+1)fa[np] =q;
            else{
                int nq = ++ cnt;
                l[nq] = l[p]+1;
                memcpy(nxt[nq],nxt[q],sizeof (nxt[q]));
                fa[nq] =fa[q];fa[np] = fa[q] =nq;
                while (nxt[p][c]==q)nxt[p][c] =nq,p = fa[p];
            }
        }
        return np;
    }
    int dfsl[maxn*2],dfsr[maxn*2];
    int dfn = 0;
    ll sum[maxn*2];
    void dfs(int u){
        dfsl[u] = ++dfn;
        sum[dfn] = Num[u];
        for (int v : E[u]){
            dfs(v);
        }
        dfsr[u] = dfn;
    }
    void build(){
        for (int i=2;i<=cnt;i++){
            E[fa[i]].push_back(i);
        }
        dfs(1);
        for (int i=1;i<=cnt;i++){
            sum[i] += sum[i-1];
        }
    }
    void query(char * s){
        int temp = 1;
        while (*s){
            int ch = *s - 'A';
            if (!nxt[temp][ch]){
                printf("0\n");
                return;
            }
            temp = nxt[temp][ch];
            s++;
        }
        ll ans = sum[dfsr[temp]] - sum[dfsl[temp] - 1];
        printf("%lld\n",ans);
    }
}sam;
struct Trie{
    int Root = 1;
    int cnt = 2;
    int nxt[maxn][26];
    int num[maxn];
    int sam_pos[maxn];
    int add(int p,int ch){
        if (!nxt[p][ch]){
            nxt[p][ch] = cnt++;
        }
        int now = nxt[p][ch];
        num[now] ++;
        return now;
    }
    void bfs(){
        queue<int> Q;
        Q.push(1);
        sam_pos[1] = 1;
        while (!Q.empty()){
            int head = Q.front();
            Q.pop();
            for (int i=0;i<26;i++){
                if (!nxt[head][i])continue;
                int now = nxt[head][i];
                sam_pos[now] = sam.add(sam_pos[head],i,num[now]);
                Q.push(now);
            }
        }
    }
}trie;
int trie_pos[maxn];
int main(){
    int n,k;
    scanf("%d%d",&n,&k);
    trie_pos[0] = 1;
    for (int i=1;i<=n;i++){
        static char s[5];
        int p;
        scanf("%s%d",s,&p);
        int ch = s[0] - 'A';
        trie_pos[i] = trie.add(trie_pos[p],ch);
    }
    trie.bfs();
    sam.build();
    for (int i=0;i<k;i++){
        static char t[maxn];
        scanf("%s",t);
        int N = strlen(t);
        reverse(t,t+N);
        sam.query(t);
    }
    return 0;
}

题解2:

考虑trie上每个点对应于一个串,这个串是从这个点走到根得到的串。我们把询问串也插入到trie中,然后求一次树上的SA,需要预处理得到每个点向跟方向跳2的幂次步到达的点。

然后处理询问的时候,就是二分得到一个 L C P > = ∣ T ∣ LCP >= |T| LCP>=T的区间,进行区间求值了。据说细节比较多,而且如果写两个log会被卡掉。

题解3:

这应该是最好写的了。对询问串建ACAM,然后拿着Trie在ACAM上跑,每次给一整条fail链贡献一下答案。拿着trie在sam上跑的意思就是找到trie每个点对应的acam节点,bfs即可。

你可能感兴趣的:(Others)