[NOI2008]赛程安排(提交答案+模拟退火)

第一道提交答案题

题面

随着奥运的来临,同学们对体育的热情日益高涨。在 ION2008 来临之际,
学校正在策划组织一场乒乓球赛。小 Z 作为一名狂热的乒乓球爱好者,这正是
他大展身手的好机会,于是他摩拳擦掌,积极报名参赛。
本次乒乓球赛采取淘汰赛制,获胜者晋级。恰好有 nnn (nnn222 的整数次幂,不
妨设 n=2kn = 2^kn=2k)个同学报名参加,因此第一轮后就会有 2k−12k-12k1 个同学惨遭淘汰,另外 2k−12k-12k1个同学晋级下一轮;第二轮后有 2k−22k-22k2 名同学晋级下一轮,… 依次类推,直到 kkk
轮后决出冠亚军:具体的,每个人都有一个 111~nnn 的初始编号,其中小 Z 编号为 111
所有同学的编号都不同,他们将被分配到 nnn 个位置中,然后按照类似下图的赛程
进行比赛:
1.1
为了吸引更多的同学参加比赛,本次比赛的奖金非常丰厚。在第 i 轮被淘汰
的选手将得到奖金 aia_iai元,而冠军将获得最高奖金 ak+1a_{k+1}ak+1 元。显然奖金应满足 a1&lt;a2&lt;…&lt;ak+1a1 &lt; a_2&lt; … &lt; a_{k+1}a1<a2<<ak+1.
在正式比赛前的热身赛中,小 Z 连连败北。经过认真分析之后,他发现主要的失败原因不是他的球技问题,而是赢他的这几个同学在球风上刚好对他构成相克的关系,所以一经交手,他自然败阵。小 Z 思索:如果在正式比赛中能够避开这几位同学,该有多好啊!
假设已知选手两两之间交手的胜率,即选手 A 战胜选手 B 的概率为 PA,BP_{A,B}PA,B (保
PA,B+PB,A=1P_{A,B} + P_{B,A}=1PA,B+PB,A=1)。于是小 Z 希望能够通过确定比赛的对阵形势(重新给每个选手安排位置),从而能够使得他获得尽可能多的奖金。你能帮助小 Z 安排一个方案,
使得他这场比赛期望获得的奖金最高么?

评分

每个测试点单独评分。
对于每一个测试点,如果你的输出文件不合法,如文件格式错误、输出解不
符合要求等,该测试点得 000 分。否则如果你的输出的期望奖金为 youransyour_{ans}yourans,参考
期望奖金为 ouransour_{ans}ourans,我们还设有一个用于评分的参数 ddd,你在该测试点中的得
分如下:
如果 yourans&gt;ouransyour_{ans} &gt; our_{ans}yourans>ourans,得 121212 分。
如果 yourans&lt;ourans∗dyour_{ans} &lt; our_{ans}*dyourans<ouransd,得 111 分。
否则得分为:
⌊yourans−ourans∗dourans−ourans∗d⌋+2\lfloor\frac{your_{ans}-our_{ans}*d}{our_{ans}-our_{ans}*d}\rfloor+2ouransouransdyouransouransd+2

题解

下面提供几种做法

乱搞1

我会输入输出!
每个测试点输出111~nnn的排列。
期望得分:1∗10=101*10=10110=10

乱搞2

我会RandomRandomRandom shuffleshuffleshuffle
输出111~nnn的随机排列。
期望得分:≥10≥1010

乱搞3

我会玩checkercheckerchecker
手动调用checker.exechecker.exechecker.exe计算期望。
期望得分:?????????

checker

