2020 CCPC Wannafly Winter Camp Day2 K.破忒头的匿名信(AC自动机)

题目:https://ac.nowcoder.com/acm/contest/4010

一看到题面,很容易就往dp上想,而dp式也很容易想到,当前dp[i]可以从dp[i - (当前前缀i所有在字典中出现的后缀长度)]位置转移过来,所有转移加上该匹配后缀的cost取个min就是当前dp[i]的值。

直觉上,dp的转移位置好像很多,似乎是n方级别的,这就不可做了。一眼看上去确实如此,于是我往许多方向思考解法,发现好像都不可做,才重新回到dp的思考上。这时候忽然留意到题目限制了所有字典串的长度之和不超过5e5,才反应过来这个转移位置,真的不多。

我们思考如何构造样例,才会让一个位置有很多转移,那么就只有把一个dp位置的所有后缀,都丢进字典里,才能使得这个位置的转移个数最多。但是每个后缀都是上一个后缀的长度+1,放的后缀越多,单个后缀的长度也就越长,由于题目限制了总和,那么最多只有根号n种长度可放进字典(这也是字符串题常见的操作了),那么这题在最坏情况单字符串(“aaaaaaa…”)下,dp的复杂度也只是O(n根号n)而不是n方。

那就可以放心大胆的dp了。

下一个问题就是如何处理匹配位置,匹配是字符串题最基本的操作,可以暴力(显然8行),kmp(也显然8行),按根号分成大小串优雅的暴力(应该ok),和ac自动机(完全ok)。

那么这题就可以用ac自动机愉快的完成多串匹配。我们对字典建立ac自动机,然后在dp的同时向上跳fail,就可以找到匹配的位置。

于是就愉快的tle了。虽然转移位置最多只有根号n个,但是在ac自动机上,fail可跳的位置并非根号n个,而是去到了n方个,tle情理之中。

这里需要使用一个自动机算法常用的优化,建trans,即只包含你需要节点的fail树,我只在回文树上用过,写在ac自动机上还是第一次,不过原理都是共通的,如果当前节点的fail节点不在字典里,那么当前节点的trans就接到父亲的trans上,如果当前节点的fail在字典里,当前节点的trans就接到他的fail上。

这个操作就让复杂度重新回到了根号级别。
(不过看了别人的博客,听说现场结构体写法不建trans直接t飞,数组写法不建trans也能ac,然而我们现场是队友敲的结构体写法,下面这份代码是我飞机延误无聊敲的)

这题在camp现场敲的时候还是很酣畅淋漓的,不断地遇到问题,然后又毫无磕绊的解决问题,最后ac(虽然wa了好几发,事实告诉我们现场不看板子敲自动机还是很有难度的…),写的真的开心hhh。

ac代码:

#include

using namespace std;
typedef long long ll;

const int maxn = 5e5 + 5;
const ll inf = 1e18;


int n, x;
char s[maxn];

struct AC_Automaton {
    int next[maxn][26];
    ll dp[maxn], val[maxn];
    int len[maxn];
    int fail[maxn], trans[maxn];
    int sz, root;

    int newNode() {
        memset(next[sz], 0, sizeof(next[sz]));
        fail[sz] = 0;
        val[sz] = inf;
        return sz++;
    }

    void init() {
        sz = 0;
        root = newNode();
        val[root] = 0;
    }

    int add() {
        int p = root;
        for (int i = 0, c; s[i]; ++i) {
            c = s[i] - 'a';
            if (!next[p][c]) {
                next[p][c] = newNode();
            }
            len[next[p][c]] = len[p] + 1;
            p = next[p][c];
        }
        val[p] = min(val[p], 1LL * x);
        return p;
    }

    void getFail() {
        queue<int> q;
        for (int i = 0; i < 26; i++) {
            if (next[root][i]) {
                q.push(next[root][i]);
                trans[next[root][i]] = root;
            } else {
                next[root][i] = root;
            }
        }
        while (!q.empty()) {
            int p = q.front();
            q.pop();

            for (int i = 0; i < 26; i++) {
                if (next[p][i]) {
                    fail[next[p][i]] = next[fail[p]][i];
                    q.push(next[p][i]);

                    int q = next[fail[p]][i];
                    if (val[q] >= inf) {
                        trans[next[p][i]] = trans[q];
                    } else {
                        trans[next[p][i]] = q;
                    }
                } else {
                    next[p][i] = next[fail[p]][i];
                }
            }
        }
    }

    void solve() {
        int p = root, lens = strlen(s + 1);
        for (int i = 1; i <= lens; ++i) {
            dp[i] = inf;
        }
        //dp要从1开始,因为dp[0]是一个必要的转移位置,dp[0] = 0
        for (int i = 1; i <= lens; ++i) {
            p = next[p][s[i] - 'a'];

            int tmp = p;
            while (tmp != root) {
                dp[i] = min(dp[i], dp[i - len[tmp]] + val[tmp]);
                tmp = trans[tmp];
            }
        }
        if (dp[lens] >= inf) {
            dp[lens] = -1;
        }
        printf("%lld\n", dp[lens]);
    }

} ac;


int main() {
    scanf("%d", &n);
    ac.init();
    for (int i = 0; i < n; ++i) {
        scanf("%s%d", s, &x);
        ac.add();
    }
    scanf("%s", s + 1);
    ac.getFail();
    ac.solve();
    return 0;
}


你可能感兴趣的:(AC自动机,ACM)