切り取り線 题解
简要题意
给你一个 的矩形,内部给你 条横平或竖直的线段,问这些线段分割矩形形成了多少联通块?
。
解法
我们试图水平地从下往上扫描这些线段来求得答案。
进一步,我们能想到在每一个扫描时刻使用并查集维护这一行上每个点所属的连通块。
考虑加入直线后对连通块的变化——本质上是并查集的操作。
我们只需要考虑横着的和竖着的两种情况。
一条竖直的线段加入
(偷个官方图)
从视觉上来看,虽然这条线段加入了,但是它并没有真正地把原来的连通块分割开来。
一条竖直的线段离开
不管这条线段左右两边原来是不是在一个连通块里,它们最后都合为一体了,对应并查集的合并操作。
一条水平的线段加入
我们可以看到,原来被竖着的线段分割形成的连通块,如果其完整地被这条横线覆盖,那么就相当于是对这些连通块在并查集里新开点。
一种朴素的解法
我们在并查集里维护上述东西,最后统计并查集里有多少个连通块,减去一即为答案。
但这样远远不够。仔细一想,瓶颈在于横线所造成的并查集新开点。如果我们每个都真的去开,岂不是太浪费时间了?考虑优化这一过程。
懒标记优化
我们不妨使用懒标记(先不去想懒标记如何实现)。对于一条水平线段的加入,我们对需要新开点的连通块打上懒标记,当下一次水平线段加入的时候,统计一下这个水平线段里包含了多少被标记过的连通块,则这些连通块必定对答案有贡献。
没有被标记的有没有贡献呢?加入完所有的线段后我们再看看并查集里有多少个连通块。因为已经被统计过的连通块都没有在并查集里新开点,所以不会造成重复统计。
实现
有插入、删除、标记,我们考虑 FHQ-Treap。
首先我们把线段分成三种:水平线段、竖直线段加入、竖直线段删除。对于这三种线段,我们按照他们操作的纵坐标升序排序。纵坐标相同的,竖直线段加入在前面,中间是水平线段,最后是竖直线段删除。
为什么要这么做呢?你可以把水平线段加入的操作看成统计之前的懒标记再打上新的懒标记。水平线段统计答案要用到之前的线段,所以不能先删,水平线段打懒标记要给后面的线段打,所以要先加入后面的线段。有人可能会问了,万一给之前的线段打上了怎么办?不要忘了我们马上就要把它删了,所以对答案没影响。
明确了操作顺序,考虑具体如何用平衡树实现。
我们对于每个连通块,记录它的左侧横坐标(你想用右侧也可以)。
分裂是按值分裂,方便我们快速通过值找到连通块。
void split(int p,int v,int &x,int &y){
if(!p){x=y=0;return;}
pushdown(p);
if(t[p].v<=v)split(t[p].rs,v,t[x=p].rs,y);
else split(t[p].ls,v,x,t[y=p].ls);
pushup(p);
}
int merge(int x,int y){
if(!x||!y)return x+y;
pushdown(x),pushdown(y);
if(t[x].rd>t[y].rd){
t[x].rs=merge(t[x].rs,y);
pushup(x);
return x;
}else{
t[y].ls=merge(x,t[y].ls);
pushup(y);
return y;
}
}
对于平衡树上每个节点,我们还要记录这个点有没有懒标记 lz
、这个点的子树里有多少个懒标记 cv
以及这个点在并查集中的编号 id
。
pushup
和 pushdown
是显然的。
void pushup(int p){
if(!p)return;
t[p].sz=t[t[p].ls].sz+t[t[p].rs].sz+1;
t[p].cv=t[t[p].ls].cv+t[t[p].rs].cv;
}
void update_cv(int p){
if(!p)return;
t[p].cv=t[p].sz;
t[p].lz=1;
}
void pushdown(int p){
if(!p)return;
if(t[p].lz){
update_cv(t[p].ls);
update_cv(t[p].rs);
t[p].cv--;
t[p].id=DSU::newnode();
t[p].lz=0;
}
}
考虑如何让竖直线段加入。
首先我们要先找到这个线段分割了哪个连通块,然后把这个连通块分割开。
int mx(int p){ //最大值所在节点
while(t[p].rs){
pushdown(p);
p=t[p].rs;
}
return p;
}
void ins(int v){
int x,y;
split(rt,v,x,y);
int p=newnode(v);
t[p].id=t[mx(x)].id;// x 的最大值,即为被分割的连通块
rt=merge(merge(x,p),y);
}
删除操作自然也不难想。
void del(int v){
int x,y,z;
split(rt,v-1,x,y);
split(y,v,y,z);
DSU::merge(t[mx(x)].id,t[y].id);
rt=merge(x,z);//因为保证线段不重合,所以可以认为 y 只有一个结点
}
打标记这个可能有点抽象。
void update(int l,int r){
int n,m,p,q;
split(rt,l-1,n,m);
split(m,r,m,q);
split(m,t[mx(m)].v-1,m,p);
ans+=t[m].cv;
update_cv(m);
rt=merge(merge(n,m),merge(p,q));
}
为什么分出来 m
之后还要去掉最右侧的一个呢?因为我们记录的是左侧坐标,最右侧的那个实际上是没有完全被这个区间包含的。
初始化直接插入一个负无穷的点表示整个区间。
最后注意数组别开小了,还有把原来矩形上下左右 条边加进去。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5+10,INF=0x3f3f3f3f3f3f3f3f,mod=1e9+7;
int w,h,n;
struct op{
int y,l,r,t;
op(){}
op(int _y,int _l,int _r,int _t):y(_y),l(_l),r(_r),t(_t){}
bool operator<(op rhs){
if(y==rhs.y)return t<rhs.t;
else return y<rhs.y;
}
}o[N*3];
int tot;
int ans;
namespace DSU{
int fa[N*100],tot;
int newnode(){tot++;return fa[tot]=tot;}
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
void merge(int x,int y){x=find(x),y=find(y),fa[x]=y;}
int calc(){
int res=0;
for(int i=1;i<=tot;i++)
if(fa[i]==i)res++;
return res;
}
}
namespace FHQ{
struct node{
int ls,rs;
int sz,rd;
int v,id;
int lz,cv;
}t[N*3];
int tot,rt;
stack<int> st;
int newnode(int v){
int p=0;
if(!st.size())p=++tot;
else p=st.top(),st.pop();
t[p].sz=1,t[p].rd=rand();
t[p].v=v;
return p;
}
void delnode(int p){
st.push(p);
memset(&t[p],0,sizeof(node));
}
void pushup(int p){
if(!p)return;
t[p].sz=t[t[p].ls].sz+t[t[p].rs].sz+1;
t[p].cv=t[t[p].ls].cv+t[t[p].rs].cv;
}
void update_cv(int p){
if(!p)return;
t[p].cv=t[p].sz;
t[p].lz=1;
}
void pushdown(int p){
if(!p)return;
if(t[p].lz){
update_cv(t[p].ls);
update_cv(t[p].rs);
t[p].cv--;
t[p].id=DSU::newnode();
t[p].lz=0;
}
}
void split(int p,int v,int &x,int &y){
if(!p){x=y=0;return;}
pushdown(p);
if(t[p].v<=v)split(t[p].rs,v,t[x=p].rs,y);
else split(t[p].ls,v,x,t[y=p].ls);
pushup(p);
}
int merge(int x,int y){
if(!x||!y)return x+y;
pushdown(x),pushdown(y);
if(t[x].rd>t[y].rd){
t[x].rs=merge(t[x].rs,y);
pushup(x);
return x;
}else{
t[y].ls=merge(x,t[y].ls);
pushup(y);
return y;
}
}
int mx(int p){
while(t[p].rs){
pushdown(p);
p=t[p].rs;
}
return p;
}
void ins(int v){
int x,y;
split(rt,v,x,y);
int p=newnode(v);
t[p].id=t[mx(x)].id;
rt=merge(merge(x,p),y);
}
void del(int v){
int x,y,z;
split(rt,v-1,x,y);
split(y,v,y,z);
DSU::merge(t[mx(x)].id,t[y].id);
rt=merge(x,z);
}
void update(int l,int r){
int n,m,p,q;
split(rt,l-1,n,m);
split(m,r,m,q);
split(m,t[mx(m)].v-1,m,p);
ans+=t[m].cv;
update_cv(m);
rt=merge(merge(n,m),merge(p,q));
}
void init(){
ins(-INF);
t[rt].id=DSU::newnode();
}
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>w>>h>>n;
for(int i=1;i<=n;i++){
int a,b,c,d;
cin>>a>>b>>c>>d;
if(b==d)o[++tot]=op(b,a,c,2);
else o[++tot]=op(b,a,0,1),o[++tot]=op(d,c,0,3);
}
o[++tot]=op(0,0,w,2);
o[++tot]=op(h,0,w,2);
o[++tot]=op(0,0,0,1);
o[++tot]=op(h,0,0,3);
o[++tot]=op(0,w,0,1);
o[++tot]=op(h,w,0,3);
sort(o+1,o+1+tot);
FHQ::init();
for(int i=1;i<=tot;i++){
if(o[i].t==1)FHQ::ins(o[i].l);
if(o[i].t==2)FHQ::update(o[i].l,o[i].r);
if(o[i].t==3)FHQ::del(o[i].l);
}
ans+=DSU::calc();
cout<<ans-1<<'\n';
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】