前言
真不是有目的地学习主席树的…(实在是因为它太上头了)
《关于我某天第二节晚修一直在看<进阶指南>可持久化数据结构这章然后学了主席树这件事》
所以新学一个数据结构只要一整节晚修。
主席树
也叫可持久化线段树、函数式线段树。其思想与可持久化 T r i e \mathtt{Trie} Trie 相似。
其实,就是在普通线段树的基础上,修改了一下 u p d a t e \mathtt{update} update 操作,使得它成为可持久化数据结构:
每次新建一个根节点,保存此次修改之后的状态。在遍历线段树的时候,对更改了的部分创建一个副本,然后直接将孩子指针指向上一个状态中没更改的部分。
下面这张图展现了对 [ 4 , 4 ] [4,4] [4,4] 修改后树的状态。
(注:图自此主席树博客)
不过主席树难以支持大部分的区间修改。原因是标记难以下传(后面有若干依赖此子树的树)。在一些特殊题目中,可以使用标记永久化代替标记的下传,例如:SP11470 TTM - To the moon。
对数组下标的划分
本质上就是用线段树实现可持久化数组。
#include<bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for(int i = a; i <= b; ++i)
const int maxn = 1e6 + 5;
int n, m;
int a[maxn], rt[maxn];
struct node{
int l, r;
int val;
}t[maxn << 5];
int tot;
inline int build(int nw, int l, int r)
{
nw = ++tot;
if(l == r)
{
t[nw].val = a[l];
return nw;
}
int mid = (l + r) >> 1;
t[nw].l = build(t[nw].l, l, mid);
t[nw].r = build(t[nw].r, mid + 1, r);
return nw;
}
inline int cpy(int x)
{
int nw = ++tot;
t[nw] = t[x];
return nw;
}
inline int update(int lst, int l, int r, int k, int d)
{
int nw = cpy(lst), mid = (l + r) >> 1;
if(l == r)
t[nw].val = d;
else
{
if(k <= mid)
t[nw].l = update(t[nw].l, l, mid, k, d);
else t[nw].r = update(t[nw].r, mid + 1, r, k, d);
}
return nw;
}
inline int query(int nw, int l, int r, int k)
{
if(l == r)
return t[nw].val;
else
{
int mid = (l + r) >> 1;
if(k <= mid)
return query(t[nw].l, l, mid, k);
else return query(t[nw].r, mid + 1, r, k);
}
}
int main()
{
scanf("%d%d", &n, &m);
rep(i, 1, n) scanf("%d", &a[i]);
rt[0] = build(531, 1, n);
rep(i, 1, m)
{
int v, opt, loc, d;
scanf("%d%d%d", &v, &opt, &loc);
if(opt == 1)
{
scanf("%d", &d);
rt[i] = update(rt[v], 1, n, loc, d);
}
else
printf("%d\n", query(rt[v], 1, n, loc)),
rt[i] = rt[v];
}
return 0;
}
对值域的划分
离散化,然后对于每个根节点 r o o t i root_i rooti,它维护的是数组从 a 1 a_1 a1 到 a i a_i ai 中每个数的出现次数。
该算法时间复杂度为 O ( ( N + M ) l o g N ) O((N+M)logN) O((N+M)logN),空间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)。
#include<bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for(int i = a; i <= b; ++i)
const int maxn = 2e5 + 5;
int n, m;
struct node{
int ls, rs;
int sum;
}t[maxn << 5];
int a[maxn], b[maxn];
int q;
int dt, tot;
int rt[maxn];
inline void build(int &nw, int l, int r)
{
nw = ++tot;
if(l == r) return;
int mid = (l + r) >> 1;
build(t[nw].ls, l, mid), build(t[nw].rs, mid + 1, r);
}
inline int addt(int lst, int l, int r)
{
int nw = ++tot;
t[nw] = t[lst], t[nw].sum += 1;
if(l == r) return nw;
int mid = (l + r) >> 1;
if(dt <= mid) t[nw].ls = addt(t[nw].ls, l, mid);
else t[nw].rs = addt(t[nw].rs, mid + 1, r);
return nw;
}
inline int query(int pl, int pr, int l, int r, int k)
{
if(l == r) return l;
int mid = (l + r) >> 1, lcnt = t[t[pr].ls].sum - t[t[pl].ls].sum;
if(k <= lcnt)
return query(t[pl].ls, t[pr].ls, l, mid, k);
else return query(t[pl].rs, t[pr].rs, mid + 1, r, k - lcnt);
}
int main()
{
scanf("%d%d", &n, &m);
rep(i, 1, n)
scanf("%d", &a[i]), b[i] = a[i];
sort(b + 1, b + n + 1);
q = unique(b + 1, b + n + 1) - b - 1;
build(rt[0], 1, q);
rep(i, 1, n)
{
dt = lower_bound(b + 1, b + q + 1, a[i]) - b;
rt[i] = addt(rt[i - 1], 1, q);
}
rep(i, 1, m)
{
int lt, rtm, kt;
scanf("%d%d%d", <, &rtm, &kt);
int ans = query(rt[lt - 1], rt[rtm], 1, q, kt);
printf("%d\n", b[ans]);
}
return 0;
}
E
m
m
m
.
.
.
Emmm...
Emmm... 更有意思的是,这道题也可以使用线段树套平衡树的树套树做法,因为平衡树具有添加和删除的功能,所以这个做法支持动态修改。具体地…没写
关于对值域的划分,P4587 [FJOI2016]神秘数(题解)是道综合性较强的例题。
得赶紧跑路了,再搞主席树我平衡树就烂地里了,逃
—— E n d \mathfrak{End} End——