[SHOI2013]阶乘字符串

题意简述

  • 定义一个由前 \(n\) 个字母组成的字符串为阶乘字符串,前 \(n\) 个小写字母的全排列都作为它的子序列出现。

  • 给定一个字符串 \(S\),询问它是否是阶乘字符串。

  • 多组数据,\(1 \le T \le 5\), \(1 \le n \le26\), \(1 \le |S| \le 450\)

分析与解答

1. 关于 \(n\) 的范围

  • 考虑一个包含前 \(n\) 种小写字母的字符串,长度至少要为多少才能满足其为阶乘字符串。换句话说,构造一个由前 \(n\) 个字母组成的阶乘字符串,使得它的长度最小。

  • 容易想到一种最 naive 的构造方式,将前 \(n\) 个小写字母顺序排列再复制 \(n\) 次,得到一个长度为 \(n^2\) 的字符串。例如,当 \(n=3\) 时,为 \(\text{abcabcabc}\)。容易证明该字符串一定为阶乘字符串。

  • 在上面的构造方式中再改进一步,删去一些无用的字符,可以得到一种形如 \(\text{abcdcbabcdcba}\ (n=4)\) 的构造方式(即以 \(\text{abcd、dcba}\) 为基础串,将当前基础串接在前一个基础串的后面)(表述不太清楚,理解万岁,理解万岁),这种构造方式长度为 \(n^2-n+1\)

  • 目前,我还没有找到更优秀的构造方式。也就是说,现在可以假定,一个字符串的长度至少为 \(n^2-n+1\),它才可能是前 \(n\) 个字母的阶乘字符串。

  • 注意到 \(22^2-22+1 = 463 \gt 450, 21^2-21+1 = 421 \lt 450\),所以当 \(n \ge 22\) 时一定无解。

  • 通过上面的分析,我们将 \(n\) 范围将至了 \(1 \le n \le 21\),这就可以保证接下来的 dp 的时空复杂度了。


2. 关于如何 dp

  • 如果以考虑前第 \(i\) 个字符为状态,每次转移时都需要枚举之前的所有子序列并一个一个 check,显然会超时。

  • 注意到 \(n \le 21\),我们不妨抛开上一个思想,考虑状压小写字母。令 \(f_i\) 表示 \(i\) 中的小写字母集合组成的所有全排列,是否都在 \(S\) 的子序列中出现过。

  • 于是转移又成了很大的问题。

  • 这种时候,常见的想法是改变状态。令 \(f_i\) 表示 \(i\) 中的小写字母集合组成的所有全排列,在 \(s\) 中全部出现过,所需要的下标最小是多少。此时,如果一个字母集合 \(i\) 不能达成阶乘字符串的要求,则 \(f_i = +\infty\)

  • 构造 \(S\) 的序列自动机,即 \(nxt_{i,j}\) 表示第 \(i\) 个位置后,字符 \(j\) 第一次出现的位置,其中 \(i \in [1,m],\ j \in [0,n)\)

  • 这种状态下的转移不妨使用刷表法。只需要考虑在当前集合中加入一种小写字母,会发生什么变化。

  • 发现如果加入一种字母 \(j\),则在原来子序列的最后只要加入一个 \(j\) 来满足有以 \(j\) 结尾的排列;而包含 \(j\) 的其他排列会在其它的转移中被考虑到。

  • 所以转移就为 \(f_{i|(1<<j)} = nxt_{f_i,j}\)。因为要保证所有的排列都能够被满足,每个 \(f_i\) 在向它转移的 \(nxt\) 中取最大值。

  • 最后判断 \(f_{(1<<n)-1}\) 是否满足条件即可。

Code

#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int MAXM = 460;
const int MAXN = 23;
const int MAXS = (1<<21) + 10;
const int INF = 0x3f3f3f3f; 

int n, m, T;
char s[MAXN];
int f[MAXS], nxt[MAXM][MAXN];

inline bool work()
{
	scanf("%d%s",&n,s+1);
	int m = strlen(s+1);
	if(n > 21) return 0;
	for(int i=0;i<n;i++)
		nxt[m][i] = INF;
	for(int i=m;i>=1;i--) // 序列自动机 nxt
	{
		for(int j=0;j<n;j++)
			nxt[i-1][j] = nxt[i][j];
		nxt[i-1][s[i]-'a'] = i;
	}
	int maxs = 1<<n;
	for(int i=0;i<maxs;i++)
		f[i] = 0;
	for(int s=0;s<maxs;s++)
	{
		if(f[s] == INF) return 0; // 如果子集都无法满足,全部也一定不满足
		for(int i=0;i<n;i++)
		{
			if(((~s)>>i)&1)
			{
				if(f[s] == INF) f[s|(1<<i)] = INF;
				else f[s|(1<<i)] = max(f[s|(1<<i)], nxt[f[s]][i]);
			}
		}
	}
	return f[maxs-1] != INF; 
}

int main()
{
	scanf("%d",&T);
	while(T--) puts(work()?"YES":"NO");
	return 0;
}
posted @ 2022-03-26 10:20  Aphrosia  阅读(68)  评论(0编辑  收藏  举报