算法模板(2):数据结构(2)

算法模板(2):数据结构(2)

1.树状数组

模板

求前缀和、改变某个数。
int lowbit(int x) {
	return x & -x;
}
//在第id的位置加上c
void add(int id, int c) {
	for (int i = id; i <= N; i += lowbit(i)) tri[i] += c;
}
//求前id项的和
int sum(int id) {
	int res = 0;
	for (int i = id; i; i -= lowbit(i)) res += tri[i];
	return res;
}
//建立树状数组O(nlogn)
for(int i = 1; i <= N; i++) add(i, a[i]);
  • 支持两种操作:快速求前缀和 O ( log ⁡ n ) O(\log n) O(logn),以及修改某一个数 O ( log ⁡ n ) O(\log n) O(logn)
  • 只要一道题可以划归到以上两种操作,那么就可以用树状数组来做。一般来讲,树状数组的题目不是那么容易看出来。
  • 其实树状数组维护的就是一段区间的和。就是图片中的那个绿色的一段。
  • 树状数组不可以处理0,树状数组的范围是数据的最大值,并不是数组的个数

二维树状数组
const int maxn = 1010;
typedef long long ll;
int N, M, A, B;
ll tr[maxn][maxn];

int lowbit(int x){
	return x & -x;
}
void add(int x, int y, ll val){
	for(int i = x; i <= N; i += lowbit(i)){
		for(int j = y; j <= M; j += lowbit(j)){
			tr[i][j] += val;
		}
	}
}
ll query(int x, int y){
	ll res = 0;
	for(int i = x; i; i -= lowbit(i)){
		for(int j = y; j; j -= lowbit(j)){
			res += tr[i][j];
		}
	}
	return res;
}
二维树状数组+区间修改
  • 用差分去做这道题。
J Matrix Subtraction
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 1010;
typedef long long ll;
int N, M, A, B;
ll tr[maxn][maxn];

int lowbit(int x){
	return x & -x;
}
void add(int x, int y, ll val){
	for(int i = x; i <= N; i += lowbit(i)){
		for(int j = y; j <= M; j += lowbit(j)){
			tr[i][j] += val;
		}
	}
}
ll query(int x, int y){
	ll res = 0;
	for(int i = x; i; i -= lowbit(i)){
		for(int j = y; j; j -= lowbit(j)){
			res += tr[i][j];
		}
	}
	return res;
}
void insert(int x1, int y1, int x2, int y2, ll val) {
	add(x1, y1, val);
	add(x1, y2 + 1, -val);
	add(x2 + 1, y1, -val);
	add(x2 + 1, y2 + 1, val);
}
int main(){
	int T;
	scanf("%d", &T);
	while(T--){
		memset(tr, 0, sizeof tr);
		scanf("%d%d%d%d", &N, &M, &A, &B);
		for(int i = 1; i <= N; i++){
			for(int j = 1; j <= M; j++){
				ll x;
				scanf("%lld", &x);
				insert(i, j, i, j, x);
			}
		}

		bool flag = true;
		for(int i = 1; i <= N; i++){
			for(int j = 1; j <= M; j++){
				ll tmp = query(i, j);
				if(tmp < 0){
					flag = false;
					break;
				}
				if(i + A - 1 > N || j + B - 1 > M) continue;
				insert(i, j, i + A - 1, j + B - 1, -tmp);
			}
			if(!flag) break;
		}
		for (int i = 1; i <= N; i++) {
			if (!flag) break;
			for (int j = 1; j <= M; j++) {
				ll tmp = query(i, j);
				if (tmp) flag = false;
			}
		}
		if(flag) printf("^_^\n");
		else printf("QAQ\n");
	}
	return 0;
}