显然,在做这道题之前,我们必须先写一个复杂度优秀的checkercheckerchecker程序。
也就是计算一个排列期望的函数。
考虑借用分治的思想。
Pw[p][k]Pw[p][k]Pw[p][k]为第ppp位选手赢得第kkk场比赛的概率。
显然Pw[p][k]Pw[p][k]Pw[p][k]可以转移为
Pw[p][k]=∑j=lrPw[p][k−1]∗Pw[j][k−1]∗P[p][j]Pw[p][k]=\sum^{r}_{j=l} Pw[p][k-1]*Pw[j][k-1]*P[p][j]Pw[p][k]=j=lrPw[p][k1]Pw[j][k1]P[p][j]
lll,rrr的含义详见checker。
那么期望的计算也十分简单了~
分析一下复杂度
T(n)=2T(n2)+O(n2)T(n)=2T(\frac{n}{2})+O(n^2)T(n)=2T(2n)+O(n2)
因此复杂度为:O(n2logn)O(n^2logn)O(n2logn)

void check(int L,int R,int dep)
{
	if(L==R){Pw[sA[L]][dep]=1;return ;}
	int mid=(L+R)/2;
	check(L,mid,dep+1);
	check(mid+1,R,dep+1);
	for(int i=L;i<=R;i++)
		Pw[sA[i]][dep]=0;
	for(int i=L;i<=mid;i++)
		for(int j=mid+1;j<=R;j++)
		{
			Pw[sA[i]][dep]+=Pw[sA[i]][dep+1]*Pw[sA[j]][dep+1]*P[sA[i]][sA[j]];
			Pw[sA[j]][dep]+=Pw[sA[i]][dep+1]*Pw[sA[j]][dep+1]*P[sA[j]][sA[i]];
		}
	if(L==1)sdist+=(Pw[1][dep+1]-Pw[1][dep])*val[dep];
}

子任务1-2

难度:1
好了,进入正题。

k=3k=3k=3,数据随机。
T1 p=0.1p=0.1p=0.1[明显送分啊qwq] T2 p=0.95p=0.95p=0.95

此时n=8n=8n=8,显然直接枚举排列每次checkcheckcheck一下,取最优解即可。
复杂度O(n!n2logn)O(n!n^2logn)O(n!n2logn)

子任务3-5

难度:2

k=4k=4k=4,数据随机。
T3 p=0.1p=0.1p=0.1[送分] T4 p=0.9p=0.9p=0.9 T5 p=0.98p=0.98p=0.98

此时n=16n=16n=16,考虑使用随机算法。
我们可以用RandomRandomRandom shuffleshuffleshuffle来随机生成一个排列再进行checkcheckcheck
然而这样并不能找到最优解。
考虑使用模拟退火算法。
初始解为111~nnn的排列。
每次交换两个数字并进行计算。
若优于当前解则直接接受这种方案,否则以一定的概率接受这种方案[可以先学一下模拟退火算法]。
然而,即使如此,这个算法也需要10−50010-50010500次模拟退火才能找到最优解。
但已足以解决这5个子任务了。

1-5总结

以上5个子任务属于常规搜索算法技巧。
并没有用到任何优化,只要能够顺利实现便可以轻松拿到555555分。

子任务6-7

难度:3

T6 k=6k=6k=6,T7 k=8k=8k=8,数据有明显规律
T6 p=0.99p=0.99p=0.99 T7 p=0.6p=0.6p=0.6

数据规模明显增大,但翻看数据会发现概率只有000,0.50.50.5,1.01.01.0三种情况。
我们先来研究第六个子任务。
2.1
如上图
再仔细观察会发现1的个数较少,我们可以先统计出每个选手对其他选手必胜的个数。
然后是这样的:

0个:16人
1个:8人
2个:4人
3个:2人
4个:1人
5个:1人

规律十分明显,我们可以猜测一下这组数据的意图。
我们或许可以构造出一组排列,使得每次比赛都有一个人必胜?
考虑使用遍历树的方法来遍历它。
将每个人看成一个节点,定义它的sizesizesize为他一定能够打败的总人数。
对于一个人iii,若他一定能够打败某人,就将他与iii号节点连边。
我们先从111号节点开始遍历,先访问sizesizesize最小的对手,并递归它的儿子。
那么答案就是这颗树的DFSDFSDFS序。
解决了子任务6,那么子任务7就简单多了。
按子任务6的方式统计一下:

