[WC2018] 即时战略

[WC2018] 即时战略

题目背景

特别提示

在洛谷提交本题时的一些注意事项(与原题面不同之处请以此处为准):

  1. 提交时请在程序里加入以下函数声明语句:
int explore(int,int);
  1. 程序开头不用,也不应该包含 rts.h 头文件。
  2. 仅支持 C++(含 C++C++11C++14C++17)提交。

题目描述

小 M 在玩一个即时战略 (Real Time Strategy) 游戏。不同于大多数同类游戏,这个游戏的地图是树形的。也就是说,地图可以用一个由 \(n\) 个结点,\(n - 1\) 条边构成的连通图来表示。这些结点被编号为 \(1 \sim n\)

每个结点有两种可能的状态:「已知的」或「未知的」。游戏开始时,只有 \(1\) 号结点是已知的。
在游戏的过程中,小 M 可以尝试探索更多的结点。具体来说,小 M 每次操作时需要选择一个已知的结点 \(x\),和一个不同于 \(x\) 的任意结点 \(y\)(结点 \(y\) 可以是未知的)。然后游戏的自动寻路系统会给出 \(x\)\(y\) 的最短路径上的第二个结点 \(z\),也就是从 \(x\) 走到 \(y\) 的最短路径上与 \(x\) 相邻的结点。此时,如果结点 \(z\) 是未知的,小 M 会将它标记为已知的。

这个游戏的目标是:利用至多 \(T\) 次探索操作,让所有结点的状态都成为已知的。然而小 M 还是这个游戏的新手,她希望得到你的帮助。

为了让游戏过程更加容易,小 M 给你提供了这个游戏的交互库,具体见「任务描述」和「实现细节」。
另外,小 M 也提供了一些游戏的提示,具体见题目的最后一节「提示」。

任务介绍

你需要实现一个函数 play,以帮助小 M 完成游戏的目标。

  • play(n, T, dataType)
    • n 为树的结点个数;
    • T 为探索操作的次数限制;
    • dataType 为该测试点的数据类型,具体见「数据规模和约定」。

在每个测试点中,交互库都会调用恰好一次 play 函数。该函数被调用之前,游戏处于刚开始的状态。

你可以调用函数 explore 来帮助你在游戏中探索更多结点,但是这个函数的调用次数不能超过 \(T\) 次。

  • explore(x, y)
    • x 为一个已知的结点;
    • y 为一个不同于 \(x\) 的任意结点(可以不是已知的结点);
    • 这个函数会返回结点 \(x\)\(y\) 的最短路径上的第二个结点的编号。

在函数 play 返回之后,交互库会检查游戏的状态:只有当每个结点都是已知的,才算游戏的目标完成。

实现方法

你需要且只能提交一个源文件 rts.cpp/c/pas 实现上述函数,且遵循下面的命名和接口。

对 C/C++ 语言的选手:

源代码中需要包含头文件 rts.h请注意在洛谷提交时不用包含该头文件)。

你需要实现的函数 play

void play(int n, int T, int dataType);

函数 explore 的接口信息如下:

int explore(int x, int y);

对 Pascal 语言的选手:

你需要使用单元 graderhelperlib

你需要实现的函数 play

procedure play(n, T, dataType : longint);

函数 explore 的接口信息如下:

function explore(x, y : longint) : longint;

输入格式

第一行包含三个整数 \(n\), \(T\), \(\texttt{dataType}\),需要保证 $2 \leqslant n \leqslant 3 \times 10^5, $ \(1 \leqslant T \leqslant 5 \times 10^6,\) \(\texttt{dataType} = 1\)\(2\)\(3\)

接下来 \(n - 1\) 行,每行两个整数 \(u\), \(v\),需要保证 \(1 \leqslant u, v \leqslant n\)\(u \ne v\),表示一条 \(u\)\(v\) 之间的边。

你的输入需要保证这 \(n - 1\) 条边构成一棵树。

输出格式

交互库会判断游戏目标是否完成。如果完成,则会输出 \(\texttt{Correct}\),否则会输出相应的错误信息。

样例 #1

样例输入 #1

4 100 1
1 3
3 4
3 2

样例输出 #1

Correct

提示

样例解释

这是使用试题目录的 grader 和正确的源程序得到的输出文件。

对于此样例,一种可能的 explore 函数的调用顺序为:

  • explore(1, 2),返回 3
  • explore(3, 2),返回 2
  • explore(2, 4),返回 3
  • explore(3, 4),返回 4

如何测试你的程序

对 C/C++ 语言的选手:

你需要在本题目录下使用如下命令编译得到可执行程序:

g++ grader.cpp rts.cpp -o rts -O2

可执行文件将从标准输入读入数据。