241. 楼兰图腾

  • 这道题,本质上是在求离散化后的数组的逆序对。
  • 以求V举例:每次遍历到一个 a [ i ] a[i] a[i],就在树状数组 t r i [ a [ i ] ] tri[a[i]] tri[a[i]] 的位置加1,因为他是一个1~N的全排列嘛。题目中的 G r e a t e r [ i ] = s u m ( N ) − s u m ( y ) Greater[i]=sum(N)-sum(y) Greater[i]=sum(N)sum(y),其实就是求 [ y + 1 , N ] [y+1, N] [y+1,N] 的和,其实也就是看看目前有多少个 y+1~N 的元素在之前已经出现过了(因为只要出现过就会加入到数组里面)。因此Greater数组里面存的就成了左面有多少个元素比他大。同理lower就是求sum(a[i] - 1),表示左边有多少个元素比 a [ i ] a[i] a[i] 小。然后把 t r i tri tri 清空再从右往左扫描一遍。
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 200010;
typedef long long ll;
int a[maxn], tri[maxn], N, Greater[maxn], lower[maxn];
int lowbit(int x) {
	return x & -x;
}
//在第id的位置加上c
void add(int id, int c) {
	for (int i = id; i <= N; i += lowbit(i)) tri[i] += c;
}
//求前id项的和
int sum(int id) {
	int res = 0;
	for (int i = id; i; i -= lowbit(i)) res += tri[i];
	return res;
}
int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
	for (int i = 1; i <= N; i++) {
		Greater[i] = sum(N) - sum(a[i]);
		lower[i] = sum(a[i] - 1);
		add(a[i], 1);
	}
	ll ans1 = 0, ans2 = 0;;
	for (int i = 1; i <= N; i++) {
		ans1 += (ll)Greater[i] * (ll)(N - a[i] - Greater[i]);
		ans2 += (ll)lower[i] * (ll)(a[i] - lower[i] - 1);
	}
	printf("%lld %lld\n", ans1, ans2);
	return 0;

  • 这个模板也适用于求不是 1~N 排列的有重复数字数组的逆序对,但是一定要注意!!!对于求逆序对的问题,如果不是一个排列的话,树状数组用到的范围就不只是 N,而是数据范围的最大值!因此,add 函数尤为注意!必须写成 i i i <= MAX_A.
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 100010, MAX_A = 100000;
typedef long long ll;
int a[maxn], tri[maxn], N, gre_pre[maxn], gre_la[maxn];

int lowbit(int x) {
	return x & -x;
}

void add(int id, int c) {
	//这个地方要万分小心!不要 i <= N,因为你要用到的树状数组区间很大!不只是 N 这么一点点!
	for (int i = id; i <= MAX_A; i += lowbit(i)) tri[i] += c;
}

int sum(int id) {
	int res = 0;
	for (int i = id; i ; i -= lowbit(i)) res += tri[i];
	return res;
}

int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
	for (int i = 1; i <= N; i++) {
		//printf("%d %d\n", sum(MAX_A), sum(a[i]));
        //求前面比它大的数
		gre_pre[i] = sum(MAX_A) - sum(a[i]);
		add(a[i], 1);
	}
	memset(tri, 0, sizeof tri);
	for (int i = N; i >= 1; i--) {
		gre_la[i] = sum(MAX_A) - sum(a[i]);
		add(a[i], 1);
	}
	ll ans = 0;
	for (int i = 1; i <= N; i++) {
        //求后面比它大的数。
		ans += min(gre_pre[i], gre_la[i]);

	}
	printf("%lld\n", ans);
	return 0;
}

242. 一个简单的整数问题

  • 给定长度为N的数列A,然后输入M行操作指令。

    第一类指令形如“C l r d”,表示把数列中第l~r个数都加d。

    第二类指令形如“Q X”,表示询问数列中第x个数的值。

  • 这道题是用树状数组去维护差分。

#include<cstdio>
typedef long long ll;
const int maxn = 100010;
ll a[maxn], tri[maxn];
int N, M;
int lowbit(int x) {
	return x & -x;
}
void add(int id, int c) {
	for (int i = id; i <= N; i += lowbit(i)) tri[i] += c;
}
ll sum(int id) {
	ll res = 0;
	for (int i = id; i; i -= lowbit(i)) res += tri[i];
	return res;
}
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]);
	}
	while (M--) {
		char op[5];
		scanf("%s", op);
		if (op[0] == 'Q') {
			int x;
			scanf("%d", &x);
			printf("%lld\n", sum(x));
		}
		else {
			int l, r, c;
			scanf("%d%d%d", &l, &r, &c);
			add(l, c), add(r + 1, -c);
		}
	}
	return 0;
}

