@description@
小M在玩一个即时战略(Real Time Strategy)游戏。不同于大多数同类游戏,这个游戏的地图是树形的。也就是说,地图可以用一个由 n 个结点,n−1 条边构成的连通图来表示。这些结点被编号为 1 ~ 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 返回之后,交互库会检查游戏的状态:只有当每个结点都是已知的,才算游戏的目标完成。
提示
这里是小M给你的一些贴心的提示:
(1)图(无向图)由结点和边构成,边是结点的无序对,用来描述结点之间的相互关系
(2)路径是一个结点的非空序列,使得序列中相邻两个结点之间都有边相连
(3)两个结点是连通的,当且仅当存在一条以其中一个结点开始、另一个结点结束的路径
(4)一个图是连通的,当且仅当这个图上的每对结点都是连通的
(5)一棵 n 个结点的树,是一个由 n 个结点,n−1 条边构成的连通图
(6)两个结点的最短路径,是指连接两个结点的所有可能的路径中,序列长度最小的
(7)在一棵树中,连接任意两个结点的最短路径,都是唯一的
(8)通过访问输入输出文件、攻击评测系统或攻击评测库等方式得分属于作弊行为,所得分数无效。
限制与约定
一共有 20 个测试点,每个测试点 5 分。
对于所有测试点,以及对于所有样例,2≤n≤3×10^5,1≤T≤5×10^6,1≤dataType≤3。 不同 dataType 对应的数据类型如下:
对于 dataType=1 的测试点,没有特殊限制。
对于 dataType=2 的测试点,游戏的地图是一棵以结点 1 为根的完全二叉树, 即,存在一个 1 ~ n 的排列 a,满足 a1=1,且结点 ai (1<i≤n) 与结点 a⌊i/2⌋ 之间有一条边相连。
对于 dataType=3 的测试点,游戏的地图是一条链, 即,存在一个 1 ~ n 的排列 a,满足结点 ai (1<i≤n) 与结点 ai−1 之间有一条边相连。
对于每个测试点,n,T,dataType 的取值如下表:
测试点编号 | n | T | dataType |
---|---|---|---|
1 | 2 | 10000 | 1 |
2 | 3 | 10000 | |
3 | 10 | 10000 | |
4 | 100 | 10000 | |
5 | 1000 | 10000 | 2 |
6 | 20000 | 300000 | |
7 | 250000 | 5000000 | |
8 | 1000 | 20000 | 3 |
9 | 5000 | 15500 | |
10 | 30000 | 63000 | |
11 | 150000 | 165000 | |
12 | 250000 | 250100 | |
13 | 300000 | 300020 | |
14 | 1000 | 50000 | 1 |
15 | 5000 | 200000 | |
16 | 30000 | 900000 | |
17 | 150000 | 3750000 | |
18 | 200000 | 4400000 | |
19 | 250000 | 5000000 | |
20 | 300000 | 5000000 |
@solution@
第一道自己切出来的 WC 题,开心~
(然而还是调试了非常久)
首先明显发现数据大概分为四个板块:
第一板块:n^2 <= T。每次 O(n) 暴力地找出一个新点,就可以 O(n^2) 找出所有点。
第二板块:dataType = 2,T 大概是 O(nlogn) 的范围。
采用分治,每次把点集分到当前块的左右子树。递归即可。
根据完全二叉树性质,深度 <= log,直接搞即可。
第三板块:dataType = 3,T 大概是 n + logn 的范围。
如果是正常的做法,我们对于每个点,需要判断是在当前已探明的这条链的左边还是右边。
也就是说最坏情况我们需要 2*n 次操作才能探明整条链。
(但是经过今年 NOI 的交互题,我学聪明了!不会做就随机化!)
假如当前已探明的这条链为 [l, r],不妨设 y 在 l 的左边,则我们可以直接探清 y ~ l 的路径上的所有点,而不需要错误试探。
假如我们的 y 是随机选取的,那么 y ~ l 这条路径上的点数期望最坏为 n/4(n 为当前未探明的点数)。
于是我们期望试探 log 次就可以探明整条链。
第四板块:dataType = 1,T 大概是 O(nlogn) 的范围。
怎么利用这 log 呢?我们不妨猜测是找到一个点需要 log 次。于是联想到分治。
假如只插一个点,我们可以使用点分治。每次 explore 要么告诉新插入的点在哪一颗子树,要么告诉新插入的点连向重心。
那么现在插入多个点怎么办?于是就可以动态维护点分树。用替罪羊的思想,设置一个平衡因子 α。如果一棵子树的占比 > α 不平衡了就暴力拍扁重构。
插入一个点,就直接接到它相邻的点的下面。
然后从这个点开始往上爬,找到最高的那个不平衡的结点 x,重构 x 所在的子树。
实现的时候有一个小细节:点分树的结构适合自下而上爬,而不适合自上而下找。
即每一次 explore 找到的是与重心相邻的点,而不是它的下一层重心。
这个时候怎么办呢?由于点分树高度不是很高,于是你暴力在点分树上爬,爬到那一层就好了。
@accepted code@
#include "rts.h"
#include <vector>
#include <cstdlib>
#include <iostream>
#include <algorithm>
using namespace std;
const double B = 0.78;
const int MAXN = 300000;
int vis[MAXN + 5];
void solve1(int n) {
vis[1] = 1;
for(int i=1;i<=n;i++) {
int f;
for(int j=1;j<=n;j++)
if( vis[j] == 1 ) f = j;
vis[f] = -1;
for(int j=1;j<=n;j++)
if( vis[j] == 0 ) {
int p = explore(f, j);
if( vis[p] == 0 ) vis[p] = 1;
}
}
}
void solve(const vector<int>&vec, int x) {
if( vec.empty() ) return ;
vector<int>l, r;
int a = -1, b = -1;
for(int i=0;i<vec.size();i++) {
if( vec[i] != x ) {
int p = explore(x, vec[i]);
if( a == -1 ) a = p;
else if( a != p && b == -1 ) b = p;
if( p == a ) l.push_back(vec[i]);
else r.push_back(vec[i]);
}
}
solve(l, a), solve(r, b);
}
void solve2(int n) {
vector<int>vec;
for(int i=1;i<=n;i++)
vec.push_back(i);
solve(vec, 1);
}
int a[MAXN + 5];
void solve3(int n) {
for(int i=1;i<=n;i++)
a[i] = i;
random_shuffle(a + 1, a + n + 1);
int l = 1, r = 1;
vis[1] = true;
for(int i=1;i<=n;i++) {
if( vis[a[i]] ) continue;
int p = explore(l, a[i]);
if( vis[p] ) {
p = explore(r, a[i]);
while( true ) {
vis[p] = true;
if( p == a[i] ) break;
p = explore(p, a[i]);
}
r = a[i];
}
else {
while( true ) {
vis[p] = true;
if( p == a[i] ) break;
p = explore(p, a[i]);
}
l = a[i];
}
}
}
struct edge{
int to; edge *nxt;
}edges[2*MAXN + 5], *adj[MAXN + 5], *ecnt=&edges[0];
int dep[MAXN + 5], fa[MAXN + 5];
void addedge(int u, int v) {
edge *p = (++ecnt);
p->to = v, p->nxt = adj[u], adj[u] = p;
p = (++ecnt);
p->to = u, p->nxt = adj[v], adj[v] = p;
}
bool tag[MAXN + 5];
void get_tag(int x, int d) {
tag[x] = true;
for(edge *p=adj[x];p;p=p->nxt) {
if( dep[p->to] < d || tag[p->to] ) continue;
get_tag(p->to, d);
}
}
int siz[MAXN + 5];
int get_siz(int x, int f) {
siz[x] = 1;
for(edge *p=adj[x];p;p=p->nxt) {
if( !tag[p->to] || p->to == f ) continue;
siz[x] += get_siz(p->to, x);
}
return siz[x];
}
int hvy[MAXN + 5];
int get_G(int x, int tot, int f) {
int ret = -1; hvy[x] = (tot - siz[x]);
for(edge *p=adj[x];p;p=p->nxt) {
if( !tag[p->to] || p->to == f ) continue;
int k = get_G(p->to, tot, x);
if( ret == -1 || hvy[k] < hvy[ret] )
ret = k;
hvy[x] = max(hvy[x], siz[p->to]);
}
if( ret == -1 || hvy[x] < hvy[ret] )
ret = x;
return ret;
}
void push(const int &x, int y, int f) {
for(edge *p=adj[y];p;p=p->nxt) {
if( !tag[p->to] || p->to == f ) continue;
push(x, p->to, y);
}
}
int divide(int t, int x, int d) {
tag[x] = false, dep[x] = d, siz[x] = t;
push(x, x, 0);
for(edge *p=adj[x];p;p=p->nxt) {
if( !tag[p->to] ) continue;
int tot = get_siz(p->to, x);
fa[divide(tot, get_G(p->to, tot, x), d + 1)] = x;
}
return x;
}
int rebuild(int x, int d) {
get_tag(x, d);
int tot = get_siz(x, 0);
return divide(tot, get_G(x, tot, 0), d);
}
int G;
void add_edge(int f, int x) {
dep[x] = dep[f] + 1, fa[x] = f, addedge(f, x), vis[x] = true;
for(int p=x;p;p=fa[p]) siz[p]++;
int p = -1, q = x;
while( q != G ) {
if( siz[q] >= B*siz[fa[q]] )
p = fa[q];
q = fa[q];
}
if( p != -1 ) {
if( p == G ) fa[G = rebuild(p, 0)] = 0;
else {
int x = fa[p];
fa[rebuild(p, dep[p])] = x;
}
}
}
void insert(int x, int p, int y) {
int f = x;
while( true ) {
add_edge(f, p);
if( p == y ) break;
f = p, p = explore(p, y);
}
}
void get(int x) {
int nw = G;
while( true ) {
int p = explore(nw, x);
if( vis[p] ) {
for(p;fa[p]!=nw;p=fa[p]); nw = p;
}
else {
insert(nw, p, x);
break;
}
}
}
void solve4(int n) {
for(int i=1;i<=n;i++)
a[i] = i;
random_shuffle(a + 1, a + n + 1);
G = 1, siz[G] = 1, vis[G] = true;
for(int i=1;i<=n;i++)
if( !vis[a[i]] ) get(a[i]);
}
void play(int n, int T, int dataType) {
if( n <= 100 ) solve1(n);
else if( dataType == 2 ) solve2(n);
else if( dataType == 3 ) solve3(n);
else solve4(n);
}
@details@
调试得好痛苦 QAQ。
你可以看到在 uoj 上我整整一页的提交 QAQ。
以后调不过还是直接重构代码,好烦啊 QAQ。
首先要弄清楚要维护哪些东西。。。比如我就用了最简单的维护:点分树的父亲 fa,管辖的结点数 siz,在点分树中的深度 dep。
然后,少用 stl(vector 啊,set 啊之类的乱七八糟的东西)!本身数据结构就是卡时间的。
假如 x 在它父亲 f 中占比太大,应该重构 f 而不是 x!!!
平衡因子 α 取 0.75 果然还是最好的。当然可以上下微调(不过调到 0.7 就 TLE 了是什么鬼)。
还有各种各样的错误 QAQ。我感觉自己的代码能力好像不太行了啊 QAQ。