@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. Axure之母版窗口

  2. Html5知识点以及兼容性

    什么的HTNL5? HTML5 是最新的 HTML 标准. HTML5 是专门为承载丰富的 web 内容而设计的,并且无需额外插件. HTML5 拥有新的语义.图形以及多媒体元素. HTML5 提供的 ...

  3. webpack学习之—— Configuration(配置)

    你可能已经注意到,很少有 webpack 配置看起来很完全相同.这是因为 webpack 的配置文件,是导出一个对象的 JavaScript 文件.此对象,由 webpack 根据对象定义的属性进行解 ...

  4. Android学习笔记之mainfest文件中android属性

    android:allowTaskReparenting 是否允许activity更换从属的任务,比如从短信息任务 切换到浏览器任务. -------------------------------- ...

  5. Mybatis使用 爬坑记录

    1.mapper.xml可以直接 使用map集合, parameterType="java.util.Map" resultType="java.util.Map&quo ...

  6. 【水滴石穿】FirstReactNativeProject

    这个是一个小demo,项目地址为https://github.com/prsioner/FirstReactNativeProject 有注册,忘记密码还有登陆,应该是用到了react-navigat ...

  7. Java ANSI转码UTF-8

    public static void change(String filepath) throws UnsupportedEncodingException, IOException{ Buffere ...

  8. 【JZOJ4790】【NOIP2016提高A组模拟9.21】选数问题

    题目描述 在麦克雷的面前有N个数,以及一个R*C的矩阵.现在他的任务是从N个数中取出R*C个,并填入这个矩阵中.矩阵每一行的法值为本行最大值与最小值的差,而整个矩阵的法值为每一行的法值的最大值.现在, ...

  9. springmvc restful风格操作

    ssm框架 controller: package com.sgcc.controller; import java.util.ArrayList; import java.util.List; im ...

  10. 网络流24题 负载平衡(DCOJ8013)

    题目描述 G 公司有 n nn 个沿铁路运输线环形排列的仓库,每个仓库存储的货物数量不等.如何用最少搬运量可以使 n nn 个仓库的库存数量相同.搬运货物时,只能在相邻的仓库之间搬运. 输入格式 文件 ...