2.线段树

  • 线段树一般是一个满二叉树,有五个基本操作:pushup, pushdown, build, modify, query。
  • 父结点:x / 2;左儿子:2x;右儿子:2x + 1。
  • 一个长度是 N 的数组,转化为线段树,线段树数组开 4N 的空间。
  • 凡是涉及到修改某个单点的,不需要加上懒标记,复杂度就是 O ( log ⁡ n ) O(\log n) O(logn)。但是修改某个区间的话还是要加上懒标记,不然复杂度会退化。我最近(2020.11.30)发现,区间修改和单点修改的modify不太一样。
  • 似乎,query返回值是节点(struct)时,与返回值时int(long long)时,函数的区间递归是两种截然不同的方式。不过这也是有一定原因的。因为返会节点的话,说明答案就包含在某一个节点里面。而返回一个值的话,说明答案可能是要把相关的节点全部加起来。
  • 再写一个小总结。关于modify那里我可算弄明白了,单点修改的时候,只需要看单点的位置在哪里就进入哪个区间,所以是:
int mid = (tr[u].l + tr[u].r) / 2;
if(x <= mid) modify(2 * u, x, v);
else modify(2 * u + 1, x, v);
pushup(u);
  • 如果是区间修改,就要看修改区间与当前节点区间的交集情况。与左区间有交集就修改左儿子,与右区间有交集就修改右儿子。所以是:
int mid = (tr[u].l + tr[u].r) / 2;
if (l <= mid) modify(2 * u, l, r, d);
if (r > mid) modify(2 * u + 1, l, r, d);
pushup(u);

1275. 最大数

给定一个正整数数列 a 1 , a 2 , … , a n a_1,a_2,…,a_n a1,a2,,an, 每一个数都在 0 ∼ p − 1 0∼p−1 0p1 之间。

可以对这列数进行两种操作:

  1. 添加操作:向序列后添加一个数,序列长度变成 n + 1 n+1 n+1
  2. 询问操作:询问这个序列中最后 L L L 个数中最大的数是多少。

程序运行的最开始,整数序列为空。

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 200010;
int M, P, N;
struct node {
	int l, r;
	int v;  //[l, r]区间最大值。
}tr[4 * maxn];

//build功能是把所有节点的左右儿子找到,也就是初始化node节点。
void build(int u, int l, int r) {  
	tr[u].l = l, tr[u].r = r;
	if (l == r) return;
	int mid = (l + r) / 2;
	build(2 * u, l, mid), build(2 * u + 1, mid + 1, r);
}
//query功能是返回[l, r]的最大值。
int query(int u, int l, int r) {
	//[tr[u].l, tr[u].r] 完全含于 [l, r]。
	if (l <= tr[u].l && tr[u].r <= r) return tr[u].v;
	int mid = (tr[u].l + tr[u].r) / 2;
	int v = 0;
	if (l <= mid) v = query(2 * u, l, r);
	if (r > mid) v = max(v, query(2 * u + 1, l, r));
	return v;
}
//pushup功能是又子节点信息,计算父结点信息。
void pushup(int u) {
	tr[u].v = max(tr[2 * u].v, tr[2 * u + 1].v);
}
//把原数组第x个位置的值修改为v
void modify(int u, int x, int v) {
	if (tr[u].l == x && tr[u].r == x) tr[u].v = v;
	else {
		int mid = (tr[u].l + tr[u].r) / 2;
		if (x <= mid) modify(2 * u, x, v);
		else modify(2 * u + 1, x, v);
		pushup(u);
	}
}
int main() {
	scanf("%d%d", &M, &P);
	char op[5];
	int last = 0;
	build(1, 1, M);
	for (int i = 0; i < M; i++) {
		scanf("%s", op);
		if (op[0] == 'Q') {
			int L; 
			scanf("%d", &L);
			last = query(1, N - L + 1, N);
			printf("%d\n", last);
		}
		else {
			int v;
			scanf("%d", &v);
			modify(1, ++N, (last + v) % P);
		}
	}
	return 0;
}

243. 一个简单的整数问题2

给定一个长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:

  1. “C l r d”,表示把 A[l],A[l+1],…,A[r] 都加上 d。
  2. “Q l r”,表示询问 数列中第 l~r 个数的和。

