序列维护

本文最后更新于:2022年8月25日 下午

大坑,填起来比较辛苦,可能慢点。

一直没时间写,估计抽个集训的空就完成了。

比较毒瘤,食用前请确保基础数据结构掌握得差不多了,不然回去学完线性表再来()

乱入的优先队列

一句话优先队列:往一个队列里边扔数,它自动给你排好序等你使用。

来一个十分简(bian)单(tai)的例题:

给定一个正数序列,求第 $k$ 小的子区间和。

$n \le 100000$

你跟我讲你不知道什么是子区间……

  • 子区间:连续的。
  • 子序列:不连续的。

扫盲工作有了大进展。

懵了没有?我早就料到了哈哈哈,你的招式不过如此!

请先克制住把我按在地上暴揍的心理,现在我把这题做法告诉你你的招式不就也上来了嘛。

首先分析题目性质:这个序列是正数

正数!正数!正数!

也就是说,我们选一个长度小的区间的和肯定比长度大的区间的和要小。

最小的,一定就是形如 $[i, i]$ 这样的区间了。

都学到这了还不知道左闭右开和左闭右闭就去自闭吧)。

记 $S[l, r]$ 为区间 $l \to r$ 的和。

你一定看得出:$S[l-1, r] \ge S[l, r]$, $S[l, r+1] \ge S[l, r]$。

我们掏出一个堆,先加进去 $[1, 1] \dots [n, n]$ 区间和。

找到一个最小的,然后从这个区间去像上面的左右拓展,找出第二小的区间,以此类推。

记住疯狂塞到堆里边。

最后记住,来个 $\mathtt{map}$ 和 $\mathtt{pair}$ 的联动,去重用,我们不能计算重复的区间。

你要代码?我都写这么清楚了居然还要代码!

好吧……既然你们仍然是一脸懵逼,我还是写一下吧:

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
#include <cstdio>
#include <queue>
#include <algorithm>
#include <utility>
#include <map>
#define N 100005

using namespace std;

// 这以上看不懂的回去复习第一部分吧

// 优先队列的结构体
struct Node {
int l, r;
long long sum;

// 构造一个函数,不要问我为什么这么写,为什么可以写一样的
// 鬼知道 C++ 的语法是森莫东东
Node(int l, int r, long long sum) : l(l), r(r), sum(sum) {}

// 这是重载了一个小根堆
bool operator < (const Node &a) const {
return sum > a.sum;
}
};

int n, a[N], k;

// 这是判重用的
map <pair <int, int>, bool> ds;

// 背不过?会读就会写,欧耶
priority_queue <Node> q;


// 代码风格所迫……
void inp() {
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);

// 压入队列中,就像之前所说的那样
q.push(Node(i, i, a[i]));
}
}

// 代码风格所迫……
void work() {

// 去找第 k 小的区间
while(k--) {
// 掏出来最小的
Node t = q.top();

// 炸了它()
q.pop();

// 如果我们来过这里
if(ds[pair <int, int> (t.l, t.r)]) {

// 一定要给它续一条命!
// 因为你要是再回去,你没有去找区间
// 但是你 while 里边减了 k!
// 所以要给它续命!
k++;
continue;
}

// 标记掉
ds[pair <int, int> (t.l, t.r)] = true;

// 你不能拓展出去
if(t.l != 1) {

// 拓展之
q.push(Node(t.l - 1, t.r, t.sum + a[t.l - 1]));
}

// 同理
if(t.r != n) {
q.push(Node(t.l, t.r + 1, t.sum + a[t.r + 1]));
}

// 输出你当前找到的区间
printf("%d %d %d\n", t.l, t.r, t.sum);
}
}

int main() {
inp();
work();
return 0;
}

前缀和与差分

前缀和是什么?

一个序列,定义一个 $pre$ 数组,$pre[i]$ 为前 $i$ 个数的和,查询一个区间 $[l, r]$ 的时候,只需要 $pre[r] - pre[l - 1]$。

那这玩意如何计算呢?

异常简单:

1
2
3
for(int i = 1; i <= n; i++) {
pre[i] = pre[i - 1] + a[i];
}

二维的东西……这东西是需要画图的,不过我觉得脑补应该可以吧……如果你非要看图(我是不会画的)就使用 gfs(google first search)。

