数据结构三
复健\(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)\)
算法流程:
- 用扫描线切分区块
- 用离散线段树维护\(X\)坐标(从左往右扫就是\(Y\)坐标)和区块的有效长度,自顶向下分裂区间,自底向上拼凑有效长度,标记加入或者减去一个矩形对有效长度的贡献
- 交互性面积并=\(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\)函数即是完成该功能的,否则可能会出错