2019牛客暑期多校训练营(第四场)I string(后缀数组 + 回文树)

题目链接
题解:
如果求出 字符串中不同的子串数量 和 反转后的字符串中不同子串的数量,那么两种的字符串合在一起之后的不同的字符串数量再 / 2 就是答案。
但是如果原来的字符串是回文串的话,那么回文串本来应该要计算两次的,但是发现它只计算了一次,所以要加上回文串的数量再 / 2。
具体做法,求不同子串的数量可以用后缀数组,先将原字符串反转加在后面,中间用一个其他字符隔开。回文子串个数可以用回文树。
代码:

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
#define PI acos(-1.0)
#define INF 0x3f3f3f3f3f3f3f3f
#define P pair<ll, int>
#define debug(x) cout << #x << ": " << x << " "
#define fastio ios::sync_with_stdio(false), cin.tie(0)
const int mod = 1e9 + 7;
const int M = 1000000 + 10;
const int N = 400000 + 10;

ll n, rk[N];
ll k, tmp[N];
bool compare_sa(int i,int j){
    if(rk[i]!=rk[j]) return rk[i]<rk[j];
    else{
        int x=i+k<=n?rk[i+k]:-1;
        int y=j+k<=n?rk[j+k]:-1;
        return x<y;
    }
}
void calc_sa(string s,int *sa){
    for(int i=0;i<=n;i++){
        sa[i]=i;
        rk[i]=i<n?s[i]:-1;
    }
    for(k=1;k<=n;k*=2){
        sort(sa,sa+n+1,compare_sa);
        tmp[sa[0]]=0;
        for(int i=1;i<=n;i++){
            tmp[sa[i]]=tmp[sa[i-1]]+(compare_sa(sa[i-1],sa[i])?1:0);
        }
        for(int i=0;i<=n;i++){
            rk[i]=tmp[i];
        }
    }
}
void calc_lcp(string s,int *sa,int *lcp){
    for(int i=0;i<=n;i++) rk[sa[i]]=i;
    int h=0;
    lcp[0]=0;
    for(int i=0;i<n;i++){
        int j=sa[rk[i]-1];
        if(h>0) h--;
        for(;j+h<n&&i+h<n;h++){
            if(s[j+h]!=s[i+h]) break;
        }
        lcp[rk[i]-1]=h;
    }
}

const int MAX = 200000 + 10;
const int ALP = 26;
struct Palindromic_Tree {
    int son[MAX][ALP]; //转移边
    int fail[MAX]; //fail 指针
    int cnt[MAX]; //当前节点表示的回文串在原串中出现了多少次
    int num[MAX]; //当前节点 fail 可以向前跳多少次
    int len[MAX]; //当前节点表示的回文串的长度
    int S[MAX]; //插入的字符串
    int last; //最后一次访问到的节点,类似 SAM
    int n; //插入的字符串长度
    int p; //自动机的总状态数

    int newnode(int l) {
        memset(son[p], 0, sizeof(son[p]));
        cnt[p] =  num[p] = 0;
        len[p] = l;
        return p++;
    }

    void init() {
        p = 0;
        newnode(0);
        newnode(-1);
        last = n = 0;
        S[n] = -1;
        fail[0] = 1;
    }

    int get_fail(int x) {
        while (S[n - len[x] - 1] != S[n]) x = fail[x];
        return x;
    }

    void add(int c) {
        c -= 'a';
        S[++n] = c;
        int cur = get_fail(last); //通过上一次访问的位置去扩展
        if (!son[cur][c]) { //如果没有对应的节点添加一个新节点
            int now = newnode(len[cur] + 2);
            fail[now] = son[get_fail(fail[cur])][c]; //通过当前节点的 fail 去扩展出新的 fail
            son[cur][c] = now;
            num[now] = num[fail[now]] + 1; //记录 fail 跳多少次
        }
        last = son[cur][c];
        cnt[last]++; //表示当前节点访问了一次
    }
    void count() {
        //如果某个节点出现一次,那么他的 fail 也一定会出现一次,并且在插入的时候没有计数
        for (int i = p - 1; i >= 0; i--) cnt[fail[i]] += cnt[i];
    }
} AUT;

string s;
int sa[N], height[N];

signed main()
{
    cin >> s;
    ll len = n = s.length();

    string st = s; reverse(s.begin(), s.end()); s = st + '#' + s;
    n = 2 * n + 1; calc_sa(s, sa), calc_lcp(s, sa, height);
    ll p = 0;
    for(int i = 1; i < n; i ++) p += height[i];
    p = n * (n + 1) / 2 - (len + 1) * (len + 1) - p;

    AUT.init();
    for(int i = 0; i < len; i ++) AUT.add(s[i]);
    ll q = AUT.p - 2;

    cout << (p + q) / 2 << endl;

    return 0;
}

/*

  Rejoicing in hope, patient in tribulation.

*/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值