连续多场 CF 都卡在 xor 题,并且以各不相同的方式寄了。不得不恶补一下这方面的处理技巧
Codeforces 访问有点慢,所以挂洛谷的题目链接
CF1605D Treelabeling
E 和 S 在玩博弈游戏, E 先手 S 后手。
给一棵 \(n\) 个节点的树,节点的值包含 \([1,n]\) 中的每一个值。
E 先随便选择一个点,占领它(点 \(u\) ), S 只能选择与这个点相邻的,没有被占领的点(点 \(v\) )且这两个点满足 \(u⊕v≤\min(u,v)\)
\(⊕\) 是异或操作
现在把树给 E 了, E 想要重新排列节点的值(树的形状不变,只是调换结点的值)来达到这个目的:
最大化第一轮能够选择的点的数量,在选了这个点之后,E 必赢。
赢:对手无路可走,你就算赢
\(u~{\rm xor}~v\le \min(u,v)\) 显然需要处理一下。容易发现它等价于 \(u\) 和 \(v\) 的最高位 1 相同。
以 \(n=7\) 为例,我们将节点的值按最高位 1 分个组:\(\{1\},\{2,3\},\{4,5,6,7\}\)
注意到一个事实:树是二分图。因为可以做二染色。以 \(n=7\) ,两侧分别为 \(3,4\) 个点为例
用 \(\{1\},\{2,3\}\) 填充 \(3\) 个点的一边,\(\{4,5,6,7\}\) 自然地对应另一边
然后我们发现无论选哪个点对方都必败
这个构造是通用的,因为分出来的组的大小是一堆 2 的整数次幂,而一定有一侧的点不超过 \(\dfrac n2\) 个,可以通过二进制拆分恰好填充
code
#include<stdio.h>
const int N=200010;
int T,n,cnt,x,y,m[2],a[N],b[N],last[N],v[2][N];
struct node { int y,pre; }E[N<<1];
void dfs(int x,int f,int c) { // 染色
v[c][++m[c]]=x;
for (int i=last[x]; i; i=E[i].pre)
if (E[i].y!=f) dfs(E[i].y,x,!c);
}
void fill(int x) { // 填充,x 为点数较少的颜色
for (int i=1,j=0; i<=m[x]; i<<=1)
if (m[x]&i) {
for (int k=0; k<i; ++k)
a[v[x][++j]]=i+k,b[i+k]=1;
}
x=(!x);
for (int i=1,j=1; i<=m[x]; ++i)
if (!a[v[x][i]]) {
while (b[j]) ++j;
a[v[x][i]]=j,++j;
}
}
int main(){
scanf("%d",&T);
while (T--) {
scanf("%d",&n),cnt=m[0]=m[1]=0;
for (int i=1; i<=n; ++i)
a[i]=b[i]=last[i]=0;
for (int i=1; i<n; ++i)
scanf("%d%d",&x,&y),
E[++cnt]={y,last[x]},last[x]=cnt,
E[++cnt]={x,last[y]},last[y]=cnt;
dfs(1,0,0),fill(m[0]>m[1]);
for (int i=1; i<=n; ++i) printf("%d ",a[i]);
putchar('\n');
}
}
CF1615D X(or)-mas Tree
给定一棵 \(n(2\le n\le2\times10^5)\) 个点的带权树,一些边的权值已经确定而其它的没有确定(用 \(-1\) 表示)。
现在有 \(m(1\le m\le2\times10^5)\) 条信息,表示 \(u_i\) 到 \(v_i\) 的路径上的权值 xor 和的 popcount 为奇数还是偶数。
试构造出这样一棵树,或者输出无解。
首先注意到 popcount ,对这东西来说每个二进制位是等价的,所以我们可以尝试把一个数直接压成一位 0/1
最直接的想法是进行二进制分解,然后把每一位都 xor 起来。结果是 \({\rm popcount}(x)\bmod 2\) 。
简单观察一下,发现这步操作似乎非常正确,现在问题转化为:
一棵树,边的权值为 0/1 ,一些边权未知,给出 \(m\) 条路径上的权值 xor 和,试构造一棵符合条件的树。
当时想到这里就不会了,然后寄了,rating-=114514
实际上只需考虑一个简单技巧:树上差分/前缀和。
随便选个根节点 \(root\) ,令 \(a_i\) 表示 \(i\) 到 \(root\) 的路径上的权值 xor 和。
那么 \(u\) 到 \(v\) 的路径上的权值 xor 和就是 \(a_u~{\rm xor}~a_v\) 。
正确性显然,\({\rm LCA}(u,v)\) 到 \(root\) 的路径走了两遍,贡献自行抵消了。
是众所周知的 trick 吧,,就我到现在才听说 /youl
然后每条信息给出的其实就是 \(a_u=a_v\) 和 \(a_u\ne a_v\) 中的一个关系,已知权值的边同理
并且 \(a_i\) 只有 0/1 两种取值
写个带权并查集就没了
code
#include<stdio.h>
const int N=200010;
int T,f,n,m,u[N],v[N],w[N],a[N],fa[N];
int find(int x) {
if (fa[x]==x) return x;
int t=fa[x]; fa[x]=find(t),a[x]^=a[t];
return fa[x];
}
int _union(int x,int y,int z) { //带权并查集
z=(__builtin_popcount(z)&1);
int fx=find(x),fy=find(y);
fa[fy]=fx,a[fy]=(a[x]^a[y]^z);
return a[fx];
}
int work() {
scanf("%d%d",&n,&m),f=0;
for (int i=1; i<=n; ++i) fa[i]=i,a[i]=0;
for (int i=2; i<=n; ++i) {
scanf("%d%d%d",u+i,v+i,w+i);
if (~w[i]) f|=_union(u[i],v[i],w[i]);
}
while (m--) {
scanf("%d%d%d",u+1,v+1,w+1);
if (~w[1]) f|=_union(u[1],v[1],w[1]);
}
for (int i=1; i<=n; ++i) find(i); //跑一遍路径压缩
return f;
}
int main() {
scanf("%d",&T);
while (T--) if (!work()) {
puts("YES");
for (int i=2; i<=n; ++i)
printf("%d %d ",u[i],v[i]),
printf("%d\n",(~w[i])?w[i]:(a[u[i]]^a[v[i]]));
}
else puts("NO");
return 0;
}
CF1625D Binary Spiders
从数列 \(\{a_n\}\) 中选出一些数,使其中任意一对数 \(x,y\) 都满足 \(x~{\rm xor}~y\ge k\) 。
给出任意一种满足要求且选出的数最多的方案,或输出无解。
考虑 01 trie 。
显然较高位不同的数相互独立。这里较高位指 \({\rm highbit}(k)\) 以上(不含)的二进制位。
一组较高位相同的数中最多只能选出两个。用 01 trie 判一下就好。
注意细节问题。
code
#include<stdio.h>
#include<ctype.h>
#include<algorithm>
#include<map>
#define rep(i,l,r) for (int i=l; i<=r; ++i)
#define add(x) ans[++m]=x
const int N=300010;
int n,m,cnt,k,hb,x,y,f,a[N],ans[N],T[N<<5][2];
void ins(int x) { // [01 trie板子] 插入
x&=(1<<hb)-1;
for (int i=hb-1,p=0; ~i; --i) {
int &t=T[p][(x>>i)&1];
t||(t=(++cnt)),p=t;
}
}
int qry(int x) { // [01 trie板子] 查询最大异或和(一数为x)
int s=0; x&=(1<<hb)-1;
for (int i=hb-1,p=0; ~i; --i) {
int t=(x>>i)&1;
if (T[p][!t]) s|=(1<<i),p=T[p][!t];
else p=T[p][t];
}
return s;
}
std::map<int,int>mp;
int read() {
int x=0; char ch=getchar();
while (!isdigit(ch)) ch=getchar();
while (isdigit(ch)) x=x*10+(ch^48),ch=getchar();
return x;
}
int main() {
n=read(),k=read();
if (!k) {
printf("%d\n",n);
rep(i,1,n) printf("%d ",i);
return 0;
}
rep(i,1,n) mp[a[i]=read()]=i;
std::sort(a+1,a+n+1);
hb=32-__builtin_clz(k); printf("%d\n",hb);
rep(i,1,n) if ((y=a[i]>>hb)==x) {
if (f) continue;
int t=qry(a[i]);
if (t>=k) add(a[i]),add(a[i]^t),f=1; //选两个
ins(a[i]);
}
else {
if (cnt&&!f) add(a[i-1]); //选一个
rep(i,0,cnt) T[i][0]=T[i][1]=0;
f=0,cnt=0,ins(a[i]),x=y;
}
if (cnt&&!f) add(a[n]);
if (m<=1) return puts("-1"),0;
printf("%d\n",m);
rep(i,1,m) printf("%d ",mp[ans[i]]);
return 0;
}
CF1628C Grid Xor
给一个 \(n\times n\) 的网格,每个点上有一个数 \(a_{i,j}\)
定义 \(b_{i,j}=a_{i-1,j}~{\rm xor}~a_{i,j-1}~{\rm xor}~a_{i,j+1}~{\rm xor}~a_{i+1,j}\)
即上下左右四个相邻数的异或和,如果在边上就不算越界的数(或者认为外围有一圈 0 )
给出 \(b\) ,求 \(a\) 中所有数的异或和。
神奇构造题,方法很多,但我一个都没想到 /kk
下面是一种方法
设 \(f_{i,j}\) 表示 \(a_{i,j}\) 参与异或的次数。我们只需在 \(b\) 中选取一些合适的元素异或起来,使所有 \(f_{i,j}\) 都是奇数。
我们从左上到右下把网格扫一遍,如果当前 \(f_{i,j}\) 为偶数,就选择 \(b_{i+1,j}\) ,并且更新它能影响到的四个 \(f\) 值
听起来很暴力,我们似乎只保证了前 \(n-1\) 行的 \(f_{i,j}\) 都是奇数,然而这个构造确实是对的
证明需要考虑一个事:在第一行任意选择一些 \(b_{1,j}\) ,对于后 \(n-1\) 行都一定会有解。
来看这样一个选择方案:
..X.....
.X.X....
X.X.X...
.X.X.X..
..X.X.X.
...X.X.X
....X.X.
.....X..
构造了一个倾斜 \(45\) 度的 X
矩阵。
每个 .
都恰与偶数个 X
相邻。
对于所有标 X
的位置,如果没选 \(b_{i,j}\) ,改为选;如果选了,改为不选。
对一个可行解进行如上变换,一定会得到另一个可行解,并且第一行特定位置的数的选择情况改变(图中为 \(b_{1,3}\) )
题目保证一定有解,我们通过适当的变换,就可以使第一行处于任意选择情况,并且仍有可行解。
总之意思就是第一行可以瞎选,反正后面能搞出解
然后我们发现第一行瞎选之后第二行的方案是确定的,因为 \(f_{1,j}\) 为偶数时必须选 \(b_{2,j}\) 才能补救,反之亦然
然后第二行也确定了,以此类推可以选完所有行
根据前面的证明,这种看似乱搞的选法就一定是可行解
代码就非常简单了
code
#include<stdio.h>
#define rev(x) x=(!x)
const int N=1010;
int T,n,ans,a[N][N]; bool f[N][N];
int main() {
scanf("%d",&T);
while (T--) {
scanf("%d",&n),ans=0;
for (int i=1; i<=n; ++i)
for (int j=1; j<=n; ++j)
scanf("%d",&a[i][j]),
f[i][j]=0;
for (int i=2; i<=n; ++i)
for (int j=1; j<=n; ++j)
if (!f[i-1][j])
ans^=a[i][j],
//rev(f[i-1][j]),
rev(f[i][j-1]),
rev(f[i][j+1]),
rev(f[i+1][j]);
printf("%d\n",ans);
}
return 0;
}