题解:CF799F Beautiful fountains rows

前言

本文同步自洛谷专栏,题目传送门

同时有 xor-hashing 和线段树做法。

分析

可以先减去全 \(0\) 区间带来的贡献,统一形式,具体数值推一推式子即可。

序列问题可以首先考虑扫描线(个人实现为扫描右端点),下面考虑如何维护贡献。

异或哈希做法

下面将各类限制称为颜色。

观察到限制形如:所有颜色的出现次数都是 \(0\) 次或奇数次。

如果要求全为偶数次,xor-hashing 是比较容易的,即要求区间异或和为 \(0\)

$\red{\text{xor-hashing}}$

具体做法是:给每一个颜色赋一个随机权值,每一个点的权值即为该点上所有颜色的权值的异或和,区间异或和(一般用到前缀和)为 \(0\) 代表所有颜色恰出现偶数次。

错误率很低,担心运气不好的话,可以多随几组权值写成结构体,各组答案均为 \(0\) 才代表正确。


事实上,只需要将各个颜色区间和答案区间的左端点 \(+1\) 即可完成转化。(可能有点抽象,可结合正确性证明理解)

正确性证明如下:

有颜色 \(c\) 覆盖区间 \([L,R]\),当前扫描到了 \(r\),一个可行答案为 \(l\)

则需证明:区间 \([L+1,R]\) 均打上颜色 \(c\),区间 \([l+1,r]\) 中颜色 \(c\) 出现了偶数次 \(\Leftrightarrow\) 区间 \([L,R]\) 均打上颜色 \(c\),区间 \([l,r]\) 中颜色 \(c\) 出现了奇数次或 \(0\) 次。

情况 \(l\ge R,r\le L,l\le L\le R\le r,L\le l\le r\le R\) 为相离或包含,容易验证,

\(L\le l\le R\le r\),则 \(l+1\) 恰能改变奇偶性,若 \(l\le L\le r\le R\),则 \(L+1\) 恰能改变奇偶性。

上述证明情况可能有重复,但不会遗漏。

统计上,开两个 map 分别维护某哈希值的出现次数与出现时的下标和,容易处理。

时间复杂度 \(O(n+m\log{m})\),采用哈希表维护可以做到 \(O(n+m)\),但不清楚会不会被 hack。

线段树做法

考虑颜色区间 \([L,R]\) 在当前扫描到的 \(r\) 产生的限制情况。

对于 \(R\le r\),则所有 \(L\le i\le R\)\(R\) 奇偶不同的 \(i\) 均不可行,这一限制在扫描线过程中每次处理 \(R=r\) 的即可。

对于 \(R>r\),则找出 \(<r\) 最大的且奇偶不同的 \(L_1\)\([1,L_1]\) 均不可行,并另外找出最小的 \(L_2\)\([L_2,r]\) 中与 \(r\) 奇偶不同的均不可行。对于每个 \(r\) 处理,查询后需要撤销。

由于分奇偶考虑,则可以分奇偶开线段树支持查询、开 set 处理第二种情况。

线段树每个结点维护最小值、最小值出现次数、最小值权值和和懒标记即可,区间不可行即为区间 \(+1\)

时间复杂度 \(O((n+m)\log{m})\)

实现

$\red{\text{xor-hashing}}$
#pragma optimize("O2")
#include<bits/stdc++.h>
using namespace std;
#define N 200005
#define gc() (rp1==rp2&&(rp2=(rp1=buf)+fread(buf,1,1<<22,stdin)),*rp1++)
#define ull unsigned long long
mt19937_64 rnd(std::chrono::high_resolution_clock::now().time_since_epoch().count());

char buf[1<<22],*rp1,*rp2;
int n,m,d[N],lst;
ull sum[N],A[N],ans=0,now;
map<ull,ull>cnt,tot;

inline int rd(){
    int a=0,c=gc();
    while(!isdigit(c)) c=gc();
    while(isdigit(c)) a=10*a+(c^48),c=gc();
    return a;
}

int main(){
    n=rd(),m=rd();//抹去左端点,则 0/奇数次->偶数次
    for(int i=1,l,r;i<=n;i++){//差分
        l=rd(),r=rd(),d[l]++,d[r+1]--,now=rnd();
        sum[l]^=now,A[l]^=now,A[r+1]^=now;
    }
    for(int i=1;i<=m;i++) A[i]^=A[i-1],sum[i]^=A[i];
    for(int i=1;i<=m;i++){
        d[i]+=d[i-1],sum[i]^=sum[i-1];
        tot[sum[i]]+=i-1,cnt[sum[i]]++;//注意先后顺序
        ans+=cnt[sum[i]]*i-tot[sum[i]]; 
    }
    for(int i=1;i<=m;i++){
        if(d[i]==0) lst++;
        else ans-=1ll*lst*(lst+1)*(lst+2)/6,lst=0;
    }
    printf("%llu",ans-1ll*lst*(lst+1)*(lst+2)/6);
    return 0;
}

