洛谷P7263 Something Comforting

题意

传送门

给一个随机生成合法括号序列的程序,给定一个合法括号序列,求随机出这个序列的概率

分析

第一要明确的,是我们该如何统计概率,这里总可能情况比较好计算,那么我们就考虑用 个数/总数 来求解概率。

那么接下来,我们先简单的来看一下程序的含义:

#include <algorithm>
#include <string>

std::string generate(int n) { // 生成一个长度为 n * 2 的括号序列
	const int len = n * 2;
	bool arr[len]; // 0 代表左括号,1 代表右括号
	for (int i = 0; i < n; ++i) arr[i] = 0;
	for (int i = n; i < len; ++i) arr[i] = 1;
	std::random_shuffle(arr, arr + len); // 随机打乱这个数组
	for (int i = 0, j, sum = 0; i < len; ) {
		sum += arr[i]? -1: 1;
		if (sum < 0) { // 出现了不合法的位置
			for (j = i + 1; j < len; ++j) {
				sum += arr[j]? -1: 1;
				if (sum == 0) break;
			}
			// 现在 i 是第一个不合法的位置,j 是 i 后面第一个合法的位置
			// ( ( ) ) ) ) ( ( ( ) ( )
			//         i     j
			for (int k = i; k <= j; ++k)
				arr[k] ^= 1; // 把这段区间全部反转
			i = j + 1;
		} else ++i;
	}
	std::string ret;
	for (int i = 0; i < len; ++i)
		ret += arr[i]? ')': '(';
	return ret;
}

首先给定了长度\(n\),生成了长度为\(2n\)的括号序列

第一步我们把其中\(1\) ~ \(n\)元素初始化为左括号,把\(n+1\) ~ \(2n\)初始化为右括号(程序里下标从\(0\)开始的)

然后\(random\) _ \(shuffle\)随机打乱这个括号序列

现在括号序列有\(n\)个左括号,\(n\)个右括号,但是不一定合法

接下来的循环就是把不合法的段修正

首先\(sum\)前缀和用于判断当前合不合法(因为左括号\(+1\)右括号\(-1\),而当右括号个数大于左括号个数就是不合法的,也就是\(sum<0\)的时候)

那么我们往后枚举,找到一个使得左右括号个数相等的位置,那么此时我们可以发现,把当前第一个不合法位置到第一个合法的位置这一段中的括号序列反转,依然合法,且原括号个数不变,也就不影响正确性

程序就是这样来保证括号序列合法的

那么这样做带给我们什么启发呢?

很容易发现,其实我们每次随机生成,原本的等概率发生变化的地方就是我们把不合法的重新变成合法的这个过程。

那么我们考虑这个过程带来的影响:

正着考虑很难,正好我们又是给定了结果求有多少种起始状态可以到达,那么我们可以考虑给定结果反向思考此时“修正”操作的影响

假设现在给出的是:

\(()(())\)

因为每次翻转过后,我们会保证的是:假设第一个不合法点为位置\(x\),第一个合法点位置为\(y\),那么\(1\) ~ \(x-1\)这段区间是合法的,且之后\(y+1\) ~ \(2n\)这段区间也是合法的

也就是说,我们每一组这样的点,对应了序列上的一段区间,且所有的这样的区间之和,恰好完全覆盖了整个序列且相互之间没有重叠

那么我们单独抽离这样的一个区间进行分析:

如果现在我们找到了这样的一个区间,那么这段区间最后会被翻转,那么对于我们答案序列来说,这样的区间就有两种情况:

\(1.\)是由翻转得到的

\(2.\)没有翻转原本就是这样的

而翻转得到的这段区间是唯一的(就是任意两个不同的括号序列翻转过后一定不相同)

而没翻转的显然也是唯一的

所以对于每一个有可能翻转的答案区间,它的原始区间都有两种可能的情况,所以我们每遇到这样的一个区间,就把方案数\(*2\)即可

那么现在的问题就是如何统计有多少个这样的区间,方案数自然就是\(2^k\)\(k\)就是个数)

所以我们考虑这样的区间,根据刚刚的例子,可能的初始序列为:

\(()))((\)\()())((\)\(()(())\)\()((())\)

共四种。

发现每次可能翻转的其实就是最外层的括号,感性证明也很容易,因为我们每次都是找到的一个极大区间进行翻转,我们每次翻转只会影响自己代表的区间,和其它部分完全没有关系

上述例子中,可供翻转的区间就是\([1,2]\)\([3,6]\)

那么现在我们也已经知道了如何就初始状态数,最后我们求一下总数,再求其逆元即可

问题变成了:给定\(n\),求长度为\(2n\)的括号序列,满足:其中有\(n\)个左括号,\(n\)个右括号,这样情况的方案数

那么我们先把\(2n\)个括号随机打乱,然后考虑构造合法情况,我们先找出\(n\)个右括号,除掉全排列去重,然后找出\(n\)个左括号,除掉全排列去重,然后得到的就是答案

写出来就是

\[\Large \frac{P_{2n}^{2n}}{P_{n}^{n}*P_{n}^{n}} =\frac{(2n)!}{(n!)^2} \]

记其为\(N\),记\(N\)的逆元为\(N'\),记当前情况方案数为\(k\),概率就是\(k*N'\)

答案\(k*N'\)

代码

#include<bits/stdc++.h>
using namespace std;
template <typename T>
inline void read(T &x){
	x=0;char ch=getchar();bool f=false;
	while(!isdigit(ch)){if(ch=='-'){f=true;}ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	x=f?-x:x;
	return ;
}
template <typename T,typename... Args> inline void read(T& t, Args&... args){read(t);read(args...);}
template <typename T>
inline void write(T x){
	if(x<0) putchar('-'),x=-x;
	if(x>9) write(x/10);
	putchar(x%10^48);
	return ;
}
template <typename T>
inline void print(T x){write(x),putchar(' ');}
#define ll long long
#define ull unsigned long long
#define inc(x,y) (x+y>=MOD?x+y-MOD:x+y)
#define dec(x,y) (x-y<0?x-y+MOD:x-y)
#define min(x,y) (x<y?x:y)
#define max(x,y) (x>y?x:y)
const int N=1e5+5,M=1e6+5,MOD=998244353;
ll QuickPow(ll x){
	ll y=MOD-2,res=1;
	while(y){
		if(y&1) res=res*x%MOD;
		y>>=1;
		x=x*x%MOD;
	}
	return res;
}
ll n,ans=1,ans1=1,sum,w=1;
char ch[N],sta[N];
int main(){
	read(n);
	for(int i=1;i<=2*n;i++) ch[i]=getchar();
	for(int i=1;i<=2*n;i++) ans=ans*i%MOD;
	for(int i=1;i<=n;i++) ans1=ans1*i%MOD;
	ans=QuickPow(ans*QuickPow(ans1*ans1%MOD)%MOD);
	for(int i=1;i<=2*n;i++){
		if(ch[i]=='(') sum++;
		else sum--;
		if(sum==0) w=w*2%MOD;
	}
	write(w*ans%MOD);
	return 0;
}
posted @ 2021-01-11 09:43  __Anchor  阅读(90)  评论(0编辑  收藏  举报