堆与左偏树

欲承之冠,以造极之锋芒;
俯首之势,作缄默之冷妄。

堆是一个树型结构,拥有左儿子和右儿子。

满足性质【父结点权值大于子结点】的堆称为大根堆
满足性质【父结点权值小于子结点】的堆称为小根堆

常见的,std::priority_queue 的初始就是一个大根堆。($\text{Stl}$ 大法吼啊)

可并堆/配对堆

$\text{OI-Wiki}$

说实话,堆需要学习的,就左偏树就够了。

左偏树

拥有线段树合并的部分功能,且码量极小。

功能

可以维护一个堆(一个集合),支持操作:

  1. 插入一个数;效率 $\mathcal O(\log n)$ ;
  2. 求取最小值;效率 $\mathcal O(1)$ ;
  3. 删除最小值;(或任意值),删除最小值的效率为 $\mathcal O(\log n)$ ;
  4. 合并堆(两棵树),效率 $\mathcal O(\log n)$ 。

而左偏树的特性就在于第四个操作。

实现

以小根堆为例。

左偏树的每一个结点都会维护一些信息:

$val$ $dist$
点权值 到最近空结点的距离

定义叶结点的 $dist$ 为 $1$ ,左偏树的左偏性质在于【左儿子的 $dist$ 大于右儿子的 $dist$】,即左子树比右子树更深。

左偏树操作的复杂度与距离成正比,且 $\mathcal O(\log n)$ 为上界。

有性质:$dist[root]\leq\mathcal O(\log n)$ 。

定义函数 $f(k)$ 表示距离为 $k$ 的点的子树至少包含了 $f(k)$ 个点。

可以知道 $f(1)=1$ ,并容易证明 $f(k)\geq 2^k-1$ 。

假设 $n=k-1$ 时成立,证明 $n=k$ 时成立。

设 $dist[root]=k$

则 $dist[root.rightson]=k-1$
且 $dist[root.leftson]\geq k-1$

则 $f(root)=f(root.leftson)+f(root.rightson)$

则 $f(n)\geq(2^{n-1}-1)+(2^{n-1}-1)+1$

化简得到 $f(n)\geq 2^n-1$ 。

所以有 $dist\leq\mathcal O(\log n)$ 。


合并

定义函数 merge(a,b) 为合并 $a,b$ 两棵子树并返回合并后的根结点。

首先比较 $a_{root}$ 和 $b_{root}$ 的深度,返回较小的值作为根结点,记为 $c$ 。

从 $c$ 开始向下递归,将剩下的子树(假设 $c$ 为 $a$ ,剩余 $b$)$b$ 与 $a$ 的右子树合并。并维护左偏性质,可以将 $c$ 的左右子树进行交换。

期望复杂度 $\mathcal O(dist[a]+dist[b])$ ,实际复杂度 $\mathcal O(\log n)$ 。


插入

建立原根堆,与需要插入的数,共两个堆,并将两个堆合并。


删除

删除根结点,并合并左子树和右子树。


查询极值

直接返回根结点。


例题

P3377

我们需要同时维护 $n$ 个左偏树和一套并查集以查询当前的 $(x,y)$ 的所属,并直接合并其并查代表元素。

这里的并查集与左偏树必须对应,而并查集并不支持删除操作,且左偏树在合并之后会形成一个新的左偏树,会与并查的映射偏离。如果每次删除操作重置初始化的话,时间复杂度会高达 $\mathcal O(n)$ ,有悖题意。

不过,容易想到,并查集的映射并不会影响我们左偏树的查询,所以,我们不需要去删除并查集的点,因为不会影响后续答案。

但是,我们必须保证并查集的代表元素是新左偏树的根结点,所以我们需要对并查集进行换根。这个操作只有一步,就是把原来的代表元素指向新的根结点就行了。不理解的读者可以去重修一下并查集。

