upd on 20220415 修复 / 更改了一些 LaTeX \LaTeX LATEX,看起来更美观(
文章目录
数据结构是什么?
数据结构(data structure)是带有结构特性的数据元素的集合,它研究的是数据的逻辑结构和数据的物理结构以及它们之间的相互关系,并对这种结构定义相适应的运算,设计出相应的算法,并确保经过这些运算以后所得到的新结构仍保持原来的结构类型。简而言之,数据结构是相互之间存在一种或多种特定关系的数据元素的集合,即带“结构”的数据元素的集合。“结构”就是指数据元素之间存在的关系,分为逻辑结构和存储结构。
(from 百度百科)
数据结构通常是用来优化算法的。因此,要根据算法的需要,选择合适的数据结构。
线性数据结构是什么?
一个有序元素的集合
特征:
- 集合中有且仅有一个“第一个元素”
- 集合中有且仅有一个“最后一个元素”
- 除最后一个元素外,每个元素均有唯一一个后继
- 除第一个元素外,每个元素均有唯一一个前驱
相对于线性结构,非线性结构的特征是一个元素可能对应多个直接前驱和多个后继。
几种常用的线性结构
串(一维数组)
略
ST表
用来解决可重复贡献问题的数据结构(如 R M Q RMQ RMQ 问题)。
这里以求最小值举例。
运用倍增的思路,用数组 s [ i ] [ j ] s[i][j] s[i][j] 表示区间 [ i , i + 2 j − 1 ] [i,i+2^j-1] [i,i+2j−1] 中的最小值,可以 O ( n log n ) O(n \log n) O(nlogn) 预处理出 s s s 数组。
查询时,由于可重复贡献,取 k = ⌊ log 2 ( r − l + 1 ) ⌋ k= \lfloor \log _2 (r - l + 1) \rfloor k=⌊log2(r−l+1)⌋,答案就为 min ( f [ l ] [ k ] , f [ r − 2 k + 1 ] [ k ] ) \min (f[l][k],f[r - 2 ^ k + 1][k]) min(f[l][k],f[r−2k+1][k])。
可以做到 O ( n log n ) O(n \log n) O(nlogn) 预处理, O ( 1 ) O(1) O(1) 预处理,相比于线段树,在多次询问时,很多时候可以省略一个 log \log log 的时间。
栈
栈
特点:后进先出(LIFO表)
实现:
-
用数组模拟
-
STL(
std::stack
),定义于头文件<stack>
中。定义:
stack <Typename> st
返回元素数量:
st.size()
返回栈顶的值:
st.top()
返回栈是否为空:
st.empty()
元素 a a a 入栈:
st.push(a)
栈顶出栈:
st.pop()
单调栈
从名字可以看出来,就是栈内元素呈单调性的栈。
维护过程
这里以元素从栈底到栈顶单调递减举例。(递增、单调不上升/不下降同理)
-
插入:
不断比较将插入的元素 a a a 的值与栈顶 t o p top top 的值,
若 a ≥ t o p a \geq top a≥top,弹出栈顶;
否则将 a a a 入栈。
例题
例题1
模板题:https://www.luogu.com.cn/problem/P5788 Luogu P5788 【模板】单调栈
给定长度为 n n n 的序列 a 1 … n a_{1\dots n} a1…n。
求每一个元素之后,第一个大于它的元素的下标。
1 ≤ n ≤ 3 × 1 0 6 1 \leq n \leq 3 \times 10^6 1≤n≤3×106, 1 ≤ a i ≤ 1 0 9 1 \leq a_i \leq 10 ^ 9 1≤ai≤109。
解法:
就是模板。
维护一个单调栈,使得从栈底到栈顶的元素单调不上升,序列中的元素按编号依次入栈。
容易发现,第 i i i 个元素入栈时,弹出的元素均为编号小于 i i i 且值小于 a i a_i ai 的元素。
将弹出的元素答案更新为 i i i 即可。
由于每一个元素都进栈一次、出栈一次,时间复杂度为 O ( n ) O(n) O(n)。
例题2
不是模板: https://vjudge.net/problem/HDU-1506 HDU 1506 / POJ 2559 / … Largest Rectangle in a Histogram
求直方图中最大矩形面积。
解法:
一道有很多种解法的题,这里只介绍单调栈的解法。
显然,对于一个高度从左往右递增的直方图,可以 O ( n ) O(n) O(n) 求解。
维护一个从左往右的矩形高度递增的单调栈。
若新插入的矩形高度小于栈顶矩形的高度,弹出栈顶,并更新答案。
在插入完所有矩形后,注意还要计算还在栈里的矩形的答案。
时间复杂度 O ( n ) O(n) O(n)。
例题3
https://vjudge.net/problem/HDU-3410 HDU 3410 Passing the Message
模板题。可以当做练习。
例题4
https://vjudge.net/problem/黑暗爆炸-2383 bzoj 2383 Ballons
很多有着固定的横坐标气球没有固定的半径,然后求从左到右依次吹这些气球,它们的半径最大是多少。
对于 40 % 40 \% 40% 的数据, n ≤ 2000 n \leq 2000 n≤2000;
对于所有数据, n ≤ 200000 n \leq 200000 n≤200000。
解法:
若一个气球右边有比这个气球半径大的气球,那么显然这个气球就不会对后面的答案造成影响。
维护一个单调栈,使栈内气球随着坐标的增加而递减。
对于新加入的气球,计算在栈顶的限制下,最大能达到的半径,
若这个半径比栈顶的半径大,则弹出栈顶。
时间复杂度 O ( n ) O(n) O(n)。
队列
队列
特点:先进先出(FIFO表)
实现:
-
数组
-
STL(
std::queue
),定义于头文件<queue>
中。定义:
queue <Typename> q
返回元素数量:
q.size()
返回是否为空:
q.empty()
返回队头:
q.front()
队头出队:
q.pop()
元素 a a a 进队:
q.push(a)
单调队列
简单来说就是:如果一个人比我小,还比我强,我就可以退役了。
是队列中元素呈单调性的队列,一般有范围限制。
维护过程
这里以 https://www.luogu.com.cn/problem/P1886(Luogu P1886 滑动窗口 /【模板】单调队列)举例。
给定长度为 n n n 的序列 a 1 … n a_{1\dots n} a1…n,对于所有 1 ≤ i ≤ n − k + 1 1\leq i \leq n - k + 1 1≤i≤n−k+1,求区间 [ i , i + k − 1 ] [i,i+k-1] [i,i+k−1] 中的最小值和最大值。
1 ≤ n ≤ 1 0 6 1\leq n \leq 10^6 1≤n≤106。
由于没有修改,显然可以用ST表做。
这里介绍用单调队列的作法。
由于有区间长度限制,我们需要用到双端队列。
可以用数组模拟,也可以使用STL中的 deque
。
deque
的使用和队列差不多,区别有:
返回队尾:q.back()
弹出队尾:q.pop_back()
弹出队头:q.pop_front()
插入队尾:q.push_back()
这里用求最小值举例,最大值同理。
每次往队列中插入元素时,判断队头元素是否超出了区间的范围,若超出则弹出对头;
判断将插入的元素 x x x 与队尾元素 q . b a c k ( ) q.back() q.back() 的大小,
若 x x x 较小,则队尾不可能为以后的最小值了,弹出队尾。
否则将 x x x 插入队尾。
插入 i i i 号元素时,队列中的队头即为区间 [ i − k + 1 , i ] [i - k + 1,i] [i−k+1,i] 的最小值。
例题
https://www.luogu.com.cn/problem/P2216 Luogu P2216 [HAOI]理想的正方形
有一个 a × b a\times b a×b 的整数组成的矩阵,现请你从中找出一个 n × n n\times n n×n 的正方形区域,使得该区域所有数中的最大值和最小值的差最小。
对于 20 % 20\% 20% 的数据, 2 ≤ a , b ≤ 100 2\leq a,b \leq 100 2≤a,b≤100, n ≤ 10 n\leq 10 n≤10;
对于所有数据, 2 ≤ a , b ≤ 1000 2\leq a,b \leq 1000 2≤a,b≤1000, n ≤ a n \leq a n≤a, n ≤ b n\leq b n≤b, n ≤ 100 n\leq 100 n≤100。
解法:
先用单调队列求出每一行中 1 × n 1 \times n 1×n 的区域中的最大最小值,再枚举每一列,求出所有 n × n n\times n n×n 区域中的最大最小值,
最后枚举所有 n × n n \times n n×n 矩形,求出答案。
时间复杂度 O ( n 2 ) O(n^2) O(n2)。
前缀和
指序列中前 n n n 个元素的和,一般用 s s s 数组表示。
维护:s[i] = s[i - 1] + a[i]
可以实现 O ( n ) O(n) O(n) 预处理, O ( 1 ) O(1) O(1) 求区间元素和,一般解决离线问题。
例题
https://www.luogu.com.cn/problem/P2671 Luogu P2671 [NOIP2015 普及组]求和
一条狭长的纸带被均匀划分出了 n n n 个格子,格子编号从 1 1 1 到 n n n。每个格子上都染了一种颜色 c o l o r i color_i colori 用 [ 1 , m ] [1,m] [1,m] 当中的一个整数表示),并且写了一个数字 n u m b e r i number_i numberi。
定义一种特殊的三元组 ( x , y , z ) (x,y,z) (x,y,z),要求满足以下两个条件:
- x x x, y y y, z z z 是整数且 x < y < z x < y < z x<y<z, y − x = z − y y - x = z - y y−x=z−y;
- c o l o r x = c o l o r z color_x=color_z colorx=colorz。
满足上述条件的三元组分数 f ( x , y , z ) f(x,y,z) f(x,y,z) 规定为 ( x + z ) × ( n u m b e r x + n u m b e r z ) (x+z) \times (number_x + number_z) (x+z)×(numberx+numberz)。
求 ∑ f ( x , y , z ) \sum f(x,y,z) % 10007 ∑f(x,y,z)。
对于前两组数据, 1 ≤ n ≤ 100 1 \leq n \leq 100 1≤n≤100, 1 ≤ m ≤ 5 1 \leq m \leq 5 1≤m≤5;
对于前四组数据, 1 ≤ n ≤ 3000 1 \leq n \leq 3000 1≤n≤3000, 1 ≤ m ≤ 100 1\leq m \leq 100 1≤m≤100;
对于第五、六组数据, 1 ≤ n ≤ 100000 1 \leq n \leq 100000 1≤n≤100000, 1 ≤ m ≤ 100000 1 \leq m \leq 100000 1≤m≤100000, c o l o r i ≤ 20 color_i \leq 20 colori≤20;
对于所有数据, 1 ≤ n ≤ 100000 1 \leq n \leq 100000 1≤n≤100000, 1 ≤ m ≤ 100000 1 \leq m \leq 100000 1≤m≤100000, 1 ≤ c o l o r i ≤ m 1 \leq color_i \leq m 1≤colori≤m, 1 ≤ n u m b e r i ≤ 100000 1 \leq number_i \leq 100000 1≤numberi≤100000。
解法:
显然,三元组中的 y y y 是没用的,限制一可以变为 2 ∣ z − x 2 | z - x 2∣z−x,即 z z z 和 x x x 模 2 2 2 的值相同。
将这 n n n 个格子按照余数和颜色分为 2 m 2m 2m 组。
考虑其中一组的情况。
a
n
s
=
∑
(
x
,
y
,
z
)
ans = \sum(x,y,z)
ans=∑(x,y,z)
=
∑
i
=
1
k
∑
j
=
i
+
1
k
(
i
+
j
)
(
n
u
m
b
e
r
i
+
n
u
m
b
e
r
j
)
\space \space \space \space \space \space \space= \sum \limits_{i = 1} ^k \sum \limits_{j = i+1} ^k (i+j)(number_i+number_j)
=i=1∑kj=i+1∑k(i+j)(numberi+numberj)
=
∑
i
=
1
k
i
(
n
u
m
b
e
r
i
(
k
−
1
)
+
∑
j
=
1
k
n
u
m
b
e
r
j
−
n
u
m
b
e
r
i
)
\space \space \space \space \space \space \space= \sum \limits_ {i=1} ^k i(number_i(k-1) + \sum \limits_{j=1} ^k number_j - number_i)
=i=1∑ki(numberi(k−1)+j=1∑knumberj−numberi)
=
∑
i
=
1
k
i
(
n
u
m
b
e
r
i
(
k
−
2
)
+
∑
j
=
1
k
n
u
m
b
e
r
j
)
\space \space \space \space \space \space \space= \sum \limits_{i=1} ^k i(number_i(k-2) + \sum \limits_{j=1} ^k number_j)
=i=1∑ki(numberi(k−2)+j=1∑knumberj)
(为什么csdn用不了 begin{align*}
和&=
。。。)
用一个数组 s 1 [ i ] [ 0 / 1 ] s1[i][0 / 1] s1[i][0/1] 表示颜色为 c o l o r i color_i colori 的格子中,编号模 2 2 2 的值为 0 / 1 0/1 0/1 的格子有多少个,
一个数组 s 2 [ i ] [ 0 / 1 ] s2[i][0/1] s2[i][0/1] 表示颜色为 c o l o r i color_i colori 的格子中,编号模 2 2 2 的值为 0 / 1 0/1 0/1 的格子中所填数字和。
枚举每一个数,统计答案即可。
时间复杂度 O ( n ) O(n) O(n)。
差分
一种与前缀和相对的策略,定义为第 i i i 个元素和第 i − 1 i - 1 i−1 个元素的差,这里用 b b b 数组表示。
容易发现第 i i i 个元素的值为 ∑ j = 1 i b [ j ] \sum_{j = 1} ^{i} b[j] ∑j=1ib[j]。
可以实现 O ( n ) O(n) O(n) 预处理, O ( 1 ) O(1) O(1) 区间修改,区间修改的复杂度十分优秀,经常在区间修改的题目中被使用。
区间修改(区间加):b[l] += x, b[r + 1] -= x
前缀和: ∑ i = 1 n a [ i ] = ∑ i = 1 n ∑ j = 1 i b [ j ] = ∑ i = 1 n ( n − i + 1 ) b [ i ] = ( n + 1 ) ∑ i = 1 n − ∑ i = 1 n i × b [ i ] \sum \limits_{i=1}^n a[i]=\sum\limits_{i=1}^n\sum\limits_{j=1}^ib[j] = \sum \limits_ {i = 1} ^ n (n - i + 1) b[i] = (n + 1)\sum\limits_{i = 1} ^ n - \sum\limits_{i = 1}^n i \times b[i] i=1∑na[i]=i=1∑nj=1∑ib[j]=i=1∑n(n−i+1)b[i]=(n+1)i=1∑n−i=1∑ni×b[i]
因此,要求前缀和时,通常在求差分数组时预处理出 i × b [ i ] i \times b[i] i×b[i] 的值。
例题
例题1
https://www.luogu.com.cn/problem/P2629 Luogu P2629 好消息,坏消息
有 n n n 条消息,每条信息有好坏度 a i a_i ai,
求 k k k 的个数,使得对于所有的 k ≤ i ≤ n k \leq i \leq n k≤i≤n 都有 ∑ j = k i a j ≥ 0 \sum \limits_{j = k} ^ i a_j \geq 0 j=k∑iaj≥0 且对于所有的 1 ≤ i ≤ k − 1 1 \leq i \leq k - 1 1≤i≤k−1 都有 ∑ j = 1 i a j + ∑ j = k n a j ≥ 0 \sum \limits_{j = 1} ^i a_j + \sum \limits_{j = k} ^n a_j \geq 0 j=1∑iaj+j=k∑naj≥0。
对于 25 % 25\% 25% 的数据, n ≤ 1000 n \leq 1000 n≤1000;
对于 75 % 75\% 75% 的数据, n ≤ 1 0 4 n \leq 10^4 n≤104;
对于 100 % 100\% 100% 的数据, n ≤ 1 0 6 n \leq 10^6 n≤106。
解法:
显然可以暴力枚举 k k k,判断每一个数是否满足条件,时间复杂度 O ( n 2 ) O(n^2) O(n2),获得 75 75 75 p t s pts pts;
我们发现,题目限制相当于将这 n n n 个数放在一个环上。
因此,我们可以将数组复制一遍到后面,即断环为链,
限制就可以转化为:对于所有 i ∈ [ k , k + n − 1 ] i \in [k,k + n - 1] i∈[k,k+n−1] 都有 ∑ j = k i ≥ 0 \sum \limits_{j = k} ^i \geq 0 j=k∑i≥0。
可以用前缀和维护区间和。
由于我们不需要依次判断所有的 i i i 是否符合条件,只需要判断 s i s_i si 最小的 i i i,
可以用单调队列维护区间中 s i s_i si 最小的 i i i,在维护单调队列的过程中更新答案即可。
时间复杂度 O ( n ) O(n) O(n)。
例题2
https://www.luogu.com.cn/problem/P2564 Luogu P2564 [SCOI2009]生日礼物
小西有一条很长的彩带,彩带上挂着各式各样的彩珠。已知彩珠有 n n n 个,分为 k k k 种。简单的说,可以将彩带考虑为 x x x 轴,每一个彩珠有一个对应的坐标(即位置)。某些坐标上可以没有彩珠,但多个彩珠也可以出现在同一个位置上。
小布生日快到了,于是小西打算剪一段彩带送给小布。为了让礼物彩带足够漂亮,小西希望这一段彩带中能包含所有种类的彩珠。同时,为了方便,小西希望这段彩带尽可能短,你能帮助小西计算这个最短的长度么?彩带的长度即为彩带开始位置到结束位置的位置差。
解法:
我们发现,若一个区间的左端点所放着的彩珠类型在这个区间中出现了不止一次,那么左端点就可以被弹出。
维护一个队列及彩珠类型的个数,按照位置从小到大枚举每一个彩珠,
每枚举到一个,就更新类型数,当类型数等于总的类型数时,更新答案。
在同一个位置上的彩珠可以分开考虑,在维护队列时多维护一维彩珠的位置即可。
P1638 和这个差不多。
优先队列
堆
一棵树,且所有节点的权值均大于等于或小于等于其父亲节点的权值。
大于等于的叫小根堆,小于等于的叫大根堆。
一般指二叉堆。
插入操作
在最右边的叶子节点右边新增一个节点,若满了则新增一层。
随后向上调整:不断与父亲节点的权值比较,若不满足条件则与父亲节点调换。
时间复杂度 O ( log n ) O(\log n) O(logn)。
删除操作(根节点)
删除并向下调整:不断与权值最大的儿子节点互换,直到成为叶子节点。
时间复杂度 O ( log n ) O(\log n) O(logn)。
优先队列
用堆实现,可以 O ( log n ) O(\log n) O(logn) 插入, O ( 1 ) O(1) O(1) 查询最大或最小值。
实现:
-
手写堆;
-
STL(
std::priority_queue
),定义于头文件<queue>
中。定义:
priority_queue <Typename> q
(大根堆)
priority_queue <Typename, vector <Typename>, greater <Typename> > q
(小根堆)返回大小:
q.size()
返回堆顶:
q.top()
删除堆顶:
q.pop()
插入元素 a a a:
q.push(a)
例题
https://vjudge.net/problem/POJ-1442 POJ 1442 Black Box
初始时 i = 0 i=0 i=0。
两种操作:
- 插入一个数 x x x;
- 将 i i i 加一,查询当前序列中从小到大排序后第 i i i 个数。
n ≤ m ≤ 30000 n \leq m \leq 30000 n≤m≤30000。
解法:
维护一个大根堆和一个小根堆,分别维护序列前 i i i 小的数和前 k − i k-i k−i 大的元素,
答案即为小根堆的堆顶。
插入时都将数插入小根堆里。
由于要保证答案为小根堆堆顶,若大根堆的堆顶大于小根堆的堆顶,则交换。
每次查询后,将小根堆堆顶弹出,插入大根堆。
时间复杂度 O ( m log m + n ) O(m \log m + n) O(mlogm+n)