1. 程式人生 > >【題解】AC自動機題解合集

【題解】AC自動機題解合集

  最近貌似大家都在搞字串?很長一段時間都沒有寫部落格了……還是補一補坑吧。

  感覺AC自動機真的非常優美了,通過在trie樹上建立fail指標可以輕鬆解決多模匹配的問題。實際上在AC自動機上的匹配可以看做是拿著一個串在上面跑,在固定一個左端點的時候儘量地向右匹配。如果發現實在是匹配不下去了,就向右挪動左端點實現新的匹配(跳轉fail指標)。基本上根據這一條理解,就可以解決大部分的問題了。

  AC自動機裸考的不多,除了匹配之外一個較常見的搭配就是和DP結合在一起。但本質上依然是在匹配串,只要根據fail指標的指向去轉移dp狀態即可。

  1.[HNOI2006]最短母串問題

    非常明確的指向:n <= 12。一眼狀壓,我們建立狀態 \(f[u][S]\) 表示在匹配到AC自動機上的狀態 \(u\) 的時候,已經匹配上的串為 \(S\) 集合時的方案數。也許會有疑問:那麼怎麼保證步數最短&能夠輸出字典序最小的解?注意AC自動機上相鄰狀態的轉移意味著添加了一個字元,這樣我們可以方便地BFS轉移。優先轉移小的字元可以保證字典序最小,發現答案後直接輸出即可。

#include <bits/stdc++.h>
using namespace std;
#define maxn 605
#define maxc 55
#define maxm 5000
int n, tot, cnt, Ans[maxn];
int ch[maxn][26], fail[maxn];
int mark[maxn], bits[30];
char s[maxc];

struct node
{
    int b;
    short a, c;
    node(short _a = 0, int _b = 0, short _c = -3
) { a = _a, b = _b, c = _c; } }g[maxn][maxm], ans; queue <node> q; void Ins(int x) { int L = strlen(s + 1), p = 0; for(int i = 1; i <= L; i ++) { int u = s[i] - 'A'; if(!ch[p][u]) ch[p][u] = ++ tot; p = ch[p][u]; } mark[p] = (mark[p] | bits[x - 1
]); } void Build() { queue <int> q; for(int i = 0; i < 26; i ++) if(ch[0][i]) q.push(ch[0][i]); while(!q.empty()) { int u = q.front(); q.pop(); for(int i = 0; i < 26; i ++) { if(ch[u][i]) { fail[ch[u][i]] = ch[fail[u]][i]; mark[ch[u][i]] |= mark[fail[ch[u][i]]]; q.push(ch[u][i]); } else ch[u][i] = ch[fail[u]][i]; } } } void DP() { q.push(node(0, 0)); g[0][0].c = -1; while(!q.empty()) { node now = q.front(); q.pop(); int u = now.a, S = now.b; if(S == bits[n] - 1) { ans = node(u, S, g[u][S].c); break; } for(int i = 0; i < 26; i ++) { int v = ch[u][i], s = S | mark[v]; if(g[v][s].c == -3) { g[v][s] = node(u, S, i); q.push(node(v, s)); } } } } int main() { scanf("%d", &n); bits[0] = 1; for(int i = 1; i < 21; i ++) bits[i] = bits[i - 1] << 1; for(int i = 1; i <= n; i ++) { scanf("%s", s + 1); Ins(i); } Build(); DP(); for(; g[ans.a][ans.b].c != -1; ans = g[ans.a][ans.b]) Ans[++ cnt] = g[ans.a][ans.b].c; for(int i = cnt; i >= 1; i --) printf("%c", Ans[i] + 'A'); return 0; }

  

  2.[JSOI2009]密碼

  emmmm……如果沒有輸出方案一說,和上題完全就是一樣的做法但是我們要輸出方案呀?想想如果想要在AC自動機上去爆搜也保證複雜度的話,大概藉助一個dp陣列表示從當前狀態往後轉移是否可能出現合法解就好了吧?所以狀態的設立定為從當前狀態走到目的狀態的方案數。記憶化搜尋大法好!(但是好像沒有人這麼寫?明明這樣寫真的又無腦又簡單呀……)