读入完成之后,交互库将调用 play 函数。如果此时你调用 explore 的次数超过 \(T\) 次,则交互库会输出详细的错误信息,并退出。

接下来交互库会判断游戏目标是否完成。如果完成,则会输出 "\(\texttt{Correct}\)",否则会输出相应的错误信息。

如果传入 explore 函数的参数非法(\(x,y\) 不在 \(1\)\(n\) 的范围内,或 \(x\) 不是已知结点,或 \(x=y\)),那么交互库会输出详细的错误信息,并退出。

如果要使用自己的输入文件进行测试,请保证输入文件符合以上格式要求,否则不保证程序能正确运行。

如何使用样例源代码

本题目录下,有针对每种语言的样例源代码 rts_sample.cpp/c/pas。选择你所需的语言,将其复制为 rts.cpp/c/pas,按照上文中提到的方式进行编译,即能通过编译得到可执行程序。

对于非正式选手,你只能选择一种语言进行作答,即你本题的试题目录下不能同时存在多个语言的 rts.cpp/c/pas,否则系统将任选一份源代码进行评测并作为最终结果。

接下来你需要修改这个文件的实现,以达到题目的要求。

子任务

一共有 20 个测试点,每个测试点 5 分。

对于所有测试点,以及对于所有样例,$2 \leqslant n \leqslant 3 \times 10^5, $ \(1 \leqslant T \leqslant 5 \times 10^6,\) \(\texttt{dataType} = 1\)\(2\)\(3\)
不同 \(\texttt{dataType}\) 对应的数据类型如下:

  • 对于 \(\texttt{dataType} = 1\) 的测试点,没有特殊限制。
  • 对于 \(\texttt{dataType} = 2\) 的测试点,游戏的地图是一棵以结点 \(1\) 为根的完全二叉树,
    即,存在一个 \(1\) ~ \(n\) 的排列 \(a\),满足 \(a_1 = 1\),且结点 \(a_i ~ (1 < i \leqslant n)\) 与结点 \(a_{\lfloor i/2 \rfloor}\) 之间有一条边相连。
  • 对于 \(\texttt{dataType} = 3\) 的测试点,游戏的地图是一条链,
    即,存在一个 \(1\) ~ \(n\) 的排列 \(a\),满足结点 \(a_i ~ (1 < i \leqslant n)\) 与结点 \(a_{i-1}\) 之间有一条边相连。

对于每个测试点,\(n, T, \texttt{dataType}\) 的取值如下表:

测试点编号 \(n =\) \(T =\) \(\texttt{dataType} =\)
1 \(2\) \(10000\) 1
2 \(3\) \(10000\) 1
3 \(10\) \(10000\) 1
4 \(100\) \(10000\) 1
5 \(1000\) \(10000\) 2
6 \(20000\) \(300000\) 2
7 \(250000\) \(5000000\) 2
8 \(1000\) \(20000\) 3
9 \(5000\) \(15500\) 3
10 \(30000\) \(63000\) 3
11 \(150000\) \(165000\) 3
12 \(250000\) \(250100\) 3
13 \(300000\) \(300020\) 3
14 \(1000\) \(50000\) 1
15 \(5000\) \(200000\) 1
16 \(30000\) \(900000\) 1
17 \(150000\) \(3750000\) 1
18 \(200000\) \(4400000\) 1
19 \(250000\) \(5000000\) 1
20 \(300000\) \(5000000\) 1

提示

这里是小 M 给你的一些贴心的提示:

  • 图(无向图)由结点和边构成,边是结点的无序对,用来描述结点之间的相互关系;
  • 路径是一个结点的非空序列,使得序列中相邻两个结点之间都有边相连;
  • 两个结点是连通的,当且仅当存在一条以其中一个结点开始、另一个结点结束的路径;
  • 一个图是连通的,当且仅当这个图上的每对结点都是连通的;
  • 一棵 \(n\) 个结点的树,是一个由 \(n\) 个结点,\(n - 1\) 条边构成的连通图;
  • 两个结点的最短路径,是指连接两个结点的所有可能的路径中,序列长度最小的;
  • 在一棵树中,连接任意两个结点的最短路径,都是唯一的;
  • 通过访问输入输出文件、攻击评测系统或攻击评测库等方式得分属于作弊行为,所得分数无效。

看数据范围容易发现是要特判链的。

然后发现你并不会 \(2n\) 的询问次数。

当我们扩展的时候,至少 \(n\) 次询问。做法大概是找一个点,然后一次操作找到他是在链的左边还是右边,在从那个方向扩展出这个点

但是如果我们找点是随机的,期望只要找 \(log n\) 个点就可以扩展出整个链,询问次数是 \(n+log n\) 的。

