数据结构三

复健\(Day5\)

数据结构三

\(4.\)线段树和树状数组

线段树模板(维护和)

包含区间修改,区间查询

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 10010
using namespace std;

int a[maxn],tree[maxn<<2];
int add[maxn<<2];

void pushup(int rt)
{
	tree[rt]=tree[rt<<1]+tree[rt<<1|1];
}

void build(int rt,int l,int r)
{
	if(l==r)
	{
		tree[rt]=a[l];
		return;
	}
	int mid=(l+r)>>1;
	build(rt<<1,l,mid);
	build(rt<<1|1,mid+1,r);
	pushup(rt);
}

void pushdown(int rt,int l,int r)
{
	if(add[rt])
	{
		add[rt<<1]+=add[rt];
		add[rt<<1|1]+=add[rt];
		int mid=(l+r)>>1;
		tree[rt<<1]+=(mid-l+1)*add[rt];
		tree[rt<<1|1]+=(r-mid)*add[rt];
		add[rt]=0;
	}
}

void change(int rt,int l,int r,int ql,int qr,int v)
{
	if(ql<=l&&qr>=r)
	{
		add[rt]+=v;
		tree[rt]+=v*(r-l+1);
		return;
	}
	pushdown(rt,l,r);
	int mid=(l+r)>>1;
	if(ql<=mid) change(rt<<1,l,mid,ql,qr,v);
	if(qr>mid) change(rt<<1|1,mid+1,r,ql,qr,v);
	pushup(rt);
}

int query(int rt,int l,int r,int ql,int qr)
{
	if(ql<=l&&qr>=r)
	{
		return tree[rt];
	}
	pushdown(rt,l,r);
	int mid=(l+r)>>1;
	int ans=0;
	if(ql<=mid) ans+=query(rt<<1,l,mid,ql,qr);
	if(qr>r) ans+=query(rt<<1|1,mid+1,r,ql,qr);
	return ans;
}

int read()
{
	int ans=0,f=1;char i=getchar();
	while(i<'0'||i>'9'){if(i=='-') f=-1;i=getchar();}
	while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();}
	return ans*f;
}

int main()
{
	int n;
	n=read();
	for(int i=1;i<=n;i++) a[i]=read();
	build(1,1,n);
	int q;
	q=read();
	while(q--)
	{
		char op;
		int l,r,v;
		cin>>op;
		l=read();r=read();
		if(op=='C')
		{
			v=read();
			change(1,1,n,l,r,v);
		}
		else if(op=='Q') printf("%d\n",query(1,1,n,l,r));
	}
	return 0;
}

线段树扫描线问题

用于求矩形面积并和周长并的一种方法

以从下往上扫描面积并为例,我们发现只有每个矩形的上下两条边会对答案造成影响,我们运用差分的方法,下边权值为\(1\),上边权值为\(-1\),我们扫描至某个矩形时,其下边权值为\(1\),相当与插入,上边\(-1\)相当于删除。

我们用一个四元组\((y,x1,x2,1/-1)\)来表示一条线段,将\(y\)按从小到大排序就可以从下往上扫描了

然后我们是从下到上扫描的,通过记录\(y\)坐标就可以知道两条线段之间的纵坐标之差(高度差),而\(x\)坐标的需要统计影响范围,我们就把所有的横坐标\(x\)都取出来,然后将其去重离散化至\([1,n]\)中,每个区间\([i,i+1]\)表示的就是第\(i\)个横坐标和第\(i+1\)个横坐标之间的长度。

用一个数组\(w[i]\)记录每个区间被覆盖的次数(也就是我们一开始的四元组中的权值的和)

当从一个\(y\)扫描到下一个\(y\)时,看那些区间的\(w\)值不为\(0\)(即现在还未被删除),然后将这些区间的长度加起来,乘以两个\(y\)的差值,就得到了这一段扫描距离得到的面积

我们维护其中的区间操作时需要用到线段树,而该题目又常为数量不大,值大的类型,故用离散线段树来维护

这里简单介绍一下离散线段树

\(1.\)其区间边界是有重叠的:对于一个根\([l,r]\),其两个子树代表的区间是\([l,mid]\)\([mid,r]\)

\(2.\)其叶子宽度为\(2\)(因为它始终代表一个区间)

\(3.\)便于维护坐标值、区间长度

struct Tree
{
	int l,r,len;//区间有效长度
	int cnt;//区间覆盖次数
}tr[N<<3];

int n,X[N];//矩形的横坐标

void build(int rt,int l,int r)
{
	tr[rt].l=X[l],tr[rt].r=X[r];
	if(r==l+1) return;
	int mid=(l+r)>>1;
	build(rt<<1,l,mid);
	build(rt<<1|1,mid,r);
}

\(lower\_bound\)返回值是一个迭代器,对于有序数组或者容器,返回指向大于等于\(key\)的第一个值,头文件是\(algorithm\)

\(eg.int\) \(a[]={2,5,6};lower\_bound(a,a+4,3)\),最后我们返回的是\(a[1](=4)\)所在的地址,若要得其下标,我们减去数组名\(a\)(代表其首地址)就可以了

扫描线模板(\(lougu5490\))

时间复杂度为\(O(nlogn)\)