0个:64人
1个:32人
2个:16人
3个:8人
4个:4人
5个:4人

也同样很有规律,但注意到sizesizesize为5的人有4个,这意味着我们无法保证编号为111的人必胜,但是,他能进前4强是必然的。
我们可以找到这4个人的编号,分别为1,65,129,193
由于我们要保证1的期望最大,所以一定要从1开始遍历。
后面的3个节点任意顺序遍历都可以,我们可以枚举顺序取最优解。
经蒟蒻枚举,这个顺序为1,193,129,65
以下是参考代码:

//针对测试点#6,7
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<ctime>
#include<cstring>
#include<vector>
using namespace std;
#define MAXN 256
#define MAXD 8
#define DB long double
#define INF 10e18
#define SE second
int n,k;
int A[MAXN+5],sA[MAXN+5],val[MAXN+5],best[MAXN+5];
int sx,sy,cnt[MAXN+5],pcnt[MAXN+5],vis[MAXN+5];
DB T,ans,st,sdist,P[MAXN+5][MAXN+5],Pw[MAXN+5][MAXD+1];
vector< pair<int,int> > G[MAXN+5];
int num;
int read()  
{  
    int x=0,f=1;char s=getchar();  
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}  
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}  
    return x*f;  
}
void check(int L,int R,int dep)
{
	if(L==R){Pw[sA[L]][dep]=1;return ;}
	int mid=(L+R)/2;
	check(L,mid,dep+1);
	check(mid+1,R,dep+1);
	for(int i=L;i<=R;i++)
		Pw[sA[i]][dep]=0;
	for(int i=L;i<=mid;i++)
		for(int j=mid+1;j<=R;j++)
		{
			Pw[sA[i]][dep]+=Pw[sA[i]][dep+1]*Pw[sA[j]][dep+1]*P[sA[i]][sA[j]];
			Pw[sA[j]][dep]+=Pw[sA[i]][dep+1]*Pw[sA[j]][dep+1]*P[sA[j]][sA[i]];
		}
	if(L==1)sdist+=(Pw[1][dep+1]-Pw[1][dep])*val[dep];
}
void slove(int x)
{
	if(vis[x])return ;
	vis[x]=1;
	A[++num]=x;
	for(int i=0;i<G[x].size();i++)
	{
		int xnt=G[x][i].SE;
		slove(xnt);
	}
}
int main()
{
	freopen("match6.in","r",stdin);
	freopen("match6.out","w",stdout);
	srand(time(NULL));
	n=read();
	while(n>(1<<k))k++;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
		{
			cin>>P[i][j];
			if(P[i][j]==1)cnt[i]++;
		}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
		if(P[i][j]==1)G[i].push_back(make_pair(cnt[j],j));
	for(int i=1;i<=n;i++)
	sort(G[i].begin(),G[i].end());
	slove(1);
	/*slove(193);
	slove(65);//for #7
	slove(129);*/
	/*for(int i=1;i<=n;i++)
	{printf("%d ",cnt[i]);pcnt[cnt[i]]++;}
	printf("\n");
	for(int i=1;i<=k;i++)printf("%d ",pcnt[i]);
	printf("\n");
	for(int i=1;i<=n;i++)if(cnt[i]==k-2)printf("%d ",i);
	printf("\n");//打表专用 */ 
	for(int i=k;i>=0;i--)
		scanf("%d",&val[i]);
	for(int i=1;i<=n;i++)sA[i]=A[i];
	sdist=0;
	check(1,n,1);
	sdist+=Pw[1][1]*val[0];
	ans=sdist;
	int P=1000;
	//cout<<ans<<endl;
	for(int i=1;i<=n;i++)
	printf("%d\n",A[i]);
}

子任务8-9

难度:2

k=7k=7k=7,T8 数据有明显规律,T9 数据有规律
T8 p=0.9p=0.9p=0.9 T9 p=0.9p=0.9p=0.9