差分又是什么呢?

差分数组的定义:

1
pre[i] = a[i] - a[i-1];

差分能干什么?

比如说:

有些东西直接统计起来,凉凉 TLE。

但是如果具有可减性,就可以使用差分来降低时间复杂度。

完美。

树状数组

现在,我给定一个序列,需要您写出一个数据结构,支持以下操作:

  1. 在第 $x$ 的位置上加上 $y$。
  2. 给出 $x$ 的前缀和。

现在该如何是好呢?

如果您使用数组,您得先统计一遍前缀和,恭喜您,我修改了序列,于是您又去改一遍前缀和,然后我接着修改序列……

有常识的人都知道,TLE 在所难免了。

这种数据结构需要什么特点?

  • 树形结构的每个节点表示一个子集。
  • 为了高效修改,每个序列上的位置只出现在少数个树形结构的节点上。
  • 为了查询高效,对任意查询,可以通过少数个树形结构的节点拼出来。

那么,有没有数据结构能来就我们一命?

树状数组可以满足您的要求。

树状数组是个什么东西?

剑鱼这东西实在毒瘤,于是我盗了 noip 的图,不过这是我氪了金的应该算是正常使用吧……

假设一个序列:$A[1] \sim A[8]$,用 $C[]$ 表示树状数组。

$C[x]$ 表示 $A[x - 2^k + 1, x]$ 这个区间的和,$k$ 是表示 $x$ 的二进制表示中第一位的 1。

蛤?

比如说,6,二进制表示 110。

则,k 就是 1。$2^k$ 就是 2。

那么这玩意怎么求呢?难不成我还要去位运算?

恭喜您,答对了,给您加 10 分。

极其简单,一句话就可以求出 $2^k$:

1
(x & -x)

我们如何去构建一个树状数组?

根据我们之前说过的一堆东西,我们可以提前根据我们手中的数据数组去构造。

1
2
3
4
5
6
7
8
void build(int c[]) {
memset(c, 0, sizeof(c));
for(int i=1; i<=n; i++) {
for(int j=i-lowbit(i)+1; j<=i; j++) {
c[i] = c[i] + t[j];
}
}
}

我们查询一个前缀的和,可以用 $O(\log n)$ 个树状数组的节点拼起来,然后成功拼出这个前缀。

求法:

1
2
3
4
5
6
7
8
int sum(int x) {
int ans = 0;
for(int i=x; i; i-=lowbit(i)) {
s += c[i];
}

return ans;
}

在 $x$ 的位置上加上 $y$。

你会发现,在 $x$ 前面的结点是不会收到影响的,只有后面的结点才会被影响。

存储输入数据的数组好说,直接一加就行了,但是树状数组里边怎么办呢?

因为之前的和它半毛钱关系都没有,我们直接从 $x$ 和它以后的那些点进行修改就可以了。

1
2
3
4
5
6
void modify(int x, int y) {
t[x] += y;
for(int i=x; i<=n; i+=lowbit(i)) {
c[i] += y;
}
}

树状数组就这么点东西了。

P3374,模板题。

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
#include <cstdio>
#include <cstring>
#define N 500005
using namespace std;

int c[N];
int n, t[N], m;

int lowbit(int x) {
return x & -x;
}
int find(int x) {
int ans = 0;
for(int i=x; i; i-=lowbit(i)) {
ans += c[i];
}
return ans;
}

void modify(int x, int y) {
t[x] += y;
for(int i=x; i<=n; i+=lowbit(i)) {
c[i] += y;
}
}

void init(int c[]) {
memset(c, 0, sizeof(c));
for(int i=1; i<=n; i++) {
for(int j=i-lowbit(i)+1; j<=i; j++) {
c[i] = c[i] + t[j];
}
}
}

void inp() {
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++) {
scanf("%d", &t[i]);
}
}

void work() {
init(c);
for(int i=1; i<=m; i++) {
int opt, x, y;
scanf("%d%d%d", &opt, &x, &y);
if(opt == 1) {
modify(x, y);
} else {
printf("%d\n", find(y)-find(x-1));
}
}
}

int main() {
inp();
work();
return 0;
}

序列维护
http://dxrprime.github.io/2022/08/25/序列维护/
作者
Shelter Prime
发布于
2022年8月25日
许可协议