@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 了。

@noi.ac - 492@ casino的更多相关文章

  1. # NOI.AC省选赛 第五场T1 子集,与&最大值

    NOI.AC省选赛 第五场T1 A. Mas的童年 题目链接 http://noi.ac/problem/309 思路 0x00 \(n^2\)的暴力挺简单的. ans=max(ans,xor[j-1 ...

  2. NOI.ac #31 MST DP、哈希

    题目传送门:http://noi.ac/problem/31 一道思路好题考虑模拟$Kruskal$的加边方式,然后能够发现非最小生成树边只能在一个已经由边权更小的边连成的连通块中,而树边一定会让两个 ...

  3. NOI.AC NOIP模拟赛 第五场 游记

    NOI.AC NOIP模拟赛 第五场 游记 count 题目大意: 长度为\(n+1(n\le10^5)\)的序列\(A\),其中的每个数都是不大于\(n\)的正整数,且\(n\)以内每个正整数至少出 ...

  4. NOI.AC NOIP模拟赛 第六场 游记

    NOI.AC NOIP模拟赛 第六场 游记 queen 题目大意: 在一个\(n\times n(n\le10^5)\)的棋盘上,放有\(m(m\le10^5)\)个皇后,其中每一个皇后都可以向上.下 ...

  5. NOI.AC NOIP模拟赛 第二场 补记

    NOI.AC NOIP模拟赛 第二场 补记 palindrome 题目大意: 同[CEOI2017]Palindromic Partitions string 同[TC11326]Impossible ...

  6. NOI.AC NOIP模拟赛 第一场 补记

    NOI.AC NOIP模拟赛 第一场 补记 candy 题目大意: 有两个超市,每个超市有\(n(n\le10^5)\)个糖,每个糖\(W\)元.每颗糖有一个愉悦度,其中,第一家商店中的第\(i\)颗 ...

  7. NOI.AC NOIP模拟赛 第四场 补记

    NOI.AC NOIP模拟赛 第四场 补记 子图 题目大意: 一张\(n(n\le5\times10^5)\)个点,\(m(m\le5\times10^5)\)条边的无向图.删去第\(i\)条边需要\ ...

  8. NOI.AC NOIP模拟赛 第三场 补记

    NOI.AC NOIP模拟赛 第三场 补记 列队 题目大意: 给定一个\(n\times m(n,m\le1000)\)的矩阵,每个格子上有一个数\(w_{i,j}\).保证\(w_{i,j}\)互不 ...

  9. NOI.AC WC模拟赛

    4C(容斥) http://noi.ac/contest/56/problem/25 同时交换一行或一列对答案显然没有影响,于是将行列均从大到小排序,每次处理限制相同的一段行列(呈一个L形). 问题变 ...

随机推荐

  1. Django-2.2.1学习感悟

    或许是看的教程比较老,跟不上2.2.1版本的Django,所以在编写代码时可以说是bug不断,这算是编程必经阶段 每当遇到django的bug logging,先看头几行再看最后几行,能自己解决最好, ...

  2. Robbin负载均衡

    Robbin是在Spring Cloud中的一个组件,是由Netfix发布的负载均衡器,有助于控制HTTP和TCP客户端的行为.它给我们提供了默认的轮询.随机等负载均衡算法.同时也可以由我们定义自己的 ...

  3. linux文件系统配置文件

    文件系统 内核提供了一个接口,用来显示一些它的数据结构,这些数据结构对于决定诸如使用的中断.初始化的设备和内存统计信息之类的系统参数可能很有用.这个接口是作为一个独立但虚拟的文件系统提供的,称为 /p ...

  4. Vue--由自动获取焦点引出的DOM、mounted、自定义指令

    一.自动获取焦点的DOM实现 <!DOCTYPE html> <html lang="en"> <head> <meta charset= ...

  5. CSS中各种居中方法

    CSS中各种居中方法,本文回顾一下,便于后续的使用. 水平居中方法 1.行内元素居中 行内元素居中是只针对行内元素的,比如文本(text).图片(img).按钮等行内元素,可通过给父元素设置 text ...

  6. mysql错误日志目录

    在windows下,一般是mysql安装目录下的data目录下 ,扩展名是.err的文件.

  7. 痞子衡嵌入式:飞思卡尔i.MX RTyyyy系列MCU外设那些事(2)- 善变的FlexRAM

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是飞思卡尔i.MX RTyyyy系列MCU的FlexRAM外设. 本文是外设系列第二篇,上一篇讲的是离内核最近的高速缓存L1 Cache, ...

  8. Struts framework

    Struts功能详解——ActionMapping对象 Struts旅程(一)Struts简介和原理 实例讲解DispatchAction和LookupDispatchAction DispatchA ...

  9. Vue调试神器之Vue.js devTools

    Vue项目中使用Vue.js devTools这款调试神器,可以极大程度的提高我们的开发效率. 安装 1.打开Chrome网上应用商店安装插件(自墙),直接搜索devTools安装即可.贵宾传送阵,请 ...

  10. 弘康人寿基于 RocketMQ 构建微服务边界总线的实践

    随着互联网+和平台化战略的兴起,各个行业的 IT 系统都在向互联网架构发展,涉及的主要技术包括微服务.消息和弹性计算等,采用微服务架构实现服务高内聚.低耦合,通过异步消息完成交易快速响应和高并发.由于 ...