题目链接:牛客IOI周赛23-提高组B
题目大意
给你n个数字串,定义牛数x:n个数字串都不是x的子串。然后给你一个长度为2000的数字s,问你[1, s]之间牛数有多少个。
思路
多个模式串匹配主串,就自然而然地会想到ac自动机了,然后问你[1,s]之间满足条件的数的个数,那么就是数位dp了,不过这里的数位dp要在Trie树上跑,还要注意判断前导零。
ac代码
#include<bits/stdc++.h>
using namespace std;
#define io cin.tie(0);ios::sync_with_stdio(false);
#define debug(x) cout<<#x<<"="<<x<<endl
#define ll long long
#define rs p<<1|1
#define ls p<<1
#define lowbit(x) x&(-x)
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;
const ll lnf = 1e18;
const int maxn = 1e6 + 5;
const double eps = 1e-10;
//ac自动机部分
const char Ch = '0';
struct node{
int next[30];
int idx, fail, cnt; //idx表示输入的第几个字符串的终止位置,fail是失配指针,cnt存改字符串出现的次数
void clear(){ //清空
memset(next, 0, sizeof(next));
idx = 0, fail = -1, cnt = 0;
}
}t[maxn];
int tot; //指针
void init(){ // 初始化
for(int i = 0; i <= tot; i ++) t[i].clear();
tot = 0;
}
void insert(string a){ //构建Trie树,添加模式串
int len = a.length(), pos = 0;
for(int i = 0; i < len; i ++){
int tmp = a[i] - Ch;
if(!t[pos].next[tmp]) t[pos].next[tmp] = ++tot;
pos = t[pos].next[tmp];
}
t[pos].cnt ++;
}
void getFail(){ //获取失配指针,使当前字符失配时跳转到具有最长公共前后缀的字符继续匹配
t[0].fail = 0; //0是虚节点,刚开始连向所有字符串的首字符,这样就形成一颗树了
queue<int> q;
for(int i = 0; i < 10; i ++){
int pos = t[0].next[i];
if(pos){
t[pos].fail = 0; //失配指针指向虚节点
q.push(pos);
}
}
while(q.size()){
int pos = q.front(); q.pop();
for(int i = 0; i < 26; i ++){
int son = t[pos].next[i], fail_pos = t[pos].fail;
if(son){
t[son].fail = t[fail_pos].next[i]; //如果父节点的失配指针指向的节点fail_pos 他也有值是i的子节点,那么son的失配指针就可以指向那个子节点了
t[son].cnt |= t[t[son].fail].cnt; //由于bfs是按照深度来遍历的,而失配指针深度肯定是比他低的,所以可以以此更新cnt
q.push(son);
}else{
t[pos].next[i] = t[fail_pos].next[i]; //否则也要指向那个子节点,就是一路往回退,最后都会退回虚节点的
}
}
}
}
//数位dp部分
int len; //主串长度
char s[maxn]; //主串
ll dp[2005][2005][2][2];
ll dfs(int pos, int now, bool limit, bool pre){ //pos主串位置,now是Trie树中的位置,limit表示取数是否有限制,pre表示前导零
if(t[now].cnt) return 0; //表示该数字串在模式串中出现过,那么直接返回0
if(pos == len + 1) return 1; //跑到最后了,返回1
if(!limit && ~dp[pos][now][limit][pre]) return dp[pos][now][limit][pre]; //记忆化
int up = limit ? s[pos] - '0' : 9; //上界
ll ans = 0;
for(int i = 0; i <= up; i ++){
//如果i==0并且有前导零,那么该数字其实还没遍历到,那么下一步还是now; 否则说明前面有值了now要进入下一个,也就是now下的数值是i的节点t[now].next[i]
ans = (ans + dfs(pos + 1, (i == 0 && pre) ? now : t[now].next[i], limit && (i == s[pos] - '0'), pre && (i == 0)) ) % mod;
}
if(!limit) dp[pos][now][limit][pre] = ans; //记忆化
return ans;
}
void solve(){
init();
cin >> s+1;
len = strlen(s+1);
int n; cin >> n;
for(int i = 1; i <= n; i ++){
string a; cin >> a;
insert(a);
}
getFail();
memset(dp, -1, sizeof(dp));
cout << (dfs(1, 0, 1, 1) - 1 + mod) % mod << endl; //求的是[1, s],dfs多了0,所以要减去1
}
int main(){
io;
solve();
return 0;
}