算法模板(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 0∼p−1 之间。
可以对这列数进行两种操作:
- 添加操作:向序列后添加一个数,序列长度变成 n + 1 n+1 n+1;
- 询问操作:询问这个序列中最后 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条指令,每条指令可能是以下两种之一:
- “C l r d”,表示把 A[l],A[l+1],…,A[r] 都加上 d。
- “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 1。Q l r x
:询问操作,你需要找到一个位置 p p p,满足 l ≤ p ≤ r l \le p \le r l≤p≤r,使得: 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 a⊕b⊕b=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 Sp−1⊕Sn⊕X最大。记 C C C = S n ⊕ X S_n\oplus X Sn⊕X,可以看出来 C C C 是一个定值,就是求 S p − 1 ⊕ C S_{p-1}\oplus C Sp−1⊕C 最大。
- 其实,可以联想到那个字典树的题,找最大异或对。把 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. 1≤n≤104,1≤m≤105.
- 下面这个做法是构造了 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;
}