Luogu P1850换教室【期望dp】By cellur925

题目传送门

首先这个题我们一看它就是和概率期望有关,而大多数时候在OI中遇到他们时,都是与dp相关的。

\(Vergil\)学长表示,作为\(NOIp2016\)的当事人,他们考前奶联赛一定不会考概率期望,结果...真香!\(qwq\)

不过\(NOIp\)还是对像我这样的菜到不会正解只会写暴力的蒟蒻来说还是很友好的==。据说这题暴力分都拿满有\(80pts+\)。作为第三题的分量真的很友好。

暑假学长就是用的这个题给我们讲的二进制枚举。性感学长在线\(debug\)

64分做法:

注意到1~15测试点的\(n\)范围在\(20\)内,考虑二进制枚举。具体来说,就是把\(n\)个时间段压在一个二进制位上,如果是0,说明他没选择换,如果是1,说明他换了(用了申请机会)。但是注意,因为申请成功是有概率的,即你换了也不一定成功,所以我们还要枚举申请的订单是否能成功。

也就是说,这是一个二进制枚举套二进制枚举。第一步,枚举申请换的教室;第二步,枚举申请的教室是否成功。

至于两点间的距离,注意到\(v<=300\),我们可以用\(floyd\)算法在\(O(v^3)\)的复杂度内完成最短路计算。

	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=e;i++)
	{
		int x=0,y=0,z=0;
		scanf("%d%d%d",&x,&y,&z);
		dis[x][y]=min(dis[x][y],z);
		dis[y][x]=min(dis[y][x],z);
	}
	for(int i=1;i<=v;i++) dis[0][i]=0,dis[i][i]=0;
	for(int k=1;k<=v;k++)
		for(int i=1;i<=v;i++)
			for(int j=1;j<=v;j++)
				dis[i][j]=min(dis[i][k]+dis[k][j],dis[i][j]);

至于二进制枚举,我们可以用一个简单的位运算技巧来简化代码。

		for(int j=0;j<n;j++)
			if((1<<j)&i) cnt++;

其中\(i\)是我们枚举的状态(压成十进制后的),满足\(if\)中的条件即证明\(j\)这位在\(i\)表示的二进制数下为1.

#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;

int n,m,v,e,fake,tot,pos;
int c[500],d[500],sta[500],fk[500],dis[500][500];
double ans=1e9,kk[500];

int main()
{
	scanf("%d%d%d%d",&n,&m,&v,&e);
	for(int i=1;i<=n;i++) scanf("%d",&c[i]);
	for(int i=1;i<=n;i++) scanf("%d",&d[i]);
	for(int i=1;i<=n;i++) scanf("%lf",&kk[i]);
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=e;i++)
	{
		int x=0,y=0,z=0;
		scanf("%d%d%d",&x,&y,&z);
		dis[x][y]=min(dis[x][y],z);
		dis[y][x]=min(dis[y][x],z);
	}
	for(int i=1;i<=v;i++) dis[0][i]=0,dis[i][i]=0;
	for(int k=1;k<=v;k++)
		for(int i=1;i<=v;i++)
			for(int j=1;j<=v;j++)
				dis[i][j]=min(dis[i][k]+dis[k][j],dis[i][j]);
	fake=(1<<n)-1;
	for(int i=0;i<=fake;i++)
	{
		int cnt=0;tot=0;pos=0;
		double re=0; 
		for(int j=0;j<n;j++)
			if((1<<j)&i) cnt++;
		//第一步 枚举申请换的教室 
		if(cnt>m) continue;
		for(int j=0;j<n;j++)
			if((1<<j)&i) sta[++tot]=j+1;
		//记录都是哪些教室申请换了 
		int res=(1<<tot)-1;
		//第二步,枚举申请的教室是否成功 
		for(int k=0;k<=res;k++)
		{
			for(int o=1;o<=n;o++) fk[o]=0;
			//清空标记 
			for(int o=0;o<tot;o++)
				if((1<<o)&k) fk[sta[o+1]]=1;
			//记录当前枚举的申请情况 
			int be=0,dist=0;
			double p=1;
			for(int qwq=1;qwq<=n;qwq++)
			{
				if(i&(1<<(qwq-1)))
				{//申请了 但是可能成功或没成功 
					int to=fk[qwq] ? d[qwq] : c[qwq];
					dist+=dis[be][to];
					be=to;
					double hu=fk[qwq] ? kk[qwq] : 1-kk[qwq];
					p*=hu;
				}
				else dist+=dis[be][c[qwq]],be=c[qwq];
			}
			re+=dist*p;
		}
		ans=min(ans,re);
	}
	printf("%.2lf",ans);
	return 0;
}

堪称二进制枚举的经典鸭

满分做法:

当然是\(dp\)辣hhh。

