「loj - 3161」「NOI2019」I 君的探险


description

题目太长,去loj看吧。

solution

对于给定的点 x 与点集 s,可以通过 modify s 中的所有点判断 s 到 x 的边数奇偶性。

进一步地,如果是奇数条边,可以通过二分找到任意一条边。这样可以 O(mlogm) 次询问找到所有边。

注意到 modify 重复次数太多,我们考虑整体二分。

但是整体二分又有问题,比如可能二分到同一条边。
解决方法是只找一个点向排在它前面的点的边。但是要满足向前为奇数又比较困难。

“题目保证测试所使用的图在交互开始之前已经完全确定,而不会根据和你的程序的交互动态构造。”

于是我们随机化排列,可以猜测到一个点往前连的边的数量是奇是偶的概率几乎是相等的(貌似最劣概率是 1/3?)。

然后就发生了这种事情:

有几个优化:
(1)check 操作可以帮助排除无用点。
(2)已有的边需要在 modify 时消除影响。

这两个是主要的。还有就是我自己写的时候遇到的问题:
(3)二分前不需要先判断点向前是否连奇数条边,直接硬刚。这样可以少一半的 modify。
(4)如果点 x <= mid,则不需要 query 直接往左边递归。这样可以少很多的 query。
(5)随机数种子使用系统库默认种子。


测试点 2~5 需要写暴力。

测试点 6~9(A) 与测试点 10~11(B) 不能使用 check,但是依然可以二分,只是跑二分之前不使用 random_shuffle。

accepted code

#include "explore.h"
#include <vector>
#include <cstdlib>
#include <iostream>
#include <algorithm>
using namespace std;

const int MAXN = 200000;

vector<int>G[MAXN + 5];
bool tag[MAXN + 5], nw[MAXN + 5]; int mcnt;
bool can_check;
void answer(int u, int v) {
	G[u].push_back(v), G[v].push_back(u);
	mcnt++, report(u, v);
	if( can_check ) {
		if( check(u) ) tag[u] = true;
		if( check(v) ) tag[v] = true;
	}
}
bool is_change(int x) {
	if( query(x) != nw[x] ) {
		nw[x] ^= 1;
		return true;
	} else return false;
}
void update(int x) {
	for(unsigned i=0;i<G[x].size();i++)
		nw[G[x][i]] ^= 1;
	modify(x);
}

int b[MAXN + 5], m, a[MAXN + 5], cnt;
void get(int L, int R, int l, int r) {
	if( l > r ) return ;
	if( L == R ) {
		for(int i=l;i<=r;i++)
			if( L < b[i] ) answer(a[L], a[b[i]]);
		return ;
	}
	int M = (L + R) >> 1, tot = l;
	for(int i=l;i<=r;i++) nw[a[b[i]]] = false;
	for(int i=L;i<=M;i++) update(a[i]);
	for(int i=l;i<=r;i++)
		if( b[i] <= M || is_change(a[b[i]]) ) swap(b[tot++], b[i]);
	for(int i=L;i<=M;i++) update(a[i]);
	get(L, M, l, tot - 1), get(M + 1, R, tot, r);
}
void solve1(int N, int M) {
	for(int i=0;i<N-1;i++) {
		modify(i);
		for(int j=i+1;j<N;j++)
			if( is_change(j) ) report(i, j);
	}
}
void explore(int N, int M) {
	if( N <= 500 ) solve1(N, M);
	else if( N % 10 == 8 ) {
		int cnta = 0, cntb = 0;
		for(int i=0;i<N;i++) {
			if( query(i) ) b[++cntb] = i;
			else modify(i), a[++cnta] = i;
		}
		for(int i=1;i<=cnta;i++) modify(a[i]);
		for(int i=1;i<=cntb;i++) a[++cnta] = b[i], b[i] = cnta;
		get(1, N / 2, 1, m = N / 2);
	} else if( N % 10 == 7 ) {
		for(int i=1;i<=N;i++) a[i] = i - 1, b[i] = i;
		get(1, N, 1, m = N);
	} else {
		can_check = true;
		for(int i=0;i<N;i++)
			G[i].push_back(i);
		
		while( mcnt != M ) {
			cnt = 0;
			for(int i=0;i<N;i++)
				if( !tag[i] ) a[++cnt] = i;
			
			random_shuffle(a + 1, a + cnt + 1);
			for(int i=1;i<=cnt;i++) nw[a[i]] = false;
			for(int i=1;i<=cnt;i++) b[i] = i;
			get(1, cnt, 1, m = cnt);
		}
	}
}

details

感觉隔壁 JOI 系列比赛的交互题也很喜欢整体二分。

树的部分还有基于异或和按位讨论的非随机算法:
(1)先找到与点 x 相邻的点的异或和,记为 sum[x]。这部分按位 modify 就可以 O(nlogn) 做。
(2)如果 x 与 sum[x] 有边相连,且 x 的度数为 1(用 check 操作判断),则 x 显然为叶子。每次剥去叶子并尝试剥去与叶子相邻的点,可以做到 O(n + m) 的复杂度。

(为什么我当时啥也想不到啊)

posted @ 2020-06-29 17:50  Tiw_Air_OAO  阅读(38)  评论(0编辑  收藏