对于每个询问,输出一个整数表示答案。

  • 懒标记。这道题每个节点维护两个值。一个是sum,表示区间和;一个是add,表示在这个区间全部都加上add这个数字。
  • pushup通常放在build和modify后面,pushdown通常放在modify和query前面
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn = 100010;
int N, M, a[maxn];
struct node {
	int l, r;
	ll sum, add;
}tr[maxn * 4];
void pushup(int u) {
	tr[u].sum = tr[2 * u].sum + tr[2 * u + 1].sum;
}
void pushdown(int u) {
    //引用千万别写错,后面&left也要加 &
	auto& root = tr[u], & left = tr[2 * u], & right = tr[2 * u + 1];
	if (root.add) {
		left.add += root.add, left.sum += (left.r - left.l + 1) * root.add;
		right.add += root.add, right.sum += (right.r - right.l + 1) * root.add;
		root.add = 0;
	}
}
void build(int u, int l, int r) {
	if (l == r) tr[u] = { l, r, a[l], 0 };
	else {
		tr[u].l = l, tr[u].r = r;
		int mid = (l + r) / 2;
		build(2 * u, l, mid), build(2 * u + 1, mid + 1, r);
		pushup(u);
	}
}
void modify(int u, int l, int r, ll d) {
	if (l <= tr[u].l && tr[u].r <= r) {
		tr[u].sum += (ll)(tr[u].r - tr[u].l + 1) * d;
		tr[u].add += d;
	}
	else {
		pushdown(u);
		int mid = (tr[u].l + tr[u].r) / 2;
		if (l <= mid) modify(2 * u, l, r, d);
		if (r > mid) modify(2 * u + 1, l, r, d);
		pushup(u);
	}
}
ll query(int u, int l, int r) {
	if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
	pushdown(u);
	int mid = (tr[u].l + tr[u].r) / 2;
	ll sum = 0;
	if (l <= mid) sum += query(2 * u, l, r);
	if (r > mid) sum += query(2 * u + 1, l, r);
	return sum;
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
	build(1, 1, N);
	while (M--) {
		char op[5];
		int l, r, d;
		scanf("%s%d%d", op, &l, &r);
		if (op[0] == 'Q') {
			printf("%lld\n", query(1, l, r));
		}
		else {
			scanf("%d", &d);
			modify(1, l, r, d);
		}
	}
	return 0;
}

3.1 可持久化字典树

  • 并不是所有数据结构都可以做可持久化,前提是这个数据结构本身的拓扑结构是不变的。比如平衡树就会有旋转,拓扑排序会改变。
  • 可持久化的意思就是可以存下来数据结构的所有历史版本。核心思想:只记录和前一个版本不一样的地方。

256. 最大异或和

  • 题意:给定一个非负整数序列 a a a,初始长度为 N N N。有 M M M 个操作,有以下两种操作类型:A x:添加操作,表示在序列末尾添加一个数 x x x,序列的长度 N N N 增大 1 1 1Q l r x:询问操作,你需要找到一个位置 p p p,满足 l ≤ p ≤ r l \le p \le r lpr,使得: a [ p ] ⊕ a [ p + 1 ]   ⊕ . . .   ⊕ a [ N ] ⊕ x a[p]\oplus a[p+1]\ \oplus ...\ \oplus a[N]\oplus x a[p]a[p+1] ... a[N]x 最大,输出这个最大值。
  • 异或有一个性质,对同一个数异或两次,结果不改变,即 a ⊕ b ⊕ b = a a\oplus b\oplus b = a abb=a。异或被称作不进位的加法,当然满足交换律。
  • 维护一个前缀和数组 S i S_i Si,不过这个前缀和是异或和。那么,答案其实就是在 [ L , R ] [L, R] [L,R] 中找到 p p p,使得 S p − 1 ⊕ S n ⊕ X S_{p-1} \oplus S_n\oplus X Sp1SnX最大。记 C C C = S n ⊕ X S_n\oplus X SnX,可以看出来 C C C 是一个定值,就是求 S p − 1 ⊕ C S_{p-1}\oplus C Sp1C 最大。
  • 其实,可以联想到那个字典树的题,找最大异或对。把 C C C 写成二进制。如果 C C C 的该位是1,就优先往0的子树寻找;如果 C C C 的该位是0,就优先往1的子树去寻找。
  • 但是在一个区间内找到某个值嘛,就得用到可持久化的字典树。

在这里插入图片描述

#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
//初始数组有 3e5 个数,后面有 3e5 个操作,所以最多有 6e5 个数
//字典树第一维大小最大是 6e5 * 24,因为 2^24 > 1e7 
const int maxn = 600010, maxm = 25 * maxn;
int N, M;
//我么想知道左子树记录的下标的最大值是否大于 L,因此存一个当前子树的下标的最大值。
int tr[maxm][2], max_id[maxm];
int s[maxn], root[maxn], idx;  //前缀和序列,不同版本根节点序列

