[ioi2018 d2t1] doll - 有趣的构造题

花了4个小时终于搞懂了这题……膜拜现场各种1h以内a掉的julao。

这道题全场大概30几人ac,难度不算太高也不算太低,算是d2最简单,全场第二简单的题。

你要构造一张图,其中起点编号为0,有m个给定的普通点,编号为1~m,你还可以添加若干个特殊点(数量自己决定,这里设为s个),编号-1~-s。所有点可以有任意多条入边,但起点普通点只能有1条出边,特殊点只能有2条出边。

每个特殊点满足:当第奇数次进入它时,会从第1条出边走出,否则会从第2条出边走出。

要求你从起点出发,一路上一共n次经过普通点(每个普通点可以经过多次,经过的顺序是给定的),同时经过每个特殊点都是偶数次,然后返回起点(第一次返回起点后即停止)。

你最多只能使用n+\log_{2}n个特殊节点。

相信大家第一眼见到这道题都会毫无头绪,毕竟构造题往往是需要开一番脑洞的,尤其这还是ioi的题,更是需要“天才级的想象力”。别急,让我们一步一步地来。

以下用圆点代表起点,方点代表普通点,三角代表特殊点,特殊点的奇偶出边分别用x和y表示。

首先我们肯定会对一些小规模或特殊数据,想一些特殊结构。比如:

只经过1次1的很简单:

经过2次1也很简单:

稍微想一下,我们就能用2个特殊点构造出经过4次1的情况:

实际上这种方法可以任意扩展到经过2^{_{n}}次1号节点,只需要\log_{2}n个特殊点:

然而我们陷入了困境,一是因为这种方法难以扩展到n不是2的幂,二是如果经过的节点并非全1,我们也没有好的方法。

实际上,由于经由序列多种多样,直接分析每个经由序列构造方案的性质来构造几乎不可能,因此我们要想一个通用的方法。

此时,“特殊点”的性质给了我很大的启发——既然特殊点是一个有2条出边的结构,那我们能否构造一个由特殊点组成的结构,它有一个入点和k条出边,且以k为循环地依次从每条出边离开?

这个结构可以看做一个“放大版特殊点”。下文中,我们称这种结构为“k开关”。一个特殊点也可以看做“2开关”。

我们发现,如果我们能对任意k实现k开关,问题就直接解决了——我们构造一个n开关,将起点连向第一个经过的普通点,所有普通点连向开关的入点,开关的前n-1条出路分别连向序列中的普通点,最后一个连向起点。

由于一个k开关在经过k次后,所有特殊点必然经过偶数次(不然要么有无用特殊点可以删除,要么无法实现以k为大小的循环),因此这个结构可以直接解决问题。

问题变成了k开关的实现。对线段树一类的数据结构比较熟悉的人容易想到一个构造方案:

如图,一个类似于满二叉树的构型。

可惜这是对于k是2的幂的情况而言。那么如果k不是2的幂呢?

一种方法是将最后一条出边之前的若干条出边直接连回入点,比如下图这个k=3的结构:

如图,要注意连向入点的出边其实是第3次经过的出边。

然而这样我们就需要将节点数补到>=k的一个2的幂,当k=2的幂+1时大约需要2k个节点,无法满足要求。

我们能直接删去一些最后若干次经过的、无用的节点吗?不能。由于在结构中行进的顺序并非简单的dfs序,因此难以找到一整棵可以直接删除的子树,导致我们无法节省节点。

那么我们一开始就直接按照中点分治?也不行。还是由于前述原因(也是由于要经过偶数次的原因),分治的两棵子树必须一样大,因此我们可能需要在一侧补一条指向入点的出边,最终算下来还是无法节约节点。

那怎么办?

此时我们要注意到特殊点数量限制是n+\log_{2}n。这个log从何而来?回顾前面的解题过程,我们发现前面恰好有一个需要\log_{2}n个特殊点的结构。

分析之,我们发现:设结构大小为k,则第2条x边会经过2^{k-1}次,第2条x边会经过2^{k-2}次……第k条x边和y边会经过1次。

如果我们将最后的y连出去(实际上是作为最后的出边连向起点),剩下的结构恰好支持我们对k-1进行二进制拆分!

具体而言,对k-1中为1的位,我们接一个位权大小的满二叉树结构;对于0的位,直接连回入点即可。如下面k-1=21的结构:

下方大三角是形如2的幂的满二叉树结构,数字指出边数量。

容易检验这个结构是符合上述所有要求(比如每个特殊点经过偶数次等)的。

如此我们就实现了k开关的设计,然后只需一次dfs就可以找到所有的出边对应的次序,连接上序列中对应的普通点。再将起点连向第一个经过的普通点,将所有普通点连向开关的入点,将最后的y边连向起点,整个结构便构建完毕。

于是这道题终于大功告成!感谢还有耐心看到这里的你。

//以下代码根据ioi提交格式编写

#include<bits/stdc++.h>
using namespace std;
#include"doll.h"
int n,m;
vector<int> a,c,x,y;
int _x[400010],_y[400010],cnt,tot,rt = 1;
bool fg[400010];
void wk1(int &q,int l,int r){
	if(l == r) return;
	q = ++cnt;
	int mid = l + r >> 1;
	wk1(_x[q],l,mid);
	wk1(_y[q],mid + 1,r);
}
void create_circuit(int _m,vector<int> _a){
	m = _m;a = _a;n = a.size();
	if(n == 1){
		c.push_back(a[0]);
		for(int i = 1;i <= m;++i) c.push_back(0);
		answer(c,x,y);return;
	}
	int i,j,k,nw = 0,nxt;
	c.resize(m + 1);c[0] = a[0];for(i = 1;i <= m;++i) c[i] = -1;--n;
	for(j = 1,k = 0;j <= n;j <<= 1,++k);
	j >>= 1;k = n;
	while(j){
		++cnt;
		if(nw) _y[nw] = cnt;
		nw = cnt;
		if(k & j) wk1(_x[nw],1,j);
		else _x[nw] = -987654321;
		j >>= 1;
	}
	_y[nw] = 987654321;
	nw = 1;
	do{
		fg[nw] = !fg[nw];
		nxt = fg[nw] ? _x[nw] : _y[nw];
		if(!nxt){
			if(fg[nw]) _x[nw] = -a[++tot];
			else _y[nw] = -a[++tot];
			nw = 1;
		}
		else if(nxt < 0) nw = 1;
		else nw = nxt;
	}while(tot < n);
	x.resize(cnt);y.resize(cnt);
	for(i = 1;i <= cnt;++i){
		x[i - 1] = _x[i] == 987654321 ? 0 : _x[i] == -987654321 ? -1 : -_x[i];
		y[i - 1] = _y[i] == 987654321 ? 0 : _y[i] == -987654321 ? -1 : -_y[i];
	} 
	answer(c,x,y);
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值