#include <bits/stdc++.h>
using namespace std;
#define maxn 100000
#define int long long
int n, m, tot, bits[30], f[115][1200][27];
int cnt, mark[maxn], fail[maxn], ch[maxn][26];
char s[maxn];

int read()
{
    int x = 0, k = 1;
    char c; c = getchar();
    while(c < '0' || c > '9') { if(c == '-') k = -1; c = getchar(); }
    while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * k;
}

void Ins(int x)
{
    int t = strlen(s + 1), p = 0;
    for(int i = 1; i <= t; i ++)
    {
        int u = s[i] - 'a';
        if(!ch[p][u]) ch[p][u] = ++ tot;
        p = ch[p][u]; 
    }
    mark[p] |=  bits[x - 1];
}

void Build()
{
    queue <int> q;
    for(int i = 0; i < 26; i ++) 
        if(ch[0][i]) q.push(ch[0][i]);
    while(!q.empty())
    {
        int u = q.front(); q.pop();
        for(int i = 0; i < 26; i ++)
        {
            int v = ch[u][i];
            if(v) 
            {
                fail[v] = ch[fail[u]][i];
                mark[v] |= mark[fail[v]];
                q.push(v);
            }
            else ch[u][i] = ch[fail[u]][i];
        }
    }
}

void Up(int &x, int y) { x = (x + y); }
int DP(int x, int y, int z)
{
    if(z == n && y == bits[m] - 1) return f[x][y][z] = 1;
    else if(z == n) return 0;
    if(f[x][y][z] != -1) return f[x][y][z];
    else f[x][y][z] = 0; 
    for(int c = 0; c < 26; c ++)
    {
        int v = ch[x][c];
        Up(f[x][y][z], DP(v, y | mark[v], z + 1));
    }
    return f[x][y][z];
}

void dfs(int x, int y, int z)
{
    if(z == n) 
    { 
        for(int i = 1; i <= cnt; i ++) printf("%c", s[i]); 
        puts(""); return; 
    }
    for(int c = 0; c < 26; c ++)
    {
        int v = ch[x][c];
        if(f[v][y | mark[v]][z + 1] > 0) 
        {
            s[++ cnt] = c + 'a'; 
            dfs(v, y | mark[v], z + 1);
            cnt --;
        }
    }
}

signed main()
{
    n = read(), m = read();
    memset(f, -1, sizeof(f));
    bits[0] = 1; for(int i = 1; i < 20; i ++) bits[i] = bits[i - 1] << 1;
    for(int i = 1; i <= m; i ++)
    {
        scanf("%s", s + 1);
        Ins(i);
    }
    Build(); DP(0, 0, 0);
    printf("%lld\n", f[0][0][0]);
    if(f[0][0][0] > 42) return 0;
    dfs(0, 0, 0);
    return 0;
}

 

 

  3.[BJOI2017]魔法咒語

    這題首先觀察一下資料範圍,發現一定是兩種做法的題(並沒有統一的資料範圍)。前面的直接暴力建立狀態 \(f[i][j]\) 表示第 \(i\) 個字元匹配到了AC自動機上的 \(j\) 狀態的方案數。可以列舉用哪一個串轉移,只要不會踩到禁忌狀態就可以轉移。為了降低複雜度,可以預處理一下。至於後面的資料,看到這麼大的資料範圍顯然矩陣。發現長度 <= 2;所以我們可以有:

  差不多這樣子去構造矩陣。狀態和轉移方式是不變的,構造矩陣優化dp就好。

#include <bits/stdc++.h>
using namespace std;
#define maxn 6300
#define maxm 205
#define mod 1000000007
int n, m1, m2, ans, tot, f[maxm][maxn];
int len[maxn], ch[maxn][26], fail[maxn];
int rec1[maxn][maxn], rec2[maxn][maxn], trans[maxn][maxm];
bool error[maxn];
char s[maxm][maxm];

