@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. python中str的常用方法汇总(1)

    a = 'strABC' # Strabc : 首字母大写,其他全部小写 b = a.capitalize() print(b) # STRABC : 全部大写 c = a.upper() print ...

  2. 稳定性专题 | StackOverFlowError 常见原因及解决方法

    导读 『StabilityGuide』是阿里多位阿里技术工程师共同发起的稳定性领域的知识库开源项目,涵盖性能压测.故障演练.JVM.应用容器.服务框架.流量调度.监控.诊断等多个技术领域,以更结构化的 ...

  3. 模拟4题解 T3奇袭

    T3奇袭 题目描述 由于各种原因,桐人现在被困在Under World(以下简称UW)中,而UW马上 要迎来最终的压力测试——魔界入侵. 唯一一个神一般存在的Administrator被消灭了,靠原本 ...

  4. Django项目:CRM(客户关系管理系统)--16--08PerfectCRM实现King_admin显示注册表的字段表头

    # king_urls.py # ————————02PerfectCRM创建ADMIN页面———————— from django.conf.urls import url from king_ad ...

  5. LintCode刷题笔记-- LongestCommonSquence

    标签:动态规划 题目描述: Given two strings, find the longest common subsequence (LCS). Your code should return ...

  6. php 简单加密解密

    <?php namespace App\Service; /* * @link http://kodcloud.com/ * @author warlee | e-mail:kodcloud@q ...

  7. idea启动报错:Access denied for user 'root '@'192.168.100.XXX' (using password: YES)

    Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean wit ...

  8. 使用C3P0和DBUtils

    1.导包 2.配置c3p0-config.xml <?xml version="1.0" encoding="UTF-8"?> <c3p0-c ...

  9. ubuntu上安装notepadpp

    Notepad++是一套非常有特色的自由软件的纯文字编辑器(许可证:GPL).有完整的中文化接口及支持多国语言编写的功能(UTF8 技术).它的功能比 Windows 中的 Notepad(记事本)强 ...

  10. 云计算、大数据、编程语言学习指南下载,100+技术课程免费学!这份诚意满满的新年技术大礼包,你Get了吗?

    开发者认证.云学院.技术社群,更多精彩,尽在开发者会场 近年来,新技术发展迅速.互联网行业持续高速增长,平均薪资水平持续提升,互联网技术学习已俨然成为学生.在职人员都感兴趣的“业余项目”. 阿里云大学 ...