树形数据结构II——线段树进阶与FHQ treap

前言

本文章记录线段树有关内容以及好写又好用的 FHQ treap。

动态开点

当线段树要维护的值域很大时(如 \(1e9\)),空间是不够用的,也有大量浪费。这时我们用动态开点,当要用到这个点时,新开一个点,可以通过传递引用值方便地实现。查询时如果走到空节点就直接返回。

这里直接引用 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,求和。

  • 修改与顺序无关。

主要应用:

  • 主席树中不同版本可以用同一个节点,如果标记下传,就会使不同版本混在一起,要实现区间修改必须用标记永久化。

参考模板(以区间加值为例):

  1. 修改
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;
}
  1. 区间查询和
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;
}

线段树合并与分裂

线段树合并

把两个线段树 \(Tx,Ty\),合并成一个线段树 \(Tz\)

为了不使时间复杂度太大,当合并时如果 \(x\)\(y\) 有一个为空,就直接返回有值的一个,也就是 \(x|y\)

如果都有的话,那就新创建一个节点 \(z\)\(z\)\(x,y\) 合并的结果,向下递归,寻找 \(z\) 的左右儿子。

当然这里要用到动态开点了。

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;
}

时间复杂度:合并两个线段树时,每次只有重合的节点才会向下递归,对于两个满线段树,时间复杂度为:\(O(nlogn)\)。因为不会多次重复的合并一个线段树,总节点数约为 \(n\),所以总时间复杂度约为 \(O(nlogn)\)

适用条件:

检查线段树合并是否适用,我们只需检查能否快速合并两个叶子节点,以及快速 pushup,而不需要支持 快速合并两个区间的信息(这是笔者在初学线段树合并时常犯的错误,即因为无法快速合并两个有交区间的信息而认为无法线段树合并)。注意这不同于 pushup,因为 pushup 合并的两个区间 无交。由于几乎所有线段树题目均满足这些条件,所以我们断言,只要能用线段树维护的信息,线段树合并就能做。

上段引用 alex_wei 博客(在后言有标明)。

线段树合并以合并权值线段树为主,主要是要查询一个区间或子树内一个权值范围的和,极值等(权值线段树相当于已经用桶排排好序了,那访问权值范围和就很容易)。

线段树分裂

建议学完 FHQ treap 再学。

按权值与按大小分裂。

设权值/大小为 \(k\),分裂 \(x\),这个线段树给 \(y\),左子树的权值/大小为 \(v\)

  • \(k<v\)\(x\) 右子树全部大于 \(k\),把 \(x\) 的右子树给 \(y\),向左子树递归。

  • \(k=v\),左子树正好满足条件,\(x\) 的右子树直接给 \(y\) 即可。

  • \(k>v\)\(x\) 的左端不用修改,直接向右端递归。

这里代码以按大小分裂为例。

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] 永无乡

用并查集维护每个点所在联通块,用权值线段树维护每个区间的大小。

合并两个连通块时,用线段树合并。

查询 \(k\) 大时采用权值线段树的方法查找即可。

#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有约会] 雨天的尾巴

还记得P3258 [JLOI2014] 松鼠的新家吧。

发放从 \(x\)\(y\) 的粮食,就用树上差分,求出 \(t=lca(x,y)\),让 \(diff[x]++,diff[y]++,diff[t]--,diff[fa[t]]--\),最后再遍历合并。

但此题还要求每个节点出现次数最多的粮食种类,那就用线段树合并,记录每个点出现次数最多的种类即可。

#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 [湖南集训] 更为厉害

从样例中就可发现分为两种情况:\(b\)\(a\) 的祖先,\(a\)\(b\) 的祖先。

前者很好解决,种类数为:\(min(dep[p]-1,k)*(size[p]-1)\)

后者为:\(\sum_{dep[p]+1\ge dep[x] \ge min(dep[x]+k,n)} size[x]-1\)

这里以每个节点深度建一个线段树,边搜索边合并,当一个节点的子树都合并完后,把此点的权值加上 \(size-1\)

查询答案时就是计算 \([dep[p]+1 min(dep[x]+k,n)]\) 权值和。

注意写的时候不要出现“眼瞎错误”,不然后面调试就会调很久,毕竟不会去关注那个地方。

#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

可以发现改变左右子树不会改变左右子树的逆序对,而是改变跨越了左右子树的逆序对,这里正反枚举一下即可。