算法流程

  1. 扫描线切分区块
  2. 离散线段树维护\(X\)坐标(从左往右扫就是\(Y\)坐标)和区块的有效长度,自顶向下分裂区间,自底向上拼凑有效长度,标记加入或者减去一个矩形对有效长度的贡献
  3. 交互性面积并=\(sum(\)区块高度\(X\)区块有效长度\()\)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 400010
using namespace std;

struct line
{
	int y,x1,x2;
	int flg;
	line(){}
	line(int y,int x1,int x2,int flg):y(y),x1(x1),x2(x2),flg(flg){}
	bool operator < (const line &rhs) const{
		return y<rhs.y;
	}
}Line[maxn<<1];

struct Tree
{
	int l,r,len;
	int cnt;
}tree[maxn<<3];//线段树开8倍空间的原因是可能会访问到叶节点的子空间

int n,X[maxn<<1];

void pushup(int rt)
{
	if(tree[rt].cnt) tree[rt].len=tree[rt].r-tree[rt].l;
	else tree[rt].len=tree[rt<<1].len+tree[rt<<1|1].len;
}

inline void build(int rt,int l,int r)
{
	tree[rt].l=X[l],tree[rt].r=X[r];
	if(r==l+1) return;
	int mid=(l+r)>>1;
	build(rt<<1,l,mid);
	build(rt<<1|1,mid,r);
}

inline void change(int rt,int l,int r,int v)
{
	if(l>=tree[rt].r||r<=tree[rt].l) return;
	if(l<=tree[rt].l&&r>=tree[rt].r)
	{
		tree[rt].cnt+=v;
		pushup(rt);
		return;
	}
	int mid=(l+r)>>1;
	if(l<=mid) change(rt<<1,l,r,v);
	if(r>=mid) change(rt<<1|1,l,r,v);
	pushup(rt);
}

inline int read()
{
	int ans=0,f=1;char i=getchar();
	while(i<'0'||i>'9'){if(i=='-') f=-1;i=getchar();}
	while(i>='0'&&i<='9'){ans=ans*10+i-'0';i=getchar();}
	return ans*f;
}

int main()
{
	n=read();
	for(int i=1;i<=n;i++)
	{
		int x1,x2,y1,y2;
		x1=read();y1=read();x2=read();y2=read();
		Line[i]=line(y1,x1,x2,1);Line[n+i]=line(y2,x1,x2,-1);
		X[i]=x1,X[n+i]=x2;
	}
	n<<=1;
	sort(Line+1,Line+n+1);
	sort(X+1,X+n+1);
	build(1,1,n);
	long long ans=0;
	for(int i=1;i<n;i++)
	{
		change(1,Line[i].x1,Line[i].x2,Line[i].flg);
		ans+=(long long)(Line[i+1].y-Line[i].y)*tree[1].len;
	}
	printf("%lld\n",ans);
	return 0;
}

\(P1904\)天际线

https://www.luogu.com.cn/problem/P1904

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#define maxn 10010
using namespace std;

struct line
{
	int x,y1,y2;
	int flg;
	line(){}
	line(int x,int y1,int y2,int flg):x(x),y1(y1),y2(y2),flg(flg){}
	bool operator < (const line &rhs) const{
		return x<rhs.x;
	}
}L[maxn<<1];

int n,Y[maxn<<1];

struct Tree
{
	int l,r;
	int cnt;
	int len;
}tree[maxn<<3];

void pushup(int rt)
{
	if(tree[rt].cnt) tree[rt].len=tree[rt].r-tree[rt].l;
	else tree[rt].len=tree[rt<<1].len+tree[rt<<1|1].len;
}

void build(int rt,int l,int r)
{
	tree[rt].l=Y[l],tree[rt].r=Y[r];
	if(r==l+1) return;
	int mid=(l+r)>>1;
	build(rt<<1,l,mid);
	build(rt<<1|1,mid,r);
}

void change(int rt,int l,int r,int v)
{
	if(r<=tree[rt].l||l>=tree[rt].r) return;
	if(l<=tree[rt].l&&r>=tree[rt].r)
	{
		tree[rt].cnt+=v;
		pushup(rt);
		return;
	}
	change(rt<<1,l,r,v);
	change(rt<<1|1,l,r,v);
	pushup(rt);
}

int main()
{
	int l,h,r;
	while(scanf("%d%d%d",&l,&h,&r)!=EOF)
	{
		L[++n]=line(l,0,h,1);
		Y[n]=0;
		L[++n]=line(r,0,h,-1);
		Y[n]=h;
	}
	sort(L+1,L+n+1);
	sort(Y+1,Y+n+1);
	build(1,1,n);
	int ans=0,ansf=0;
	for(int i=1;i<=n;i++)
	{
		while(L[i].x==L[i+1].x&&i<n)
		{
			change(1,0,L[i].y2,L[i].flg);
			i++;
		}
		change(1,0,L[i].y2,L[i].flg);
		ans=tree[1].len;
		if(ans==ansf) continue;
		else
		{
			printf("%d %d ",L[i].x,ans);
			ansf=ans;
		}
	}
	return 0;
}

可以采用扫描线的方法来做,不过有一个点要注意,可能有很多个建筑的横坐标相同,所以我们在遇到相同横坐标的建筑时,需要不断地进行线段覆盖,即\(73\)行代码处所示的\(while\)函数即是完成该功能的,否则可能会出错

posted on 2023-08-03 14:55  dolires  阅读(14)  评论(0)    收藏  举报

导航