题解[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);
}