[Kuangbin AC自动机] HDU3247 Resource Archiver(AC自动机+BFS+状态DP)
前情提要:网上大部分题解,有的没有对存在资源串为其他资源串的子串进行处理,有的甚至直接对资源串结尾不做任何处理直接放到数组进行bfs,数组大小只开了13,只要资源串数量、长度够,可以随便让这些代码RE,可见原题数据之弱,所以特地写一篇题解在这里启示后人。如果代码还有bug欢迎在评论区留言~。
特别感谢这位博主的博客,改了一下他的spfa,换成普通的bfs。
AC自动机 + BFS最短路 + 状压DP(TSP问题)
题意
给出 \(n\) 个资源串,\(m\) 个病毒串,将资源串拼接成一个串,必须包含所有的资源串,
可以重叠,但是不能包含病毒串,问最小的长度为多少?
数据范围
\(1\leq n \leq 10\)
\(1\leq m \leq 1000\)
Each resource is at most \(1000\) characters long.
The total length of all virus codes is at most \(50000\)
思路
- 多模式匹配问题考虑AC自动机,观察 \(n\) 的范围较小,可以考虑状压DP实现。
- 在AC自动机中记录每个资源串的结尾节点,问题等价于不走过病毒串结点,走完所有资源串结尾节点的最短路径长度,其实是一个TSP问题
- 考虑如何表示状态
- 我们发现虽然资源串只有 \(10\) 个不到,但是病毒串数量很大,如果像上面的题那样必然 MLE。考虑优化
- 对于我们真正有用的其实只有资源串的结尾,然而由于AC自动机自身的特性,一个资源串能够标记的节点不止一个,这时候我们应该怎么办呢?
- 事实是,对于每个资源串真正有用的只有他本身自己的结点,贪心的来看,如果由于fail指针传递,其他点有这个资源串状态的距离必然比其大。
- 或者你又会觉得如果一个资源串是另一个资源串的子串的时候,应该怎么看呢?答案是预处理出所有不是其他串子串的资源串。这点非常的重要,也是csdn上大部分题解都没有关心的点,然后会导致代码被hack(原题数据过弱)
- 那么我们现在就拿到了不超过 10 个的资源串结点,这个时候就可以开开心心套 TSP 模板了!(注意处理 TSP 起点的状态)
- 最后,给出状态表示和转移:
- 状态表示:
f[i][j]
表示状态为 \(i\) 时,停留在 \(j\) 资源串结尾点的最短路径长度。 - 状态转移:枚举状态、枚举起点、枚举终点,判断是不是病毒串结点,然后按照 TSP 问题的一般形式转移即可。
- 状态表示:
- 时间复杂度: \(O(m + 2^n * n^2)\)
Solution
#include<bits/stdc++.h>
typedef long long ll;
typedef unsigned long long ull;
typedef std::pair<int, int> PII;
typedef std::pair<ll, ll> PLL;
#define x first
#define y second
#define pb push_back
#define mkp make_pair
#define endl "\n"
#define Memset(x) memset(x, 0, sizeof x)
using namespace std;
const int M = 60010, S = 2, N = 15;
int tr[M][S], idx, fail[M], ed[N];
int g[N][N], n, m, dp[1 << 11][N];
bool st[M], fl[N];
void insert(string s, int id, int t){
int p = 0;
for(int i = 0; i < s.size(); i++){
int c = s[i] - '0';
if(!tr[p][c]) tr[p][c] = ++idx;
p = tr[p][c];
}
if(t)
ed[id] = p;
else
st[p] = true;
}
void build(){
queue<int> q;
for(int i = 0; i < S; i++)
if(tr[0][i])
q.push(tr[0][i]);
while(q.size()){
auto u = q.front);
q.pop();
if(st[fail[u]]) st[u] = true;
for(int i = 0; i < S; i++){
if(tr[u][i])
fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
else
tr[u][i] = tr[fail[u]][i];
}
}
}
void init(){
idx = 0;
Memset(fail);
Memset(tr);
Memset(st);
memset(dp, 0x3f, sizeof dp);
memset(g, 0x3f, sizeof g);
}
int dist[M];
void bfs(int start, int id){
queue<int> q;
memset(dist, 0x3f, sizeof dist);
dist[start] = 0;
q.push(start);
while(q.size()){
auto u = q.front();
q.pop();
for(int k = 0; k < S; k++){
int v = tr[u][k];
if(st[v] || dist[v] != 0x3f3f3f3f) continue;
dist[v] = dist[u] + 1;
q.push(v);
}
}
for(int i = 0; i < n; i++)
g[id][i] = dist[ed[i]];
}
bool cmp(string a, string b){
return a.size() > b.size();
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
while(cin >> n >> m, n || m){
init();
vector<string> v, str;
for(int i = 0; i < n; i++){
string s;
cin >> s;
v.pb(s);
}
memset(fl, false , sizeof fl);
sort(v.begin(), v.end(), cmp); // 处理资源串是其他资源串的情况
for(int i = 0; i < v.size(); i++){
for(int j = i + 1; j < v.size();j ++){
if(v[i].find(v[j]) != -1)
fl[j] = true;
}
}
int tot = 0;
for(int i = 0; i < v.size(); i++)
if(!fl[i]){
insert(v[i], tot++, 1);
str.pb(v[i]);
}
n = tot;
for(int i = 0; i < m; i++){
string s;
cin >> s;
insert(s, 0, 0);
}
build();
for(int i = 0; i < n; i++){
dp[1 << i][i] = str[i].size(); // 根节点到资源点最短距离一定是其长度
bfs(ed[i], i);
}
// 做一次 TSP 状压问题
for(int i = 0; i < 1 << n; i++)
for(int j = 0; j < n; j++)
if(i >> j & 1){
for(int k = 0; k < n; k++){
if(!(i >> k & 1) && g[j][k] != 0x3f3f3f3f)
dp[i ^ (1 << k)][k] = min(dp[i ^ (1 << k)][k], dp[i][j] + g[j][k]);
}
}
int ans = 0x3f3f3f3f;
for(int i = 0; i < n; i++)
ans = min(ans, dp[(1 << n) - 1][i]);
cout << ans << endl;
}
return 0;
}(