P4569 [BJWC2011] 禁忌 题解

P4569 [BJWC2011] 禁忌 题解

前言:这道题补好了我的 \(AC\) 自动机和矩阵优化 \(dp\),功德无量。

题意:在字母集合 \(a→alphabet\) 中随机生成长度为 \(len\) 的字符串 \(s\) ,给定已知的数个字符串 \(T-i\),问将 \(s\) 分割成含有字符串 \(T_i\) 的个数的期望。

仔细看看挺套路的,因此不难想到 \(AC\) 自动机 \(+dp\)

套路地,我们设 \(dp[i][j]\) 为字符串 \(s\) 匹配到第 \(i\) 位,在 \(AC\) 自动机上匹配到了第 \(j\) 个节点时的期望。

贪心地,字符串 \(s\) 的分割最优情况是每找到一个禁忌串就在这个串的后面砍一刀,因此转移时我们要分类讨论,讨论当前节点是不是某一个 \(T_i\) 的终点,如果不是,就正常转移下去,否则就直接将答案的贡献记录到根节点上来转移。

具体地,进行第 \(k\)\(dp\) ,遍历到每一个点 \(x\) 时,检查它的每个子节点 \(y\) ,如果 \(vis[y]=1\),即 \(y\) 是禁忌串的终点,就在根节点 \(dp[k][1]\) 处记录贡献,否则在 \(dp[k][y]\) 处记录贡献。贡献是 \(\frac{1}{alp}\)

如果你真的这样做,恭喜你可以获得 \(0pts\)\(RE\ or\ MLE,\ because\ of\ len\le\ 10^9\)

考虑到每个\(dp[i]\)的转移中只是存在类似\(dp[j]\times\frac{1}{alp}\)这样的转移,不含其他项(其实含了也无妨),是一维是简单递推形式,因此考虑矩阵乘法优化。

具体地,设矩阵

\[F[i]=|f[1]\ f[2]\ f[3]\ ...\ f[n]\ ans| \]

那么我们只需要在自动机的遍历当中记录每一项可以由什么转移而来,记录这个系数在矩阵中就可以实现矩阵快速幂的转移了。

考虑到这个矩阵在初始形态下无意义,因此设为单位矩阵,在 \(AC\) 自动机的遍历下建出系数矩阵 \(bas\) ,再进行快速幂即可。

最终答案 \(bas[1][tot+1]\)

代码:

// keep on debuging !
#include <bits/stdc++.h>
#define N 105
using namespace std;
int n, len, alp;
long double tmp;
int trie[N][30];
int tot = 1; //由于中途会转移到根节点,不妨将根节点设为 1
bool vis[N];
void insert(char *s) {
	int len = strlen(s), p = 1;
	for (int i = 0; i < len; i++) {
		int ch = s[i] - 'a';
		if (!trie[p][ch])
			trie[p][ch] = ++tot;
		p = trie[p][ch];
	}
	vis[p] = true;
} 
int fail[N];
queue<int>q;
void build() {
	for (int i = 0; i < alp; i++)
		trie[0][i] = 1;
	q.push(1);
	while (!q.empty()) {
		int p = q.front();
		vis[p] |= vis[fail[p]];
		q.pop();
		for (int i = 0; i < alp; i++)
			if (trie[p][i]) {
				q.emplace(trie[p][i]);
				fail[trie[p][i]] = trie[fail[p]][i]; 
			}
			else
				trie[p][i] = trie[fail[p]][i];
	}
}
struct XYCYX {
	long double a[N][N];
	XYCYX () {
		memset(a, 0, sizeof a);
	}
	XYCYX operator * (const XYCYX &b) const{
		XYCYX res;
		for (int i = 1; i <= tot + 1; i++)
			for (int j = 1; j <= tot + 1; j++)
				for (int k = 1; k <= tot + 1; k++)
					res.a[i][j] += a[i][k] * b.a[k][j];
		return res;
	}
}ans, bas;
void init() {
	for (int i = 1; i < N; i++)
		ans.a[i][i] = 1.0;
	for (int p = 1; p <= tot; p++) 
		for (int i = 0; i < alp; i++) {
			if (vis[trie[p][i]]) {
				bas.a[p][1] += tmp; //回到根节点
				bas.a[p][tot + 1] += tmp;
			}
			else 
				bas.a[p][trie[p][i]] += tmp;
		}
	bas.a[tot + 1][tot + 1] = 1.0;
}
void qpow(int y) {
	while (y) {
		if (y & 1)
			ans = ans * bas;
		bas = bas * bas;
		y >>= 1;
	} 
}
char s[N];
int main() {
	scanf("%d%d%d", &n, &len, &alp);
	tmp = (long double)((long double)1.0 / (long double)alp);
	for (int i = 1; i <= n; i++) {
		scanf("%s", s);
		insert(s);
	}
	build();
	init();
	qpow(len);
	printf("%.10Lf\n", ans.a[1][tot + 1]);
	return 0;
}

upd on 2024.8.23:
对于矩阵优化的一些注意小细节:

  • 一般地,\(ans\) 矩阵我们设为单位阵。
  • \(bas\) 矩阵不用特殊处理,根据题目设出即可。
posted @ 2024-03-02 10:21  长安19路  阅读(83)  评论(0)    收藏  举报