void insert(int i, int k, int p, int q) {
	if (k < 0) {
		max_id[q] = i;
		return;
	}
	int v = (s[i] >> k) & 1;
    //当前位是 v,需要新建立一条链,把其他没有修改的地方复制过来,就是 v ^ 1.
	if (p) tr[q][v ^ 1] = tr[p][v ^ 1];
	tr[q][v] = ++idx;
	insert(i, k - 1, tr[p][v], tr[q][v]);
	max_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]);
}

int query(int root, int C, int L) {
	int p = root;
	for (int i = 23; i >= 0; i--) {
		int v = (C >> i) & 1;
		if (max_id[tr[p][v ^ 1]] >= L) p = tr[p][v ^ 1];
		else p = tr[p][v];
	}
	return C ^ s[max_id[p]];
}

int main() {
	scanf("%d%d", &N, &M);
	max_id[0] = -1;
	root[0] = ++idx;
	insert(0, 23, 0, root[0]);
	for (int i = 1; i <= N; i++) {
		int x;
		scanf("%d", &x);
		s[i] = s[i - 1] ^ x;
		root[i] = ++idx;
		insert(i, 23, root[i - 1], root[i]);
	}
	char op[2];
	int l, r, x;
	while (M--) {
		scanf("%s", op);
		if (op[0] == 'A') {
			scanf("%d", &x);
			N++;
			s[N] = s[N - 1] ^ x;
			root[N] = ++idx;
			insert(N, 23, root[N - 1], root[N]);
		}
		else {
			scanf("%d%d%d", &l, &r, &x);
			//接下来要在 [L - 1, R - 1] 这个区间内寻找所求值。
			printf("%d\n", query(root[r - 1], s[N] ^ x, l - 1));
		}
	}
	return 0;
}

3.2 可持久化线段树(主席树)

255. 第K小数

  • 给定长度为N的整数序列A,下标为 1~N。现在要执行 M M M 次操作,其中第 i i i 次操作为给出三个整数 l i , r i , k i l_i,r_i,k_i li,ri,ki,求 A [ l i ] , A [ l i + 1 ] , … , A [ r i ] A[l_i],A[l_{i+1}],…,A[r_i] A[li],A[li+1],,A[ri] (即A的下标区间 [ l i , r i ] [l_i,r_i] [li,ri]) 中第 k i k_i ki 小的数是多少。
  • 主席树很难进行区间修改操作。

主席树思想是每个位置都维护一个线段树,线段树的节点是值的范围,然后第 i 个线段树中某个区间[x, y]维护的是,1~i 中数字在[x, y]范围内的个数。这里利用到了前缀和的思想。

  • 这道题大致的思路就是,把离散化之后的数组标记为 0 ~ N - 1. 假设原数组为 4, 1, 2, 3, 5. 那么区间的变化过程是这样的,对于每一个 a i a_i ai,假设在整个数组中是第 m 小的数字,那么统计一下 [1, i] 有多少数的序号小于等于 i,[i + 1, 1] 有多少个数的序号大于 i.
0 0 0 1 0
1 0 0 1 0
1 1 0 1 0
1 1 1 1 0
1 1 1 1 1
  • 如果要找第k个大的数,修改两个地方
lower_bound(nums.begin(), nums.end(), x, greater<int>()) - nums.begin();
sort(nums.begin(), nums.end(), greater<int>());
  • 需要尤其注意一点的是,线段树结点中的数据 l l l r r r 存放的不是权值的区间边界,而是左儿子和右儿子编号.
#include<bits/stdc++.h>
using namespace std;
const int maxn = 100010, maxm = 10010;
int N, M, a[maxn];
vector<int> nums;
struct node {
	int l, r;
	int cnt;
}tr[maxn * 4 + maxn * 17];

//空间开到 4N + N * log(N)
int root[maxn], idx;
int find(int x) {
	return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
}

