数据结构与算法的常见应用,涵盖并查集、树状数组、线段树、ST表和分块算法等。涉及并查集的路径压缩与秩压缩优化,树状数组的区间查询与更新操作,线段树的区间求和与区间最值查询,ST表的倍增技术用于区间最值查询等。
并查集
并查集是一种树形的数据结构,它用于处理一些不交集的
合并 及 查询 问题。
它支持两种操作:
查找(Find):确定某个元素处于哪个子集;
合并(Union):将两个子集合并成一个集合。
并不能提供删除操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int [] father;int [] rank; UnionSet(int n){ father = new int [n]; rank = new int [n]; for (int i = 0 ;i < n;i++) father[i] = i; } void join (int x, int y) { x = find(x); y = find(y); if (x != y){ if (rank[x] > rank[y]) father[y] = x; else { if (rank[x] == rank[y]) rank[y]++; father[x] = y; } } } int find (int x) { if (x == father[x]) return x; return father[x] = find(father[x]); }
树状数组
单点修改 区间查询
1 2 3 4 5 6 7 8 9 10 11 12 13 static long [] tr; static int lowbit (int x) { return x & -x; } static void add (int idx, int w) { for (int i = idx;i <= n;i += lowbit(i)) tr[i] += w; } static long sum (int idx) { long res = 0 ; for (int i = idx;i > 0 ;i -= lowbit(i)) res += tr[i]; return res; } System.out.println(sum(r)-sum(l-1 ));
add时间复杂度
sum时间复杂度
区间修改 单点查询
1 2 3 4 5 6 7 static int [] tr, ori; static int lowbit (int x) {...}static void add (int idx, int w) {...}static long sum (int idx) {...}for (int i = 1 ;i <= n;i++) add(i, ori[i]-ori[i-1 ]); add(l, w); add(r+1 , -w); System.out.println(sum(idx));
区间修改 区间查询
位置p的前缀和 =
在等式最右侧的式子 中, d [1] 被用了 p 次, d [2] 被用了 p − 1 次…..那么我们可以写出:
位置p的前缀和 =
那么我们可以维护两个差分数组的前缀和(d [i ]是 a [i ]的 差 分 数 组 ):
一个数组是 sum 1[i ] = ∑d [i ]
, 另一个数组是 sum 2[i ] = ∑d [i ] * i
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static long [] tr1, tr2; static int [] ori;static int lowbit (int x) {...}static void add (int idx, long w) { for (int i = idx;i <= n;i += lowbit(i)){ tr1[i] += w; tr2[i] += w*idx; } } static long sum (int idx) { long res = 0 ; for (int i = idx;i > 0 ;i -= lowbit(i)){ res += (idx+1 )*tr1[i]-tr2[i]; } return res; } for (int i = 1 ;i <= n;i++) add(i, ori[i]-ori[i-1 ]); add(l, w); add(r+1 , -w); System.out.println(sum(r)-sum(l-1 ));
最值问题
树状数组 C [i ] 所包含的区间[i − l o w b i t (i ) + 1, i ] ,
其中区间的个数是 个, C [i ] 一定包含A [i ]
树状数组下标都从1开始
image-20250601173428110
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 static void update (int x) { for (int i = x;i <= n;i += lowbit(i)){ tr[i] = ori[i];· for (int j = 1 ;j < lowbit(i);j <<= 1 ) tr[i] = max(tr[i], tr[i-j]); } } static int query (int l, int r) { int ans = ori[r]; while (l <= r){ ans = max(ans, ori[r--]); while (r-lowbit(r) >= l){ ans = max(ans, tr[r]); r -= lowbit(r); } } return ans; } public static void main (String[] args) throws Exception{ for (int i = 1 ;i <= n;i++){ ori[i] = nextInt(); update(i); } while (m-- > 0 ){ io = in.readLine().split(" " ); char ops = io[0 ].charAt(0 ); int x = Integer.parseInt(io[1 ]), y = Integer.parseInt(io[2 ]); if (ops == 'U' ){ ori[x] = y; update(x); } else System.out.println(query(x, y)); } }
求解GCD,LCM等其他问题只需要把里面max()换成GCD()就行了
项链
线段树
线段树将每个长度不为
的区间划分成左右两个区间递归求解,把整个线段划分为一个树形结构,通过合并左右两区间信息来求得该区间的信息。
有个大小为 5 的数组 a = {10, 11, 12, 13, 14} ,要将其转化为线段树,有以下做法:设线段树的根节点编号为
1 ,用数组d 来保存我们的线段树, d [i ] 用来保存线段树上编号为i 的节点的值(这里每个节点所维护的值就是这个节点所表示的区间总和)。
image-20250601173516107
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 static long [] f = new long [N], v = new long [N], a = new long [M];static void build (int k, int l, int r) { if (l == r){ f[k] = a[l]; return ; } int m = l+r >> 1 ; build(k*2 , l, m); build(k*2 +1 , m+1 , r); f[k] = f[k*2 ]+f[k*2 +1 ]; } static void down (int k, int l, int r, int m) { if (v[k] == 0 ) return ; f[k*2 ] += v[k]*(m-l+1 ); f[k*2 +1 ] += v[k]*(r-m); v[k*2 ] += v[k]; v[k*2 +1 ] += v[k]; v[k] = 0 ; } static void add (int k, int l, int r, int s, int t, long w) { if (s <= l && r <= t){ f[k] += w*(r-l+1 ); v[k] += w; return ; } int m = l+r >> 1 ; down(k, l, r, m); if (s <= m) add(k*2 , l, m, s, t, w); if (t > m) add(k*2 +1 , m+1 , r, s, t, w); f[k] = f[k*2 ] + f[k*2 +1 ]; } static long sum (int k, int l, int r, int s, int t) { if (s <= l && r <= t) return f[k]; int m = l+r >> 1 ; down(k, l, r, m); long res = 0 ; if (s <= m) res += sum(k*2 , l, m, s, t); if (t > m) res += sum(k*2 +1 , m+1 , r, s, t); return res; } build(1 , 1 , n); add(1 , 1 , n, x, y, w); sum(1 , 1 , n, x, y);
关于线段树的空间:d = n e w
i n t [n < < 2]
开四倍空间
再提供一个C++版本的动态开点 版本:
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 struct node { int val = 0 , add = 0 ; node *left = nullptr , *right = nullptr ; }; void down (node* cur, int l, int r, int m) { int t = cur->add; if (cur->left == nullptr ) cur->left = new node (); if (cur->right == nullptr ) cur->right = new node (); cur->left->val += t*(m-l+1 ); cur->right->val += t*(r-m); cur->left->add += t; cur->right->add += t; cur->add = 0 ; } void upd (node* cur, int l, int r, int s, int t, int val) { if (s <= l && r <= t){ cur->val += val*(r-l+1 ); cur->add += val; return ; } int m = l+r >> 1 ; down (cur, l, r, m); if (s <= m) upd (cur->left, l, m, s, t, val); if (m+1 <= t) upd (cur->right, m+1 , r, s, t, val); cur->val = cur->left->val+cur->right->val; } int qry (node* cur, int l, int r, int s, int t) { if (s <= l && r <= t) return cur->val; int m = l+r >> 1 , res = 0 ; down (cur, l, r, m); if (s <= m) res += qry (cur->left, l, m, s, t); if (m+1 <= t) res += qry (cur->right, m+1 , r, s, t); return res; } node* root; int MAXV = 1e9 ;NumArray (vector<int >& nums) { root = new node (); for (int i = 1 ;i <= nums.size ();i++){ upd (root, 1 , MAXV, i, i, nums[i-1 ]); } } void update (int index, int val) { upd (root, 1 , MAXV, index+1 , index+1 , val); } int sumRange (int left, int right) { return qry (root, 1 , MAXV, left+1 , right+1 ); }
ST表(离线区间查询)
ST表 (Sparse
Table,稀疏表 )是一种简单的数据结构,主要用来解决RMQ (Range
Maximum/Minimum
Query,区间最大/最小值查询 )问题。它主要应用倍增 的思想,可以实现O (n log n ) 预处理、
O (1) 查询。
主要思想:倍增
ST表板子
F [i ][j ]: 原数组区间[i , i + 2j − 1] 的最值
预处理原理
f[j][i] = Max(f[j][i-1], f[j+(1<<(i-1))][i-1]);
image-20250601173733251
查询原理
res = Math.max(f[l][s], f[r-(1<<s)+1][s]);
需要找到两个[l , r ] 的子区间,它们的并集恰是
[l , r ] (可以相交)。
image-20250601173757465
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int [][] f = new int [N][17 ]; static int [] log2 = new int [N];private static void init () { for (int i = 2 ;i <= n;i++) log2[i] = log2[i/2 ]+1 ; for (int i = 1 ;i <= n;i++) f[i][0 ] = A[i]; for (int i = 1 ;i <= 17 ;i++){ for (int j = 1 ;j + (1 <<i)-1 <= n;j++){ f[j][i] = Math.max(f[j][i-1 ], f[j+(1 <<(i-1 ))][i-1 ]); } } } private static int query (int l, int r) { int s = log2[r-l+1 ]; int res = Math.max(f[l][s], f[r-(1 <<s)+1 ][s]); return res; }
其实ST表不仅能处理最大值/最小值,凡是符合结合律 且可重复贡献 的信息查询都可以使用ST表高效进行。什么叫可重复贡献呢?设有一个二元运算
f (x , y ) ,满足
f (a , a ) = a ,则f (x , y ) 是可重复贡献的。显然最大值(max)、最小值(min)、最大公因数(GCD)、最小公倍数(LCM)、按位或(|)、按位与(&) 都符合这个条件。可重复贡献 的意义在于,可以对两个交集不为空的区间进行信息合并 。区间和就不具有这个性质,求区间和的时候采用的预处理区间重叠了,则会导致重叠部分被计算两次;即f (a , a ) = 2a 。
分块
P3372 【模板】线段树
1 - 洛谷 | 计算机科学教育新生态
传说中的根号算法,比线段树和树状数组好理解,优雅的暴力。
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 static int n, m, bel[]=new int [N];static int sq, st[]=new int [N], ed[]=new int [N];static long a[]=new long [N], sum[]=new long [N], f[]=new long [N];static void update (int l, int r, long v) { if (bel[l] == bel[r]){ for (int i = l;i <= r;i++){ a[i] += v; sum[bel[i]] += v; } }else { for (int i = l;i <= ed[bel[l]];i++){ a[i] += v; sum[bel[i]] += v; } for (int i = st[bel[r]];i <= r;i++){ a[i] += v; sum[bel[i]] += v; } for (int i = bel[l] + 1 ; i < bel[r]; ++i) f[i] += v; } } static long query (int l, int r) { long ans = 0 ; if (bel[l] == bel[r]){ for (int i = l;i <= r;i++) ans += a[i]; }else { for (int i = l;i <= ed[bel[l]];i++) ans += a[i]+f[bel[i]]; for (int i = st[bel[r]];i <= r;i++) ans += a[i]+f[bel[i]]; for (int i = bel[l] + 1 ; i < bel[r]; ++i) ans += sum[i]+f[i]*(ed[i]-st[i]+1 ); } return ans; } static void solve () throws Exception { sq = (int )Math.sqrt(n); for (int i = 1 ;i <= sq;i++){ st[i] = n/sq*(i-1 )+1 ; ed[i] = n/sq*i; } ed[sq] = n; for (int i = 1 ; i <= sq; ++i){ for (int j = st[i]; j <= ed[i]; ++j){ bel[j] = i; sum[i] += a[j]; } } update(l, r, x); query(l, r); }
算算时间复杂度
首先看查询操作:
接下来是修改操作:
利用均值不等式可知,当 ,即 时,单次操作的时间复杂度最优,为
。
对于数组长度为 n ,查询和修改次数为 m ,时间复杂度为
莫队
基于分块 思想,复杂度为
。
一般来说,如果可以在 O(1) 内从 [l,r] 的答案转移到 [l−1,r] 、 [l+1,r]
、 [l,r−1] 、 [l,r+1]
这四个与之紧邻的区间的答案,则可以考虑使用莫队。
SPOJ
DQUERY - D-query
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 static int [] a = new int [N], cnt = new int [N], ans = new int [N];static int [][] q = new int [N][3 ];static void add (int p) { if (cnt[a[p]]++ == 0 ) cur++; } static void del (int p) { if (--cnt[a[p]] == 0 ) cur--; } static void solve () throws Exception{ n = ni(); sq = (int )Math.sqrt(n); for (int i = 1 ;i <= n;i++) a[i] = ni(); m = ni(); for (int i = 1 ;i <= m;i++){ q[i][0 ] = ni(); q[i][1 ] = ni(); q[i][2 ] = i; } Arrays.sort(q, 1 , m+1 , (x, y)->{ if (x[0 ]/sq == y[0 ]/sq){ if (x[0 ]/sq % 2 == 1 ) return x[1 ]/sq-y[1 ]/sq; return y[1 ]/sq-x[1 ]/sq; } return x[0 ]/sq - y[0 ]/sq; }); for (int i = 1 ;i <= m;i++){ int s = q[i][0 ], t = q[i][1 ], dx = q[i][2 ]; while (l > s) add(--l); while (l < s) del(l++); while (r > t) del(r--); while (r < t) add(++r); ans[dx] = cur; } for (int i = 1 ;i <= m;i++) out.println(ans[i]); }
线性基
性质
原数列里的任何一个数都可以通过线性基里的数异或表示出来
线性基里任意一个子集的异或和都不为0
一个数列可能有多个线性基,但是线性基里数的数量一定唯一,而且是满足性质一的基础上最少的
线性基中元素互相异或,异或集合不变。
用处
快速查询一个数是否可以被一堆数异或出来
快速查询一堆数可以异或出来的最大/最小值
快速查询一堆数可以异或出来的第k大值
1 2 3 4 5 6 7 8 9 10 11 12 static long [] d = new long [63 ]; static void add (long x) { for (int i = 62 ;i >= 0 ;i--){ if ((x >> i & 1 ) == 0 ) continue ; if (d[i] == 0 ){ d[i] = x; break ; } x ^= d[i]; } if (x != 0 ) ++cnt; }
1 2 3 4 5 Boolean ask (long x) { for (R int i=62 ;i>=0 ;i--) if ((x >> i & 1 ) == 1 ) x ^= d[i]; return x==0 ; }
前置知识:a ^ b ^ c = 0 ⇒ a
^ b = c且a ^ c = b且b ^ c = a
对于一个数x 不能成功插入线性基 。显然就是它在尝试插入时异或若干个数之后变成了0 。
则有:x ∧ d [a ]∧ d [b ]∧ d [c ]∧ … = 0
即:d [a ]∧ d [b ]∧ d [c ]∧ … = x
说明x 肯定能由原序列中数异或得到 。
1 2 3 4 5 static long getMax () { long ans = 0 ; for (int i = 62 ;i >= 0 ;i--) ans = Math.max(ans, ans^d[i]); return ans; }
1 2 3 4 5 6 7 8 9 static long getMax () { if (cnt != n) return 0 ; long ans = 0 ; for (int i = 0 ;i <= 63 && ans == 0 ;i++){ if (d[i] != 0 ) ans = d[i]; } return ans; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static void rebuild () { for (int i = 62 ;i >= 0 ;i--) { for (int j = i - 1 ; j >= 0 ; j--) { if ((d[i]>>j & 1 ) == 1 ) d[i] ^= d[j]; } } for (int i = 0 ;i <= 62 ;i++) if (d[i]!=0 ) p[m++]=d[i]; } static long Kmin (int k) { if (m != n) --k; if (k == 0 ) return 0 ; if (k >= (1L << m)) return -1 ; long res = 0 ; for (int i = 0 ;i <= 62 ;i++) if ((k>>i & 1 ) == 1 ) res ^= p[i]; return res; }