序列异或求贡献

序列异或求贡献是一类常见的题目,经典做法无非是求前后缀,按进制位拆贡献累计答案,但是需要对具体问题具体分析。

异或和之和

设前缀异或和为 \(sum_i\)\(sum_0\)=0),对 \(sum_i\) 二进制拆位。\(tot1_k\) 为二进制拆位第 \(k\) 位为 \(1\) 的数的个数,\(tot0_k\) 为二进制拆位第 \(k\) 位为 \(0\) 的数的个数。推式子:

\[\sum_{L=1}^{n}\sum_{R=L}^n\bigoplus_{i=L}^Ra_i \]

\[=\sum_{L=1}^{n}\sum_{R=L}^nsum_R\oplus sum_{L-1} \]

\[=\sum_{k=0}^{20}tot1_k\times tot0_k\times 2^k \]

#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

\[\sum_{R=1}^{n}\sum_{L=1}^R(R-L+1)\bigoplus_{i=L}^Ra_i \]

\[=\sum_{R=1}^{n}\sum_{L=1}^R(R-L+1)(sum{_R}\oplus sum_{L-1}) \]

\[=R\sum_{R=1}^{n}\sum_{L=1}^R(sum{_R}\oplus sum_{L-1})-\sum_{L=1}^R(L-1)\times (sum{_R}\oplus sum_{L-1}) \]

\[=\sum_{k=0}^{20}\times 2^kR\sum_{R=1}^{n}\sum_{L=1}^R[sum{_R}\oplus sum_{L-1}==1]-\sum_{L=1}^R(L-1)\times [sum{_R}\oplus sum_{L-1}==1] \]

没啥特殊的,顶多是多预处理一个 \((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\) 嘛)。

推式子:

\[\begin{aligned} &\sum_{i=1}^{n}\sum_{j=1}^n(a_i\oplus a_j)^2\\ =&\left(\sum_{m=0}^{31}\sum_{k=0}^{31}tot01_{m,k}\times tot10_{m,k}\times 2^{k+1}\right)+\\ &\left(\sum_{m=0}^{31}\sum_{k=0}^{31}tot00_{m,k}\times tot11_{m,k}\times 2^{k+1}\right)+\\ &\left(\sum_{m=0}^{31}\times tot1_{m}\times tot0_{m}\times 2^{2k}\right)\\ \end{aligned} \]

当然你也可以边扫边统计贡献,一样的。

#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\) 为前缀和。

\[\begin{aligned} &\bigoplus\sum_{L=1}^{n}\sum_{R=L}^n\sum_{i=L}^Ra_i\\ =&\bigoplus\sum_{L=1}^{n}\sum_{R=L}^nsum_R-sum_{L-1}\\ \end{aligned} \]

对每一位进行考虑,如果当前位 \(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

双倍经验:Present

首先当然还是分拆 \(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;
}
posted @ 2025-10-25 15:32  Melting_Pot  阅读(11)  评论(0)    收藏  举报