[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;
}(
posted @ 2022-04-30 15:40  Roshin  阅读(30)  评论(0编辑  收藏  举报
-->