状态和转移感觉设计并不难。我们考虑设计这样一个状态:\(f[i][j][0]\)\(f[i][j][1]\)。其中\(f[i][j][0]\)表示当前是第\(i\)节课,之前包括现在共已申请了\(j\)个订单,当前没有申请订单。而\(f[i][j][1]\)即为当前申请了订单。

显然我们当前的状态是从之前的状态转移而来的,当前有换和不换两种选择(状态设计中),那么上一个状态即\(i-1\)也有换与不换两种选择。我们只要分别捋清楚就行了。只是需要注意的是:首先期望是相加的,因为概率的基本性质,互斥的事件可加。(我也布吉岛这么说是否准确\(qwq\))其次,只要存在申请订单的时刻,那么必然有申请成功和申请失败两种事件,因为申请订单是一个随机事件,成功不是必然事件,也不是不可能事件,于是我们需要分别算他们的期望,相加。

于是我们得到了十分冗杂的转移方程(虽然麻烦,但是明白上一点后非常清楚好想)

\(f[i][j][0]=min(f[i-1][j][0],f[i-1][j][1]+k[i-1]*dis[c[i]][d[i-1]]+(1-k[i-1])*dis[c[i]][c[i-1]])\)

\(f[i][j][1]=min(f[i-1][j-1][0]+k[i]*dis[d[i]][c[i-1]]+(1-k[i])*dis[c[i]][c[i-1]],f[i-1][j-1][1]+k[i]*k[i-1]*dis[d[i]][d[i-1]]+k[i]*(1-k[i-1])*dis[d[i]][c[i-1]]\)

最后我们找出最小的合法答案。

#include<cstdio>
#include<algorithm>
#include<cstring>

using namespace std;

int n,m,v,e;
int c[3000],d[3000],dis[500][500];
double ans=1e9,k[3000],f[2500][2500][3];

int main()
{
	scanf("%d%d%d%d",&n,&m,&v,&e);
	for(int i=1;i<=n;i++) scanf("%d",&c[i]);
	for(int i=1;i<=n;i++) scanf("%d",&d[i]);
	for(int i=1;i<=n;i++) scanf("%lf",&k[i]);
	memset(dis,0x3f,sizeof(dis));
	for(int i=1;i<=v;i++) dis[i][i]=0,dis[0][i]=0;
	for(int i=1;i<=e;i++)
	{
		int x=0,y=0,z=0;
		scanf("%d%d%d",&x,&y,&z);
		dis[x][y]=min(dis[x][y],z);
		dis[y][x]=min(dis[y][x],z);
	}
	for(int kk=1;kk<=v;kk++)
		for(int i=1;i<=v;i++)
			for(int j=1;j<=v;j++)
				dis[i][j]=min(dis[i][j],dis[i][kk]+dis[kk][j]);
	for(int i=1;i<=n;i++)
		for(int j=0;j<=m;j++)
			f[i][j][0]=f[i][j][1]=1e9;
	f[1][0][0]=0;
	f[1][1][1]=0;
	for(int i=2;i<=n;i++)
		for(int j=0;j<=m;j++)
		{
			f[i][j][0]=min(f[i-1][j][0]+dis[c[i]][c[i-1]],f[i-1][j][1]+k[i-1]*dis[c[i]][d[i-1]]+(1-k[i-1])*dis[c[i]][c[i-1]]);
			if(j==0) continue;
			f[i][j][1]=min(f[i-1][j-1][0]+k[i]*dis[d[i]][c[i-1]]+(1-k[i])*dis[c[i]][c[i-1]],f[i-1][j-1][1]+k[i]*k[i-1]*dis[d[i]][d[i-1]]+k[i]*(1-k[i-1])*dis[d[i]][c[i-1]]+(1-k[i])*k[i-1]*dis[c[i]][d[i-1]]+(1-k[i])*(1-k[i-1])*dis[c[i]][c[i-1]]);
		}
	for(int i=0;i<=m;i++)
		for(int j=0;j<=1;j++)
			ans=min(ans,f[n][i][j]);
	printf("%.2lf",ans);
	return 0;
}

另一些细节:

本题变量名极其容易搞混,因为太多了orz\(Vergil\)学长就是这样在考场上\(68pts->8pts\)。平时的点边习惯用\(n\)而这里是\(v\)

\(f\)数组的赋初值:\(f[1][0][0]=0\)\(f[1][1][1]=0\)

转移的时候注意边界。\(i\)从2开始,第二种转移在\(j=0\)时不能进行转移

题出的好!难度适中,覆盖知识点广,题目又着切合实际的背景,解法比较自然。给出题人点赞 !

posted @ 2018-10-27 17:56  cellur925&Chemist  阅读(172)  评论(0编辑  收藏  举报