题目意思
有n个字符串,每个字符串都有对应的权值,那么我们要组建一个长度为m的字符串,并且要是这个字符串的权值最大,如果最大权值小于零,输出"No Rabbit after 2012!",否则输出最大的权值
解决思路
这里我们选择相应的字符串,要在Trie树上选择,并且我们还要进行模式串的匹配(匹配最大的数值),那么我们就一定会用到AC自动机,并且我们这里要确定一定长度的字符串使得权值最大,那么我们就会用到状压DP
这里我们用到的状态压缩就是表示那个字符串被选到了,哪个字符串没有被选到,选到用1表示,没有选到用0表示,那么拥有的状态总数就是:(1<< n)-1
首先是我们的AC自动机和我们平时的有什么区别,平时我们是在字符串的最后一个字符位置的val数组进行加1操作,但是这个中这样做就没有意义了,因为我们这里要用状态表示哪一个关键词被选择哪一个没有被选择,那么我们这里应该储存的是(1<<i)(i表示的是第几个输入的字符串)
然后在我们构建失配指针,如果我们想最原始的构建方法的话,我们找到一个节点是关键词的末尾节点的话,我们还需要向上进行回溯,一直到根节点,要把这个路径中经过的所有涵盖关键词的字符串全部找出来,或者我们像《AC自动机模板》中最后一个模板中一样,我们添加一个last数组,使得每次last中回溯的都是字符串的末尾适配节点,但是这样我们还是需要回溯,有没有一种不需要回溯的方法呢?答案是有的,这里就得益于我们的状态压缩,因为每个字符串的状态都是不一样的,我们只需要对他的失配节点进行 '|' 操作,就可以得到到达这个节点,所有涵盖的字符串信息!!实质上他的意思就是:如果到这里已经是关键字符串了,那么是他子串的关键字符串也一定存在在这里面
这样我们的AC自动机部分就可以是完成了
最后是我们的状压DP部分dp[i][j][state]:字符串长度为i,现在在树中的节点编号为j,并且字符串的状态为state时,是否可行!
状态转移方程:if(dp[i][j][state] == true) dp[ i+1 ] [ ch[j][k] ][ state | val[ ch[j][k] ] ] (k表示编号为j的第k个子节点)
我们可以想到:i+1长度的字符串信息时之和长度为i的字符串信息有关,那么我们这里就可以想到滚动数组了!
程序实现
#include <iostream> #include <cstdio> #include <cstring> #define INF 0x3f3f3f3f using namespace std; const int N = 11; const int MAXN = 1010; int n,m; int ch[MAXN][N],sz,fail[MAXN],val[MAXN]; int v[N]; void Init() { memset(fail,0,sizeof(fail)); memset(ch[0],0,sizeof(ch[0])); sz = 1;val[0] = 0; } int idx(char x) { if(x == 'A') return 0; else if(x == 'T') return 1; else if(x == 'G') return 2; return 3; } void Insert(char *s,int x) { int u = 0,len = strlen(s); for(int i = 0;i < len;i ++) { int c = idx(s[i]); if(!ch[u][c]) { memset(ch[sz],0,sizeof(ch[sz])); val[sz] = 0; ch[u][c] = sz++; } u = ch[u][c]; } val[u] = 1<<x;//这里将每个字符串的存在状态存放在val数组中 } void Get_fail() { int que[MAXN]; //数组模拟队列,更快一些 int l = 0,r = 0; que[r++] = 0; while(l < r) { int u = que[l++]; for(int i =0 ;i < 4;i ++) { int v = ch[u][i]; if(!v) ch[u][i] = ch[fail[u]][i]; else { que[r++] = v; if(u)//因为我们这里fail指针开始初始化都为0,所以这里就不需要考虑根节点直接连接的点,之需要考虑父亲节点不是根节点的点 fail[v] = ch[fail[u]][i]; val[v] |= val[fail[v]]; //将前面所有的字符串状态更新到当前节点 } } } } int Get_num(int num)//计算状态num下,权值大小 { int ans = 0; for(int i =0 ;i < n;i ++) if(num & (1<<i)) ans += v[i]; return ans; } bool dp[2][MAXN][1<<N]; void Solve() { memset(dp,0,sizeof(dp)); dp[0][0][0] = 1;//初始化状态,很重要!!!! for(int i = 1;i <= m;i ++)//枚举长度 { memset(dp[i&1],0,sizeof(dp[i&1]));//滚动数组 for(int j = 0;j < sz;j ++)//父亲节点枚举 for(int k = 0;k < 4;k ++)//儿子节点枚举 for(int hh = 0; hh < (1<<n); hh ++) //所有的状态枚举 if(dp[(i+1)&1][j][hh]) dp[i&1][ ch[j][k] ][ hh|val[ch[j][k]] ] = 1; } int ans = -1*INF; for(int j = 0;j < sz;j ++) for(int k = 0;k < (1<<n);k ++) if(dp[m&1][j][k]) ans = max(ans,Get_num(k)); if(ans >= 0) printf("%d\n",ans); else printf("No Rabbit after 2012!\n"); } int main() { while(~scanf("%d%d",&n,&m)) { Init(); char s[105]; for(int i = 0;i < n;i ++) { scanf("%s%d",s,&v[i]); Insert(s,i); } Get_fail(); Solve(); } }
参考博客
https://blog.csdn.net/martinue/article/details/50895963
https://www.cnblogs.com/kuangbin/p/3164106.html
后记
上面的描述会有点啰嗦......如果有错误,欢迎大家在下面评论区评论,谢谢!