P3989 [SHOI2013] 阶乘字符串 题解

· · 题解

题目大意

给定一个字符串,看其是不是 n 个小写字母全排列组成的子序列(注意不是子串

更新记录

upd:2023-10-02 22:05:50 第一版,提出反例 abcdabcadbac

upd:2023-10-04 21:34:47 第二版,发现更优的构造方法(By @Ac_forever)

首先,我们可以很容易发现,n>21 时完全构造不出合法的方案,所以答案一定为 NO

证明:然鹅,网上说 \dbinom{450}{21}<21! 是不正确的, 因为我自己算了一遍发现不对。

然后就是所谓的 n^2-n+1n(n-1)+1,是错误的、更优的构造方法为:

'a'+n-1x,则重复 n-3 遍从 ax,然后再为 ax-1,再为 ax,再为 a+1x-1,最后为 a

举个例子可能会更好(n=5):

\underbrace{abcde\ abcde}_{n-3\text{遍}}\ \underbrace{abcd}_{\text{去尾}}\ \underbrace{a\ e}_{\text{头、尾}}\ \underbrace{bcd}_{\text{去头去尾}}\ \underbrace{a}_{\text{固定了}}

但是我不会证明

但是又有前车之鉴,所以说,只能是最小字符串长度的上限n^2-n

那么我们假定就是这个就是最小字符串的大致范围,那么发现当 n=22n^2-n=462>450,所以当 n>21 时完全构造不出合法的方案。

发现可以状压。

f_s 表示进行状态转移,s 表示我们组成的前排列中字母的存在情况。例如 s=(101)_2 表示有字母 ac

定义函数 $\text{nxt}(i,j)$ 表示第 $i$ 位后面的第一个字符 $j$ 的位置(不含自己) 然后枚举新加入的字符 `c`,易得: $$ f_{s|c}=\max(f_{s|c},\text{nxt}(f_s,c))\qquad c\not\in s $$ 为什么是 $\max$?因为有两种情况: 1. $f_{s|c} > \text{nxt}(f_s,c)$,那么这时候 $f_{s|c}$ 是包含的 $c$ 的,满足条件 2. $f_{s|c} < \text{nxt}(f_s,c)$,那么这时候 $f_{s|c}$ 是不包含的 $c$ 的,要把 $c$ 包括进去 最后判断 $f_{(\underbrace{111\cdots111}_{\text{(n-1)个1}})_2}$,也就是 $f_{(1<<n)-1}$ 是不是合法的即可。 ```c++ #include <cstdio> #include <cstring> #include <algorithm> #define ll long long #define INF (ll)(1e17) using namespace std; ll t; ll n; char S[500]; ll nxt[500][26]; ll f[(1<<21)+10]; int main() { scanf("%lld", &t); while(t--) { scanf("%lld", &n); scanf("%s", S + 1); ll m = strlen(S + 1); if(n > 21) { printf("NO\n"); continue; } for(ll c = 0; c < n; c++) nxt[m][c] = INF; for(ll s = 0; s < (1 << n); s++) { f[s] = 0; } for(ll i = m; i >= 1; i--) { for(ll c = 0; c < n; c++) { nxt[i - 1][c] = nxt[i][c]; } nxt[i - 1][(ll)(S[i]) - 'a'] = i; } for(ll s = 0; s < (1 << n); s++) { for(ll c = 0; c < n; c++) { if((s | (1 << c)) != s) { if(f[s] == INF) f[s | (1 << c)] = INF; else f[s | (1 << c)] = max(f[s | (1 << c)], nxt[f[s]][c]); } } } if(f[(1 << n) - 1] == INF) printf("NO\n"); else printf("YES\n"); } // ( •̀ ω •́ )\ } ```