【XSY4231】人赢(图论,Hall定理,Trie树,树形DP)

首先二分答案,设为 \(mid\)

现在的问题是:若 \(a_i\oplus a_j\geq mid\),则 \(i,j\) 之间有一条连边,判断是否存在一种选边方式使得每个点都恰好在一个简单环上(可以是自环或二元环)。

这个判定条件有点奇怪,一开始感觉有些性质,考场上除了想到只能是奇环或二元环就没想到啥了……

结果是一个妙妙转化,简单环可以看成置换,那么现在问题就变成了:是否存在一个 \(1\sim n\) 的排列 \(p\),使得 \(\forall i,a_i\oplus a_{p_i}\geq mid\)

如果我们建一个二分图,然后若 \(a_i\oplus a_j \geq mid\) 则让左边的 \(i\) 向右边的 \(j\) 连一条边,那么现在问题就变成了这个二分图是否存在完美匹配。

Hall 定理:

对于一个二分图 \(A,B\)\(|A|\leq |B|\)),设 \(To(S)\) 为与点集 \(S\) 相连的所有点组成的点集,那么这个二分图存在完美匹配(\(A\) 中的点全部被匹配)当且仅当:对于任意一个 \(A\) 的子集 \(S\),均有 \(|To(S)|\geq |S|\)

根据浩二定理,我们只需要求出 \(|To(S)|-|S|\) 的最小值然后判断即可,但我们显然不能把边全部建出来(\(O(n^2)\) 级别)。

一种很妙的做法是先把所有的 \(a_i\) 插入 Trie 树中,然后设 \(f_{i,j}\) 表示仅考虑二分图左边在 \(i\) 子树内的点(即在 \(i\) 子树中的 \(a_i\))和右边在 \(j\) 子树内的点构成的导出子图,求出这个子图 \(|To(S)|-|S|\) 的最小值。

这里保证 \(i,j\) 在 Trie 树的同一层中且 \(i,j\) 之前的层异或起来和 \(mid\) 是一样的。

换言之,假设 \(i,j\) 都在第 \(k\) 层(即 \(i,j\) 的左右儿子都是按二进制下第 \(k\) 位是 \(0/1\) 来分的),那么 \(i,j\) 的第 \(k+1\) 位到最高位异或起来和 \(mid\) 的第 \(k+1\) 位到最高位是一样的,即我们尚不清楚 \(i\) 子树内的点和 \(j\) 子树内的点的连边关系。

那么我们最后要求的就是 \(f_{rt,rt}\)

考虑转移,设 \(l_i,r_i\) 分别表示 \(i\) 的左儿子(\(0\) 边)和右儿子(\(1\) 边),\(l_j,r_j\) 同理。设 \(sz_u\) 表示 Trie 树上 \(u\) 子树内包含多少个 \(a_i\)

\(mid\) 的第 \(k\) 位分类讨论:

  • 若为 \(0\),则 \(l_i\) 内的任何一个点都对 \(r_j\) 内的所有点有连边,\(r_i\) 内的任何一个点都对 \(l_j\) 内的所有点有连边。

    讨论 \(S\) 的选取范围:

    • 若什么都不选,则为 \(0\)
    • 若只在 \(l_i\) 内选,那么 \(To(S)\) 一定包含 \(r_j\) 内的所有点,那么我们只需要让 \(l_i\)\(l_j\) 的导出子图中的 \(|To(S)|-|S|\) 最小,则为 \(sz_{r_j}+f(l_i,l_j)\)
    • 若只在 \(r_i\) 内选,同理可得为 \(sz_{l_j}+f(r_i,r_j)\)
    • 若在 \(l_i,r_i\) 中同时有选,那么 \(To(S)\) 就包含了 \(l_j,r_j\) 内的所有点,是固定的,那么我们只需要让 \(|S|\) 最大即可,则为 \(sz_{l_j}+sz_{r_j}-sz_{l_i}-sz_{r_i}=sz_j-sz_i\)
  • 若为 \(1\),此时尽可能有 \(l_i\)\(r_j\) 之间的连边和 \(r_i\)\(l_j\) 之间的连边,化为两个子问题,有 \(f(i,j)=f(l_i,r_j)+f(r_i,l_j)\)

观察 DP 的转移,发现这个 DP 事实上是 \(O(n\log V)\) 的(Trie 树大小),感性的理解是把 \(i,j\) 当做两个指针,那么相当于 \(i,j\) 各把整棵 Trie 树遍历了一遍。

看起来严谨一点的证法可以设 \(f(i,j)=O(s_i+s_j)\)(其中 \(s_u\) 表示 Trie 树中 \(u\) 子树的大小),然后根据转移方程归纳证明。

再带上二分,这个算法的时间复杂度为 \(O(n\log^2 V)\) 的,有点卡常,注意一些边界情况。

#include<bits/stdc++.h>

#define N 500010

using namespace std;

inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<1)+(x<<3)+(ch^'0');
		ch=getchar();
	}
	return x*f;
}

const int V=30*N;

int n,a[N];
int node=1,ch[V][2],size[V];

void insert(int x)
{
	int u=1;
	size[u]++;
	for(int i=29;i>=0;i--)
	{
		bool v=(x>>i)&1;
		if(!ch[u][v]) ch[u][v]=++node;
		u=ch[u][v];
		size[u]++;
	}
}

int mid;

int dp(int a,int b,int k)
{
	if(!a) return 0;
	if(!b) return -size[a];
	if(k<0) return min(0,size[b]-size[a]);
	const int la=ch[a][0],ra=ch[a][1];
	const int lb=ch[b][0],rb=ch[b][1];
	if((mid>>k)&1) return dp(la,rb,k-1)+dp(ra,lb,k-1);
	return min(min(0,size[b]-size[a]),min(size[rb]+dp(la,lb,k-1),size[lb]+dp(ra,rb,k-1)));
}

int main()
{
	n=read();
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		insert(a[i]);
	}
	int l=0,r=(1<<30)-1,ans;
	while(l<=r)
	{
		mid=(l+r)>>1;
		if(dp(1,1,29)>=0) ans=mid,l=mid+1;
		else r=mid-1;
	}
	printf("%d\n",ans);
	return 0;
}
posted @ 2022-10-31 07:47  ez_lcw  阅读(74)  评论(0编辑  收藏  举报