线段树部分采用 zkw 线段树写法,应用了标记永久化,用时为前者约 \(2.5\) 倍。

$\red{\text{code}}$
#pragma optimize("O2")
#include<bits/stdc++.h>
using namespace std;
#define N 200005
#define gc() (rp1==rp2&&(rp2=(rp1=buf)+fread(buf,1,1<<22,stdin)),*rp1++)
#define mid ((l+r)>>1)
#define mn(a) tr[a].minn
#define tg(a) tr[a].tag
#define ct(a) tr[a].cnt
#define sm(a) tr[a].sum

char buf[1<<22],*rp1,*rp2;
int n,m,lst,up,d[N],e[N];
vector<int>L[N];
set<int>st[2];
long long ans=0;

inline int rd(){
    int a=0,c=gc();
    while(!isdigit(c)) c=gc();
    while(isdigit(c)) a=10*a+(c^48),c=gc();
    return a;
}

class segtree{//改造 zkw 线段树
    private:
    struct node{
        int minn,tag,cnt;long long sum;
        inline void clear(){minn=n,tag=0,cnt=0,sum=0;}
    }tr[N<<2];
    inline void pushup(int p){
        mn(p)=min(mn(p<<1),mn(p<<1|1)),ct(p)=0,sm(p)=0;
        if(mn(p<<1)==mn(p)) ct(p)+=ct(p<<1),sm(p)+=sm(p<<1);
        if(mn(p<<1|1)==mn(p)) ct(p)+=ct(p<<1|1),sm(p)+=sm(p<<1|1);
        mn(p)+=tg(p);//标记永久化
    }
    public:
    void build(int f){
        tr[up+1].clear(),tr[up].clear();//统一写法
        for(int i=f;i<=m;i+=2){
            tr[i+up+1].clear(),mn(i+up)=0;
            ct(i+up)=1,sm(i+up)=i-1;
        }
        for(int i=up-1;i>=1;i--) tg(i)=0,pushup(i);
    }
    void modify(int l,int r,int d){
        for(l+=up-1,r+=up+1;l<(r^1);){//(l,r)
            if(~l&1) mn(l+1)+=d,tg(l+1)+=d;
            if(r&1) mn(r-1)+=d,tg(r-1)+=d;
            l>>=1,r>>=1,pushup(l),pushup(r);
        }
        for(l>>=1;l;l>>=1) pushup(l);
    }
    void query(int qr,int s,int p=1,int l=0,int r=up-1){
        if(r<=qr){//这里采用递归较为方便
            if(mn(p)+s==0) ans+=1ll*qr*ct(p)-sm(p);
            return;
        }
        s+=tg(p),query(qr,s,p<<1,l,mid);
        if(mid<qr) query(qr,s,p<<1|1,mid+1,r);
    }
}T[2];//每棵线段树仅维护奇数或偶数的答案

inline void check(int i,int x){//处理右端点 r>i 的情况
    if(!st[0].empty()||!st[1].empty()){//向奇偶不同的左端点加上限制
        int l=m;
        if(!st[0].empty()) l=min(l,*st[0].begin());
        if(!st[1].empty()) l=min(l,*st[1].begin());
        if(l<=i) T[~i&1].modify(l,i,x);
    }//向不可行前缀施加限制
    auto it=st[~i&1].lower_bound(i);
    if(it!=st[~i&1].begin()){
        it--,T[0].modify(1,*it,x),T[1].modify(1,*it,x);
    }
}

int main(){
    n=rd(),m=rd(),up=1<<__lg(m<<1);
    T[1].build(1),T[0].build(2);
    for(int i=1,l,r;i<=n;i++){//差分
        l=rd(),r=rd(),d[l]++,d[r+1]--;
        L[r].emplace_back(l),e[l]++;
    }
    for(int i=1;i<=m;i++){
        d[i]+=d[i-1];
        if(d[i]==0) lst++;
        else ans-=1ll*lst*(lst+1)*(lst+2)/6,lst=0;
    }
    for(int i=1;i<=m;i++){
        if(e[i]) st[i&1].insert(i);
        for(auto &l:L[i]){//处理 r<=i 的情况
            T[~i&1].modify(l,i,1),e[l]--;//ban 掉
            if(e[l]==0) st[l&1].erase(l);
            if((l+i+1)&1) continue;
            T[0].modify(1,l,1),T[1].modify(1,l,1);//ban 掉
        }
        check(i,1),T[1].query(i,0),T[0].query(i,0);
        check(i,-1);//复原
    }
    printf("%lld",ans-1ll*lst*(lst+1)*(lst+2)/6);
    return 0;
}

posted @ 2026-06-05 11:04  Wxb2010  阅读(9)  评论(0)    收藏  举报