AC自动机+状压DP HDU 4057

题目意思

有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

后记

上面的描述会有点啰嗦......如果有错误,欢迎大家在下面评论区评论,谢谢!

猜你喜欢

转载自blog.csdn.net/li1615882553/article/details/80157747
今日推荐