题解[CF888G]

原题链接

提供一个易 (luan) 懂 (gao) 的分治算法。

容易发现如果将序列每次分成两半,每一半都得到了最优方案,

此时要做的就是在两块中分别找出一个数让其异或和最小,这可以用 \(\text{01trie}\) 轻松实现。

但不过此题中分的方法很有讲究,并不能直接取两端点的中点。

通过几次观察可以得出这样的结论:

  • 将原序列去重排好序后,在每次分治时传一个参数 d (最开始 $ d=30 $ ) ,
    每次按二进制下按第 d 位是 0 还是 1 分成两半,
  • 接下来对两边继续分治时 $ d\leftarrow d-1 $ ,就能得到最优方案。

证明:
设这样得到的答案为 x ,在第 d 位上将一个数从另一半中放入这一半所得答案为 y ,

此时第 d 位上至少会贡献 $ 2\cdot 2^d $ 。

则 $ y\geq 2\cdot2^d $ , \(x \leq 1+2+\dots +2^{d-1}+2^d\)

\(y-x\geq 2^d-(1+2+\dots +2^{d-1})=1>0\)

\(y>x\) ,证毕。

再考虑 $ \text{01trie} $ 如何实现。

这只需让每次递归完后区间内的点都插入即可,

先递归左边,再在右边查与左边各选两数的最小异或和,再递归右边,递归到只有一个数时插入后返回。

时间复杂度是 $ O(n\log(n)) $ 。

这个算法实际上也无形地建了一个 $ \text{01trie} $ 出来,非要有个名字不妨称为 $ \text{01trie} $ 分治。

代码:

using namespace std;
const int N=2e5+10,K=30,NN=5e6+10;
int n,nn,x,y,rt,res,tot,cnt,tmp;
long long ans;int a[N];char ch;
inline void read(int &x){
	x=0;ch=getchar();
	while(ch<'0'||ch>'9')ch=getchar();
	while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();
}
struct trie{int s[2],sz;}t[N*K];
void insert(int &k,int x,int d,int v){
	if(!k)k=++tot;t[k].sz+=v;if(d<0)return;
	bool b=x&(1<<d);insert(t[k].s[b],x,d-1,v);
}
void inquiry(int k,int x,int d){
	if(d<0)return;bool b=x&(1<<d);
	if(t[t[k].s[b]].sz)inquiry(t[k].s[b],x,d-1);
	else res+=1<<d,inquiry(t[k].s[b^1],x,d-1);
}
void solve(int l,int r,int d){
	if(l==r)return insert(rt,a[l],K,1);
	register int pos=r+1,i;
	for(i=r;i>=l;--i)if((a[i]>>d)&1)pos=i;
	if(pos==r+1||pos==l)return solve(l,r,d-1);
	solve(l,pos-1,d-1);
	tmp=1<<30;
	for(i=pos;i<=r;++i){
		res=0;inquiry(rt,a[i],K);
		tmp=min(tmp,res);
	}
	ans+=tmp;
	solve(pos,r,d-1);
}
main(){
	read(n);register int i;
	for(i=1;i<=n;++i)read(a[i]);
	sort(a+1,a+n+1);n=unique(a+1,a+n+1)-a-1;
	solve(1,n,K);
	printf("%lld",ans);
}
posted @ 2021-08-18 11:12  Y_B_X  阅读(174)  评论(0编辑  收藏  举报