struct matrix
{
    int a[205][205];
    matrix() { memset(a, 0, sizeof(a)); }
    friend matrix operator *(const matrix& a, const matrix& b)
    {
        matrix c;
        memset(c.a, 0, sizeof(c.a));
        int t = tot * 2;
        for(int i = 1; i <= t; i ++)
            for(int j = 1; j <= t; j ++)
                for(int k = 1; k <= t; k ++)
                    c.a[i][j] = (c.a[i][j] + 1ll * a.a[i][k] * b.a[k][j] % mod) % mod;
        return c;
    }
}M;

void Up(int &x, int y) { x = (x + y); if(x >= mod) x -= mod; }
void Ins(int x)
{
    int p = 0; len[x] = strlen(s[x] + 1);
    for(int i = 1; i <= len[x]; i ++)
    {
        int v = s[x][i] - 'a';
        if(!ch[p][v]) ch[p][v] = ++ tot;
        p = ch[p][v];
    }
    error[p] = 1;
}

int Get(int u, int x)
{
    int p = u;
    for(int i = 1; i <= len[x]; i ++)
    {
        int v = s[x][i] - 'a';
        if(error[p]) return -1;
        p = ch[p][v];
    }
    if(error[p]) return -1;
    return p;
}

void Build()
{
    queue <int> q;
    for(int i = 0; i < 26; i ++) if(ch[0][i]) q.push(ch[0][i]);
    while(!q.empty())
    {
        int u = q.front(); q.pop();
        for(int i = 0; i < 26; i ++) 
        {
            int v = ch[u][i];    
            if(v)
            {
                fail[v] = ch[fail[u]][i];
                error[v] |= error[fail[v]];
                q.push(v);
            }
            else ch[u][i] = ch[fail[u]][i];
        }
    }
    for(int j = 0; j <= tot; j ++)
        for(int i = 1; i <= m1; i ++) 
        {
            trans[j][i] = Get(j, i); if(trans[j][i] == -1) continue;
            if(len[i] == 1) rec1[j + 1][trans[j][i] + 1] ++;
            else rec2[j + 1][trans[j][i] + 1] ++;
        }
}

void DP1()
{
    f[0][0] = 1;
    for(int k = 0; k <= n; k ++)
        for(int i = 0; i <= tot; i ++)
        {
            if(!f[k][i]) continue;
            for(int j = 1; j <= m1; j ++)
            {
                int t = trans[i][j];
                if(t == -1) continue;
                else if(k + len[j] <= n) Up(f[k + len[j]][t], f[k][i]);
            }
        }
    for(int i = 0; i <= tot; i ++) 
        if(!error[i]) Up(ans, f[n][i]);
}

matrix Qpow(int timer)
{
    matrix base;
    memset(base.a, 0, sizeof(base.a));
    for(int i = 1; i <= 2 * tot; i ++) base.a[i][i] = 1;
    for(; timer; timer >>= 1, M = M * M)
        if(timer & 1) base = base * M;
    return base;
}

void DP2()
{
    tot ++; int t = 2 * tot;
    for(int i = tot + 1; i <= t; i ++) M.a[i][i - tot] = 1;
    for(int i = 1; i <= tot; i ++)
        for(int j = tot + 1; j <= t; j ++)
            M.a[i][j] = rec2[i][j - tot];
    for(int i = tot + 1; i <= t; i ++)
        for(int j = tot + 1; j <= t; j ++)
            M.a[i][j] = rec1[i - tot][j - tot];
    
    matrix ret = Qpow(n), S;
    memset(S.a, 0, sizeof(S.a)); S.a[1][tot + 1] = 1;
    S = S * ret;
    for(int i = tot + 1; i <= t; i ++) 
        if(!error[i - tot - 1]) Up(ans, S.a[1][i]);
}

signed main()
{
    scanf("%d%d%d", &m1, &m2, &n);
    for(int i = 1; i <= m1; i ++) scanf("%s", s[i] + 1), len[i] = strlen(s[i] + 1);
    for(int i = 1; i <= m2; i ++) scanf("%s", s[m1 + 1] + 1), Ins(m1 + 1);
    Build();
    if(n <= 100) DP1(); else DP2();
    printf("%d\n", ans);
    return 0;
}