线段树2

扫描线

给定 \(n\) 个矩形的左上角坐标和右下角坐标的位置,求这些矩形的并的面积和周长。

在讲面积和周长的解法之前,我们先来看一个 first of all 的题目。

线段树覆盖问题

一个数组 \(a\) 初始全为 0,长度为 \(n\),有 \(m\) 个修改操作,就是把下标 \(l_i\sim r_i\) 的数组 \(a\) 加上 \(v_i\)\(i=1,2,\cdots m\)\(v_i\in \plusmn 1\)),每个操作完毕后都请输出数组中有多少个值大于 0 的数(即被覆盖的有多少个)。

upd@2022.10.13: 这个问题有更简单的做法,可转化为维护最小值及最小值个数!

这个问题我们构造一棵线段树,每个节点要维护的值有三个:\(cnt\)(该节点所代表的区间被完整地覆盖了多少次),\(t\)(除去完整地覆盖的次数这个区间被覆盖的数有多少个),\(lazy\)(某一时刻我们可能对这个节点进行了修改它还没有来得及下传到它下面的节点这个待加的值就存在这里)。

知道了要维护什么相信你如歌学好了线段树就能自己推导出一些东西了,下面直接给代码。

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int t[N<<2],cnt[N<<2],lazy[N<<2]; 
void pushup(int l,int r,int k){
	cnt[k]=min(cnt[k<<1],cnt[k<<1|1]);
	int mid=(l+r)/2; t[k]=0;
	if(cnt[k<<1]>cnt[k]) t[k]+=mid-l+1; else t[k]+=t[k<<1];
	if(cnt[k<<1|1]>cnt[k]) t[k]+=r-mid; else t[k]+=t[k<<1|1];
}
void pushdown(int k){
	if(lazy[k]){
		cnt[k<<1]+=lazy[k];
		lazy[k<<1]+=lazy[k];
		cnt[k<<1|1]+=lazy[k];
		lazy[k<<1|1]+=lazy[k];
		lazy[k]=0;
	}
}
void updata(int L,int R,int v,int l,int r,int k){
	if(L<=l && r<=R){
		cnt[k]+=v;
		lazy[k]+=v;
		return;
	}
	pushdown(k);
	int mid=(l+r)/2;
	if(L<=mid) updata(L,R,v,l,mid,k<<1);
	if(R>mid) updata(L,R,v,mid+1,r,k<<1|1);
	pushup(l,r,k);
}
int main()
{
	int n,m;
	cin>>n>>m;
	while(m--){
		int l,r,c;
		cin>>l>>r>>c;
		updata(l,r,c,1,n,1);
		if(cnt[1]) cout<<n<<endl;
		else cout<<t[1]<<endl;
	}
	return 0;
} 

面积

顾名思义,扫描线是指一条平行于坐标轴的直线,从 \(-\infin\)\(\infin\) 沿着横轴或者纵轴的方向扫描,哪个方向其实无伤大雅,我们这里就向着 y 轴正方向扫描。那么我们存下每个矩形上面的边和下面的边的①纵坐标②左端点的横坐标③右端点的横坐标④它是下面的边还是上面的边(分别用1和-1表示)。因此这些矩形就被简化成了 2n 条平行于横轴的带正负权的线段,我们按着扫描线扫到它的先后顺序处理每一条线段。为了方便我们把①②③④的值分别记作 \(x,l,r,p\),我们把整个 x 轴看成一个数组 \(a\),初始时 \(a_{-\infin\sim\infin}=0\),对于当前我们扫到的线段,把 \(a_{l\sim r-1}+=p\)。为什么是 \(r-1\)呢?因为我们一般的数组它是一个单元一个单元的小盒子,但是我们的坐标它是一条线,我们把相邻坐标之间的位置看成数组的单元,那么我们就可以得出一个 l~r 的线段它是覆盖了 r-l 个单元的,为了方便我们就把它们的下标分别记成 l~r-1,那么反过来数组 \(a_{l}\sim a_r\) 它是对应的坐标 \(l\sim r+1\) 的。

我们在把这个线段对应在 \(a\) 上修改之前,先记录一下当前 \(a\) 中有多少个单元被覆盖,这就是我们刚才第一部分的内容。这个值 \(\times\) 上一条线段到这一条线段之间的距离就等于扫描线在扫描两条线段之间扫过的面积,你自己想一想,是不是这个道理。为什么我们只需要处理线段就行了呢?因为只有到了一条线段,扫描线上有面积的部分的覆盖长度才会发生改变。