其实这两个子任务的规律比两个子任务明显的多。
先来看看子任务8:
在这里插入图片描述
极易发现数据每行从上往下单调递减,同时,数据每列从左往右单调递增。
十分容易想出一种贪心的方法,即:一号选手在第一个位置,其余选手按编号从大往小排列。
显然这种方法算出的方案是最优的。
再先来看看子任务9:
2.3
感觉很乱?
确实,但这组数据还是有它的规律。
我们单独对行与行,列与列之间两两进行分析,可以发现

ai,1&gt;aj,1a_{i,1}&gt;a_{j,1}ai,1>aj,1则对于 ∀p∈[1,n]\forall p\in[1,n]p[1,n]ai,p&gt;aj,pa_{i,p}&gt;a_{j,p}ai,p>aj,p
a1,i&gt;a1,ja_{1,i}&gt;a_{1,j}a1,i>a1,j则对于 ∀p∈[1,n]\forall p\in[1,n]p[1,n]ap,i&gt;ap,ja_{p,i}&gt;a_{p,j}ap,i>ap,j
反之亦然

那我们能得到什么呢?
对比子任务8,发现它也满足这样一个条件。
综上,我们可以看出,其实9仅仅只是将8的数据编号打乱了而已。
仅仅需要排序,再重新标号即可。
这里仅提供子任务8的代码

//针对测试点#8
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<ctime>
#include<cstring>
#include<vector>
using namespace std;
#define MAXN 256
#define MAXD 8
#define DB long double
#define INF 10e18
#define SE second
int n,k;
int A[MAXN+5],sA[MAXN+5],val[MAXN+5],best[MAXN+5];
int sx,sy,cnt[MAXN+5];
DB T,ans,st,sdist,P[MAXN+5][MAXN+5],Pw[MAXN+5][MAXD+1];
vector< pair<int,int> > G[MAXN+5];
int num;
int read()  
{  
    int x=0,f=1;char s=getchar();  
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}  
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}  
    return x*f;  
}
void check(int L,int R,int dep)
{
	if(L==R){Pw[sA[L]][dep]=1;return ;}
	int mid=(L+R)/2;
	check(L,mid,dep+1);
	check(mid+1,R,dep+1);
	for(int i=L;i<=R;i++)
		Pw[sA[i]][dep]=0;
	for(int i=L;i<=mid;i++)
		for(int j=mid+1;j<=R;j++)
		{
			Pw[sA[i]][dep]+=Pw[sA[i]][dep+1]*Pw[sA[j]][dep+1]*P[sA[i]][sA[j]];
			Pw[sA[j]][dep]+=Pw[sA[i]][dep+1]*Pw[sA[j]][dep+1]*P[sA[j]][sA[i]];
		}
	if(L==1)sdist+=(Pw[1][dep+1]-Pw[1][dep])*val[dep];
}
int main()
{
	freopen("match8.in","r",stdin);
	freopen("match8.out","w",stdout);
	srand(time(NULL));
	n=read();
	while(n>(1<<k))k++;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			cin>>P[i][j];
	A[1]=1;
	for(int i=2;i<=n;i++)
	A[n-i+2]=i;
	for(int i=k;i>=0;i--)
		scanf("%d",&val[i]);
	for(int i=1;i<=n;i++)sA[i]=A[i];
	sdist=0;
	check(1,n,1);
	sdist+=Pw[1][1]*val[0];
	ans=sdist;
	int P=1000;
	//cout<<ans<<endl;
	for(int i=1;i<=n;i++)
	printf("%d\n",A[i]);
}

6-9总结

这类子任务对选手的观察能力要求较高,但对于熟练掌握提答技巧的选手来说较为简单。
其核心在于找到该组数据的特殊性质。

子任务10

难度:3

k=7k=7k=7,数据随机
p=0.85p=0.85p=0.85

