树形数据结构II——线段树进阶与FHQ treap
前言
本文章记录线段树有关内容以及好写又好用的 FHQ treap。
线段树也可以理解为记忆化分治,支持:区间询问某个信息(信息可合并),区间修改某个信息。
对于懒标记:
-
懒标记要满足结合律。
-
每个线段树只能支持一个懒标记(可以看作结构体)。
-
可以看成矩阵乘法。
动态开点
当线段树要维护的值域很大时(如
这里直接引用 oi wiki 的代码:
void update(int& p, int s, int t, int x, int f) { // 引用传参
if (!p) p = ++cnt; // 当结点为空时,创建一个新的结点
if (s == t) {
sum[p] += f;
return;
}
int m = s + ((t - s) >> 1);
if (x <= m)
update(ls[p], s, m, x, f);
else
update(rs[p], m + 1, t, x, f);
sum[p] = sum[ls[p]] + sum[rs[p]]; // pushup
}
int query(int p, int s, int t, int l, int r) {
if (!p) return 0; // 如果结点为空,返回 0
if (s >= l && t <= r) return sum[p];
int m = s + ((t - s) >> 1), ans = 0;
if (l <= m) ans += query(ls[p], s, m, l, r);
if (r > m) ans += query(rs[p], m + 1, t, l, r);
return ans;
}
小例题:Physical Education Lessons。
标记永久化
顾名思义就是懒标记不下发,也就是没有 pushdown,当然也没有 pushup 的必要了。
原版懒标记下放依托查询时的递归,实现区间的延时修改,从而实现时间的节约,所以可能有些节点到最后都没有被真正修改,但修改后累加的区间和一是真实的。
但这里标记根本就没有下放,询问前与询问后 sum,tag
就没变过,这里的查询就只有计算区间和的目的。
实现:如当对一个区间修改时
-
完全包含的区间,标记懒标记返回。
-
部分包含的区间,直接修改,然后向下递归。
当查询时,累计递归路径的懒标记和。
使用条件:
-
区间修改后可以快速更新,如取 min/max,求和。
-
修改与顺序无关。
主要应用:
- 主席树中不同版本可以用同一个节点,如果标记下传,就会使不同版本混在一起,要实现区间修改必须用标记永久化。
参考模板(以区间加值为例):
- 修改
void change(int p, int l, int r, int x, int y, int k)
{
t[p].sum += (min(r, y) - max(l, x) + 1) * k;//所有的区间都改。
if (x <= l && r <= y)// 包含的打标记,为了子节点的计算。
{
t[p].tag += k;
return p;
}
int mid = (l + r) >> 1;
if (x <= mid)
change(t[p].l, l, mid, x, y, k);
if (y > mid)
change(t[p].r, mid + 1, r, x, y, k);
return p;
}
- 区间查询和
int ask(int p, int l, int r, int x, int y, int s)
{
if (x <= l && r <= y)
return t[p].sum + (min(r, y) - max(l, x) + 1) * s;
int mid = (l + r) >> 1, res = 0;
s += t[p].tag;
if (x <= mid)
res += ask(t[p].l, l, mid, x, y, s);
if (y > mid)
res += ask(t[p].r, mid + 1, r, x, y, s);
return res;
}
线段树合并与分裂
线段树合并
把两个线段树
为了不使时间复杂度太大,当合并时如果
如果都有的话,那就新创建一个节点
当然这里要用到动态开点了。
int mer(int l, int r, int x, int y) {
if(!x || !y) return x | y;//第一种情况
int mid = (l + r) >> 1, z = ++tot; // tot 是总节点个数
if(l == r)
{
/*
合并叶子 x 和 y
sum[z]=sum[x]+sum[y];
*/
return z;//叶子节点返回
}
t[z].son[0] = mer(l, mid, t[x].son[0], t[y].son[0]);
t[z].son[1] = mer(mid + 1, r, t[x].son[1], t[y].son[1]);
pushup(z);
return z;
}
也可以这样写
int merge(int x, int y) {
if(!x || !y) return x | y;
int z = ++node;
ls[z] = merge(ls[x], ls[y]);
rs[z] = merge(rs[x], rs[y]);
/* 合并叶子 x 和 y
sum[z]=sum[x]+sum[y];
*/
return z;
}
时间复杂度:合并两个线段树时,每次只有重合的节点才会向下递归,对于两个满线段树,时间复杂度为:
适用条件:
检查线段树合并是否适用,我们只需检查能否快速合并两个叶子节点,以及快速 pushup,而不需要支持 快速合并两个区间的信息(这是笔者在初学线段树合并时常犯的错误,即因为无法快速合并两个有交区间的信息而认为无法线段树合并)。注意这不同于 pushup,因为 pushup 合并的两个区间 无交。由于几乎所有线段树题目均满足这些条件,所以我们断言,只要能用线段树维护的信息,线段树合并就能做。
上段引用 alex_wei 博客(在后言有标明)。
线段树合并以合并权值线段树为主,主要是要查询一个区间或子树内一个权值范围的和,极值等(权值线段树相当于已经用桶排排好序了,那访问权值范围和就很容易)。
注:
-
线段树合并是永久的。
-
合并的复杂度是均摊的,所以 DAG 是不能用线段树合并的。
线段树分裂
建议学完 FHQ treap 再学。
按权值与按大小分裂。
设权值/大小为
-
, 右子树全部大于 ,把 的右子树给 ,向左子树递归。 -
,左子树正好满足条件, 的右子树直接给 即可。 -
, 的左端不用修改,直接向右端递归。
这里代码以按大小分裂为例。
void split(int x,int &y,int k)
{
if(!x) return ;
y=nd();
ll v=t[t[x].son[0]].v;
if(k>v)
split(t[x].son[1],t[y].son[1],k-v);
else
swap(t[x].son[1],t[y].son[1]);
if(k<v)
split(t[x].son[0],t[y].son[0],k);
t[y].v=t[x].v-k;
t[x].v=k;
}
线段树分裂的适用范围不多,与后面的 FHQ treap 类似,并适用范围更广(可以代替?)。
例题
I 到 VII 为线段树合并。
I.P3224 [HNOI2012] 永无乡
用并查集维护每个点所在联通块,用权值线段树维护每个区间的大小。
合并两个连通块时,用线段树合并。
查询
#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
int n, m, tot;
int fa[N], a[N], rt[N];
struct node
{
int son[2], siz, id;
} t[N];
int read()
{
int sgn = 0, x = 0;
char c = getchar();
while (!isdigit(c))
sgn |= (c == '-'), c = getchar();
while (isdigit(c))
x = x * 10 + c - '0', c = getchar();
return sgn ? -x : x;
}
void pt(int x)
{
printf("!!!!\n now: %d\n son: %d %d\n siz %d\n id %d\n\n", x, t[x].son[0], t[x].son[1], t[x].siz, t[x].id);
}
int find(int x)
{
if (x == fa[x])
return x;
return fa[x] = find(fa[x]);
}
void pushup(int p)
{
t[p].siz = t[t[p].son[0]].siz + t[t[p].son[1]].siz;
}
int ins(int p, int l, int r, int pos, int id)
{
if (!p)
p = ++tot;
if (l == r)
{
t[p].siz++;
t[p].id = id;
return p;
}
int mid = (l + r) >> 1;
if (pos <= mid)
t[p].son[0] = ins(t[p].son[0], l, mid, pos, id);
else
t[p].son[1] = ins(t[p].son[1], mid + 1, r, pos, id);
pushup(p);
return p;
}
int mer(int x, int y, int l, int r)
{
if (!x || !y)
return x | y;
if (l == r)
{
t[x].siz += t[y].siz;
return x;
}
int mid = (l + r) >> 1;
t[x].son[0] = mer(t[x].son[0], t[y].son[0], l, mid);
t[x].son[1] = mer(t[x].son[1], t[y].son[1], mid + 1, r);
pushup(x);
return x;
}
int ask(int x, int k, int l, int r)
{
if (l == r)
return t[x].id;
int mid = (l + r) >> 1, ans = 0;
if (t[t[x].son[0]].siz >= k)
ans = ask(t[x].son[0], k, l, mid);
else
ans = ask(t[x].son[1], k - t[t[x].son[0]].siz, mid + 1, r);
return ans;
}
int main()
{
n = read(),
m = read();
for (int i = 1; i <= n; i++)
fa[i] = i;
for (int i = 1; i <= n; i++)
{
int x = read();
rt[i] = ins(rt[i], 1, n, x, i);
}
for (int i = 1; i <= m; i++)
{
int x = read(), y = read();
int u = find(x), v = find(y);
if (u == v)
continue;
fa[v] = u;
rt[u] = mer(rt[u], rt[v], 1, n);
rt[v] = rt[u];
}
int Q = read();
while (Q--)
{
char c = getchar();
while (c > 'Z' || c < 'A')
c = getchar();
int x = read(), y = read();
if (c == 'B')
{
int u = find(x), v = find(y);
if (u == v)
continue;
fa[v] = u;
rt[u] = mer(rt[u], rt[v], 1, n);
rt[v] = rt[u];
}
else
{
int ans = ask(rt[find(x)], y, 1, n);//记得这里要找一下祖宗
printf("%d\n", ans == 0 ? -1 : ans);
}
}
return 0;
}
II.P4556 [Vani有约会] 雨天的尾巴
发放从
但此题还要求每个节点出现次数最多的粮食种类,那就用线段树合并,记录每个点出现次数最多的种类即可。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,m;
int las[N],to[N],nxt[N],cnt,tot;
int f[N][21],dep[N],ans[N],rt[N];
struct node
{
int ls,rs,sum,res;
}tr[N*80];
void add(int u,int v)
{
cnt++;
nxt[cnt]=las[u];
las[u]=cnt;
to[cnt]=v;
}
void init(int u,int fa)
{
dep[u]=dep[fa]+1;
f[u][0]=fa;
for(int i=1;i<=20;i++)
f[u][i]=f[f[u][i-1]][i-1];
for(int e=las[u];e;e=nxt[e])
{
int v=to[e];
if(v==fa)continue;
init(v,u);
}
}
int lca(int u,int v)
{
if(dep[u]<dep[v])swap(u,v);
for(int i=20;i>=0;i--)
if(dep[u]-(1<<i)>=dep[v])
u=f[u][i];
if(v==u)return v;
for(int i=20;i>=0;i--)
{
if(f[u][i]!=f[v][i])
u=f[u][i],v=f[v][i];
}
return f[u][0];
}
void pushup(int p)
{
if(tr[tr[p].ls].sum<tr[tr[p].rs].sum)
{
tr[p].res=tr[tr[p].rs].res;
tr[p].sum=tr[tr[p].rs].sum;
}
else
{
tr[p].res=tr[tr[p].ls].res;
tr[p].sum=tr[tr[p].ls].sum;
}
}
int addtree(int p,int l,int r,int co,int val)
{
if(!p)p=++tot;
if(l==r)
{
tr[p].res=co;
tr[p].sum+=val;
return p;
}
int mid=l+r>>1;
if(co<=mid)tr[p].ls=addtree(tr[p].ls,l,mid,co,val);
if(co>mid)tr[p].rs=addtree(tr[p].rs,mid+1,r,co,val);
pushup(p);
return p;
}
int merge(int u,int v,int l,int r)
{
if(!u||!v) return u|v;
if(l==r)
{
tr[u].sum+=tr[v].sum;
return u;
}
int mid=(l+r)>>1;
tr[u].ls=merge(tr[u].ls,tr[v].ls,l,mid);
tr[u].rs=merge(tr[u].rs,tr[v].rs,mid+1,r);
pushup(u);
return u;
}
void dfs(int u,int fa)
{
for(int e=las[u];e;e=nxt[e])
{
int v=to[e];
if(v==fa)continue;
dfs(v,u);
rt[u]=merge(rt[u],rt[v],1,1e5);
}
ans[u]=tr[rt[u]].res;
if(tr[rt[u]].sum==0)ans[u]=0;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v);add(v,u);
}
init(1,0);
for(int i=1;i<=m;i++)
{
int u,v,z;
scanf("%d%d%d",&u,&v,&z);
int t=lca(u,v);
rt[u]=addtree(rt[u],1,1e5,z,1);
rt[v]=addtree(rt[v],1,1e5,z,1);
rt[t]=addtree(rt[t],1,1e5,z,-1);
rt[f[t][0]]=addtree(rt[f[t][0]],1,1e5,z,-1);
}
dfs(1,0);
for(int i=1;i<=n;i++)
printf("%d\n",ans[i]);
return 0;
}
III.P3899 [湖南集训] 更为厉害
从样例中就可发现分为两种情况:
前者很好解决,种类数为:
后者为:
这里以每个节点深度建一个线段树,边搜索边合并,当一个节点的子树都合并完后,把此点的权值加上
查询答案时就是计算
注意写的时候不要出现“眼瞎错误”,不然后面调试就会调很久,毕竟不会去关注那个地方。
#include <bits/stdc++.h>
using namespace std;
const int N = 6e5 + 10;
int n, Q;
int las[N], to[N], nxt[N], cnt;
int dep[N], siz[N], tot, rt[N];
struct node
{
int son[2];
long long sum;
} t[N * 20];
void add(int u, int v)
{
nxt[++cnt] = las[u];
las[u] = cnt;
to[cnt] = v;
}
void pushup(int p)
{
t[p].sum = t[t[p].son[0]].sum + t[t[p].son[1]].sum;
}
int mer(int x, int y, int l, int r)
{
if (!x || !y)
return x | y;
int z = ++tot;
if (l == r)
{
t[z].sum = t[x].sum + t[y].sum;
return z;
}
int mid = (l + r) >> 1;
t[z].son[0] = mer(t[x].son[0], t[y].son[0], l, mid);
t[z].son[1] = mer(t[x].son[1], t[y].son[1], mid + 1, r);
pushup(z);
return z;
}
void change(int &p, int pos, int l, int r, int k)
{
if (!p)
p = ++tot;
if (l == r)
{
t[p].sum += k;
return;
}
int mid = (l + r) >> 1;
if (pos <= mid)
change(t[p].son[0], pos, l, mid, k);
else
change(t[p].son[1], pos, mid + 1, r, k);
pushup(p);
}
void dfs(int u, int fa)
{
dep[u] = dep[fa] + 1;
siz[u] = 1;
for (int e = las[u]; e; e = nxt[e])
{
int v = to[e];
if (v == fa)
continue;
dfs(v, u);
siz[u] += siz[v];
rt[u] = mer(rt[u], rt[v], 1, n);
}
change(rt[u], dep[u], 1, n, siz[u] - 1);
}
long long ask(int p, int x, int y, int l, int r)
{
if (x <= l && r <= y)
return t[p].sum;
int mid = (l + r) >> 1;
long long val = 0;
if (x <= mid)
val += ask(t[p].son[0], x, y, l, mid);
if (mid < y)
val += ask(t[p].son[1], x, y, mid + 1, r);
return val;
}
int main()
{
scanf("%d%d", &n, &Q);
for (int i = 1; i < n; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add(u, v), add(v, u);
}
dfs(1, 0);
while (Q--)
{
int p, k;
scanf("%d%d", &p, &k);
long long ans = 1ll * min(k, dep[p] - 1) * (siz[p] - 1);
ans += ask(rt[p], dep[p] + 1, min(dep[p] + k, n), 1, n);
printf("%lld\n", ans);
}
return 0;
}
IV.P3521 [POI2011] ROT-Tree Rotations
可以发现改变左右子树不会改变左右子树的逆序对,而是改变跨越了左右子树的逆序对,这里正反枚举一下即可。
但如何
使用权值线段树维护。
树上每个结点是一棵线段树,把左右子树的权值线段树合并成一个线段树(这里不用新开节点)。
交换前后的逆序对就可快速算出:
ans1 += t[t[x].son[1]].siz * t[t[y].son[0]].siz;
ans2 += t[t[y].son[1]].siz * t[t[x].son[0]].siz;
这里可以不用记录每个节点的逆序对个数,每合并一次就累加一次。
ans+=min(ans1,ans2);
ans1=ans2=0;
此题的输入有些毒瘤,因为它是按照根左右的顺序遍历,我们先递归搜索左右子树后,再向上合并。遇到子节点时加入权值线段树内返回即可。
code:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5 + 10;
struct node
{
int son[2], siz;
} t[N * 40];
int n, tot, ans1, ans2, ans, root;
void change(int &p, int l, int r, int pos)
{
if (!p)
p = ++tot;
if (l == r)
{
t[p].siz++;
return;
}
int mid = (l + r) >> 1;
if (pos <= mid)
change(t[p].son[0], l, mid, pos);
else
change(t[p].son[1], mid + 1, r, pos);
t[p].siz = t[t[p].son[0]].siz + t[t[p].son[1]].siz;
}
int mer(int x, int y, int l, int r)
{
if (!x || !y)
return x | y;
if (l == r)
{
t[x].siz += t[y].siz;
return x;
}
ans1 += t[t[x].son[1]].siz * t[t[y].son[0]].siz;
ans2 += t[t[y].son[1]].siz * t[t[x].son[0]].siz;
int mid = (l + r) >> 1;
t[x].son[0] = mer(t[x].son[0], t[y].son[0], l, mid);
t[x].son[1] = mer(t[x].son[1], t[y].son[1], mid + 1, r);
t[x].siz = t[t[x].son[0]].siz + t[t[x].son[1]].siz;
return x;
}
int dfs(int x)
{
int t;
scanf("%lld", &t);
if (!t)
{
int lson = 0, rson = 0;
lson = dfs(lson);
rson = dfs(rson);
ans1 = ans2=0;
x = mer(lson, rson, 1, n);
ans += min(ans1, ans2);
}
else
change(x, 1, n, t);
return x;
}
signed main()
{
scanf("%lld", &n);
int root = 0;
dfs(root);
printf("%lld", ans);
return 0;
}
V.Distinctification
VI.P5327 [ZJOI2019] 语言
VII.P6773 [NOI2020] 命运
P5494 【模板】线段树分裂
由于作者对 FHQ treap 的喜爱,还是用了平衡树的解法,见这篇题解
线段树二分
顾名思义就是在线段树上二分。
由于线段树是个分治结构,每次一都是区间减半,满足二分的性质。
下面是一个例子:
单点修改,全局查询
一个长度为
显然前缀和是满足单调性的,所以可以每次询问都二分答案,时间为:
考虑优化,可以发现线段树本来就满足分治结构,那可以在询问时如果
其实这里也可以直接在树状数组上二分。
单点修改,区间查询
如果限制询问区间为
设当前递归区间为:
-
判断如果加上这个区间的贡献如何,如果不合法就返回无解
-
如果
,返回找到这个值。 -
否则就递归左右子树来找答案。
递归左子树。
递归右子树。
可以发现第一步中的第三点是满足后面两个条件的,可以合并简化代码。
下面是伪代码:
int ask(int p,int x,int y,int lim,int &sum/*maxx 等题目要求的维护的变量*/)
{
if(x<=L[p]&&R[p]<=y)
{
if(sum+val[p]<=lim/*这里判断这个区间是否完全不在所求的区间内*/)
{
sum+=val[p];
return /*判无解的标记,可以写 -1,哪个好判断写哪个*/;
}
if(L[p]==R[p]) return L[p];
}
int mid=(L[p]+R[p])>>1;
if(x<=mid)
{
int now=ask(p<<1,x,y,lim,sum);
if(now!=/*无解的标记*/) return now;
}
if(mid<y) return ask(p<<1|1,x,y,lim,sum);
}
带负权的单点修改,全局查询
如果权值可以是负的,可以直接以前缀和为叶节点,维护区间最大值,查询就很简单。
例题:[ABC292Ex] Rating Estimator,CF689D,CF241B。
FHQ treap
几乎涵盖了 treap 的功能,码量不大,好写。
FHQ 的核心思想在于分裂和合并(这里可以借鉴线段树分裂与合并)。
算法简述
直接看着代码学吧。
定义了一个结构体来存储每个节点。
struct node
{
int son[2], v, rk, siz;
} t[N];
son
表示左右儿子。
v
表示权值。
rk
表示随机的排名。
siz
表示子树大小。
新建节点
int nd(int v)
{
int x = ++tot;
t[x].v = v, t[x].siz = 1, t[x].rk = rand();
return x;
}
更新答案
比较简单,参考线段树。
void pushup(int x)
{
t[x].siz = t[t[x].son[0]].siz + t[t[x].son[1]].siz+1;
}
分裂
此数据结构的核心。
p
当前节点。
v
此树以 v
为分界,小于 v
的分在左子树,大于 v
的分在右子树。
x
左树根节点。
y
右树根节点。
搜到叶子节点返回。
if (!p)
return x = y = 0, void();
如果当前节点的值小于等于 v
,说明左子树都小于 v
,那就到右子树继续分裂。
反之分裂点在左子树,到左子树上分裂。
void spl(int p, int v, int &x, int &y)
{
if (!p)
return x = y = 0, void();
if (t[p].v <= v)
{
x = p;
spl(t[p].son[1], v, t[p].son[1], y);
}
else
{
y = p;
spl(t[p].son[0], v, x, t[p].son[0]);
}
pushup(p);
}
如按大小分裂也一样:
void spl(int p, int v, int &x, int &y)
{
if (!p)
return x = y = 0, void();
if (sz[ls[p]] >= v)
spl(ls[p], v, x, ls[y = p]);
else
spl(rs[p], v - sz[ls[p]] - 1, rs[x = p], y);
pushup(p);
}
这里分裂相当于把一颗树分裂成两半,递归的过程就是寻找每个节点的新左右儿子,从而来分裂成两颗二叉树。
合并
根据 treap 定义的 rk
,rk
越大,优先级越高。
这里都规定
当
当
int mer(int x, int y)
{
if (!x || !y)
return x | y;
if (t[x].rk < t[y].rk)
{
t[y].son[0] = mer(x, t[y].son[0]);
pushup(y);
return y;
}
else
{
t[x].son[1] = mer(t[x].son[1], y);
pushup(x);
return x;
}
}
插入
按权值
void ins(int v)
{
int x = 0, y = 0;
spl(root, v - 1, x, y);
root = mer(mer(x, nd(v)), y);
}
删除
按权值
由于只删一个节点,把
然后再与
void del(int v)
{
int x = 0, y = 0, z = 0;
spl(root, v, x, z);
spl(x, v - 1, x, y);
y = mer(t[y].son[0], t[y].son[1]);
root = mer(mer(x, y), z);
}
查询第 大的数
这里就仿照权值线段树的查找方式即可。
int get_k(int k)
{
int p = root;
while (1)
{
if (k <= t[t[p].son[0]].siz)
p = t[p].son[0];
else if (k == t[t[p].son[0]].siz + 1)// siz+1 是还包含 p 这个节点
return t[p].v;
else
k -= t[t[p].son[0]].siz + 1, p = t[p].son[1];
}
}
查询此数 的排名
按照
int get_rank(int v)
{
int x = 0, y = 0, ans = 0;
spl(root, v - 1, x, y), ans = t[x].siz + 1;
root = mer(x, y);
return ans;
}
查询前驱/后缀
暴力搜索即可。
int pre(int v)
{
int p = root, ans;
while (1)
{
if (!p)
return ans;
if (v <= t[p].v)
p = t[p].son[0];
else
ans = t[p].v, p = t[p].son[1];
}
}
int suc(int v)
{
int p = root, ans;
while (1)
{
if (!p)
return ans;
if (v >= t[p].v)
p = t[p].son[1];
else
ans = t[p].v, p = t[p].son[0];
}
}
至此所有基础操作就完成了,可过模板题。
例题
I.P3391 【模板】文艺平衡树
平衡树维护区间,那么每个节点存储的就是对应位置的值,而它的中序遍历就是整个区间。
只需要将要翻转的区间分裂(这里是按子树大小分裂的)出来,打上标记再合并即可。
这码量应比 splay 少。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m, tot, root;
struct node
{
int son[2], rk, siz, tag, v;
} t[N];
void pushup(int x)
{
t[x].siz = t[t[x].son[0]].siz + t[t[x].son[1]].siz + 1;
}
void pushdown(int x)
{
if (!t[x].tag)
return;
t[t[x].son[0]].tag ^= 1;
t[t[x].son[1]].tag ^= 1;
swap(t[x].son[0], t[x].son[1]);
t[x].tag = 0;
}
void spl(int p, int v, int &x, int &y)
{
if (!p)
return x = 0, y = 0, void();
pushdown(p);
if (v <= t[t[p].son[0]].siz)
{
y = p;
spl(t[p].son[0], v, x, t[p].son[0]);
}
else
{
x = p;
spl(t[p].son[1], v - t[t[p].son[0]].siz - 1, t[p].son[1], y);
}
pushup(p);
}
int mer(int x, int y)
{
if (!x || !y)
return x | y;
pushdown(x), pushdown(y);
if (t[x].rk < t[y].rk)
{
t[y].son[0] = mer(x, t[y].son[0]);
pushup(y);
return y;
}
else
{
t[x].son[1] = mer(t[x].son[1], y);
pushup(x);
return x;
}
}
int nd(int v)
{
int x = ++tot;
t[x].siz = 1, t[x].v = v, t[x].rk = rand();
return x;
}
void ins(int v)
{
int x = 0, y = 0;
spl(root, v - 1, x, y);
root = mer(mer(x, nd(v)), y);
}
void change(int l, int r)
{
int x = 0, y = 0, z = 0;
spl(root, r, x, z), spl(x, l - 1, x, y);
t[y].tag ^= 1;
root = mer(x, mer(y, z));
}
void print(int p)
{
if (!p)
return;
pushdown(p);
print(t[p].son[0]);
printf("%d ", t[p].v);
print(t[p].son[1]);
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
ins(i);
for (int i = 1; i <= m; i++)
{
int l, r;
scanf("%d%d", &l, &r);
change(l, r);
}
print(root);
return 0;
}
II.P2042 [NOI2005] 维护数列
毒瘤数据结构题。
以下献上注意点与个人调试时的错误点。
-
空间卡的紧,所以删除的节点要拿一个栈存起,以便重复使用。
-
对于求最大子列和,我们在每个节点多开三个值:
lx
,rx
,mx
,分别指前缀最大和,后缀最大和,这个序列的最大子序列和。 -
附初值时覆盖的懒标记一定附为正无穷,而不是零。
-
对于 pushup 操作的提醒:
这里要合并 siz
,sum
与上面的三个值。
容易想到这样合并(记得把父节点合并):
node mul( node a, node b)
{
node c;
c.lx = max(a.lx, a.sum + b.lx);
c.rx = max(b.rx, a.rx + b.sum);
c.mx = max(max(a.mx, b.mx), a.rx + b.lx);
c.siz = a.siz + b.siz;
c.sum = a.sum + b.sum;
return c;
}
void pushup(int x)
{
node c;
c.lx = c.rx = c.mx = c.sum = t[x].v;
c.siz = 1;
t[x] = mul( t[t[x].son[0]], t[t[x].son[1]]);
t[x] = mul(t[x], c);
}
但这样又有问题,上面新建的 c
的左右儿子等信息没有传递下去。
就改成:
void pushup(int i)
{
if (!i)
return;
int i0 = t[i].son[0], i1 = t[i].son[1];
t[i].siz = t[i0].siz + t[i1].siz + 1;
t[i].sum = t[i0].sum + t[i1].sum + t[i].v;
t[i].lx = max(max(t[i0].lx, t[i0].sum + t[i].v + t[i1].lx), 0);
t[i].rx = max(max(t[i1].rx, t[i1].sum + t[i].v + t[i0].rx), 0);
t[i].mx = max(t[i0].rx + t[i1].lx, 0) + t[i].v;
if (i0)
t[i].mx = max(t[i].mx, t[i0].mx);
if (i1)
t[i].mx = max(t[i].mx, t[i1].mx);
}
- pushdown 操作的提醒:
-
翻转的懒标记记得把左右子树的
lx
,rx
交换。 -
覆盖的懒标记记得先传懒标记到左右节点,然后再更新左右节点的
lx,rx,mx
。
t[t[x].son[1]].v = t[t[x].son[0]].v = t[t[x].son[1]].tag2 = t[t[x].son[0]].tag2 = t[x].tag2;
t[t[x].son[0]].pd(t[x].tag2);
t[t[x].son[1]].pd(t[x].tag2);
t[x].tag2 = inf;
这里的 pd 函数为:
void pd(int tag)
{
sum = tag * siz;
lx = rx = max(0, sum);
mx = max(tag, sum);//注意不能为空序列,这里都要调自闭了。
}
-
删除时记得把所有值都初始化。
-
分裂时注意范围。
int pos = read(), nn = read();
spl(root, pos - 1, x, y), spl(y, nn, y, z);
//不是:spl(root, pos - 1, x, y), spl(y, pos+nn-1, y, z);
- 覆盖操作时不能只打个标记就走,一定要更新分裂出来的那个子树(参考线段树)。
code。
III.P4146 序列终结者
有了上题的磨练,此题就简单很多。
但注意边界问题,当更新最大值时,最大有可能时负数,如果访问到零节点就会发生错误,所以把零界点的最大值附为
注意开 long long
。
code。
到这里,相信对 FHQ treap 有了初步的认识与应用,接下来是不那么裸的题。
IV.T-Shirts
可以想到对质量从大到小排序,相同就按价格从小到大排序。
对于每个物品,卖得起的客户就买,记录答案即可。
时间复杂度:
考虑优化,按用户还有的钱排序,每次找到一个连续大于
但如果每次都暴力减去并插入时间复杂度太高,考虑把整个区间分成三部分:
这里暴力插入的节点每次都会减去它的一半以上,每个节点插入的次数为:
总时间复杂度为:
后言
参考资料
Alex wei 的博客:平衡树 & LCT ,线段树的高级用法。
command_block 的博客:关于线段树上的一些进阶操作
ღꦿ࿐ 的博客:CF702F 题解
oi wiki :线段树。
木xx木大:线段树相关技巧的小小总结。
foreverlasting:线段树分治总结。
ycx's blog:标记永久化 学会了祭。
ttjb:题解 P3372 【【模板】线段树 1】。
修改日志
2024.06.10 完成动态开点,标记永久化,线段树合并(部分例题未完成),FHQ treap。
2024.09.02 完成线段树二分。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战