[HAOI2016]找相同字符(SAM&SA)

[HAOI2016]找相同字符

题目描述

给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。两个方案不同当且仅当这两个子串中有一个位置不同。

输入格式

两行,两个字符串 s 1 , s 2 s_1,s_2 s1,s2,长度分别为 n 1 , n 2 n_1,n_2 n1,n2

解法1:后缀数组SA

该题和这道题很像,可以先去学习一下该题的解法我的博客
我们注意到一个性质:对于两个后缀子串,它们的最长公共前缀就是我们可以取的子串的开头位置在后缀的起点的个数。比如 a a b b 和 a a a a aabb和aaaa aabbaaaa的最长公共前缀是 a a aa aa,那么我们可以取的相同子串就是 a 和 a a a和aa aaa
那么这题有两个串,我们参考那道题的解法,利用容斥,先把两个串合并求一下答案,再分别对两个串求一下,减去它们即可。

int height[N];
int n, sa[N], rk[N << 1], oldrk[N << 1], id[N], cnt[N], px[N]; 
//rk[i]编号为i的后缀的排名,sa[i]第i小的后缀的排名,px[i] = rk[id[i]];  
bool cmp(int x, int y, int w) {
    return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w]; 
} 
void init() {
    memset(height, 0, sizeof height);
    memset(cnt, 0, sizeof cnt);
    memset(rk, 0, sizeof rk);
    memset(cnt, 0, sizeof cnt);
    memset(id, 0, sizeof id);
}
LL ans;
void solve(string s, int flag) {
    init();
    n = s.size() - 1;
    int m = max(n, 300);
    for(int i = 1; i <= n; i++) {
        rk[i] = s[i] - '0', sa[i] = i;
    }
    for(int i = 1; i <= n; i++) ++cnt[rk[i]];
    for(int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
    for(int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;
    int p = 0, i;
    for(int w = 1; ; w <<= 1, m = p) {
        for(p = 0, i = n; i > n - w; i--) id[++p] = i;
        for(int i = 1; i <= n; i++) {
            if(sa[i] > w) {
                id[++p] = sa[i] - w;
            }
        }
        memset(cnt, 0, sizeof cnt);
        for(int i = 1; i <= n; i++) ++cnt[px[i] = rk[id[i]]];
        for(int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
        for(int i = n; i >= 1; i--) sa[cnt[px[i]]--] = id[i];
        memcpy(oldrk, rk, sizeof rk);
        for(p = 0, i = 1; i <= n; i++)
            rk[sa[i]] = cmp(sa[i], sa[i - 1], w) ? p : ++p;
        if(p == n) {
            for(int i = 1; i <= n; i++) sa[rk[i]] = i;
            break;
        } 
    }
    for(int i = 1, k = 0; i <= n; i++) {
        if(k) --k;
        while(s[i + k] == s[sa[rk[i] - 1] + k]) k++;
        height[rk[i]] = k;
    }
    stack<int> st;
    vector<LL> f(n + 1);
    for(int i = 2; i <= n; i++) {
        while(st.size() && height[st.top()] >= height[i]) st.pop();
        if(st.size() == 0) f[i] = height[i] * 1LL * (i - 1);
        else {
            f[i] = f[st.top()] + 1LL * height[i] * (i - st.top());
        }
        st.push(i);
        ans = ans + flag * 1LL * f[i];
    }
    
}
signed main() {
#ifdef JANGYI
    freopen("input.in", "r", stdin);
    freopen("out.out", "w", stdout);
    // auto now = clock();
#endif
    string a, b;
    cin >> a >> b;
    solve(' ' + a + "~" + b, 1);
    solve(' ' + a, -1);
    solve(' ' + b, -1);
    cout << ans << '\n';

// #ifdef JANGYI

//     cout << "================================" << endl;
//     cout << "Program run for " << (clock() - now) / (double)CLOCKS_PER_SEC * 1000 << " ms." << endl;
// #endif
    return 0;
}   

解法二:后缀自动机SAM

我们先对两个串建自动机,我们知道在 p a r e n t parent parent树中从初始状态到每个点的路径都是一个子串,那么我们做一遍 d f s dfs dfs求每个子串出现的次数。
对于在两个SAM上通过相同转移而到达的两个结点(即使它们编号不同,且不在一个SAM上),它们表示的是同一子串,那么计算即可。

char s1[N], s2[N];
struct Suffix_Automation {
    int len[M], link[M], sz[M];
    int h[M], ne[M], e[M], idx;
    int ch[M][26];
    int siz, last;
    inline void init() {
        memset(len, 0, sizeof len);
        memset(h, -1, sizeof h);
        memset(link, 0, sizeof link);
        memset(ch, 0, sizeof ch);
        siz = last = 0;
        link[0] = -1;
    }
    inline void extend(char *str) {
        int n = strlen(str);
        for(int i = 0; i < n; i++) {
            int cur = ++siz, p = last, c = str[i] - 'a';
            sz[cur] = 1;
            len[cur] = len[p] + 1;//初始化新节点
            while(p != -1 && !ch[p][c]) { //跳link添加边
                ch[p][c] = cur;
                p = link[p];
            }
            if(p == -1) link[cur] = 0; //跳到了初始节点
            else {
                int q = ch[p][c]; //找到出边
                if(len[q] == len[p] + 1) link[cur] = q;
                else { //分裂节点
                    int copy = ++ siz;
                    len[copy] = len[p] + 1;
                    link[copy] = link[q];
                    for(int i = 0; i < 26; i++) ch[copy][i] = ch[q][i];
                    while(p != -1 && ch[p][c] == q) {
                        ch[p][c] = copy;
                        p = link[p];
                    }
                    link[q] = link[cur] = copy;
                }
            }
            last = cur;
        }
    }
    inline void add(int a, int b) {
        e[idx] = b; ne[idx] = h[a]; h[a] = idx++;
    }
    inline void dfs(int u) {
        for(int i = h[u]; ~i; i = ne[i]) {
            int j = e[i];
            dfs(j);
            sz[u] += sz[j];
        }
    }
    inline void add_tree() {
        for(int i = 1; i <= siz; i++) add(link[i], i);
        dfs(0);
    }

}SAM1, SAM2;

LL ans = 0;
void dfs(int x, int y) {
    for(int i = 0; i < 26; i++) {
        if(SAM1.ch[x][i] && SAM2.ch[y][i]) 
            dfs(SAM1.ch[x][i], SAM2.ch[y][i]);
    }
    if(x != 0 && y != 0) ans += SAM1.sz[x] * 1LL * SAM2.sz[y];
}
signed main() {
#ifdef JANGYI
    freopen("input.in", "r", stdin);
    freopen("out.out", "w", stdout);
    // auto now = clock();
#endif
    scanf("%s%s", s1, s2);

    SAM1.init(); SAM2.init();
    SAM1.extend(s1); SAM2.extend(s2);
    SAM1.add_tree(); SAM2.add_tree();

    dfs(0, 0);

    cout << ans << '\n';

// #ifdef JANGYI

//     cout << "================================" << endl;
//     cout << "Program run for " << (clock() - now) / (double)CLOCKS_PER_SEC * 1000 << " ms." << endl;
// #endif
    return 0;
}   

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值