猪尾巴 pigtail 题解

题面

求字符集大小为 \(C\),长度为 \(n\) 的所有字符串中,有多少个字符串不包含长度为 \(k\) 的回文子串,答案对 \(998244353\) 取模。

思路

通过观察题目,我们可以发现 \(k\) 的最大值只有 \(25\)。考虑定义 \(dp_{i,S}\) 表示前 \(i\) 位中,后 \(k\) 位字符的相等关系的最小表示为 \(S\),且不考虑后 \(k\)时的方案数。

这里对相等关系做一个解释:我们用相同的数表示哪几位上的字符是相同的。例如,\(1\ 1\ 2\ 3\ 2\ 1\),就表示第 \(1,2,6\) 号位是相同的,\(3,5\) 号位是相同的。

但因为按照后 \(k\) 位不是回文的来做比较麻烦,所以我们这里的相等关系指的是:数相同的位上字符一定相同,数不同的位上字符不一定不相同。

然后我们可以通过容斥来求出答案,只需要在转移时乘上容斥系数。

现在我们考虑如何从 \(dp_{i,S}\) 转移到 \(dp_{i+1,S^\prime}\)

我们先考虑在 \(S\) 最左边被弹出去的那一个字符(记作 \(a\)),我们需要看它与 \([i-k+2,i]\) 中字符的关系:

  • 若存在一个在 \([i-k+2,i]\) 中的字符与 \(a\) 相等。此时,\(a\) 与后面区间中的数存在相等关系,所以我们不能够给它随意赋值,所以只有 \(1\) 种方案。
  • 若不存在一个在 \([i-k+2,i]\) 中的字符与 \(a\) 相等。此时,\(a\) 没有与后面区间中的数存在相等关系,所以我们可以给它赋值,此时就有 \(C\) 种方案。

然后考虑新加入的那个字符。很显然,我们无法保证加入它后能够直接做到不回文(因为先前我们对相等关系的定义“数不同的位上字符不一定不相同”),所以有以下两种情况:

  • 随便放入一个字符(可以与前面的相同,也可以不同),此时的容斥系数就是 \(1\)。新得到的 \(S^\prime\) 就是从 \(S\) 中删掉 \(i-k+1\) 再随意加上一个字符。
  • 放入一个字符,使得 \([i-k+2,i+1]\) 可以构成一个回文串,此时的容斥系数就是 \(-1\)。新得到的 \(S^\prime\) 就是从 \(S\) 中删掉 \(i-k+1\) 再加上一个字符并使它们回文。

那么最后的答案就是 \(\sum dp_{n,s}\times C^{\mid S\mid}\),其中 \(\mid S\mid\) 表示 \(S\) 中不同字符的个数。

可以发现,当 \(k=25\) 时,\(S\) 的数量也只有大约 \(10^5\) 种,所以开数组时可以用滚动数组,然后要提前把所有 \(S\) 的状态预处理出来。

然后就再讲一下预处理的部分。

在预处理时,我们用一个 dfs 来求出所有的可能状态。分成两部分:

  • 第一部分,随意添加一个字符,此时,我们就需要把 \(S\) 的第一个字符弹掉,让后将 \(S\) 中的最大值 \(+1\) 加进去,相当于加入了一个新的字符。

  • 第二部分,使其变为回文串。具体的,我们用一个并查集来维护,我们将第 \(i\) 项和第 \(k-i-1\)(下标从 \(0\) 开始)所对应的值放到同一个连通块中,最后在将每个数设为其数值上在并查集中的根。

然后因为我们求的是最小表示,所以要注意离散化一下。

贴一份代码:

#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll mod = 998244353;
ll n, k, c, mypow[30], dp[5][100005], ans;
int fa[30], nxt[100005][2], vis[30], diff[100005], pre[100005], tcnt, tim;
bool flg[100005];
vector<int> v;
map<vector<int>, int> mp;
int FindSet(int x) {return fa[x] == x ? x : fa[x] = FindSet(fa[x]);}
void Merge(int x, int y) {int x_ = FindSet(x), y_ = FindSet(y); if(x_ != y_) fa[x_] = y_;}
void down(vector<int> &v, int k) {//离散化函数,求最小的表示
	int id = 0; tim++;
	for(int i = 0; i < k; i++) {
		if(vis[v[i]] != tim) vis[v[i]] = tim, pre[v[i]] = ++id;//用的是时间戳
		v[i] = pre[v[i]];
	}
}
int init(vector<int> v1, int k) {//预处理所有状态 
	if(mp[v1]) return mp[v1];//已经到过了直接返回
	vector<int> v2 = v1;
	int pos = ++tcnt;//没到过,新开一个点
	mp[v1] = pos;
	for(int i = 0; i < k; i++) {
		diff[pos] = max(diff[pos], v1[i]);//求最大的编号
		if(i > 0 && v1[0] == v1[i]) flg[pos] = true;//判断 k-i+1 是否会影响到 [k-i+2,i]
	}
	v2.erase(v2.begin()), v2.push_back(diff[pos] + 1);//删除+加入
	down(v2, k), nxt[pos][0] = init(v2, k);//离散化+记录下一个状态,这里是随意放入字符
	for(int i = 1; i <= k; i++) fa[i] = i;//初始化并查集
	for(int i = 0; i < k; i++) Merge(v2[i], v2[k - i - 1]);//合并,使其变为回文串
	for(int i = 0; i < k; i++) v2[i] = FindSet(v2[i]);//修改为回文串
	down(v2, k), nxt[pos][1] = init(v2, k);//离散化+记录下一个状态,这里是变为回文串
	return pos;
}
int main() {
	scanf("%lld %lld %lld", &n, &k, &c); int fst = k & 1, lst = n & 1;
	mypow[0] = 1; for(int i = 1; i <= k; i++) mypow[i] = mypow[i - 1] * c % mod;
	if(k > n) return 0, printf("%lld", mypow[n]);//特判 k > n 的情况,此时每一位都可以随意选。
	for(int i = 1; i <= k; i++) v.push_back(i);
	init(v, k); dp[fst][1] = 1, dp[fst][2] = mod - 1;//初始化,这里的 [1] 不是回文串,而 [2] 则是回文串
	for(int i = k + 1; i <= n; i++) {
		int now = i & 1, pre = now ^ 1;
		for(int j = 1; j <= tcnt; j++) dp[now][j] = 0;
		for(int j = 1; j <= tcnt; j++) {//dp 转移
			if(flg[j]) dp[now][nxt[j][0]] = (dp[now][nxt[j][0]] + dp[pre][j]) % mod, dp[now][nxt[j][1]] = (dp[now][nxt[j][1]] - dp[pre][j] + mod) % mod;
			else dp[now][nxt[j][0]] = (dp[now][nxt[j][0]] + dp[pre][j] * c % mod) % mod, dp[now][nxt[j][1]] = (dp[now][nxt[j][1]] + dp[pre][j] * (mod - c) % mod) % mod;
		}
	}
	for(int i = 1; i <= tcnt; i++) ans = (ans + dp[lst][i] * mypow[diff[i]] % mod) % mod;//统计答案
	printf("%lld", ans);
	return 0;
}
posted @ 2024-09-24 23:24  ddxrS  阅读(16)  评论(0)    收藏  举报