@description@
这是 2019 年欧皇排位赛,n 位欧皇种子选手参与了本次角逐。
每位种子选手都有一个长度为 m 的数字串,数字串的每一位都是 [0,c] 之间的整数,不同的选手可能拥有相同的数字串。第 i 位选手持有的数字串为 si。
随着比赛的开始,大屏幕上会显示一个空字符串 T。接着有一台机器不断地按照 1/(c+1) 的概率随机从 [0,c] 之间抽取一个整数 x,然后将 x 放在 T 的末尾。为了决出谁是真正的欧皇,令 pi 表示当 T 首次包含 si 作为连续子串时 T 中字符的数量的期望值,那么 pi 最低的选手 i 获胜。
现在已知 n 位选手已经按照 p 从小到大排序,即 p1≤p2≤p3≤⋯≤pn,但是这些选手持有的数字串却有一些部位被他们隐藏了(用 ? 表示)。请写一个程序,计算将这些数字串补充完整,使得 p1≤p2≤p3≤⋯≤pn 成立,有多少种可能的方案。两种方案被视为不同的当且仅当存在至少一名选手 i 满足 si 在两个方案中不同。
@solution@
input
第一行包含三个正整数 n,m,c,分别表示选手的数量、数字串的长度以及随机范围。
接下来 n 行,每行一个长度为 m 的字符串 si,依次表示每个选手持有的数字串。
output
输出一行一个整数,即满足条件的方案数。因为答案可能很大,请对1000000007(=10^9+7)取模输出。
sample input
2 4 1
??1?
10?1
sample output
12
对于 100% 的数据,n≤8, m≤50, c≤9,且数字串每一位要么是 ? ,要么是 [0,c] 之间的整数。
@solution@
前排提醒:配合代码一起看可能会好一些。
@part - 1@
我们不妨先看若给定一个字符串 S,怎么求解对应的期望步数 p(S)。
根据期望 dp 的套路,可以令 dp[i] 表示从 S 的第 i 个字符开始到最后一个字符所需期望步数。
转移时,要么成功匹配到 i+1 个字符;要么失配,使用 kmp 求出此时的失配后的位置。
于是可以愉快地进行高斯消元。
因为这是一个线性的序列,根据期望的性质可以得到:
i -> j 的期望步数 = i -> k 的期望步数 + k -> j 的期望步数。
令 a[i] 表示从空串到前缀 i 的期望步数,令 b[i] 表示从前缀 i 到前缀 i+1 的期望步数,则 a[i+1] = a[i] + b[i]。
我们先设置一个辅助集合 trans(i),表示从前缀 i 出发如果失配,将会转移到的位置的集合。
则:
\[b[i] = \frac{1}{c+1}\sum_{x\in trans(i)}(a[i]-a[x]+b[i])+1\]
由于 trans(i) 集合的大小始终为 c,通过等价变形:
\[b[i] = c*a[i]-\sum_{x\in trans(i)}a[x]+(c+1)\]
将上式代入 \(a[i+1] = a[i] + b[i]\),得:
\[a[i+1] = (c+1)*(a[i]+1) - \sum_{x\in trans(i)}a[x]\]
看起来比上面那个高斯消元要好些,但还是不够。
我们不妨进一步探求一下 trans(i) 的规律。
再设置一个辅助集合 pre-suf(i)。x 属于该集合当且仅当 S 的前 i 个字符组成的字符串中,长度为 x 的前缀与后缀相同且 x ≠ 0。
则当 \((x+1)\in trans(i)\) 时,就有 \(x\in pre-suf(i)\)。但反之不一定成立。
我们现在可以归纳得出一个(神仙)结论:
\[a[i]=\sum_{x\in pre-suf(i)}(c+1)^x\]
证明使用归纳法。边界条件显然。考虑已知 <=i 得证,证明 i+1 也满足。
若某个 x 满足 \((x+1)\in trans(i)\) 时,由上有 \(x\in pre-suf(i)\)。于是对于所有 \((y+1)\in pre-suf(x+1)\),都有 \(y\in pre-suf(i)\)。
而我们要减去 a[x+1],相当于是要减去 \(\sum_{(y+1)\in pre-suf(x+1)}(c+1)^{(y+1)}\),即 \(\sum_{(y+1)\in pre-suf(x+1)}(c+1)^y*(c+1)\)。
综合以上,我们减去了 \((x+1)\in trans(i),z\in pre-suf(i)\) 中 S[z+1] = S[x+1] 对答案的贡献。
那我们保留下来的是什么呢?其实就是 \(z\in pre-suf(i)\) 中 S[z+1] = S[i+1] 对答案的贡献。即 \((z+1)\in pre-suf(i+1)\) 的贡献。
于是就可以归纳得证了。
还有一个证明的细节:为什么式子中是 (a[i]+1) 而不是 a[i]。这个 +1 感性理解就是长度为 0 的前后缀。
@part - 2@
你看不懂上面在说什么没关系,你只需要知道有这么一个结论:
\[p(S) = \sum_{i=1}^m(a_i*(c+1)^x)\]
其中 p(S) 是 S 的期望步数,ai 是一个 01 数组,当 S 中长度为 i 的前缀与后缀相同时为 1,否则为 0。毕竟题解中也只是一句令人自闭的“经过漫长的推导”
我们可以从 m-1 开始(注意 am 显然等于 1)从大到小爆搜出所有 a 序列,然后发现当 m = 50 的时候也只有 2249 个(神仙*2)。
注意边搜边用并查集找出哪些位置一定相同,判断当前的 a 序列是否合法,以及时地进行剪枝。
那上面的结论有什么作用呢?我们并不需要求解 p(S) 啊。
结论长得非常像一个 (c+1) 进制的数,于是就有以下两个推广结论:
(1)每一个 a 序列唯一对应一个 p(S)。
(2)可以通过比较两个 a 序列翻转后的字典序比较其对应的 p(S) 的大小。
我们可以搜索的时候按字典序搜索(即先搜 0 后搜 1),对应的 p(S) 形成天然的顺序,无需再排序。
设计一个简单 dp 来求解最终答案。
令 dp[i][j] 表示前 i 个人,第 i 个人使用的是第 j 大的 p(S) 的方案数。于是:
\[dp[i][j] = (\sum_{k\le j}dp[i-1][k])*f(i, j)\]
其中 f(i, j) 表示在第 i 个人所拥有的字符串与第 j 大的 a 序列双重限制下可行字符串的方案数。
@part - 3@
考虑已知第 i 个人所拥有的字符串,怎么求解某个 a 序列对应多少可行字符串。
事实上,一个 a 序列包含两类信息:某些前后缀相等,某些前后缀不相等。
前一个好处理:使用并查集即可。后一个可以运用容斥解决。
具体来说,我们通过前一类限制求出哪些字符是等价类。
首先等价类中的字符与第 i 个人所拥有的字符串已知信息不能矛盾。
其次如果等价类中的字符在第 i 个人所拥有的字符串中存在已知信息,则该等价类只有唯一方案。
否则该等价类有 (c+1) 种方案。
而容斥方面,我们从 p(S) 较大的 a 开始,从大到小逐一容斥。
这样,当扫描到某一个序列 ai 时,因为它减去的重复对象是以 ai 为子集的序列,所以肯定 p(S) 比 ai 对应的要大,在之前已经容斥好了。
所以,我们 ai 只需直接减去以 ai 为子集的序列即可。
@accepted code@
#include<vector>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int MOD = int(1E9) + 7;
inline int add(int a, int b) {return (a + b)%MOD;}
inline int mul(int a, int b) {return 1LL*a*b%MOD;}
inline int sub(int a, int b) {return add(a, MOD-b);}
struct node{int f[50 + 5]; ll s;}tmp;
vector<node>vec;
int n, m, c, tot;
char s[8 + 5][50 + 5];
int fa[50 + 5];
inline int find(int x) {
return fa[x] = (x == fa[x]) ? x : find(fa[x]) ;
}
void unite(int x, int y) {
if( find(x) != find(y) )
fa[find(x)] = find(y);
}
void build(node x) {
for(int i=1;i<=m;i++)
fa[i] = i;
for(int i=1;i<=m;i++)
if( (x.s>>i)&1 ) {
for(int j=1;j<=i;j++)
unite(j, m-i+j);
}
}
bool check(int x) {
build(tmp);
for(int i=x;i<=m;i++)
if( !((tmp.s>>i)&1) ) {
bool flag = true;
for(int j=1;j<=i;j++)
if( find(j) != find(m-i+j) ) {
flag = false;
break;
}
if( flag )
return false;
}
return true;
}
void dfs(int x) {
if( x == 0 ) {
build(tmp);
for(int i=1;i<=m;i++)
tmp.f[i] = fa[i];
vec.push_back(tmp);
tot++; return ;
}
if( check(x) ) dfs(x - 1);
tmp.s ^= (1LL<<x);
if( check(x) ) dfs(x - 1);
tmp.s ^= (1LL<<x);
}
int dp[8 + 5][2500 + 5], pw[50 + 5], f[2500 + 5];
bool tag[50 + 5];
void solve(int x) {
for(int i=tot-1;i>=0;i--) {
bool flag = true;
for(int j=1;j<=m;j++) {
for(int k=j+1;k<=m;k++) {
if( s[x][j] != '?' && s[x][k] != '?' ) {
if( (s[x][j] != s[x][k]) && (vec[i].f[j] == vec[i].f[k]) ) {
// printf(". %d %d %d %d\n", x, i, j, k);
flag = false;
break;
}
}
}
if( !flag ) break;
}
if( !flag ) f[i] = 0;
else {
// printf("? %d %d\n", x, i);
for(int j=1;j<=m;j++)
tag[j] = false;
for(int j=1;j<=m;j++)
if( s[x][j] != '?' )
tag[vec[i].f[j]] = true;
int cnt = 0;
for(int j=1;j<=m;j++)
if( vec[i].f[j] == j )
if( !tag[j] ) cnt++;
f[i] = pw[cnt];
}
for(int j=i+1;j<tot;j++)
if( (vec[i].s & vec[j].s) == vec[i].s )
f[i] = sub(f[i], f[j]);
}
}
int main() {
scanf("%d%d%d", &n, &m, &c);
for(int i=1;i<=n;i++)
scanf("%s", s[i] + 1);
tmp.s = (1LL<<m), dfs(m - 1);
pw[0] = 1;
for(int i=1;i<=m;i++)
pw[i] = mul(pw[i-1], c + 1);
dp[0][0] = 1;
for(int i=1;i<=n;i++) {
solve(i);
for(int j=0;j<tot;j++)
for(int k=0;k<=j;k++)
dp[i][j] = add(dp[i][j], mul(dp[i-1][k], f[j]));
}
int ans = 0;
for(int i=0;i<tot;i++)
ans = add(ans, dp[n][i]);
printf("%d\n", ans);
}
@details@
康复计划 - 4。
a 序列可以存成long long的二进制啊。。。。
一开始没想到,脑子傻了。。。
容斥一开始也没想清楚,后来看了 std 才懂的。
感觉我大概可以 AFO 了。