6515. 【GDOI2020模拟03.28】数一数(one)

题目

n n n m m m列的格子,每列的格子中有一个是 1 1 1,其它的都是 0 0 0
对于某一列,第 i i i行是 1 1 1的概率是 p i p_i pi
从任意点出发,每次从当前列走到下一列的同一行、上一行,或下一行。
贡献是沿路的数字和。
f ( m ) f(m) f(m)为最大值的期望。计算 lim ⁡ m → inf ⁡ f ( m ) m \lim_{m \to \inf} \frac{f(m)}{m} limminfmf(m)


思考历程

话说题目描述是错了,按照题目描述来就根本过不了样例……
这个东西搞死了绝大多数人半天……
了解真正题意之后,发现 n < = 2 n<=2 n<=2时输出 1 1 1,于是就有分了。


正解

题目求的是“最大值”的“期望”,而不是“期望”的“最大值”,这一点上本来就已经很恶心了。考虑假如每个格子上的数都确定的情况下怎么搞。
这是个小学生dp问题: g i , j g_{i,j} gi,j表示到 ( i , j ) (i,j) (i,j)的最大贡献。
换一种更好看的记法:对于每一列,将最大值找出来,然后将所有的值变成自己和最大值的差值。
可以发现,差值的范围是 [ 0 , n − 1 ] [0,n-1] [0,n1]。这样一列的状态最多只有 n n n^n nn种。
用bfs将这些状态全部搜出来。搜出来发现状态种类数其实更少,不超过 500 500 500
建出一个图,每个点代表一个状态,边代表哪个状态有多少概率转移到哪些状态。

m m m趋向于无限的时候,对于得到的第 m m m列的状态,状态出现的概率是恒定的。
既然是恒定的,不妨对于每个状态,根据它的前驱状态列个方程。
不要忘了所有状态概率加起来为 1 1 1
集齐一堆方程之后解之,就可以知道每种状态出现的概率。

接下来考虑计算这些状态的贡献。
先说做法:对于某个状态,枚举下一列 1 1 1的位置,然后看看这个状态能转移到下一列的 1 1 1的位置中,有没有最大值。如果有,就计入贡献。
感性地解释一下:由于是极限,所以我们并不关心之前产生的最大值是多少。我们只需要关心它在到达无限列的时候,进行一次操作的增量。 m m m趋向于无穷时,将 f ( m ) f(m) f(m)看成一个类似于一次函数的东西,之前的最大值无论是多少只能看做是常数,但是下一步的增量可以看做一次项系数(到达无限的时候,增量不变),众所周知计算极限的时候只有最高次项是有意义的。


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 6
#define ll long long
#define mo 1000000007
inline ll qpow(ll x,int y=mo-2){
	ll res=1;
	for (;y;y>>=1,x=x*x%mo)
		if (y&1)
			res=res*x%mo;
	return res;
}
int n;
int a[N];
int pown[N+1];
int id[46656],num,re[500];
int q[46656];
struct EDGE{
	int to,p;
	EDGE *las;
} e[500*6];
int ne;
EDGE *last[500];
inline void link(int u,int v,int p){
	e[ne]={v,p,last[u]};
	last[u]=e+ne++;
}
#define get(x,k) ((x)/pown[k]%n)
inline void init(){
	int t[6];
	int head=1,tail=1;
	q[1]=0;
	id[0]=++num;
	re[num]=0;
	while (head<=tail){
		int x=q[head++];
		for (int i=0;i<n;++i){
			for (int j=0;j<n;++j){
				int mn=get(x,j);
				if (j-1>=0)
					mn=min(mn,get(x,j-1));
				if (j+1<n)
					mn=min(mn,get(x,j+1));
				t[j]=(mn-(i==j));	
			}
			if (t[i]==-1)
				for (int j=0;j<n;++j)
					t[j]++;
			int s=0;
			for (int j=0;j<n;++j)
				s+=pown[j]*t[j];
			if (!id[s]){
				id[s]=++num;
				re[num]=s;
				q[++tail]=s;
			}
			link(id[s],id[x],a[i]);
		}
	}
}
int eql[500][500],cnt;
int p[500];
void calc(){
	for (int i=1;i<=num;++i){
		int j=i;
		for (;j<=num && eql[i][j]==0;++j);
		if (i!=j)
			for (int k=0;k<=num;++k)
				swap(eql[i][k],eql[j][k]);
		ll invi=qpow(eql[i][i]);
		for (int k=0;k<=num;++k)
			eql[i][k]=(eql[i][k]*invi)%mo;
		for (++j;j<=num;++j){
			ll c=eql[j][i];
			for (int k=0;k<=num;++k)
				eql[j][k]=((eql[j][k]-c*eql[i][k])%mo+mo)%mo;
		}
	}
	for (int i=num;i>=1;--i){
		ll s=eql[i][0];
		for (int j=i+1;j<=num;++j)
			s=((s-(ll)eql[i][j]*p[j])%mo+mo)%mo;
		p[i]=s;
	}
}
int main(){
	freopen("one.in","r",stdin);
	freopen("one.out","w",stdout);
	scanf("%d",&n);
	pown[0]=1;
	for (int i=1;i<=n;++i)
		pown[i]=pown[i-1]*n;
	for (int i=0;i<n;++i){
		double x;
		scanf("%lf",&x);
		a[i]=int(x*10)*qpow(10)%mo;
	}
	init();
	++cnt;
	eql[cnt][0]=1;
	for (int i=1;i<=num;++i)
		eql[cnt][i]=1;
	for (int i=2;i<=num;++i){
		++cnt;
		eql[cnt][i]=mo-1;
		for (EDGE *ei=last[i];ei;ei=ei->las)
			(eql[cnt][ei->to]+=ei->p)%=mo;
	}
	calc();
	ll ans=0;
	for (int i=1;i<=num;++i){
		ll s=0;
		for (int j=0;j<n;++j)
			if (get(re[i],j)==0 || (j-1>=0 && get(re[i],j-1)==0) || (j+1<n && get(re[i],j+1)==0))
				s+=a[j];
		s%=mo;
		ans=(ans+s*p[i])%mo;
	}
	printf("%lld\n",ans);
	return 0;
}


总结

真是……
败在数学。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值