P10763 题解

Linge_Zzzz

2024-11-20 19:00:55

Solution

如果你 RE 4pts,并且只有一个点 WA,那么去看我代码注释。

鲜花

模拟赛上写了巨大码量的离散化+线段树,用了三种不一样的线段树。写了 5k。还没调出来。看了 大佬的博客 学习了特别厉害的写法,遂写篇题解分享之。

Sol

看到这种题肯定扫描线。不妨把所有竖着的线段提取出来。考虑什么时候不合法。

  1. 手玩样例一可发现,我们需要把所有竖着的线段的横坐标奇偶分类。如果一个纵坐标在加入和删除时的横坐标奇偶性不同,则不合法。

  2. 手玩样例二可发现,如果存在一条竖着的长度为奇数的连通块,则不合法。

  3. 手玩样例三可发现,如果场上只有横坐标为偶数的线段或只有横坐标为奇数的线段,当前横坐标才是可能合法的。而且所谓的“当前横坐标”也需要特判。当场上的线段横坐标和当前横坐标奇偶性不同时需要减一。我在场上这个地方写挂了所以没调出来。

此时我们有一个非常 naive 的做法:把所有出现过的纵坐标离散化,然后在这上面建立两种线段树。第一种线段树支持区间覆盖成 0/1,查询全局有没有长度为奇数的连通块。第二种线段树支持区间覆盖成 0/1,查询一个区间内的 0/1 存在性。我在场上为了解决第三种限制还又开了个线段树记录插入时的横坐标。。。

这种写法是非常臭的。显然有更优美的写法。

下面要讲的写法本质是平衡树。但由于其维护信息的特殊性,可以使用 STL map 来代替。平衡树天生不需要离散化,所以避免了很多讨论。。。

我们把那两种线段树取缔掉。其实连通块在纵向看就是一条条线段,所以我们只需要维护线段的属性。建立两个 int 到 int 的 map,第一个维护左端点对应的右端点是哪个,第二个维护左端点所对应的线段的横坐标的奇偶性。

插入操作,由于我们的多边形十分的优美,不会保证有线段重叠,所以可以直接在 map 里插入。插入之后判一下左右有没有能合并的,如果有就合并上。

删除操作,其实就是把线段分裂开。直接 upper_bound 找相交线段然后分裂开就行。记得判一下两个相交的线段的横坐标奇偶性一不一样,不一样直接输出当前 ans。

又因为我们只关心全局的信息,所以我们在插入、合并的同时只需要记录有没有长度为奇数的线段就可以。

关于复杂度正确性,可以考虑均摊。线段插入的时候,每合并一条线段,场上线段减少 1,产生 O(\log n) 复杂度。线段分裂的时候,会先删除一些线段,每删除一条线段场上线段减少 1,产生 O(\log n) 复杂度,然后产生 O(1) 个新线段。所以总复杂度是 O(n\log n) 的。

Code

代码有注释。

#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef pair<int,int> pii;
#define fi first
#define se second
#define mp make_pair
#define pb push_back
const int N=2e5+10,INF=0x3f3f3f3f3f3f3f3f,mod=1e9+7;
int n,m,x[N],y[N];
struct line{
    int x,l,r;
    line(){}
    line(int x,int l,int r):x(x),l(l),r(r){}
    bool operator<(line y){return x<y.x;}
}c[N];
int tot=0;
map<int,int> seg,par; 
int pcnt[2],ocnt,ans;
void del(int l){// 删除一条线段,同时维护信息 
    ocnt-=(seg[l]-l)%2;
    pcnt[par[l]]--;
    seg.erase(l);
    par.erase(l);
}
void ins(int l,int r,int p){// 插入一条线段,左右端点为 l,r,横坐标奇偶性为 p,同时维护信息 
    ocnt+=(r-l)%2;
    pcnt[p]++;
    par[l]=p;
    auto it=seg.insert(mp(l,r)).fi;
    int ll=prev(it)->fi,rl=next(it)->fi,rr=next(it)->se;
    //只有横坐标奇偶性相同才能合并 
    if(seg[ll]==l&&par[ll]==p)del(ll),del(l),ins(ll,r,p);//往左合并 
    else if(r==rl&&p==par[rl])del(l),del(rl),ins(l,rr,p);//往右合并 
}
void spl(int l,int r,int x,int p){
    int y=seg[x];
    if(l>=y||r<=x)return;
    if(p!=par[x]){//不同奇偶线段有交集 
        cout<<ans<<endl;
        exit(0);
    }
    del(x);
    if(x<l)ins(x,l,p);//往左分裂 
    if(y>r)ins(r,y,p);//往右分裂 
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>x[i]>>y[i];
    x[0]=x[n],y[0]=y[n];
    for(int i=1;i<=n;i++)
        if(x[i]==x[i-1])
            c[++tot]=line(x[i],min(y[i],y[i-1]),max(y[i],y[i-1]));
    sort(c+1,c+1+tot);
    seg[-INF]=-INF,seg[INF]=INF;//设置边界避免分讨首尾迭代器 
    int lst=0;
    for(int i=1;i<=tot;i++){
        if(c[i].x!=lst){// 如果横坐标改变,统计一次答案 
            if(ocnt){
                cout<<ans<<endl;
                exit(0);
            }
            if(!pcnt[0])ans=max(ans,c[i].x-!(c[i].x%2));
            if(!pcnt[1])ans=max(ans,c[i].x-c[i].x%2);
        }
        auto it=seg.upper_bound(c[i].l);
        if(prev(it)->se<=c[i].l&&c[i].r<=it->fi)ins(c[i].l,c[i].r,c[i].x%2);//不存在就插入 
        else while(it->se>=c[i].l)spl(c[i].l,c[i].r,(it--)->fi,c[i].x%2);//一直删,直到没有相交的线段
        /*上一行的错误示范: 
        else while(it->se>=c[i].l)spl(c[i].l,c[i].r,it->fi,c[i].x%2),it--;
        原因是 (it--)->fi 会先传参,再 it-- 最后执行函数。如果执行函数之后再 it-- 会因为 it 已经被删除而出错。 
        错误写法会 RE 4pts,并且只有一个点 WA。 
        */
        lst=c[i].x;
    }
    cout<<m<<endl;
    return 0;
}