• @noi.ac



    @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_{xin trans(i)}(a[i]-a[x]+b[i])+1 ]

    由于 trans(i) 集合的大小始终为 c,通过等价变形:

    [b[i] = c*a[i]-sum_{xin trans(i)}a[x]+(c+1) ]

    将上式代入 (a[i+1] = a[i] + b[i]),得:

    [a[i+1] = (c+1)*(a[i]+1) - sum_{xin trans(i)}a[x] ]

    看起来比上面那个高斯消元要好些,但还是不够。
    我们不妨进一步探求一下 trans(i) 的规律。
    再设置一个辅助集合 pre-suf(i)。x 属于该集合当且仅当 S 的前 i 个字符组成的字符串中,长度为 x 的前缀与后缀相同且 x ≠ 0。
    则当 ((x+1)in trans(i)) 时,就有 (xin pre-suf(i))。但反之不一定成立。

    我们现在可以归纳得出一个(神仙)结论:

    [a[i]=sum_{xin pre-suf(i)}(c+1)^x ]

    证明使用归纳法。边界条件显然。考虑已知 <=i 得证,证明 i+1 也满足。
    若某个 x 满足 ((x+1)in trans(i)) 时,由上有 (xin pre-suf(i))。于是对于所有 ((y+1)in pre-suf(x+1)),都有 (yin 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),zin pre-suf(i)) 中 S[z+1] = S[x+1] 对答案的贡献。
    那我们保留下来的是什么呢?其实就是 (zin 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_{kle 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
    ", x, i, j, k);
    						flag = false;
    						break;
    					}
    				}
    			}
    			if( !flag ) break;
    		}
    		if( !flag ) f[i] = 0;
    		else {
    		//	printf("? %d %d
    ", 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
    ", ans);
    }
    

    @details@

    康复计划 - 4。

    a 序列可以存成long long的二进制啊。。。。
    一开始没想到,脑子傻了。。。

    容斥一开始也没想清楚,后来看了 std 才懂的。

    感觉我大概可以 AFO 了。

  • 相关阅读:
    Kafka之消费者与消费者组
    Kafka之生产者
    基于Redis+Lua的分布式限流
    限流方案常用算法讲解
    分布式服务限流
    微服务框架服务调用与容错
    ZooKeeper实现服务注册中心
    微服务注册中心
    让我自己来整理
    Netty框架
  • 原文地址:https://www.cnblogs.com/Tiw-Air-OAO/p/11078362.html
Copyright © 2020-2023  润新知