@noi.ac - 492@ casino
@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 出发如果失配,将会转移到的位置的集合。
则:
\]
由于 trans(i) 集合的大小始终为 c,通过等价变形:
\]
将上式代入 \(a[i+1] = a[i] + b[i]\),得:
\]
看起来比上面那个高斯消元要好些,但还是不够。
我们不妨进一步探求一下 trans(i) 的规律。
再设置一个辅助集合 pre-suf(i)。x 属于该集合当且仅当 S 的前 i 个字符组成的字符串中,长度为 x 的前缀与后缀相同且 x ≠ 0。
则当 \((x+1)\in trans(i)\) 时,就有 \(x\in pre-suf(i)\)。但反之不一定成立。
我们现在可以归纳得出一个(神仙)结论:
\]
证明使用归纳法。边界条件显然。考虑已知 <=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) 是 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) 的方案数。于是:
\]
其中 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的更多相关文章
- # NOI.AC省选赛 第五场T1 子集,与&最大值
NOI.AC省选赛 第五场T1 A. Mas的童年 题目链接 http://noi.ac/problem/309 思路 0x00 \(n^2\)的暴力挺简单的. ans=max(ans,xor[j-1 ...
- NOI.ac #31 MST DP、哈希
题目传送门:http://noi.ac/problem/31 一道思路好题考虑模拟$Kruskal$的加边方式,然后能够发现非最小生成树边只能在一个已经由边权更小的边连成的连通块中,而树边一定会让两个 ...
- NOI.AC NOIP模拟赛 第五场 游记
NOI.AC NOIP模拟赛 第五场 游记 count 题目大意: 长度为\(n+1(n\le10^5)\)的序列\(A\),其中的每个数都是不大于\(n\)的正整数,且\(n\)以内每个正整数至少出 ...
- NOI.AC NOIP模拟赛 第六场 游记
NOI.AC NOIP模拟赛 第六场 游记 queen 题目大意: 在一个\(n\times n(n\le10^5)\)的棋盘上,放有\(m(m\le10^5)\)个皇后,其中每一个皇后都可以向上.下 ...
- NOI.AC NOIP模拟赛 第二场 补记
NOI.AC NOIP模拟赛 第二场 补记 palindrome 题目大意: 同[CEOI2017]Palindromic Partitions string 同[TC11326]Impossible ...
- NOI.AC NOIP模拟赛 第一场 补记
NOI.AC NOIP模拟赛 第一场 补记 candy 题目大意: 有两个超市,每个超市有\(n(n\le10^5)\)个糖,每个糖\(W\)元.每颗糖有一个愉悦度,其中,第一家商店中的第\(i\)颗 ...
- NOI.AC NOIP模拟赛 第四场 补记
NOI.AC NOIP模拟赛 第四场 补记 子图 题目大意: 一张\(n(n\le5\times10^5)\)个点,\(m(m\le5\times10^5)\)条边的无向图.删去第\(i\)条边需要\ ...
- NOI.AC NOIP模拟赛 第三场 补记
NOI.AC NOIP模拟赛 第三场 补记 列队 题目大意: 给定一个\(n\times m(n,m\le1000)\)的矩阵,每个格子上有一个数\(w_{i,j}\).保证\(w_{i,j}\)互不 ...
- NOI.AC WC模拟赛
4C(容斥) http://noi.ac/contest/56/problem/25 同时交换一行或一列对答案显然没有影响,于是将行列均从大到小排序,每次处理限制相同的一段行列(呈一个L形). 问题变 ...
随机推荐
- go语言:获取字符串长度
go语言字符串底层由字节数组实现,使用UTF-8编码,初始化以后不能被修改 获取字符串长度 一.当字符串中所有字符都是单字节字符时,使用 len 函数获取字符串的长度 package main imp ...
- vagrant简介
什么是vagrant? 简单理解,就是可以通过Vagrant这个工具管理虚拟机,比如说想创建一个centos环境的虚拟机,不需要安装系统这么麻烦,通过vagrant可以快速创建 官网地址:https: ...
- day38 04-Spring加载配置文件
Spring的工厂类ApplicationContext有两个子类:ClassPathXmlApplicationConext和FileSystemXmlApplication都可以用来加载配置文件. ...
- day36 06-Hibernate抓取策略:set集合上的抓取策略
你在做查询的时候它可以帮你关联出它的一些相应的关联对象.那么它关联这个对象的时候是在什么时候发送的这些语句以及它是如何把这些数据拿出来的? 知道延迟检索是怎么回事了,而且它也能够产生这个代理对象.当你 ...
- vmstat-虚拟内存查看实例
虚拟内存运行原理 在系统中运行的每个进程都需要使用到内存,但不是每个进程都需要每时每刻使用系统分配的内存空间.当系统运行所需内存超过实际的物理内存,内核会释放某些进程所占用但未使用的部分或所有物理内存 ...
- 转:解决Onethink上传视频的问题 超棒的解决方案
用过Onethink的都知道,它是不能上传视频的. 有人想到用上传附件的方式上传视频,但是结果……就是提示没有上传文件. 要是正常上传个一两兆的图片啊,压缩文件什么的还是可以的. 所以,重点来了 怎么 ...
- js对象属性方法大总结
数组(Array):系列元素的有序集合: 详细演示请看:[js入门系列演示·数组 ] http://www.cnblogs.com/thcjp/archive/2006/08/04/467761.ht ...
- Java IO:字节流与字符流
https://blog.csdn.net/my_truelove/article/details/53758412 字符和字节之间可以互相转化,中间的参照就是编码方式. 相当于给你一个密码本,按照这 ...
- leetcode 198-234 easy
198. House Robber 相邻不能打劫,取利益最大化. 思想:当前值和前一个和的总数 与 前一个和 做大小比较,取最大值,重复该步骤. class Solution { publ ...
- 使用DIV+CSS布局网站的优点和缺陷
随着WEB2.0标准化设计理念的普及,国内很多大型门户网站已经纷纷采用DIV+CSS制作方法,从实际应用情况来看,此种方法绝对好于表格制作页面的方法. 如今大部分网站仍然采用表格嵌套内容的方式来制作网 ...