目录
luogu P3372线段树模板
#include<bits/stdc++.h>
#define MAXN 1000001
#define ll long long
using namespace std;
unsigned ll n,m,a[MAXN],ans[MAXN<<2],tag[MAXN<<2];
inline ll ls(ll x)
{
return x<<1;
}
inline ll rs(ll x)
{
return x<<1|1;
}
void scan()
{
cin>>n>>m;
for(ll i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
}
}
inline void push_up(ll p)
{
ans[p]=ans[ls(p)]+ans[rs(p)];
}
void build(ll p,ll l,ll r)
{
tag[p]=0;
if(l==r)
{
ans[p]=a[l];
return ;
}
ll mid=(l+r)>>1;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
push_up(p);
}
inline void f(ll p,ll l,ll r,ll k)
{
tag[p]=tag[p]+k;
ans[p]=ans[p]+k*(r-l+1);
}
inline void push_down(ll p,ll l,ll r)
{
ll mid=(l+r)>>1;
f(ls(p),l,mid,tag[p]);
f(rs(p),mid+1,r,tag[p]);
tag[p]=0;
}
inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k)
{
if(nl<=l&&r<=nr)
{
ans[p]+=k*(r-l+1);
tag[p]+=k;
return ;
}
push_down(p,l,r);
ll mid=(l+r)>>1;
if(nl<=mid)
{
update(nl,nr,l,mid,ls(p),k);
}
if(nr>mid)
{
update(nl,nr,mid+1,r,rs(p),k);
}
push_up(p);
}
ll query(ll q_x,ll q_y,ll l,ll r,ll p)
{
ll res=0;
if(q_x<=l&&r<=q_y)
{
return ans[p];
}
ll mid=(l+r)>>1;
push_down(p,l,r);
if(q_x<=mid)
{
res+=query(q_x,q_y,l,mid,ls(p));
}
if(q_y>mid)
{
res+=query(q_x,q_y,mid+1,r,rs(p));
}
return res;
}
int main()
{
ll a1,b,c,d,e,f;
scan();
build(1,1,n);
while(m--)
{
scanf("%lld",&a1);
switch(a1)
{
case 1:
{
scanf("%lld%lld%lld",&b,&c,&d);
update(b,c,1,n,1,d);
break;
}
case 2:
{
scanf("%lld%lld",&e,&f);
printf("%lld\n",query(e,f,1,n,1));
break;
}
}
}
return 0;
}
洛⾕ P3801 红⾊的幻想乡
n*m(n, m <= 105)的矩形区域中,进⾏如下两个操作: 1 x y : 蕾⽶莉亚站在坐标(x,y)的位置向四个⽅向释放⽆限的 红雾,当两个红雾相遇在同⼀点,则会沉降消失。 2 x1 y1 x2 y2 : 询问左上点为(x1,y1),右下点为(x2,y2)的矩 形范围内,被红雾遮盖的地区的数量
- ⼀块n*m⼦矩阵中,施加 过红雾的⾏数为x、列数为y,则相遇抵消的点数 为x*y, 则红雾覆盖的点 数为 。
洛⾕ P2824 [HEOI2016/TJOI2016]排序
给出⼀个1到n的全排列,现在对这个全排列序列进⾏m次局部排序, 排序分为两种:
1:(0,l,r)表示将区间[l,r]的数字升序排序;
2:(1,l,r)表示将区间[l,r]的数字降序排序。
最后询问第q位置上的数字。
- 二分q点的答案ans,原序列中小于等于ans的看成0,大于ans的看成1,这样区间排序就变成区间求和然后区间修改了
洛⾕ P4198 楼房重建
小 A 的楼房外有一大片施工工地,工地上有 N 栋待建的楼房。每天,这片工地上的房子拆了又建、建了又拆。他经常无聊地看着窗外发呆,数自己能够看到多少栋房子。
为了简化问题,我们考虑这些事件发生在一个二维平面上。小 A 在平面上 (0,0)(0,0)(0,0) 点的位置,第 iii 栋楼房可以用一条连接 (i,0) 和 的线段表示,其中 为第 i 栋楼房的高度。如果这栋楼房上任何一个高度大于 0 的点与 (0,0) 的连线没有与之前的线段相交,那么这栋楼房就被认为是可见的。
施工队的建造总共进行了 M 天。初始时,所有楼房都还没有开始建造,它们的高度均为 0。在第 i 天,建筑队将会将横坐标为 的房屋的高度变为 (高度可以比原来大—修建,也可以比原来小—拆除,甚至可以保持不变—建筑队这天什么事也没做)。请你帮小 A 数数每天在建筑队完工之后,他能看到多少栋楼房?
- 首先,把所有的高度a[i]除以坐标i,得到从(0,0)望过去的直线斜率,判断楼与楼是否遮挡直接比较斜率大小即可
- ans[k]表示节点k区间中的“单调递增序列”度(从左到右能看到的楼的数量)
- 考虑由左右儿子的ans求出父亲节点的ans
- 左儿子的ans都可以对父亲节点的ans做出贡献,而右儿子的ans则可能已经被左儿子中的楼遮挡,设左儿子的最大斜率为h,若能用求出节点k区间中大于h的楼的“单调递增序列”度,则
- 那么如何写这个calc(k,h)函数呢?还是分情况讨论
- 若左子树中没有大于h的,则返回
- 若左子树中有大于h的,则全部对有贡献,右子树对ans[k]贡献的答案中都是大于h的楼房,则这个答案全部对有贡献,返回的就是
- 这样调用一次函数复杂度是,总共调用次,总复杂度
李超线段树
用于解决区间添加一次函数、单点查询所有一次函数最大值的问题
洛⾕ P4097 [HEOI2013]Segment
要求在平⾯直⻆坐标系下维护两个操作:
- 在平⾯上加⼊⼀条线段。记第 i 条被插⼊的线段的标号为 i
- 给定⼀个数 k, 询问与直线 x = k 相交的线段中,交点最靠上的线段的编号。
- 维护一个线段树,线段树每个节点维护对应区间的“优势线段”,即区间中点对应值最大的线段
- 当在一个节点上添加一条线段时,分情况讨论:
- 当新线段完全被原优势线段覆盖时,新线段没用,不做修改
- 当新线段完全覆盖原优势线段时,原优势线段没用,将整个子树优势线段改为新线段
- 当两者有交点时,新优势线段为两者中中点对应值较大的一条,另一条递归添加到交点所在的一侧儿子中
- 写着写着可以发现——这个标记可以永久化
- 标记永久化:标记不进行下传,查询某一点时,取根到该叶子路径上所有标记的最优值
动态开点线段树
我们来观察一下普通线段树的左儿子和右儿子的表示方法
左儿子:p<<1
右儿子:p<<1|1
这样,虽然我们可以直接算出左右儿子,比较方便,但是,这样也浪费了大量的空间
链式储存法:即对一个节点维护其左右儿子的编号,指向它的左右儿子,这样,只有当访问某个节点时,我们才需要建立该节点,减小了空间
//单点修改
int insert(int k,int l,int r,int x)
{
if(!k)
{
k=++tot;
}
if(l==r)
{
return sum[k]=a[l],k;
}
if(x<=mid)
{
ls[k]=insert(ls[k],l,mid,x);
}
else
{
re[k]=insert(re[k],mid+1,r,x);
}
sum[k]=sum[ls[k]]+sum[rs[k]];
return k;
}
//区间查询
int query(int k,int l,int r,int x,int y)
{
if(x<=l&&r<=y)
{
return sum[k];
}
int mid=(l+r)>>1,res=0;
if(x<=mid)
{
res+=query(ls[k],l,mid,x,y);
}
if(y>mid)
{
res+=query(rs[k],mid+1,r,x,y);
}
return res;
}
可持久化线段树(主席树)
对于一个经过m次修改操作的线段树,可持久化线段树能维护其所有的m个历史版本
我们发现,每次修改只会修改线段树中的个节点,因此我们可以只对这个点开新节点,其余沿用旧树上的点
//单点修改
void update(int &o,int l,int r,int last,int p,int v)
{
o=++tot;
ls[o]=ls[last];
rs[o]=rs[last];
if(l==r)
{
a[o]=v;
return ;
}
int m=(l+r)>>1;
if(p<=m)
{
update(ls[o],l,m,ls[last],p,v);
}
else
{
update(rs[o],m+1,r,rs[last],p,v);
}
}
查询无区别
静态区间第k小
给定n个数的序列,m次查询区间[l,r]的第k小值,n,m<=2e5
权值数组:一个数组a满足a[i]值为i的数的个数
权值线段树:维护权值数组的线段树
假如我们获得了[l,r]的权值线段树,查询时若左儿子区间和>=k则递归左儿子,若<k则递归右儿子即可
如何求[l,r]的权值线段树
可持久化线段树维护的权值线段树
即为[l,r]的权值线段树
//查询
int query(int s,int t,int l,int r,int x)
{
if(l==r)
{
return l;
}
int cnt=sum[ls[t]]-sum[ls[s]];
if(cnt>=x)
{
return query(ls[s],ls[t],l,mid,x);
}
else
{
return query(rs[s],rs[t],mid+1,r,x-cnt);
}
}
将两棵动态开点线段树合并到一起(对应位置相加)
如果一方对应位置没有点的话就用另一方的,都有就递归下去
复杂度
//合并
int merge(int p1,int p2,int l,int r)//表示将树p2的信息合并到树p1上
{
if(!p1||!p2)
{
return p1+p2;
}
if(l==r)
{
s[p1]=s[p1]+s[p2];
return p1;
}
int mid=(l+r)>>1;
ls[p1]=merge(ls[pl],ls[p2],l,mid);
rs[p1]=merge(rs[p1],rs[p2],mid+1,r);
s[p1]=s[ls[pl]]+s[rs[pl]];
return p1;
}
启发式合并
定义:将n个集合,合并集合A,B时,将较小的集合中的元素一个个插入到较大集合中,单次复杂度为
复杂度:
证明:每个数在跟比它所在集合更大的集合合并时会产生1的贡献,所在集合大小至少翻一倍,最多产生次贡献
黑科技
用于解决一些插入容易实现而删除难以实现的问题
用一个的代价将问题转化成只有插入
线段树分治
一个n个节点的图,m次操作,每次插入一条边,删除一条边。Q次查询,每次查询图是不是二分图。n,m<=1e5
- 线段树分治实际上是一种维护时间区间的数据结构,同样是利用线段树的分治性,让复杂度保证在了级别
- 开一棵维护操作时间的线段树,我们考虑如何维护一个操作影响的时间区间,假设一个操作影响的时间时[L,R],将其呈现在线段树上找这个询问所在时间点,把根到叶子路径上的所有影响计算一遍,就能得出这个询问的答案
- 每次递归整棵线段树,到达一个节点时执行该节点上的所有操作,离开时撤销