对于洛谷的这道模板题,会出现一个输出 -1 的情况,所以给出一个 $Del[x]$ 数组表示当前数是否被删除过,即可

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<ctime>
#include<iomanip>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#define gh() getchar()
#define re register
typedef long long ll;
template<class T>
inline void read(T &x)
{
x=0;
char ch=gh(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=gh();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=gh();
if(t) x=-x;
}
template<class T,class ...T1>
inline void read(T &x,T1 &...x1)
{
read(x),read(x1...);
}
template<class T>
inline void write(T x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
}
template<class T>
inline bool checkMax(T &x,T &y)
{
return x<y?x=y,1:0;
}
template<class T>
inline bool checkMin(T &x,T &y)
{
return x>y?x=y,1:0;
}
const int MAXN=2e5+10;
const int INF=0x3f3f3f3f;
int N,M,Val[MAXN],Dist[MAXN];
int Fa[MAXN],Le[MAXN],Ri[MAXN];
bool Del[MAXN];
inline bool Cmp(int x,int y)
{
if(Val[x]==Val[y]) return x<y;
return Val[x]<Val[y];
}
int getFa(int x)
{
return Fa[x]==x?x:Fa[x]=getFa(Fa[x]);
}
int merge(int x,int y)
{
if(!x||!y) return x+y;
if(Cmp(y,x)) std::swap(x,y);
Ri[x]=merge(Ri[x],y);
if(Dist[Ri[x]]>Dist[Le[x]]) std::swap(Le[x],Ri[x]);
Dist[x]=Dist[Ri[x]]+1;
return x;
}
int main()
{
// freopen("leftist-tree.in","r",stdin);
// freopen("leftist-tree.out","w",stdout);
read(N,M);
Dist[0]=-1;
for(int i=1;i<=N;++i) read(Val[i]),Fa[i]=i;
while(M--)
{
int opt;
read(opt);
if(opt==1)
{
int x,y;read(x,y);
if(Del[x]||Del[y]) continue;
x=getFa(x),y=getFa(y);
if(x!=y)Fa[x]=Fa[y]=merge(x,y);
}
else
{
int x;read(x);
if(Del[x])
{
puts("-1");
continue;
}
x=getFa(x);
Del[x]=1;
write(Val[x]),puts("");
Fa[x]=Fa[Ri[x]]=Fa[Le[x]]=merge(Le[x],Ri[x]);
Dist[x]=Le[x]=Ri[x]=0;
}
}
return 0;
}
/*
5 5
1 5 4 2 3
1 1 5
1 2 5
2 2
1 4 2
2 2
*/

P4331

这道题的思路似乎也出奇的多,什么线段树,贪心等等。

首先,把严格递增的要求转换为非严格就会好求得多,所以设:

$a_i’=a_i+i,b_i’=b_i+i$

根据数学知识的经验,给定一个数列 $a_i$ ,构造一个数 $b$ 使得 $\displaystyle\sum|a_i-b|$ 最小的话,$b$ 必定是 $a$ 的中位数

那么,这道题现在就有两个情况:

  1. 对于其一个子区间,其中 $a_{l\sim r}$ 单调递增,则对应 $b_{l\sim r}$ 一一对应;
  2. 对于其一个子区间,其中 $a_{l\sim r}$ 单调递减,则对应 $b_{l\sim r}$ 取中位数。

所以我们将原序列拆分成若干个单调区间,然后合并。但是,对于区间与区间之间的答案可能会互相影响。

然后有一个非常离谱但是是对的的结论:

合并两个区间并再次取出中位数。

这点几乎所有题解都没有提及,所幸 $\text{yxc}$ 给出了证明(虽然我还是不会证),也可以参考某一篇集训队论文,也给出了证明,这道题也是改编于此。

所以我们从前往后遍历 $a$ 数列,并在线维护中位数,更新答案。

对于维护中位数,可以建立一个只有 $\lceil\frac{size}{2}\rceil$ 的大根堆,此时的堆顶就是中位数。

而区间的合并也不一定会按照顺序,所以选择用栈维护。

AC Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include<bits/stdc++.h>
template<class T>
inline void read(T &x)
{
x=0;
char ch=getchar(),t=0;
while(ch<'0'||ch>'9') t|=ch=='-',ch=getchar();
while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
if(t) x=-x;
}
template<class T,class ...T1>
inline void read(T &x,T1 &...x1)
{
read(x),read(x1...);
}
template<class T>
inline void write(T x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
}
typedef long long ll;
const int MAXN=1e6+10;
int N,Val[MAXN],Dist[MAXN],Le[MAXN],Ri[MAXN];
struct Seg
{
int end,rt,siz;
}Stk[MAXN];
int Top,ans[MAXN];
int merge(int x,int y)
{
if(!x||!y) return x+y;
if(Val[x]<Val[y]) std::swap(x,y);
Ri[x]=merge(Ri[x],y);
if(Dist[Ri[x]]>Dist[Le[x]]) std::swap(Le[x],Ri[x]);
Dist[x]=Dist[Ri[x]]+1;
return x;
}
inline int pop(int x)
{
return merge(Le[x],Ri[x]);
}
int main()
{
read(N);
for(int i=1;i<=N;++i)
{
read(Val[i]);
Val[i]-=i;
}
for(int i=1;i<=N;++i)
{
auto cur=(Seg){i,i,1};Dist[i]=1;
while(Top&&Val[cur.rt]<Val[Stk[Top].rt])
{
cur.rt=merge(cur.rt,Stk[Top].rt);
if(cur.siz%2&&Stk[Top].siz%2)
cur.rt=pop(cur.rt);
cur.siz+=Stk[Top].siz;--Top;
}
Stk[++Top]=cur;
}
for(int i=1,j=1;i<=Top;++i)
{
while(j<=Stk[i].end)
ans[j++]=Val[Stk[i].rt];
}
ll res=0;
for(int i=1;i<=N;++i) res+=std::abs(Val[i]-ans[i]);
write(res),puts("");
for(int i=1;i<=N;++i) write(ans[i]+i),putchar(' ');
}
/*
5
2 5 46 12 1
*/