介绍
1.树状数组
在做题时,我们经常遇到给出一个序列,然后在序列上进行一些操作,并在操作后询问一些关于序列的题。一般有以下几种:
- 单点修改,区间查询
- 区间修改,单点查询
- 区间修改,区间查询
如果是暴力的话,那么单点操作就是 O ( 1 ) O(1) O(1),区间操作就是 O ( n ) O(n) O(n),如果用了树状数组,那么每次操作都是 O ( l o g n ) O(log_n) O(logn)
树状数组就不细说了,真的想了解可以看这,下面放一下代码:
void modify(int u,int x)
{
for(int i=u;i<=n;i+=lowbit(i))
a[i]+=x;
}
void query(int r)
{
int rt=0;
for(int i=r;i;i-=lowbit(i))
rt+=a[i];
return rt;
}
int query(int l,int r)
{
return query(r)-query(l-1);
}
//其中lowbit(i)返回i的最低位代表的数,可以通过(i&-i)计算得到(采用补码的机子上)。
P3374 【模板】树状数组 1
P3368 【模板】树状数组 2
提示:在做序列有关的题时,要灵活运用差分,有时候甚至要用二次差分,三次差分等,但这样做能大大降低你的代码量,思维量等。
线段树
先看模板:P3372 【模板】线段树 1
线段树的实现:
线段树主要是把一段大区间 平均地划分 成两段小区间进行维护,再用小区间的值来更新大区间。这样既能保证正确性,又能使时间保持在
log
\log
log 级别(因为这棵线段树是平衡的)。也就是说,一个
[
L
,
R
]
[L,R]
[L,R] 的区间会被划分成
[
L
,
⌊
L
+
R
2
⌋
]
\left[L,\left\lfloor\frac{L+R}{2}\right\rfloor\right]
[L,⌊2L+R⌋]和
[
⌊
L
+
R
2
⌋
+
1
,
R
]
\left[\left\lfloor\frac{L+R}{2}\right\rfloor+1,R\right]
[⌊2L+R⌋+1,R] 这两个小区间进行维护,直到
L
=
R
L = R
L=R。
下图就是一棵 $[1,10] $的线段树的分解过程(相同颜色的节点在同一层)
懒标记
标记的含义本区间已经被更新过了,但是子区间却没有被更新过,被更新的信息是什么(区间求和只用记录有没有被访问过,而区间加减乘除等多种操作的问题则要记录进行的是哪一种操作)。
这里再引入两个很重要的东西: 相对标记 和 绝对标记 。
相对标记:指的是可以共存的标记,且打标记的顺序与答案无关,即标记可以叠加。 比如说给一段区间中的所有数字都
+
a
+a
+a ,我们就可以把标记叠加一下,比如上一次打了一个
+
1
+1
+1 的标记,这一次要给这一段区间
+
5
+5
+5,那么就把
+
1
+1
+1的标记变成
+
6
+6
+6。
绝对标记:是指不可以共存的标记,每一次都要先把标记下传,再给当前节点打上新的标记。这些标记不能改变次序,否则会出错。 比如说给一段区间的数字重新赋值,或是给一段区间进行多种操作
#include<iostream>
#include<cstdio>
#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;
}
值域线段树
值域线段树不是一种新的数据结构,而是线段树的一种用法:我们将线段树的每个不同位置当成某个整 数变量的取值,这样线段树其实可以充当一个可重集。如果我们在每个节点处维护一下该区间上数的个 数,我们就可以在线段树上通过一种类似二分的方式找到整棵线段树的第 K K K 大的数。值域线段树是支持 “减法的”。如果我们将数组中的数从左到右一个一个加到值域线段树中,并且保留下各个时刻的值域线 段树版本,那么我们在“做完减法的值域线段树上”二分就可以求出区间第 K K K 小。
动态开点
如果我们是将线段树作为值域线段树,或者线段树一段区间的值不用建子节点就可以直接算出来,那么
我们还可以动态开节点。
当值域范围为
1
0
9
10^9
109 这样很大时,无法直接建树,我们使用动态开点可以规避空间问题。
在1到1e9值域内,区间操作,L R val ,表示 L-R中每个值出现了val次。单点查询,询问pos值出现了几次
struct node
{
node *ls;
node *rs;
long long sum,tag;
}*root,*zero,pool[N*60],*tail=pool;
node *newnode(long long x)
{
node *nd=++tail;
nd->ls=nd->rs=zero;
nd->sum=x;
nd->tag=0;
return nd; }
void push_down(node *nd)
{
if(nd->tag)
{
if(nd->ls==zero)
nd->ls=newnode(nd->tag);
if(nd->rs==zero)
nd->rs=newnode(nd->tag);
} }
nd->ls->sum=nd->rs->sum=nd->tag;
nd->ls->tag=nd->rs->tag=nd->tag;
nd->tag=0;
void modify(node *nd,long long l,long long r,const long L,const long long R,long
long data)
{
if(l!=r) push_down(nd);
if(L<=l&&R>=r)
{
nd->sum=data;
nd->tag=data;
return;
}
long long mid=(l+r)/2;
if(L<=mid)
{
if(nd->ls==zero) nd->ls=newnode(0);
modify(nd->ls,l,mid,L,R,data);
}
if(R>mid) {
if(nd->rs==zero) nd->rs=newnode(0);
modify(nd->rs,mid+1,r,L,R,data);
}
}
long long query(node *nd,long long l,long long r,const long long pos)
{
if(nd->tag) return nd->tag;
if(l==r)
{
return nd->sum;
}
push_down(nd);
long long mid=(l+r)/2;
if(pos<=mid)
{
if(nd->ls==zero) return 0;
return query(nd->ls,l,mid,pos);
}
if(pos>mid)
{
if(nd->rs==zero) return 0;
return query(nd->rs,mid+1,r,pos);
} }
int main() {
zero=++tail;
zero->sum=0;
zero->tag=0;
zero->ls=zero->rs=zero;
root=newnode(0);
}
P3939 数颜色(这题可以被 vector 水过去)
3.ST表 (RMQ)
给定一个序列 a a a,再给出 m m m 个询问:求区间 [ L , R ] [L,R] [L,R] 的最小值/最大值/gcd…,要求每个询问 O ( 1 ) O(1) O(1) 回答。
做法:用
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示区间
[
i
,
i
+
2
j
−
1
]
[i,i+2^j-1]
[i,i+2j−1] 的最大值,转移:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
−
1
]
,
f
[
i
+
(
1
<
<
j
−
1
)
]
[
j
−
1
]
)
f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1])
f[i][j]=max(f[i][j−1],f[i+(1<<j−1)][j−1]),这个可以预处理,询问直接找两个可以覆盖
[
L
,
R
]
[L,R]
[L,R] 的区间取
m
a
x
max
max 就行。
代码:
int f[MAXN][21];
for (int i = 1; i <= n; ++i)
f[i][0] = read();
for (int i = 1; i <= 20; ++i)
for (int j = 1; j + (1 << i) - 1 <= n; ++j)
f[j][i] = max(f[j][i - 1], f[j + (1 << (i - 1))][i - 1]);
for (int i = 0; i < m; ++i)
{
int l = read(), r = read();
int s = Log2[r - l + 1];
printf("%d\n", max(f[l][s], f[r - (1 << s) + 1][s]));
}
LCA
P3379 【模板】最近公共祖先(LCA)
和 RMQ 有点像,
f
[
i
]
[
j
]
f[i][j]
f[i][j] 表示
i
i
i 的
2
j
2^j
2j 级祖先,递推式就是
f
[
i
]
[
j
]
=
f
[
f
[
i
]
[
j
−
1
]
]
[
j
−
1
]
f[i][j]=f[f[i][j-1]][j-1]
f[i][j]=f[f[i][j−1]][j−1]。然后倍增往上跳就行了。
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
int nxt[200010],head[200010],tov[200010];
int n,m,x,y,tot,s;
int f[100010][25];
int d[100010];
void add(int u,int f)
{
nxt[++tot]=head[u];head[u]=tot;tov[tot]=f;
nxt[++tot]=head[f];head[f]=tot;tov[tot]=u;
}
inline ll read()
{
ll x=0,f=1;
char c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c<='9'&&c>='0'){x=10*x+(c-'0');c=getchar();}
return x*f;
}
void qwe(int u,int father)
{
d[u]=d[father]+1;
for(int i=0;i<20;i++) f[u][i+1]=f[f[u][i]][i];
for(int e=head[u];e!=0;e=nxt[e])
{
int v=tov[e];
if(v==father) continue;
f[v][0]=u;
qwe(v,u);
}
}
int LCA(int x,int y)
{
if(d[x]<d[y]) swap(x,y);
for(int i=20;i>=0;i--)
{
if(d[f[x][i]]>=d[y]) x=f[x][i];
if(x==y)
return x;
}
for(int i=20;i>=0;i--)
if(f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
return f[x][0];
}
int main()
{
n=read();m=read();s=read();
for(int i=1;i<n;i++)
{
x=read();y=read();
add(x,y);
}
qwe(s,0);
for(int i=1;i<=m;i++)
{
x=read();y=read();
printf("%d\n",LCA(x,y));
}
return 0;
}
树链剖分
P3384 【模板】重链剖分/树链剖分
定义:在一棵有根树上,我们称一个节点
u
u
u 的子节点
v
v
v 为节点
u
u
u 的重儿子当且仅当v是所有子节点中大小最 大的(如果有多个,则随便选定一个),其余节点称为轻儿子。如果我们对于所有内部节点(有儿子的 节点)都选择一个节点作为其重儿子,那么我们就得到了一个轻重链划分,我们称连接节点及其重儿子 的边为重边,反之则为轻边,一条由重边组成的链称为重链。
性质:树的任意一条路径都可以表示成
O
(
log
n
)
O(\log_n)
O(logn) 条重链和轻边组成。
我们只需要证明任意一个节点到根节点的路径最多需要
O
(
log
n
)
O(\log_n)
O(logn) 条轻边即可。我们考虑从某个节点向上走,每走过一条轻边到达一个点,该点所代表的子树大小至少翻倍,所以要走到根节点,至多经过
O
(
l
o
g
n
)
O(log_n)
O(logn) 条轻边。
我们可以通过调整DFS时子节点的访问顺序,让它先走重儿子,这样任何一条重链就是连续的一段了。那么我们任意一条链(单点算作长度为1的重链)都可以由至多 l o g n log_n logn 条重链表示,从而只需要 l o g n log_n logn 个区间。(所以树链剖分弄出来的那个序就是一个特殊的DFS序)。
如下图所示(加粗的边表示重边,其余则为轻边)。
#include <bits/stdc++.h>
#define mid ((l+r)>>1)
#define ls x<<1
#define rs x<<1|1
#define lson ls,l,mid
#define rson rs,mid+1,r
#define len (r-l+1)
#define u(x) ans=(ans+x)%mod
using namespace std;
const int N = 2e5+5;
int n,m,r,mod,res;
int sum,beg[N],nex[N],to[N],a[N],new_a[N];
int t[N<<2],laz[N<<2];
int son[N],id[N],fa[N],cnt,dep[N],siz[N],top[N];
void add(int x,int y)
{
to[++sum]=y;
nex[sum]=beg[x];
beg[x]=sum;
}
void pushdown(int x,int le)
{
laz[ls] += laz[x];laz[rs] += laz[x];
t[ls] = (t[ls]+laz[x]*(le-(le>>1)))%mod;
t[rs] = (t[rs]+laz[x]*(le>>1))%mod;
laz[x] = 0;
}
void build(int x,int l,int r)
{
if(l==r)
{
t[x] = new_a[l];
if(t[x]>mod)t[x]%=mod;
return;
}
build(lson);build(rson);
t[x] = (t[ls]+t[rs])%mod;
}
int query(int x,int l,int r,int L,int R)
{
int ans = 0;
if(L<=l&&r<=R)return t[x]%mod;
else
{
if(laz[x])pushdown(x,len);
if(L<=mid)u(query(lson,L,R));
if(R>mid)u(query(rson,L,R));
}
return ans;
}
void upd(int x,int l,int r,int L,int R,int k)
{
if(L<=l&&r<=R)
{
laz[x]+=k;
t[x]+=k*len;
}
else
{
if(laz[x])pushdown(x,len);
if(L<=mid)upd(lson,L,R,k);
if(R>mid)upd(rson,L,R,k);
t[x]=(t[ls]+t[rs])%mod;
}
}
void dfs1(int x,int fax,int dp)
{
dep[x] = dp;fa[x] = fax;siz[x] = 1;
int zh_siz = -1;
for(int i = beg[x];i;i = nex[i])
{
int y = to[i];
if(y == fax)continue;
dfs1(y,x,dp+1);
siz[x] += siz[y];
if(siz[y]>zh_siz){zh_siz=siz[y];son[x]=y;}
}
}
void dfs2(int x,int topx)
{
id[x] = ++cnt;new_a[cnt] = a[x];top[x] = topx;
if(!son[x])return;
dfs2(son[x],topx);
for(int i = beg[x];i;i = nex[i])
{
int y = to[i];
if(y!=fa[x]&&y!=son[x])dfs2(y,y);
}
}
int qSum(int x,int y)
{
int ans = 0;
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]])swap(x,y);
u(query(1,1,n,id[top[x]],id[x]));
x = fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
return (ans+query(1,1,n,id[x],id[y]))%mod;
}
void updSum(int x,int y,int k)
{
k %= mod;
while(top[x]!=top[y])
{
if(dep[top[x]]<dep[top[y]])swap(x,y);
upd(1,1,n,id[top[x]],id[x],k);
x = fa[top[x]];
}
if(dep[x]>dep[y])swap(x,y);
upd(1,1,n,id[x],id[y],k);
}
int qSon(int x){return query(1,1,n,id[x],id[x]+siz[x]-1);}
void updSon(int x,int k){upd(1,1,n,id[x],id[x]+siz[x]-1,k);}
inline int rd()
{
int x=0,f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int main()
{
n = rd();m = rd();r = rd();mod = rd();
for(int i = 1;i <= n;i++)a[i] = rd();
for(int i = 1;i < n;i++)
{
int x = rd(),y = rd();
add(x,y);add(y,x);
}
dfs1(r,0,1);
dfs2(r,r);
build(1,1,n);
while(m--)
{
int op = rd(),x = rd(),y;
if(op == 1){y = rd();updSum(x,y,rd());}
else if(op == 2)printf("%d\n",qSum(x,rd()));
else if(op == 3)updSon(x,rd());
else printf("%d\n",qSon(x));
}
return 0;
}
平衡树
P3369 【模板】普通平衡树
具体看代码注释:
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<algorithm>
#include<climits>
typedef long long LL;
using namespace std;
int RD(){
int out = 0,flag = 1;char c = getchar();
while(c < '0' || c >'9'){if(c == '-')flag = -1;c = getchar();}
while(c >= '0' && c <= '9'){out = out * 10 + c - '0';c = getchar();}
return flag * out;
}
//第一次打treap,不压行写注释XD
const int maxn = 1000019,INF = 1e9;
//平衡树,利用BST性质查询和修改,利用随机和堆优先级来保持平衡,把树的深度控制在log N,保证了操作效率
//基本平衡树有以下几个比较重要的函数:新建,插入,删除,旋转
//节点的基本属性有val(值),dat(随机出来的优先级)
//通过增加属性,结合BST的性质可以达到一些效果,如size(子树大小,查询排名),cnt(每个节点包含的副本数)等
int na;
int ch[maxn][2];//[i][0]代表i左儿子,[i][1]代表i右儿子
int val[maxn],dat[maxn];
int size[maxn],cnt[maxn];
int tot,root;
int New(int v){//新增节点,
val[++tot] = v;//节点赋值
dat[tot] = rand();//随机优先级
size[tot] = 1;//目前是新建叶子节点,所以子树大小为1
cnt[tot] = 1;//新建节点同理副本数为1
return tot;
}
void pushup(int id){//和线段树的pushup更新一样
size[id] = size[ch[id][0]] + size[ch[id][1]] + cnt[id];//本节点子树大小 = 左儿子子树大小 + 右儿子子树大小 + 本节点副本数
}
void build(){
root = New(-INF),ch[root][1] = New(INF);//先加入正无穷和负无穷,便于之后操作(貌似不加也行)
pushup(root);//因为INF > -INF,所以是右子树,
}
void Rotate(int &id,int d){//id是引用传递,d(irection)为旋转方向,0为左旋,1为右旋
int temp = ch[id][d ^ 1];//旋转理解:找个动图看一看就好(或参见其他OIer的blog)
ch[id][d ^ 1] = ch[temp][d];//这里讲一个记忆技巧,这些数据都是被记录后马上修改
ch[temp][d] = id;//所以像“Z”一样
id = temp;//比如这个id,在上一行才被记录过,ch[temp][d]、ch[id][d ^ 1]也是一样的
pushup(ch[id][d]),pushup(id);//旋转以后size会改变,看图就会发现只更新自己和转上来的点,pushup一下,注意先子节点再父节点
}//旋转实质是({在满足BST的性质的基础上比较优先级}通过交换本节点和其某个叶子节点)把链叉开成二叉形状(从而控制深度),可以看图理解一下
void insert(int &id,int v){//id依然是引用,在新建节点时可以体现
if(!id){
id = New(v);//若节点为空,则新建一个节点
return ;
}
if(v == val[id])cnt[id]++;//若节点已存在,则副本数++;
else{//要满足BST性质,小于插到左边,大于插到右边
int d = v < val[id] ? 0 : 1;//这个d是方向的意思,按照BST的性质,小于本节点则向左,大于向右
insert(ch[id][d],v);//递归实现
if(dat[id] < dat[ch[id][d]])Rotate(id,d ^ 1);//(参考一下图)与左节点交换右旋,与右节点交换左旋
}
pushup(id);//现在更新一下本节点的信息
}
void Remove(int &id,int v){//最难de部分了
if(!id)return ;//到这了发现查不到这个节点,该点不存在,直接返回
if(v == val[id]){//检索到了这个值
if(cnt[id] > 1){cnt[id]--,pushup(id);return ;}//若副本不止一个,减去一个就好
if(ch[id][0] || ch[id][1]){//发现只有一个值,且有儿子节点,我们只能把值旋转到底部删除
if(!ch[id][1] || dat[ch[id][0]] > dat[ch[id][1]]){//当前点被移走之后,会有一个新的点补上来(左儿子或右儿子),按照优先级,优先级大的补上来
Rotate(id,1),Remove(ch[id][1],v);//我们会发现,右旋是与左儿子交换,当前点变成右节点;左旋则是与右儿子交换,当前点变为左节点
}
else Rotate(id,0),Remove(ch[id][0],v);
pushup(id);
}
else id = 0;//发现本节点是叶子节点,直接删除
return ;//这个return对应的是检索到值de所有情况
}
v < val[id] ? Remove(ch[id][0],v) : Remove(ch[id][1],v);//继续BST性质
pushup(id);
}
int get_rank(int id,int v){
if(!id)return 0;//若查询值不存在,返回;因为最后要减一排除哨兵节点,想要结果为-1这里就返回0
if(v == val[id])return size[ch[id][0]] + 1;//查询到该值,由BST性质可知:该点左边值都比该点的值(查询值)小,故rank为左儿子大小 + 1
else if(v < val[id])return get_rank(ch[id][0],v);//发现需查询的点在该点左边,往左边递归查询
else return size[ch[id][0]] + cnt[id] + get_rank(ch[id][1],v);//若查询值大于该点值。说明询问点在当前点的右侧,且此点的值都小于查询值,所以要加上cnt[id]
}
int get_val(int id,int rank){
if(!id)return INF;//一直向右找找不到,说明是正无穷
if(rank <= size[ch[id][0]])return get_val(ch[id][0],rank);//左边排名已经大于rank了,说明rank对应的值在左儿子那里
else if(rank <= size[ch[id][0]] + cnt[id])return val[id];//上一步排除了在左区间的情况,若是rank在左与中(目前节点)中,则直接返回目前节点(中区间)的值
else return get_val(ch[id][1],rank - size[ch[id][0]] - cnt[id]);//剩下只能在右区间找了,rank减去左区间大小和中区间,继续递归
}
int get_pre(int v){
int id = root,pre;//递归不好返回,以循环求解
while(id){//查到节点不存在为止
if(val[id] < v)pre = val[id],id = ch[id][1];//满足当前节点比目标小,往当前节点的右侧寻找最优值
else id = ch[id][0];//无论是比目标节点大还是等于目标节点,都不满足前驱条件,应往更小处靠近
}
return pre;
}
int get_next(int v){
int id = root,next;
while(id){
if(val[id] > v)next = val[id],id = ch[id][0];//同理,满足条件向左寻找更小解(也就是最优解)
else id = ch[id][1];//与上方同理
}
return next;
}
int main(){
build();//不要忘记初始化[运行build()会连同root一并初始化,所以很重要]
na = RD();
for(int i = 1;i <= na;i++){
int cmd = RD(),x = RD();
if(cmd == 1)insert(root,x);//函数都写好了,注意:需要递归的函数都从根开始,不需要递归的函数直接查询
else if(cmd == 2)Remove(root,x);
else if(cmd == 3)printf("%d\n",get_rank(root,x) - 1);//注意:因为初始化时插入了INF和-INF,所以查询排名时要减1(-INF不是第一小,是“第零小”)
else if(cmd == 4)printf("%d\n",get_val(root,x + 1));//同理,用排名查询值得时候要查x + 1名,因为第一名(其实不是)是-INF
else if(cmd == 5)printf("%d\n",get_pre(x));
else if(cmd == 6)printf("%d\n",get_next(x));
}
return 0;
}
参考文章:
【算法】线段树详解
平衡树【Treap】