序列异或求贡献
序列异或求贡献是一类常见的题目,经典做法无非是求前后缀,按进制位拆贡献累计答案,但是需要对具体问题具体分析。
异或和之和
设前缀异或和为 \(sum_i\)(\(sum_0\)=0),对 \(sum_i\) 二进制拆位。\(tot1_k\) 为二进制拆位第 \(k\) 位为 \(1\) 的数的个数,\(tot0_k\) 为二进制拆位第 \(k\) 位为 \(0\) 的数的个数。推式子:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int n,ans(0);
int a[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
a[0]=0;
for(int i=1;i<=n;++i) cin>>a[i],a[i]^=a[i-1];
for(int k=0;k<21;++k){
int tot[2]={0,0};
for(int i=0;i<=n;++i){
ans+=tot[!(a[i]>>k&1)]*(1<<k);//边扫边统计和最后算乘积是一样的
tot[1]+=a[i]>>k&1;
tot[0]+=!(a[i]>>k&1);
}
}
cout<<ans<<endl;
return 0;
}
Sum of XOR Functions
没啥特殊的,顶多是多预处理一个 \((i-1)\times [sum{_R}\oplus sum_{L-1}==1]\)。
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=3e5+10,p=998244353;
int n,ans(0);
int a[N],s[N];
signed main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int k=0;k<=30;k++){
s[1]=0;
for(int j=1;j<=n;j++) s[j+1]=s[j]^(a[j]>>k&1);
vector<int> cnt(2,0);
vector<int> sum(2,0);
for(int j=1;j<=n+1;j++){
ans=(ans+cnt[s[j]^1]*j%p*(1ll<<k))%p;
ans=(ans-sum[s[j]^1]*(1ll<<k)+p)%p;
cnt[s[j]]++;
sum[s[j]]=(sum[s[j]]+j)%p;
}
ans=ans%p;
}
cout<<ans<<endl;
return 0;
}
异或平方和
据说是 \(ACM\) 省赛金牌题......真的假的
对 \(a_i\) 二进制拆位:
设 \(tot10_{m,k}\) 为二进制拆位第 \(m\) 位为 \(1\),\(k\) 位为 \(0\) 的 \(a_i\) 的个数。
设 \(tot01_{m,k}\) 为二进制拆位第 \(m\) 位为 \(0\),\(k\) 位为 \(1\) 的 \(a_i\) 的个数。
设 \(tot11_{m,k}\) 为二进制拆位第 \(m\) 位为 \(1\),\(k\) 位为 \(1\) 的 \(a_i\) 的个数。
设 \(tot00_{m,k}\) 为二进制拆位第 \(m\) 位为 \(0\),\(k\) 位为 \(0\) 的 \(a_i\) 的个数。
设 \(tot0_{m}\) 为二进制拆位第 \(m\) 位为 \(0\) 的 \(a_i\) 的个数。
设 \(tot1_{m}\) 为二进制拆位第 \(m\) 位为 \(1\) 的 \(a_i\) 的个数(实际上这个不就是 \(n-tot0_m\) 嘛)。
推式子:
当然你也可以边扫边统计贡献,一样的。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10,p=1e9+7;
int n,ans(0);
int a[N],fac[N];
int c[100],c01[50][50],c10[50][50],c11[50][50],c00[50][50];
signed main(){
// freopen("data","r",stdin);
// freopen("my.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
fac[0]=1;
for(int i=1;i<=50;++i) fac[i]=fac[i-1]*2%p;
for(int k=0;k<32;++k){
for(int i=1;i<=n;++i){
c[k]+=a[i]>>k&1;
}
for(int m=k+1;m<32;++m){
for(int i=1;i<=n;++i){
c01[k][m]+=!(a[i]>>k&1)&&(a[i]>>m&1);
c10[k][m]+=(a[i]>>k&1)&&!(a[i]>>m&1);
c11[k][m]+=(a[i]>>k&1)&&(a[i]>>m&1);
c00[k][m]+=!(a[i]>>k&1)&&!(a[i]>>m&1);
}
}
}
for(int k=0;k<32;++k){
for(int m=k+1;m<32;++m){
ans=(ans+c01[k][m]*c10[k][m]%p*fac[1+m+k])%p;
ans=(ans+c00[k][m]*c11[k][m]%p*fac[1+m+k])%p;
}
ans=(ans+c[k]*(n-c[k])*fac[k<<1]%p);
}
cout<<(ans*2)%p<<endl;
return 0;
}
异或和
稍微有点不一样,转换一下思路,但是总体肯定还是“前缀和+拆位算贡献”的思想。
依旧是设 \(sum_i\) 为前缀和。
对每一位进行考虑,如果当前位 \(k\) 想要产生贡献的话,有以下几种可能(设 \(sum_i\) 的前 \(k-1\) 位构成的数为 \(cnt_{i,k-1}\)):
-
\(sum_R\) 第 \(k\) 位为 \(0\),\(sum_{L-1}\) 第 \(k\) 位为 \(1\)
- \(cnt_{R,k-1} \ge sum_{L-1}\) 时,此时前 \(k-1\) 位不产生借位,当前位产生借位。
-
\(sum_R\) 第 \(k\) 位为 \(1\),\(sum_{L-1}\) 第 \(k\) 位为 \(0\)
- \(cnt_{R,k-1} < sum_{L-1}\) 时,此时前 \(k-1\) 位产生借位,当前位不借位。
-
\(sum_R\) 第 \(k\) 位与 \(sum_{L-1}\) 第 \(k\) 位相同
- \(cnt_{R,k-1} < sum_{L-1}\) 时,此时前 \(k-1\) 位产生借位,当前位不借位。
由此可以权值为下标统计当前位前缀 \(01\) 数量和,由上述几种情况计算贡献数。
统计可以由两颗权值树状数组完成,记得离散化。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,ans(0),gs(0);
int a[N],fac[N],tem[N];
int num[N][35],res[35];
struct BIT{
#define lowbit(x) (x&(-x))
int t[N];
void add(int pos,int v){
if(!pos) t[pos++]+=v;
for(int i=pos;i<=N-10;i+=lowbit(i)) t[i]+=v;
}
int sum(int pos){
if(!pos) return t[0];
int res(0);
for(int i=pos;i;i-=lowbit(i)) res+=t[i];
return res;
}
}t[35][2];
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i){
cin>>a[i];
fac[i]=fac[i-1]+a[i];
for(int k=0;k<32;++k) num[i][k]=fac[i]&((1<<k+1)-1);
}
for(int k=0;k<32;++k){
for(int i=0;i<=n;++i) tem[i]=num[i][k];
sort(tem,tem+n+1);
gs=unique(tem,tem+n+1)-tem;
for(int i=0;i<=n;++i)
num[i][k]=lower_bound(tem,tem+gs,num[i][k])-tem;
}
for(int k=0;k<32;++k){
for(int i=0;i<=n;++i){
int f1=t[k][1].sum(num[i][k-1]),f0=t[k][0].sum(num[i][k-1]);
int siz1=t[k][1].sum(N-10),siz0=t[k][0].sum(N-10);
res[k]+=fac[i]>>k&1?f0+siz1-f1:siz0-f0+f1;
t[k][(fac[i]>>k)&1].add(num[i][k-1],1);
}
}
for(int k=0;k<32;++k) ans+=(res[k]&1)*(1<<k);
cout<<ans<<endl;
}
这道题可以用序列分治的技巧来完成,我之前写过类似的博客,我们就下一道题(双倍经验)中用这种方法吧。
[COCI 2024/2025 #4] Xor
首先当然还是分拆 \(k\) 位进行考虑,当第 \(k\) 位时,\(a_i\) 与 \(a_j\) 第 \(k\) 的和有两种可能:进位与不进位。类似于之前的题目,我们对 \(a_i\) 的第 \(k\) 位分类讨论产生贡献的情况:
当 \(a_i\) 的第 \(k\) 位是 \(1\) 时:
- \(a_j\) 的第 \(k\) 位需要为 \(1\),且前 \(k-1\) 位构成的数与 \(a_i\) 的前 \(k-1\) 位构成的数之和大于 \(2^k-1\)(前 \(k-1\) 位产生进位)。
- \(a_j\) 的第 \(k\) 位需要为 \(0\),且前 \(k-1\) 位构成的数与 \(a_i\) 的前 \(k-1\) 位构成的数之和小于 \(2^k-1\)(前 \(k-1\) 位不产生进位)。
当 \(a_i\) 的第 \(k\) 位是 \(0\) 时:
- \(a_j\) 的第 \(k\) 位需要为 \(1\),且前 \(k-1\) 位构成的数与 \(a_i\) 的前 \(k-1\) 位构成的数之和小于等于 \(2^k-1\)(前 \(k-1\) 位不产生进位)。
- \(a_j\) 的第 \(k\) 位需要为 \(0\),且前 \(k-1\) 位构成的数与 \(a_i\) 的前 \(k-1\) 位构成的数之和大于 \(2^k-1\)(前 \(k-1\) 位产生进位)。
考虑使用对每一位序列分治:在递归的同时以前 \(k-1\) 位构成的数为索引归并排序,每层递归维护 \(1\) 与 \(0\) 出现次数前后缀,利用不回退双指针(注意要从中间往两边扫),只计算跨中点贡献,最后归并排序即可。
这样,归并排序与双指针就可以实现 \(\Theta(n\log n\log V)\) 的复杂度。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int n,ans(0);
int res[50];
struct Node{
int a,val;
bool operator<(const Node &x)const{return val<x.val;}
}dat[N];
int tmpa[N],tmpv[N];
void solve(int l,int r,int k){
if(l==r) return res[k]+=dat[l].a>>(k-1)&1,void();
int mid((l+r)>>1);
solve(l,mid,k),solve(mid+1,r,k);
int ll(mid),rr_(mid+1),rr[2],maxn((1LL<<k)-1LL);
int cnt0(0),cnt1(0);
rr[0]=rr[1]=0;
for(int i=mid+1;i<=r;++i)
cnt0+=(!(dat[i].a>>k&1)),cnt1+=(dat[i].a>>k&1);
for(;ll>=l;ll--){
for(;dat[rr_].val+dat[ll].val<=maxn&&rr_<=r;rr_++){
rr[1]+=dat[rr_].a>>k&1;
rr[0]+=!(dat[rr_].a>>k&1);
}
if(dat[ll].a>>k&1) res[k]+=rr[0]+cnt1-rr[1];
else res[k]+=rr[1]+cnt0-rr[0];
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) cin>>dat[i].a;
for(int k=0;k<31;++k){
for(int i=1;i<=n;++i) dat[i].val=dat[i].a&((1<<k)-1);
sort(dat+1,dat+n+1);
solve(1,n,k);
if(res[k]&1) ans|=1<<k;
}
cout<<ans<<endl;
return 0;
}
实际上本题还可以更优,首先是排序,可以用基数排序优化掉排序时的一个 \(\log n\),然后是计算贡献时直接用双指针扫描排序后的序列(因为本题所求的数对和不用什么特殊处理),当然代码细节还是有不少的。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int n,ans(0);
int res[50];
struct Node{
int a,val;
}dat[N];
int cnt0[N],cnt1[N];
void solve(int l,int r,int k){
int ll(r),rr(l),maxn((1LL<<k)-1LL);
cnt1[0]=cnt0[0]=0;
for(int i=l;i<=r;++i){
cnt0[i]=cnt0[i-1]+(!(dat[i].a>>k&1));
cnt1[i]=cnt1[i-1]+(dat[i].a>>k&1);
}
for(;ll>=l;ll--){
for(;dat[rr].val+dat[ll].val<=maxn&&rr<=r;) rr++;
if(rr>r||(dat[rr].val+dat[ll].val>maxn&&ll^rr)) rr--;
if(dat[ll].a>>k&1) res[k]+=cnt0[min(ll,rr)]+cnt1[r]-cnt1[max(ll,rr)]+(dat[ll].val*2>maxn);
else res[k]+=cnt1[min(ll,rr)]+cnt0[r]-cnt0[max(ll,rr)]+(dat[ll].val*2>maxn);
//为了保证左右指针交叉时不会重复计算贡献,当然左右指针相等时的贡献要加回来
}
}
int tmpa[N],tmpv[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) cin>>dat[i].a,dat[i].val=0;
for(int k=0;k<31;++k){
solve(1,n,k);
int cnt(0);
for(int i=1;i<=n;++i) tmpa[i]=tmpv[i]=0;
for(int i=1;i<=n;++i) if(!(dat[i].a>>k&1)) tmpa[++cnt]=dat[i].a,tmpv[cnt]=dat[i].val;
for(int i=1;i<=n;++i) if(dat[i].a>>k&1) tmpa[++cnt]=dat[i].a,tmpv[cnt]=dat[i].val+(1<<k);
for(int i=1;i<=n;++i) dat[i]={tmpa[i],tmpv[i]};
if(res[k]&1) ans|=1<<k;
}
cout<<ans<<endl;
return 0;
}
P3401 洛谷树
把”异或和之和“挂到了树上而已,再带个修改罢了,套个树链剖分加线段树维护前缀异或和即可,注意细节。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e4+10;
int n,q;
int to[N<<1],head[N],nxt[N<<1],ew[N<<1];
int fa[N],val[N],dfn[N],siz[N],son[N],dep[N],top[N],rev[N];
int rt[11],cnt;
namespace TREE{
#define lson t[pos].ls
#define rson t[pos].rs
struct Node{
int dat,ls,rs,tag;
}t[(N<<2)*10];
void push_up(int pos){
t[pos].dat=t[lson].dat+t[rson].dat;
}
void add_tag(int pos,int l,int r,int v){
t[pos].tag^=v;
if(v) t[pos].dat=(r-l+1-t[pos].dat);
}
void push_down(int pos,int l,int r){
if(!t[pos].tag) return;
int mid((l+r)>>1);
add_tag(lson,l,mid,t[pos].tag);
add_tag(rson,mid+1,r,t[pos].tag);
t[pos].tag=0;
}
void build(int &pos,int l,int r,int k){
if(!pos) pos=++cnt;
if(l==r) return t[pos].dat=val[rev[l]]>>k&1,void();
int mid((l+r)>>1);
build(lson,l,mid,k);
build(rson,mid+1,r,k);
push_up(pos);
}
void modify(int &pos,int l,int r,int ql,int qr,int v){
if(!pos) pos=++cnt;
if(ql<=l&&r<=qr) return add_tag(pos,l,r,v);
push_down(pos,l,r);
int mid((l+r)>>1);
if(ql<=mid) modify(lson,l,mid,ql,qr,v);
if(mid<qr) modify(rson,mid+1,r,ql,qr,v);
push_up(pos);
}
int query(int pos,int l,int r,int ql,int qr){
if(!pos) return 0;
if(ql<=l&&r<=qr) return t[pos].dat;
push_down(pos,l,r);
int ans(0),mid((l+r)>>1);
if(ql<=mid) ans+=query(lson,l,mid,ql,qr);
if(mid<qr) ans+=query(rson,mid+1,r,ql,qr);
return ans;
}
}
void add(int u,int v,int w){
to[++to[0]]=v,nxt[to[0]]=head[u],ew[to[0]]=w,head[u]=to[0];
to[++to[0]]=u,nxt[to[0]]=head[v],ew[to[0]]=w,head[v]=to[0];
}
void dfs1(int u,int f){
dep[u]=dep[fa[u]=f]+(siz[u]=1);
for(int i=head[u];i;i=nxt[i]){
if(to[i]==f) continue;
val[to[i]]=ew[i]^val[u];
dfs1(to[i],u);
siz[u]+=siz[to[i]];
if(siz[to[i]]>siz[son[u]]) son[u]=to[i];
}
}
void dfs2(int u,int tp){
top[rev[dfn[u]=++dfn[0]]=u]=tp;
if(son[u]) dfs2(son[u],tp);
for(int i=head[u];i;i=nxt[i])
if(to[i]^son[u]&&to[i]^fa[u]) dfs2(to[i],to[i]);
}
int qry(int a,int b){
int ans(0),len(dep[a]+dep[b]);
for(int k=0;k<10;++k){
int res(0),x(a),y(b);
while(top[x]^top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
res+=TREE::query(rt[k],1,n,dfn[top[x]],dfn[x]);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
res+=TREE::query(rt[k],1,n,dfn[x],dfn[y]);
ans+=res*(len-2*dep[x]+1-res)*(1<<k);
}
return ans;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>q;
for(int i=1,u,v,w;i<n;++i){
cin>>u>>v>>w;
add(u,v,w);
}
dfs1(1,0),dfs2(1,1);
for(int k=0;k<10;++k) TREE::build(rt[k],1,n,k);
for(int i=1,opt,u,v,w;i<=q;++i){
cin>>opt>>u>>v;
if(opt==1){
cout<<qry(u,v)<<endl;
}else{
cin>>w;
if(dep[u]>dep[v]) swap(u,v);
for(int k=0;k<10;++k){
int delta=TREE::query(rt[k],1,n,dfn[v],dfn[v])^TREE::query(rt[k],1,n,dfn[u],dfn[u])^(w>>k&1);
TREE::modify(rt[k],1,n,dfn[v],dfn[v]+siz[v]-1,delta);
}
}
}
return 0;
}

浙公网安备 33010602011771号