Loading

【题解】P2322 - [HNOI2006] 最短母串问题

题目大意

题目链接

给出 \(n\) 个长度不超过 \(50\) 的字符串 \([S_1, S_n]\),试构造出一个字符串 \(T\) 使得 \([S_1, S_n]\) 均为 \(T\) 的子串。

\(1 \leq n \leq 12\)

解题思路

看到题目发现数据范围极小,因为 \(AC\) 自动机的时间复杂度是 \(\mathcal{O}(n + m)\),因此题目的时间复杂度大概率十分极端。我们发现构造字符串 \(T\) 似乎并没有直观的构造方法,因此我们考虑按字典序和长度 枚举 所有的字符串 \(T\) 可能的形态,快速判断该字符串是否能够包含 \([S_1, S_n]\) 从而得出长度最短且字典序最小的字符串 \(T\)。具体实现显然可以使用 广搜

我们考虑如何表示出一个字符串 \(T\) 是否包含 \([S_1, S_n]\)。因为我们需要用到多模式串匹配,所以肯定需要建一棵 \(Trie\) 树。先考虑从 \(Trie\) 树上的结点转移到其子结点时状态发生的变化,显然在子结点的状态为当前结点的状态与子结点包含的字符串的并集。换言之,从当前结点转移到子结点以后会增加若干个在当前状态中不存在的字符串。我们的需求是把这个状态简洁地表示出来,同时快速地求出两个状态的并集。由于算法自带 \(Trie\) 树结点个数 \(\times 26\) 的常数,因此我们需要尽量在 \(\mathcal{O}(1)\) 时间内维护。观察数据范围可知我们可以使用 状压 来表示状态。

不妨给 \(Trie\) 树上的每一个结点都附加一个权值,这个权值小于等于 \(2^{12} - 1\)。在这个权值的二进制表示中,如果从右往左数的第 \(i\) 位为 \(1\),说明该结点表示的字符串包含 \(S_i\)。显然初始值可以在插入 \(Trie\) 树的时候维护。对于一个结点,显然它的 \(fail\) 指针指向的结点表示的字符串一定是该结点表示的字符串的后缀。因此如果 \(fail\) 指针指向的结点附加权值的第 \(i\) 位为 \(1\),那么该结点附加权值的第 \(i\) 位也应该为 \(1\)。因为可能有重复的字符串,所以我们无论是维护初始值还是维护 \(fail\) 指针的附加权值时都需要取按位或的值。

这样我们就可以开始考虑广搜了。对于一个结点,因为 \(AC\) 自动机连接的 \(fail\) 指针使得可能经过两次结点 \(u\),因此我们的状态应该定义为 \((a, b)\),其中 \(a\) 是当前搜索到的 \(Trie\) 树结点,\(b\) 是当前已经包含的字符串的状态压缩。当从结点 \(i\) 转移到其子结点时,产生的状态压缩应该为 \(b\) 与子结点附加权值的按位或。如果子结点的该状态压缩未被访问,我们搜索这个状态。注意我们枚举子结点是按字典序枚举的,因此可以在满足长度最短的情况下使得字典序最小。

因为题目要求空间限制为 \(\texttt{32.00MB}\),因此我们还需要考虑对空间进行一些优化。每次搜索到一个状态就记录下该状态对应的字符串 \(T\) 显然是不可取的。注意到每次转移仅在字符串 \(T\) 末尾新增一个字符,所以我们可以考虑给每一个搜索过程中的状态都赋予一个编号,每次记录下当前状态新增的字符以及转移前的状态编号。最后从当前状态开始记录字符并不断按照记录的状态编号回溯即可。具体见代码。

参考代码

#include <cstdio>
#include <cstring>
#include <stack>
#include <queue>
using namespace std;

const int maxn = 650;
const int maxm = (1 << 12) + 5;

struct node
{
	int son[26];
	int fail, vis;
} tree[maxn];

int n, tot, res;
int nxt[maxm * maxn];
char s[maxn], w[maxm * maxn];
bool vis[maxn][maxm];

void insert(char *s, int x)
{
	int t = 0, m = strlen(s);
	for (int i = 0; i < m; i++)
	{
		int c = s[i] - 'A';
		if (!tree[t].son[c])
			tree[t].son[c] = ++tot;
		t = tree[t].son[c];
	}
	tree[t].vis |= (1 << x);
}

void get_fail()
{
	queue<int> q;
	for (int i = 0; i < 26; i++)
	{
		if (tree[0].son[i])
		{
			tree[tree[0].son[i]].fail = 0;
			q.push(tree[0].son[i]);
		}
	}
	while (!q.empty())
	{
		int t = q.front();
		q.pop();
		for (int i = 0; i < 26; i++)
		{
			if (tree[t].son[i])
			{
				tree[tree[t].son[i]].fail = tree[tree[t].fail].son[i];
				tree[tree[t].son[i]].vis |= tree[tree[tree[t].son[i]].fail].vis;
				q.push(tree[t].son[i]);
			}
			else
				tree[t].son[i] = tree[tree[t].fail].son[i];
		}
	}
}

int main()
{
	int cnt_vis = 0;
	stack<char> ans;
	queue<int> qnode, qvis;
	scanf("%d", &n);
	// Trie 
	for (int i = 1; i <= n; i++)
	{
		scanf("%s", s);
		insert(s, i - 1);
	}
	// AC 自动机 
	get_fail();
	qnode.push(0);
	qvis.push(0);
	vis[0][0] = true;
	while (!qnode.empty())
	{
		int f = qnode.front();
		int st = qvis.front();
		qnode.pop(), qvis.pop();
		// 从第 0 位到第 n - 1 位均为 1 说明搜索结束
		// 此时对应的数值大小为 2 ^ n - 1 
		if (st == (1 << n) - 1)
		{
			while (cnt_vis)
			{
				ans.push(w[cnt_vis]);	// 从后往前记录下字符串 T 的字符 
				cnt_vis = nxt[cnt_vis];		// 按照记录下的转移前的状态编号回溯 
			}
			while (!ans.empty())
			{
				char t = ans.top();
				ans.pop();
				putchar(t);
			}
			puts("");
			break;
		}
		for (int i = 0; i < 26; i++)
		{
			if (!vis[tree[f].son[i]][st | tree[tree[f].son[i]].vis])
			{
				vis[tree[f].son[i]][st | tree[tree[f].son[i]].vis] = true;
				qnode.push(tree[f].son[i]);
				qvis.push(st | tree[tree[f].son[i]].vis);
				// 从当前结点转移到子结点,新增字符为 i + 'A'
				// 子结点转移前的状态为当前结点的状态 
				res++, w[res] = i + 'A', nxt[res] = cnt_vis; 
			}
		}
		cnt_vis++;
	}
	return 0;
}
posted @ 2021-08-18 21:57  kymru  阅读(112)  评论(0编辑  收藏  举报