那么当数据大了或者坐标出现负数时,我们就不可能存的下这样一个所谓的 \(a\) 数组,因此我们不可避免地需要进行离散化。显然我们只需要对纵坐标(\(x\) 的值)离散化,那么我们把所有的 \(x\) 排个序,用 \(Map_v\) 代表 \(v\) 在数组 \(b\) 中的位置下标,用 \(b_{v'}\) 代表离散化后的数 \(v'\) 对应的坐标。这时数组 \(a\) 的一个单元就不是两个相邻坐标之间的位置了,而是两条纵坐标相邻的线段之间的位置了,而离散化之后的 \(a_{l\sim r}\) 实际上的坐标应该是 \(b_l\sim b_{r+1}\),这里请理解一下。到此我们面积部分就讲解完毕了。

暂无代码

周长

对于周长,我们就应该把每一个矩形的左、右边存到一个数组 \(seg1\) 中,上、下边存到一个数组 \(seg2\) 中,存发根上面差不多;大体上就是一根扫描线从左扫到右,依次处理左右边的线段,在 y 轴上看覆盖了多少个,假如在加入了一条线段之后覆盖了 \(c1\) 个单元,没有加入这条线段前覆盖了 \(c2\) 个单元,那么这条边对周长产生的贡献就是 \(|c1-c2|\),到最后我们就计算完了并的纵向的周长,横向的周长,我们就同理地用一根从下扫到上的扫描线做一遍就好了。

这里需要注意的是当两条线段完全重叠或者部分重叠时,我们的程序可能会把它们按不重叠来算,你可以手动模拟一下这个程序执行过程,那么我们的解决方案就是把原来是矩形左边的线段们(\(p=1\))先处理,后处理那些原来是矩形右边的线段(\(p=-1\)),这样我们的程序就会认为是那些新的矩形边先加入进来,从而就当成一个重叠来计算了。这里可能还要靠你自己画个图想一下,光讲不大好讲通。因此我们把线段按 \(x\) 从小到大排序的时候的比较函数就应该多加一句:当 \(x\) 的值相等时,返回 \(p\) 由大及小。

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5; int whi=0;
long long t[N<<2],cnt[N<<2],lazy[N<<2];
map<int,int> Map; vector<int> vec[2];
struct node {
    int x,l,r,p;
} seg1[N],seg2[N];
bool equal(node a,node b){
    return a.x==b.x && a.l==b.l && a.r==b.r;
}
bool cmp(node a,node b){
    if(a.x==b.x) return a.p>b.p;
    return a.x<b.x;
}
int Unique(node a[N],int SIZE){
    sort(a+1,a+SIZE+1,cmp);
    int tot=0;
    for(int i=1;i<=SIZE;i++)
        if(!equal(a[i],a[i-1]))
            a[++tot]=a[i];
    return tot;
}
void pushup(int l,int r,int k){
	cnt[k]=min(cnt[k<<1],cnt[k<<1|1]);
	int mid=(l+r)/2; t[k]=0;
	if(cnt[k<<1]>cnt[k]) t[k]+=vec[whi][mid+1]-vec[whi][l]; else t[k]+=t[k<<1];
	if(cnt[k<<1|1]>cnt[k]) t[k]+=vec[whi][r+1]-vec[whi][mid+1]; else t[k]+=t[k<<1|1];
}
void pushdown(int l,int r,int k){
	if(!lazy[k]) return;
	cnt[k<<1]+=lazy[k];
	lazy[k<<1]+=lazy[k];
	cnt[k<<1|1]+=lazy[k];
	lazy[k<<1|1]+=lazy[k]; 
	lazy[k]=0;
}
void updata(int L,int R,int V,int l,int r,int k){
    if(L<=l && r<=R){
        cnt[k]+=V;
        lazy[k]+=V;
        return;
    }
    pushdown(l,r,k);
    int mid=(l+r)/2;
    if(L<=mid) updata(L,R,V,l,mid,k<<1);
    if(R>mid) updata(L,R,V,mid+1,r,k<<1|1);
    pushup(l,r,k);
}
int main()
{
    int n,x1,y1,x2,y2,num=0,num1=0,num2=0;
    long long ans=0,pre,tmp;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x1>>y1>>x2>>y2;
        vec[0].push_back(x1),vec[1].push_back(y1),vec[0].push_back(x2),vec[1].push_back(y2);
        seg1[++num]=(node){x1,y1,y2,1};
        seg2[num]=(node){y1,x1,x2,1};
        seg1[++num]=(node){x2,y1,y2,-1};
        seg2[num]=(node){y2,x1,x2,-1};
    }
    sort(vec[1].begin(),vec[1].end()); for(int i=0;i<vec[1].size();i++) Map[vec[1][i]]=i;
    
    sort(seg1+1,seg1+num+1,cmp);
	
	memset(t,0,sizeof(t)); pre=0; memset(cnt,0,sizeof(cnt)); memset(lazy,0,sizeof(lazy)); whi++;
    for(int i=1;i<=num;i++){
        updata(Map[seg1[i].l],Map[seg1[i].r]-1,seg1[i].p,0,vec[1].size()-2,1);
        if(cnt[1]>0) tmp=vec[1].back()-vec[1].front(); else tmp=t[1];
        ans+=abs(tmp-pre);
        pre=tmp;
    }
    
    sort(seg2+1,seg2+num+1,cmp);
	
    sort(vec[0].begin(),vec[0].end()); for(int i=0;i<vec[0].size();i++) Map[vec[0][i]]=i;
	memset(t,0,sizeof(t)); pre=0; memset(cnt,0,sizeof(cnt)); memset(lazy,0,sizeof(lazy)); whi--;
    for(int i=1;i<=num;i++){
        updata(Map[seg2[i].l],Map[seg2[i].r]-1,seg2[i].p,0,vec[0].size()-2,1);
        if(cnt[1]>0) tmp=vec[0].back()-vec[0].front(); else tmp=t[1];
        ans+=abs(tmp-pre);
        pre=tmp;
    }
    cout<<ans<<endl;
    return 0;
}

注:这个代码是给定的是左下角坐标和右上角坐标,题目链接是 https://www.luogu.com.cn/problem/P1856。

posted @ 2021-06-30 18:43  pengyule  阅读(39)  评论(0编辑  收藏  举报