但如何 \(O(1)\) 的求出交换前与交换后的逆序对个数?

使用权值线段树维护。

树上每个结点是一棵线段树,把左右子树的权值线段树合并成一个线段树(这里不用新开节点)。

交换前后的逆序对就可快速算出:

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 的喜爱,还是用了平衡树的解法,见这篇题解

线段树分治

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,那就到右子树继续分裂。

反之分裂点在左子树,到左子树上分裂。

img

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 定义的 rkrk 越大,优先级越高。

这里都规定 \(t[x].v<t[y].v\)

\(t[x].rk<t[y].rk\)\(x\)\(y\) 的左子树。

\(t[x].rk>t[y].rk\)\(y\)\(x\) 的右子树。

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;
    }
}

插入

按权值 \(v-1\) 把树分裂成两颗树,把此节点合并左子树与右子树。

void ins(int v)
{
    int x = 0, y = 0;
    spl(root, v - 1, x, y);
    root = mer(mer(x, nd(v)), y);
}

删除

按权值 \(v-1\) 把树分裂成三颗树 \(x,y,z\),分别表示:小于 \(v\) 的子树,等于 \(v\) 的子树 \(y\),大于 \(v\) 的子树。

由于只删一个节点,把 \(y\) 的左子树与右子树合并即可,\(y\) 节点就被删除。

然后再与 \(x\) 子树合并。

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);
}

查询第 \(k\) 大的数

这里就仿照权值线段树的查找方式即可。

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];
    }
}

查询此数 \(v\) 的排名

按照 \(v-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] 维护数列

毒瘤数据结构题。

以下献上注意点与个人调试时的错误点。

  1. 空间卡的紧,所以删除的节点要拿一个栈存起,以便重复使用。

  2. 对于求最大子列和,我们在每个节点多开三个值:lx,rx,mx,分别指前缀最大和,后缀最大和,这个序列的最大子序列和。

  3. 附初值时覆盖的懒标记一定附为正无穷,而不是零。

  4. 对于 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);
}
  1. 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);//注意不能为空序列,这里都要调自闭了。
}
  1. 删除时记得把所有值都初始化。

  2. 分裂时注意范围。

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);
  1. 覆盖操作时不能只打个标记就走,一定要更新分裂出来的那个子树(参考线段树)。

双倍经验

code

III.P4146 序列终结者

有了上题的磨练,此题就简单很多。

但注意边界问题,当更新最大值时,最大有可能时负数,如果访问到零节点就会发生错误,所以把零界点的最大值附为 \(-inf\)

注意开 long long

code

到这里,相信对 FHQ treap 有了初步的认识与应用,接下来是不那么裸的题。

IV.T-Shirts

可以想到对质量从大到小排序,相同就按价格从小到大排序。

对于每个物品,卖得起的客户就买,记录答案即可。

时间复杂度:\(O(n^2)\)

考虑优化,按用户还有的钱排序,每次找到一个连续大于 \(c\) 的区间,减去 \(c\)。但问题是每次减去无法保证序列的单增性,这里考虑用平衡树维护有序区间。

但如果每次都暴力减去并插入时间复杂度太高,考虑把整个区间分成三部分:\([0,c],(c,2\times c],(2 \times c, inf)\)

\([0,c]\) 不用管,\((2 \times c,inf)\) 打上标记就行,因为减去 \(c\) 后一定比第一个区间的数大,保持了递增性,中间的区间一个个暴力插入第一个区间即可。

这里暴力插入的节点每次都会减去它的一半以上,每个节点插入的次数为:\(log(c)\)

总时间复杂度为:\(O(n \times (\sum log (c_i)+n))\)

code

后言

参考资料

Alex wei 的博客:平衡树 & LCT 线段树的高级用法

command_block 的博客:关于线段树上的一些进阶操作

ღꦿ࿐ 的博客:CF702F 题解

oi wiki :线段树

木xx木大:线段树相关技巧的小小总结

foreverlasting:线段树分治总结

ycx's blog:标记永久化 学会了祭

ttjb:题解 P3372 【【模板】线段树 1】

修改日志

2024.06.10 完成动态开点,标记永久化,线段树合并(部分例题未完成),FHQ treap。

posted @ 2024-06-10 16:02  hutongzhou  阅读(10)  评论(0编辑  收藏  举报