然后看正常的树的情况。先看特殊性质二叉树,我们要找到离这个点最近的目前已经构造出来的树上的那个点。如果是完全二叉树,我们一层层走,只要 \(log n\) 次就可以找到。

那一般情况呢?我们可以沿着点分树去找,询问次数也是 \(O(nlogn)\) 的。我们就解决了询问次数的问题

但是我们不可能每一次都重构点分树?

定期重构?要卡常。

考虑用类似替罪羊树的思想,当点分树上某一个点超过他父亲的 \(\alpha\) 时重构点分树上他父亲的子树。\(\alpha\)\(0.75\) 是比较好的。

#include<bits/stdc++.h>
// #include "rts.h"
using namespace std;
const int N=3e5+5;
mt19937 gen(time(0));
int explore(int,int);
int vs[N],b[N],sz[N],fa[N],cnt,mn,rt,rrt;//a:原树到点分树 b:点分树到原树 
unordered_map<int,int>a[N];
vector<int>g[N];
struct lian{
	int st[N],c,fir,sec,x;
	vector<int>g[N];
	void doit(int x,int y)
	{
		int ls=x;
		while(ls^y)
		{
			int k=explore(ls,y);
			vs[k]=1,++c;
			g[ls].push_back(k),g[k].push_back(ls);
			ls=k;
		}
	}
	void play(int n)
	{
		int tp=0;
		vs[1]=c=1;
		doit(1,2);
		fir=2,sec=1; 
		while(c^n)
		{
			tp=0;
			for(int i=1;i<=n;i++)
				if(!vs[i])
					st[++tp]=i;
			int k=gen()%tp+1;
			int x=explore(fir,st[k]);
			if(x^g[fir][0])
			{
				vs[x]=1,++c;
				g[fir].push_back(x);
				g[x].push_back(fir);
				doit(x,st[k]),fir=st[k];
			}
			else
				doit(sec,st[k]),sec=st[k];
		}
	}
}p;
pair<int,int> find(int x,int s)
{
	int k=explore(x,s);
	if(!vs[k])
		return make_pair(x,k);
	return find(a[x][k],s);
}
void fnd(int x,int y,int all)
{
	sz[x]=1;
	int mx=0;
	for(int v:g[x])
	{
		if(v^y&&!vs[v])
		{
			fnd(v,x,all);
			sz[x]+=sz[v];
			mx=max(mx,sz[v]);
		}
	}
	mx=max(mx,all-sz[x]);
	if(mx<mn)
		mn=mx,rrt=x;
	assert(sz[x]);
}
int findrt(int x,int n)
{
	mn=2000000000,rrt=0;
	fnd(x,0,n);
	return rrt;
}
void sou(int x)
{
	vs[x]=0,++cnt;
	for(int v:g[x])
		if(fa[a[x][v]]==x)
			sou(a[x][v]);
}
void getsz(int x,int y)
{
	sz[x]=1;
	for(int v:g[x])
		if(!vs[v]&&v^y)
			getsz(v,x),sz[x]+=sz[v];
}
void solve(int x)
{
	getsz(x,0);
	vs[x]=1;
	for(int v:g[x])
	{
		if(!vs[v])
		{
			int k=findrt(v,sz[v]);
			solve(k);
			a[x][v]=k,b[k]=v;
			fa[k]=x;
			a[v][x]=0;
		}
	}
}
int rebuild(int x)
{
	cnt=0;
	sou(x);
	int k;
	solve(k=findrt(x,cnt));
	return k;
}
void add(int x,int y)
{
	a[x][y]=b[y]=y;
	fa[y]=x;
	vs[y]=1;
	int z=y,p=0;
	while(z)
		sz[z]++,z=fa[z];
	z=y;
	while(z^rt)
	{
		if(sz[z]>sz[fa[z]]*0.9)
			p=fa[z];
		z=fa[z];
	}
	g[x].push_back(y),g[y].push_back(x);
	if(p)
	{
		if(p==rt)
			fa[rt=rebuild(p)]=0;
		else
		{
			int q=b[p],f=fa[p],k=rebuild(p); 
			a[f][q]=k,b[k]=q;
			fa[k]=f;
		}
	}
	assert(rt);
}
void play(int n, int T, int dataType) {
	if(dataType==3)
		p.play(n); 
	else
	{
		vs[1]=1;
		b[1]=rt=1;
		for(int i=2;i<=n;i++)
		{
			if(vs[i])
				continue;
			pair<int,int> k=find(rt,i);
			while(k.second^i)
			{
				vs[k.second]=1;
				add(k.first,k.second); 
				k=make_pair(k.second,explore(k.second,i));
			}
			add(k.first,k.second);
		}
	}
}
posted @ 2023-10-30 20:46  灰鲭鲨  阅读(17)  评论(0编辑  收藏  举报