题意:给定一些单词,这些单词必须要是一个目标串的子串,同时给定一些串,这些串不能够出现在目标串中,其余一些串只会带来不同附加值。现在问满足前两者的情况下,要求附加值最大。数据给定一个原始串,现在要求在这些串中删除一些字符,输出在满足要求的情况下删除最少的字符并保证附加值尽可能的大。
分析:这题的一个暴力的方法肯定就是对于每个字符枚举删或者不删,然后选择一种方案即可。在这个蛮力法的后面注意到其实在枚举的时候还是有很多重复计算的,比如前a个字符删除或者不删除某个字符对于后面的选择是一样的,而题目要求拥有所有的必有串,因此可以将所有的必有串建立一个ac自动机,然后根据枚举在ac自动机相应节点(状态)的位置从而避免了纯粹的暴力枚举。在匹配的过程中保证不允许非法串被包含进去,设dp[i][j][k]表示处理到第i个字符时,ac自动机中的状态是j,并且包含必有串的状态压缩之后的值为k的最少操作次数和相应的最大的附加值(具体体现在两个三维数组)。那么枚举i-1所有的状态,就能够根据第i个字符在ac自动机中进行转移。
#include <cstdlib> #include <cstdio> #include <cstring> #include <iostream> #include <algorithm> #include <queue> using namespace std; const int N = 1605; const int inf = 0x3f3f3f3f; int n; int sz; char str[105]; int del[2][N][1<<8]; // 记录某一状态下最少的更改次数 int sco[2][N][1<<8]; // 在满足最少次数的情况下最大的得分,优先更改次数减少,其次是得分的最大化 struct Ac_auto { int ch[N][26]; int fail[N]; int sta[N]; int gain[N]; char kill[N]; // 只取0,1两个值 int idx, root; int newnd() { memset(ch[idx], 0, sizeof (ch[idx])); gain[idx] = sta[idx] = fail[idx] = kill[idx] = 0; return idx++; } void init() { idx = 0, root = newnd(); } void insert(char ss[], int val) { int len = strlen(ss); int p = root; for (int i = 0; i < len; ++i) { char c = ss[i]-'a'; if (!ch[p][c]) ch[p][c] = newnd(); p = ch[p][c]; } // 由于题目中说明了不包括相同的单词,因此只需要等于赋值 if (val == 999) sta[p] = 1 << sz; else if (val == -999) kill[p] = 1; else gain[p] = val; } void build() { // 构建失败指针,ac自动机的精华 queue<int>q; for (int i = 0; i < 26; ++i) { if (ch[root][i]) { q.push(ch[root][i]); } } while (!q.empty()) { int p = q.front(); q.pop(); for (int i = 0; i < 26; ++i) { int v = ch[p][i]; int x = fail[p]; if (v) { q.push(v); while (x && !ch[x][i]) x = fail[x]; // 选择一个最长的后缀串,使fail指针指向它 fail[v] = ch[x][i]; // 无论有没有指向i的节点,其fail指针会指向0 kill[v] |= kill[fail[v]]; // 如果该字符串之中含有非法的串,那么非法性质会传递过来 sta[v] |= sta[fail[v]]; // 状态也需要传递 gain[v] += gain[fail[v]]; // 传递值 } else { ch[p][i] = ch[x][i]; } } } } }; Ac_auto ac; void solve() { memset(del, 0x3f, sizeof (del)); // 对于删除次数初始化为正无穷大 memset(sco, 0x80, sizeof (sco)); // 对于得分初始化为负无穷大 int cur = 0, nxt = 1; del[cur][0][0] = 0; // 初始化没有字符时不包含任何合法和非法的状态的最少更改字符数为0 sco[cur][0][0] = 0; // 配套的获得的价值为0 int len = strlen(str); int lim = 1 << sz; const int &idx = ac.idx; for (int i = 0; i < len; ++i) { // i表示匹配到了那个字符 int c = str[i]-'a'; memset(del[nxt], 0x3f, sizeof (del[nxt])); memset(sco[nxt], 0x80, sizeof (sco[nxt])); for (int j = 0; j < idx; ++j) { // j记录在ac自动机中匹配到的位置 for (int k = 0; k < lim; ++k) { // k表示压缩后的包含必须子串的情况 if (del[cur][j][k] == inf) continue; // 如果这个状态没有计算过,其也就无法转移到新的状态 // 如果删除该字符 if (del[nxt][j][k] > del[cur][j][k] + 1) { del[nxt][j][k] = del[cur][j][k] + 1; sco[nxt][j][k] = sco[cur][j][k]; } else if (del[nxt][j][k] == del[cur][j][k] + 1) { if (sco[nxt][j][k] < sco[cur][j][k]) { sco[nxt][j][k] = sco[cur][j][k]; } } // 如果匹配该字符 int np = ac.ch[j][c]; if (ac.kill[np]) continue; // 如果不能够匹配或者该子串不允许被包含 int gain = ac.gain[np]; int nsta = k|ac.sta[np]; if (del[nxt][np][nsta] > del[cur][j][k]) { del[nxt][np][nsta] = del[cur][j][k]; sco[nxt][np][nsta] = sco[cur][j][k] + gain; } else if (del[nxt][np][nsta] == del[cur][j][k]) { if (sco[nxt][np][nsta] < sco[cur][j][k] + gain) { sco[nxt][np][nsta] = sco[cur][j][k] + gain; } } } } swap(cur, nxt); } int xdel = inf, xsco; for (int i = 0; i < idx; ++i) { if (xdel > del[cur][i][lim-1]) { xdel = del[cur][i][lim-1]; xsco = sco[cur][i][lim-1]; } else if (xdel == del[cur][i][lim-1]) { xsco = max(xsco, sco[cur][i][lim-1]); } } if (xdel == inf) { puts("Banned"); } else { printf("%d %d\n", xdel, xsco); } } int main() { int T, ca = 0; scanf("%d", &T); while (T--) { int val; sz = 0; ac.init(); scanf("%d", &n); for (int i = 0; i < n; ++i) { scanf("%s %d", str, &val); ac.insert(str, val); if (val == 999) ++sz; } ac.build(); scanf("%s", str); printf("Case %d: ", ++ca); solve(); } return 0; }