「loj - 3161」「NOI2019」I 君的探险
description
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) 的复杂度。
(为什么我当时啥也想不到啊)。