int build(int l, int r) {
	int p = ++idx;

	if (l == r) return p;
	int mid = (l + r) / 2;
	tr[p].l = build(l, mid), tr[p].r = build(mid + 1, r);
	return p;
}
//p 为原来的线段树的结点,q 为复制的线段是的结点
int insert(int p, int l, int r, int x) {
	int q = ++idx;
	tr[q] = tr[p];
	if (l == r) 
    {
		tr[q].cnt++;
		return q;
	}
	int mid = (l + r) / 2;
	if (x <= mid) tr[q].l = insert(tr[p].l, l, mid, x);
	else tr[q].r = insert(tr[p].r, mid + 1, r, x);
	tr[q].cnt = tr[tr[q].l].cnt + tr[tr[q].r].cnt;
	return q;
}
int query(int q, int p, int l, int r, int k) {
	if (l == r) return r;
    //统计权值线段树[l, mid]区间内,出现在询问区间[L, R]内的数字有多少个。
    //因为是权值线段树,因此左子树的所有数都小于右子树。
	int cnt = tr[tr[q].l].cnt - tr[tr[p].l].cnt;
	int mid = (l + r) / 2;
	if (k <= cnt) return query(tr[q].l, tr[p].l, l, mid, k);
	else return query(tr[q].r, tr[p].r, mid + 1, r, k - cnt);
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; i++) {
		scanf("%d", &a[i]);
		nums.push_back(a[i]);
	}
	sort(nums.begin(), nums.end());
	nums.erase(unique(nums.begin(), nums.end()), nums.end());
	root[0] = build(0, nums.size() - 1);

	for (int i = 1; i <= N; i++) {
		root[i] = insert(root[i - 1], 0, nums.size() - 1, find(a[i]));
	}
    
	while (M--) {
		int l, r, k;
		scanf("%d%d%d", &l, &r, &k);
		printf("%d\n", nums[query(root[r], root[l - 1], 0, nums.size() - 1, k)]);
	}
	return 0;
}

4.平衡树 Treap

253. 普通平衡树

在这里插入图片描述

  • 左旋与右旋是不影响中序遍历的顺序。
  • 设置两个哨兵节点,一个是正无穷INF,一个是负无穷-INF。
  • zig那个函数里面,p对应的就是x,q对应的就是y。
  • 天哪,我把rand()函数去掉,改成了 t r [ i d x ] . v a l = 1 tr[idx].val = 1 tr[idx].val=1,然后运行时间翻了好几倍!
#include<cstdio>
#include<algorithm>
#include<cstdlib>
using namespace std;
//INF的值要根据题目的数据范围而定。
const int maxn = 100010, INF = 1e8;
int N;
struct node {
	int l, r;    //左右子树
	int key, val;   //key是二叉搜索树的权值,val是堆的权值。
	int cnt, sz;   //cnt是节点出现多少次,sz是与其相连的子树里面有多少个数。
}tr[maxn];
int root, idx;   //idx表示当前已经分配到的节点。

void pushup(int p) {
	tr[p].sz = tr[tr[p].l].sz + tr[tr[p].r].sz + tr[p].cnt;
}

//创建节点
int get_node(int key) {
	tr[++idx].key = key;
	tr[idx].val = rand();
	//创建一个新的节点意味着这个值之前没有出现过,而且一定是叶节点。
	tr[idx].cnt = tr[idx].sz = 1;
	return idx;
}


//这个地方一定要传入引用
void zig(int& p) {    //右旋
	int q = tr[p].l;
	tr[p].l = tr[q].r, tr[q].r = p, p = q;
	pushup(tr[p].r), pushup(p);
}

void zag(int& p) {    //左旋
	int q = tr[p].r;
	tr[p].r = tr[q].l, tr[q].l = p, p = q;
	pushup(tr[p].l), pushup(p);
}

void build() {
	get_node(-INF), get_node(INF);
	root = 1, tr[1].r = 2;
	pushup(root);
	if (tr[1].val < tr[2].val) zag(root);
}

void insert(int& p, int key) {
	if (!p) p = get_node(key);
	else if (tr[p].key == key) tr[p].cnt++;
	else if (tr[p].key > key) {
		insert(tr[p].l, key);
		if (tr[tr[p].l].val > tr[p].val) zig(p);
	}
	else {
		insert(tr[p].r, key);
		if (tr[tr[p].r].val > tr[p].val) zag(p);
	}
	pushup(p);
}

void remove(int& p, int key) {
	if (!p) return;
	if (tr[p].key == key) {
		if (tr[p].cnt > 1) tr[p].cnt--;
		else if (tr[p].l || tr[p].r) {
			if (!tr[p].r || tr[tr[p].l].val > tr[tr[p].r].val) {
				zig(p);
				remove(tr[p].r, key);
			}
			else {
				zag(p);
				remove(tr[p].l, key);
			}
		}
		else p = 0;
	}
	else if (tr[p].key > key) remove(tr[p].l, key);
	else remove(tr[p].r, key);
	pushup(p);
}

