题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2296
解题思路:
给出n个串和串的价值,询问长度为L的串最多能拥有的价值,并输出价值最大的串中长度最小中字典序最小的串。
AC自动机的作用是跑到最长相同后缀对应的单词前缀处使得最快到达下一个单词
DP:
定义:
dp[i][j]表示长度为i,且第i个字母对应AC自动机节点j的字符串的最大价值
string rec[i][j],记录对应dp位置的这个字符串
初始状态:
dp[0][0] = 0, rec[0][0] = "";
状态转移:
枚举当前状态的所有子节点去更新:v表示子节点,u表示当前节点,val[v]表示v节点后缀链接上所有单词价值和.(到达这个节点,所有后缀的单词相当于都到过了)
即 dp[i+1][v] = max(dp[i+1][v],dp[i][u]+val[v]),然后储存的字符串相当暴力的一起对应修改就可以了
问题1:为什么rec[][]不能开一维,要开二维:
只开一维储存当前枚举长度到各个节点的最大价值,乍一看没有问题,但是在当前层DP调用某rec[u]时,他可能是当前层更新好的字符串,实际上极有可能已经“面目全非”而且长度也不对,这个错误随着DP的进行逐渐放大,最后求出来的就是长度过长且不对的答案
同时,我们开二维还解决了一个问题:价值相同时,我们要优先取最短的,因此rec[][]储存不同长度下价值最大后保证字典序最小的串。
问题2:当前的状态究竟能否去更新子节点:
只有被更新过(当前长度能到)的节点才能去更新子节点,因此我们把dp初始化为0?不,0不行,没走到一个单词末尾都是0,因此dp=0的点是要更新的,所以我们初始化为-1,这样如果它能被更新了,至少会变为0.
这样枚举当前长度i时,对于dp[i][] = -1的,直接跳过
代码:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<set>
#include<map>
#include<unordered_map>
#include<algorithm>
#include<vector>
using namespace std;
#define ll long long
#define ull unsigned long long
#define for1(i,a,b) for (int i=a;i<=b;i++)
#define for0(i,a,b) for (int i=a;i<b;i++)
#define rof1(i,a,b) for (int i=a;i>=b;i--)
#define rof0(i,a,b) for (int i=a;i>b;i--)
#define pb push_back
#define fi first
#define se second
#define debug(x) printf("----Line %s----\n",#x)
#define pt(x,y) printf("%s = %d\n",#x,y)
#define INF 0x3f3f3f3f
#define dfl(x) ll x;scanf("%I64d",&x)
#define df2l(x,y) ll x,y;scanf("%I64d %I64d",&x,&y)
#define df(x) int x;scanf("%d",&x);
#define df2(x,y) int x,y;scanf("%d %d",&x,&y)
#define mod 1000000007
#define duozu(T) int T;scanf("%d",&T);while (T--)
const int N = 15;
const int maxnode = 100*10+5;//模式串数量*长度
const int ALP = 26;//字符种类数
char s[105][N];
struct AC_am
{
queue<int>que;
int sz;
int trie[maxnode][ALP];
int fail[maxnode];
int last[maxnode];
int val[maxnode];//储存当前节点信息,如是否为单词节点等等
int newnode(int x){
memset(trie[x],0,sizeof trie[x]);
val[x] = 0;
return sz++;
}
void init(){
newnode(sz = 0);
}
int idx(char ch){//实际字符串转化为字典树对应节点,根据题目做出具体改变
return ch-'a';
}
void insert(char *s,int x){
int u = 0;
for (int i=0;s[i];i++){
int c = idx(s[i]);
if (!trie[u][c]){
trie[u][c] = newnode(sz);
}
u = trie[u][c];
}
val[u] += x;
}
void build(){
fail[0] = 0;
for (int c=0;c<ALP;c++){
int v = trie[0][c];
if (v){
que.push(v);
fail[v] = 0;
last[v] = 0;
}
}
while (!que.empty()){
int u = que.front();que.pop();
for (int c=0;c<ALP;c++){
int v = trie[u][c];
if (!v){
trie[u][c] = trie[fail[u]][c];
continue;
}
fail[v] = trie[fail[u]][c];
last[v] = val[fail[v]]? fail[v]:last[fail[v]];
val[v] += val[last[v]];///这个很关键
que.push(v);
}
}
}
}ac;
int dp[55][1005];
string rec[55][1005];
pair<int,int>ans[50*1000+5];
int maxval;
int main()
{
//freopen("C:/Users/DELL/Desktop/input.txt", "r", stdin);
//freopen("C:/Users/DELL/Desktop/output.txt", "w", stdout);
int L,n,x;
duozu(T){
memset(dp,-1,sizeof dp);
ac.init();
scanf("%d %d",&L,&n);
for0(i,0,n) scanf("%s",s[i]);
for0(i,0,n) scanf("%d",&x),ac.insert(s[i],x);
ac.build();
dp[0][0] = 0;
rec[0][0] = "";
for0(l,0,L){
for0(u,0,ac.sz){
if (dp[l][u]==-1) continue;
for0(c,0,26){
int v = ac.trie[u][c];
if (dp[l+1][v]<dp[l][u]+ac.val[v]){
rec[l+1][v] = rec[l][u];
rec[l+1][v].pb('a'+c);
dp[l+1][v] = dp[l][u]+ac.val[v];
}
else if (dp[l+1][v]==dp[l][u]+ac.val[v]){
string tmp = rec[l][u];
tmp.pb('a'+c);
if (tmp<rec[l+1][v]) rec[l+1][v] = tmp;
}
}
}
}
maxval = 0;
int tot = 0;
for1(l,1,L){///记录所有价值最大的对应长度以及结尾节点
for0(u,1,ac.sz){
if (dp[l][u]>maxval){
tot = 0;
maxval = dp[l][u];
ans[tot].fi = l,ans[tot++].se = u;
}
else if (dp[l][u]==maxval) ans[tot].fi = l,ans[tot++].se = u;
}
}
if (maxval==0){puts("");continue;}
//pt(maxval,maxval);
string ANS = rec[ans[0].fi][ans[0].se];
for0(i,1,tot){///找出长度最短时字典序最小的,貌似只要下一个长度比当前答案长直接break就行了毕竟答案是按照长度小到大存入的
if ( (rec[ans[i].fi][ans[i].se].length()<ANS.length())
|| (rec[ans[i].fi][ans[i].se].length()==ANS.length() && rec[ans[i].fi][ans[i].se]<ANS) ) ANS = rec[ans[i].fi][ans[i].se];
}
cout << ANS << endl;
}
return 0;
}