SMOJ 1767 子串个数 (AC自动机+状压DP)

题目:

读入n(n <= 6)个字符串,每个字符串长度不超过50,且字符串都是由小写字母构成。读入一个整数L,L <= 50, 现在要求你构造一个长度是L的全部由小写字母构成的字符串X,而且要满足读入的n个字符串当中,恰好有C个字符串是你构造的字符串X的子串,那么你最多可以构造出多少个不同的X?答案模1000000009。


输入格式:
第一行,三个整数N,C,L。 0 <= C <= N。
接下来有N行,每行一个字符串。


输出格式:
一个整数


输入样例 :
4 2 3
a
aa
aaa
aaaa


输出样例:
50
样例解释: (合法的字符串的形式只能是:”Xaa” 或 “aaX”, 其中 X 是字符’b’-‘z’中的任意一个)


题解:

这题一看就是AC自动机+状压dp,难点在于两点:
一是状压的方程该如何推,首先一定以长度为阶段,每个子串是前缀树(Trie)中串的后缀,打标记则代表有完整串。方程通过给AC自动机节点编号每次贡献给儿子可以搞定,并不难想。
二是AC自动机该如何构图以满足转移方程的次序。我们知道每个点的信息不只是当前的标记信息,还要或上fail指针一路上去的信息(即其后缀的信息),所以我们在bfs求失败指针时可预处理好。有一个难点在于当前节点可以转移到其失败指针所指向的节点,但经我们观察发现这样转移并没有用,因为这是相反方向的转移,即本节点信息已经包含其fail了,但可转移到其儿子节点。于是我们为了简单起见,直接在构图时将空儿子节点指向其失败指针的儿子,没有就指向根(不能指向空,否则有些状态转移不到),注意这是一个递归的过程。然后我们直接在Trie图上进行状压dp就行了,dp部分比较简单,但构出Trie图以及处理指针指向需要好好思索。
时间复杂度O(L*Node*2^n*26+构图);

代码:
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#define MAXL 55
#define NN 55
#define N 7
#define MOD 1000000009

using namespace std;
int n, C, L, dp[NN][N*MAXL][(1<<N)+5];
int cur, cnt[(1<<N)+5], ans;
char x[MAXL];
struct Trie{
    Trie *son[30], *fail;
    int s, num;
    void Clear(){
      for(int i = 0; i < 26; i++)  son[i] = NULL;
      s = 0;
      num = cur;
    }
};
Trie *Root, *q[N*MAXL], Node[N*MAXL];

inline Trie *NewTnode(){
    Node[cur].Clear();
    return Node+cur++;
}
void AC_Insert(char *x, int id){
    int len = strlen(x);
    Trie *now = Root;
    for(int i = 0; i < len; i++){
      int pos = x[i] - 'a';
      if(!now->son[pos])  now->son[pos] = NewTnode();
      now = now->son[pos];
    }
    now->s = (1 << (id - 1));
}

void AC_Buildfail(){
    Root->fail = NULL;
    q[0] = Root;
    int head = 0, tail = 0;

    while(head <= tail){
      Trie *now = q[head++];
      for(int i = 0; i < 26; i++){
        if(now->son[i]){
          q[++tail] = now->son[i];
          now->son[i]->fail = (now == Root) ? Root : now->fail->son[i];
          now->son[i]->s |= now->son[i]->fail->s;
        }
        else  now->son[i] = (now == Root) ? Root : now->fail->son[i];
      }
    }
}

void Count(){
    for(int s = 0; s < (1<<n); s++){
      int t = s;
      while(t){
        if(t & 1)  cnt[s] ++;
        t >>= 1;
      }
    }
}
int main(){

    scanf("%d%d%d", &n, &C, &L);
    Count();

    Root = NewTnode();
    for(int i = 1; i <= n; i++){
      scanf("%s", &x);
      AC_Insert(x, i);
    }

    AC_Buildfail();

    memset(dp, 0, sizeof(dp));
    dp[0][0][0] = 1;

    for(int i = 0; i <= L; i++)
     for(int j = 0; j < cur; j++)
      for(int s = 0; s < (1<<n); s++){
        if(!dp[i][j][s])  continue;
        for(int k = 0; k < 26; k++){
          int nj = Node[j].son[k]->num, ns = Node[j].son[k]->s;
          dp[i+1][nj][s|ns] = (dp[i+1][nj][s|ns] + dp[i][j][s]) % MOD;
        }
      }

    for(int j = 0; j < cur; j++)
     for(int s = 0; s < (1<<n); s++)  
       if(cnt[s] == C)  ans = (ans + dp[L][j][s]) % MOD;
      printf("%d\n", ans);
    return 0;
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值