int get_rank_by_key(int p, int key) {  //通过数值找排名
	if (!p) return 0;    //本题不会出现这种情况
	if (tr[p].key == key) return tr[tr[p].l].sz + 1;
	if (tr[p].key > key) return get_rank_by_key(tr[p].l, key);
	return tr[tr[p].l].sz + tr[p].cnt + get_rank_by_key(tr[p].r, key);
}
int get_key_by_rank(int p, int rank){  //通过排名找数值
	if (!p) return INF;    //本题不会出现这种情况
	if (tr[tr[p].l].sz >= rank)  return get_key_by_rank(tr[p].l, rank);
	if (tr[tr[p].l].sz + tr[p].cnt >= rank)  return tr[p].key;
	return get_key_by_rank(tr[p].r, rank - tr[tr[p].l].sz - tr[p].cnt);
}
int get_prev(int p, int key) {   //找到严格小于key的最大的数
	if (!p) return -INF;
	if (tr[p].key >= key) return get_prev(tr[p].l, key);
	return max(tr[p].key, get_prev(tr[p].r, key));
}
int get_next(int p, int key) {   //找到严格大于key的最小的数
	if (!p) return INF;
	if (tr[p].key <= key) return get_next(tr[p].r, key);
	return min(tr[p].key, get_next(tr[p].l, key));
}

int main() {
	build();
	scanf("%d", &N);
	while (N--) {
		int op, x;
		scanf("%d%d", &op, &x);
		if (op == 1) insert(root, x);
		else if (op == 2) remove(root, x);
		else if (op == 3) printf("%d\n", get_rank_by_key(root, x) - 1);
		else if (op == 4) printf("%d\n", get_key_by_rank(root, x + 1));
		else if (op == 5) printf("%d\n", get_prev(root, x));
		else printf("%d\n", get_next(root, x));
	}
	return 0;
}

5.AC自动机

在这里插入图片描述

  • 平凡串:空串.

1282. 搜索关键词

  • 题意:给定 n n n 个长度不超过 50 50 50 的由小写英文字母组成的单词,以及一篇长为 m m m 的文章。请问,有多少个单词在文章中出现了. 1 ≤ n ≤ 1 0 4 , 1 ≤ m ≤ 1 0 5 . 1 \le n \le 10^4,1\le m \le 10^5. 1n104,1m105.
  • 下面这个做法是构造了 T r i e Trie Trie 图,线性时间复杂度.
#include<bits/stdc++.h>
using namespace std;
const int maxn = 10010, maxs = 55, maxm = 1000010;
int N;
int tr[maxn * maxs][26], cnt[maxn * maxs], idx;
char str[maxm];
int q[maxn * maxs], ne[maxn * maxs];

void insert() {
	int p = 0;
	for (int i = 0; str[i]; i++) {
		int t = str[i] - 'a';
		if (!tr[p][t]) tr[p][t] = ++idx;
		p = tr[p][t];
	}
	cnt[p]++;
}

void build() {
	int hh = 0, tt = -1;
	for (int i = 0; i < 26; i++) {
		if (tr[0][i]) q[++tt] = tr[0][i];
	}
	while (hh <= tt) {
		int t = q[hh++];
		for (int i = 0; i < 26; i++) {
			int p = tr[t][i];
			if (!p) tr[t][i] = tr[ne[t]][i];
			else {
				ne[p] = tr[ne[t]][i];
				q[++tt] = p;
			}
		}
	}
}

void init() {
	for (int i = 0; i <= idx; i++) {
		fill(tr[i], tr[i] + 26, 0);
		cnt[i] = ne[i] = 0;
	}
	idx = 0;
}

int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		init();
		scanf("%d", &N);
		for (int i = 0; i < N; i++) {
			scanf("%s", str);
			insert();
		}
		build();
		scanf("%s", str);
		int res = 0;
		for (int i = 0, j = 0; str[i]; i++) {
			int t = str[i] - 'a';
			j = tr[j][t];
			int p = j;
			while (p) {
				res += cnt[p];
				cnt[p] = 0;
				p = ne[p];
			}
		}
		printf("%d\n", res);
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值