没错,这就是阻止你AK脚步的罪魁祸首。
即使出题人如此毒瘤,但我们仍然能够想办法让自己的答案在这个数据点上得到高分。
怎么做呢?
回顾一下之前我们用于子任务3-5的模拟退火算法。
在之前的算法中,我们的初值设为111~nnn的排列,而每次仅交换两个数。
是不是不够“随机”呢?
考虑将初值RandomRandomRandom shuffleshuffleshuffle一下,每次随机交换多个数。[当然,也不能交换多了,不然就失掉了原来最优解的性质]
这样的话,找到最优解的概率就大大提升了。
据测试,这样做可以直接通过子任务8-9,子任务10可以获得777分及以上的高分,在提答中已经算是很高的分数了。
如果你有时间的话,可以尝试将程序跑上一天,或许就能得到101010分甚至121212分的分数了。[ps:这道题仅有最后一个数据点有得到12分的可能]

//针对测试点1-5以及测试点10的部分分 
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<ctime>
#include<cstring>
using namespace std;
#define MAXN 256
#define MAXD 8
#define DB long double
#define INF 10e18
int n,k;
int A[MAXN+5],sA[MAXN+5],val[MAXN+5],best[MAXN+5];
int sx,sy;
DB T,ans,st,sdist,P[MAXN+5][MAXN+5],Pw[MAXN+5][MAXD+1];
int read()  
{  
    int x=0,f=1;char s=getchar();  
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}  
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}  
    return x*f;  
}
void check(int L,int R,int dep)
{
	if(L==R){Pw[sA[L]][dep]=1;return ;}
	int mid=(L+R)/2;
	check(L,mid,dep+1);
	check(mid+1,R,dep+1);
	for(int i=L;i<=R;i++)
		Pw[sA[i]][dep]=0;
	for(int i=L;i<=mid;i++)
		for(int j=mid+1;j<=R;j++)
		{
			Pw[sA[i]][dep]+=Pw[sA[i]][dep+1]*Pw[sA[j]][dep+1]*P[sA[i]][sA[j]];
			Pw[sA[j]][dep]+=Pw[sA[i]][dep+1]*Pw[sA[j]][dep+1]*P[sA[j]][sA[i]];
		}
	if(L==1)sdist+=(Pw[1][dep+1]-Pw[1][dep])*val[dep];
}
int main()
{
	freopen("match10.in","r",stdin);
	//freopen("match10.out","w",stdout);
	srand(time(NULL));
	n=read();
	while(n>(1<<k))k++;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
		cin>>P[i][j];
	for(int i=k;i>=0;i--)
		scanf("%d",&val[i]);
	ans=0;
	int P=100;
	for(int i=1;i<=n;i++)A[i]=i;
	random_shuffle(A+1,A+n+1);
	while(P--)
	{
		T=10000000000;
		while(T>0.01)
		{
			for(int i=1;i<=n;i++)sA[i]=A[i];
			do
			{
				do{
				sx=rand()%(n-1)+2,sy=rand()%(n-1)+2;
				}while(sx==sy||sx==sy+1||sx+1==sy);
				swap(sA[sx],sA[sy]);
			}while(rand()%2);
			sdist=0;
			check(1,n,1);
			sdist+=Pw[1][1]*val[0];
			if(sdist>st)
			{
				st=sdist;for(int i=1;i<=n;i++)A[i]=sA[i];
				if(sdist>ans){ans=sdist;for(int i=1;i<=n;i++)best[i]=sA[i];}
			}
			else if(exp((st-sdist)/T)<(rand()%100000)/100000.0)
			{st=sdist;for(int i=1;i<=n;i++)A[i]=sA[i];}
			T*=0.99;
		}
	}
	cout<<ans<<endl;
	for(int i=1;i<=n;i++)
	printf("%d\n",best[i]);
}

总结

总的来说,这道题算NOI中提交答案类型题目的简单题。
掌握随机化技巧,寻找数据特殊性质是关键。

转载于:https://www.cnblogs.com/Panda-hu/p/11145751.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值