This is a data structure summary.
并查集
一个维护连通性的神器,时间复杂度几乎为常数级别(加了按秩合并后就真的是常数级别了)
把(n)个点看成一个森林,用一个数组记录每个点的父亲,初始时均为自己。
要合并两棵树时,也就是合并集合,把一棵个树的父亲指向另一棵树就行了。
要查询两个点是否在同一集合内,只需要看他们的根节点是否是同一个点就行了。
int find(int x){
return f[x] == x ? x : find(f[x]);
}
可以发现,我们只要判断连通性,并不需要记录具体路径是什么,于是就有了路径压缩
int find(int x){
return f[x] == x ? x : f[x] = find(f[x]);
}
就是我们直接把路径中的每个点指向其根节点,省去了后续查询中的很多不必要的操作。
合并操作:
void merge(int x, int y){
f[find(y)] = find(x);
}
按秩合并
记录每个集合的大小,合并时把小的集合指向大的集合。
边带权
即带权并查集,每条边都有个权值,在路径压缩的时候把路径中的所有边权加起来。
//洛谷 P1196 银河英雄传说
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <stack>
#include <list>
#define rep(i,m,n) for(int i=m;i<=n;++i)
#define dop(i,m,n) for(int i=m;i>=n;--i)
#define lowbit(x) (x&(-x))
#define ll long long
#define INF 2147483647
#define re register
#define Open(s) freopen(s".in","r",stdin);freopen(s".out","w",stdout);
#define Close fclose(stdin);fclose(stdout);
#define pause system("pause");
using namespace std;
inline int read(){
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')w = -1;ch = getchar();}
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0',ch = getchar();
return s * w;
}
const int MAXN = 30010;
int f[MAXN], down[MAXN], size[MAXN], top[MAXN];
int n;
int find(int x){
if(f[x] != x){
int fa = find(f[x]);
down[x] += down[f[x]];
f[x] = fa;
}
return f[x];
}
void merge(int x,int y){
int f1 = find(x), f2 = find(y);
if(f1 != f2){
down[f1] += size[f2];
size[f2] += size[f1];
f[top[f1]] = f2;
find(x); find(y);
}
}
void init(){
for(int i = 1; i <= n; ++i)
f[i] = top[i] = i, size[i] = 1;
}
int a, b;
int main(){
cin >> n; init();
while(233){
char ch = getchar();
while(ch != 'M' && ch != 'C' && ch != -1) ch = getchar();
if(ch == -1) break;
if(ch == 'M') a = read(), b = read(), merge(a, b);
else a = read(), b = read(), printf("%d
", find(a) == find(b) ? abs(down[a] - down[b]) - 1 : -1);
}
return 0;
}
栈
先进先出
队列
先进后出
链表
就是一条链嘛
倍增
RMQ问题
·问题:给定一个数列,求区间最大(小)值。
ST表
以区间最大值为例。
倍增的思想,用(f[i][j])表示从第(i)个数开始的(2^j)个数里的最大值。
初始时对于每个(iin [1,n]),(f[i][0]=a[i]),(a[i])即第(i)个数。
然后枚举(j),(f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1])),预处理就完成了。
然后是查询,设要查询的区间为([l,r]),设(p)为使(2^p)不大于((r-l+1))的最大的数,则区间最大值为(max(f[l][p], f[r-(1<<p)+1][p]))
时间复杂度:预处理(O(nlog n)),查询(O(1))
//洛谷 P3865 【模板】ST表
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
inline int read(){
int s=0, w=1;
char ch = getchar();
while(ch <= '0' || ch > '9'){ if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
const int maxn = 100010;
int Log[maxn], f[maxn][20], n, m, a, b;
int Query(int l, int r){
int k = Log[r - l + 1];
return max(f[l][k], f[r - (1 << k) + 1][k]);
}
int main(){
n = read(); m = read();
Log[0]=-1;
for(int i = 1; i <= n; ++i) f[i][0] = read();
for(int i = 1; i <= n; ++i) Log[i] = Log[i / 2]+1;
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]);
while(m--){
a = read(); b = read();
printf("%d
", Query(a, b));
}
return 0;
}
树状数组
·基本用途:维护数列前缀和,支持单点修改
·原理:把(n)写成二进制形式,维护每个(1)出现的位置的值。。。我也说不清。
·实现:
#define lowbit(x) (x & (-x))
单点修改:
void add(int s,int k){ //在s位置加上k
while(s<=n){
sum[s]+=k;
s+=lowbit(s);
}
}
查询前缀和:
int query(int k){ //查询前k个数的前缀和
int ans=0;
while(k){
ans+=sum[k];
k-=lowbit(k);
}
return ans;
}
·树状数组的优势:常数小,代码短
·树状数组的劣势:适用范围小,不易扩展
·树状数组扩展:区间修改,单点查询
插入时把每个数的值当作以该数为最后一个数的前缀和,那个插入的时候只需要在这个位置插入(a_i-a_{i-1})就行了,在这里执行前缀和得到的就是这个数的值,其实就是差分的思想。修改时,设修改的区间为([l,r]),修改的值为(p),那么我们对(l)的位置加上(p),对(r+1)的位置再加上(-p)就行了。
//洛谷 P3368 【模板】树状数组2
#include <iostream>
#include <cstdio>
#include <cstring>
#define lowbit(x) (x&(-x))
using namespace std;
const int MAXN = 500010;
int n,m;
int tree[MAXN], a[MAXN];
void add(int k, int num){
while(k <= n){
tree[k] += num;
k += lowbit(k);
}
}
int sum(int k){
int s = 0;
while(k){
s += tree[k];
k ^= lowbit(k);
}
return s;
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
scanf("%d", &a[i]);
add(i, a[i] - a[i-1]);
}
for(int i = 1; i <= m; i++){
int c, x, y, z;
scanf("%d", &c);
if(c == 1) scanf("%d%d%d", &x, &y, &z), add(x, z), add(y + 1, -z);
else scanf("%d", &x), printf("%d
", sum(x));
}
return 0;
}
线段树
·用途:维护区间(最大值,和,等等等等,甚至最大子序列)
·思想:一分为二,分别维护。比如要维护一个(n)个数的数列的区间和。建一棵树,根节点维护([1,n]),左儿子维护([1,n/2]),右儿子维护([n/2+1,n]),以此类推,递归处理,当维护的区间(l=r)时停止递归,每个节点都用自己的左儿子和右儿子的值来更新自己。
·Lazy标记:如果修改或查询时直接到底的话,那么复杂度仍是(O(n))级别的,因为我们要修改/查询到底,直到把只维护一个数的节点也修改/查询了,这样线段树的作用就失去了。Lazy标记应运而生。当当前的区间在要修改/查询的区间之内时,不再往下递归修改,只修改/查询当前节点,然后在当前节点打一个Lazy标记。下次访问到该节点的时候根据标记修改子树,并下传标记。
//洛谷 P3372 【模板】线段树1
#include <iostream>
#include <cstdio>
#define left now << 1
#define right (now << 1) | 1
#define ll long long
using namespace std;
inline int read(){
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')w = -1;ch = getchar();}
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0',ch = getchar();
return s * w;
}
const int MAXN = 100010;
const int MAXM = 200010;
ll sum[MAXN << 2], a[MAXN], lazy[MAXN << 2];
int dep[MAXN], dfsx[MAXN], n, head[MAXN], ID, num, m, son[MAXN], s[MAXN], f[MAXN], t[MAXN], pos[MAXN];
void Build(int now, int l, int r){
if(l == r) sum[now] = a[l];
else{
int mid = (l + r) / 2;
Build(left, l, mid);
Build(right, mid + 1, r);
sum[now] = sum[left] + sum[right];
}
}
void Pushdown(int len, int now){
if(lazy[now]){
lazy[left] += lazy[now];
lazy[right] += lazy[now];
sum[left] += lazy[now] * (len - (len >> 1));
sum[right] += lazy[now] * (len >> 1);
lazy[now] = 0;
}
return;
}
ll Query(int now, int l, int r, int wl, int wr){
if(wl <= l && wr >= r) return sum[now];
if(wl > r || wr < l) return 0;
Pushdown(r - l + 1, now);
int mid = (l + r) / 2;
return Query(left, l, mid, wl, wr) + Query(right, mid + 1, r, wl, wr);
}
void Change(int now, int l, int r, int wl, int wr, int add){
if(wl <= l && wr >= r){ sum[now] += add * (r - l + 1), lazy[now] += add; return; }
if(wl > r || wr < l) return ;
Pushdown(r - l + 1, now);
int mid = (l + r) / 2;
Change(left, l, mid, wl, wr, add);
Change(right, mid + 1, r, wl, wr, add);
sum[now] = sum[left] + sum[right];
}
int A, B, C;
int main(){
n = read();m = read();
for(int i = 1; i <= n; ++i) a[i] = read();
Build(1, 1, n);
for(int i = 1; i <= m; ++i){
A = read();
if(A == 1){
A = read(); B = read(); C = read();
Change(1, 1, n, A, B, C);
}
else {
A = read(); B = read();
printf("%lld
",Query(1, 1, n, A, B));
}
}
return 0;
}
(zkw)线段树
待补。
可持久化线段树
·用途:一般的数据结构都是维护最新状态,可持久化数据结构就是能恢复“第(k)次操作之前的状态”。
·思想:线段树每次修改需要修改的节点数是((log n))级别的,于是每次修改后对修改了的节点建立一个副本,之后通过副本恢复状态就行了,空间复杂度(O(nlog n))。
详细:点我
主席树
扫描线
·基本用途:求矩形面积并。
待补。
分块
·用途:维护区间,能实现某些线段树难以实现的,也难以实现某些线段树能实现的。
·优势:代码短,实用性强。
·劣势:时间复杂度较高。
·思想:大段维护,小段朴(bao)素(li)。
·实现:把一个数列分成(sqrt n)块,每块大小(sqrt n),最后不足一块的算一块。以维护区间和为例,先预处理出每块的和,时间复杂度(O(n)),查询时把要查询的区间([l,r])分成三部分:
1、左边不足一块的部分。
2、中间很多块的部分。
3、右边不足一块的部分。
对1、3部分朴(bao)素(li)求解,第2部分我们是维护了的,直接加上中间每一块的和就行了。
修改时,
单点修改:把存对应值数组直接修改,然后再对这个点所在的块进行修改。
区间修改:把区间分成三个部分后,对左右部分朴(bao)素(li)修改,然后对中间的每一块使用Lazy数组标记一下修改了多少,这里的Lazy数组和线段树里的类似,但又有不同,这里的Lazy数组显然不能下传标记,因为当一个块里只有一部分点需要查询时,把Lazy数组下放的话,那么这个块里其他的点的值就不会修改,所以,这里引入标记永久化,查询时不对保存值的数组进行修改,也不下放标记,只是单纯的把计数器加上这块的Lazy值乘以这一块中要查询的数的个数。区间修改完成。
一般的时间复杂度(O(nsqrt n)),块的大小用(sqrt n)的原因是为了平均朴(bao)素(li)和维护的复杂度,使得左右两边和中间大块统计/修改时的时间复杂度都是(O(sqrt n))。块的大小也因具体问题而异。
·分块的进阶应用:区间众数
莫队
待补。
树链剖分
·用途:维护和查询树上的链
·思想:把一棵树剖成一些链,为每个点编个号,使得每一条链上的点的编号是连续的,这样我们就可以用数据结构(比如线段树)来维护每一条链了。
重链剖分
明确几个概念:
·重儿子:子树节点数最多的儿子
·重边:到重儿子的边
·重链:全由重边组成的链
显然可以用(dfs)求出每个点的子树大小,重儿子也得以求出,然后再一遍(dfs),对于每个点,如果有重儿子,都优先遍历其重儿子,这样就能使得这条重链上的编号是连续的,本次(dfs)的(dfs)序就是每个点的编号。然后用数据结构维护即可。
长链剖分
重儿子就是深度最深的儿子。然后和重链剖分一样处理。
用途:线性时间内处理一些和深度有关的(DP)。
先(O(1))继承重儿子的信息,然后暴力转移其它儿子的信息。
例如这题
平衡树
Treap
待学。
Splay
待补。