单调栈/单调队列
单调栈
单调栈Monotone
Stack ,顾名思义,是一种栈状结构,且栈内元素单调。它既满足栈的“先进后出F
I L
O ”性质,也符合元素从栈顶到栈底呈单调排列(或者从栈底到栈顶)。根据单调性的不同,大致可以分为“单调递增栈”和“单调递减栈”。若无特殊说明,单调栈的递增/递减判断顺序是从栈顶到栈底 。
在单调栈构建过程中,有如下要求:
当前待插入元素与栈顶元素满足全栈的单调关系,则直接插入顶端。
反之,弹出栈顶元素直到满足第一点,此时直接插入。
例如一个单调递增栈的实现:
1 2 3 4 5 6 stack<int > stk; void insert (int x) { while (!stk.empty () && stk.top () > x) stk.pop (); stk.push (x); }
没错就这么短
依据单调栈的这些性质,人们探索出了一种能在
时间复杂度内求解下一大元素N G
E 问题(或者下一小元素)的算法。
我们新建一个数组用来记录下一大元素的下标,数组 nge[i]
即表示原序列中下标
的元素的下一大元素的下标。让单调栈存储元素下标,当每次待插入的元素大于栈顶下标对应的元素时,则记录当前栈顶到
nge
中,并弹出栈顶。如此一来便处理得到了一个完整的
nge
数组
P5788 [模板]
单调栈
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 #include <bits/stdc++.h> #define N 3000010 using namespace std;typedef long long ll;stack<int > stk; ll arr[N]; int nge[N];int n;void NGE () { for (int i = 1 ; i <= n; i++) { while (!stk.empty () && arr[i] > arr[stk.top ()]) { nge[stk.top ()] = i; stk.pop (); } stk.push (i); } } int main () { cin >> n; for (int i = 1 ; i <= n; i++) cin >> arr[i]; NGE (); for (int i = 1 ; i <= n; i++) cout << nge[i] << ' ' ; return 0 ; }
单调队列
单调队列Monotone
Queue 其实和单调栈非常相似——给队列规定一个定长,让它在一段序列上滑动,当队首元素符合单调性则推入,否则就弹出队首直到满足单调性为止,同时,队列随着每次的滑动会自然抛除末尾的某个值。这样一来,单调队列就可以解决区间内最值的问题。
首先,我们依据单调栈里面的单调性检验,弹出队头不符合单调性的值。接着,把当前的值插入队头。如果队头元素的下标与队尾元素下标的差值超过了队列长度,则自然淘汰掉。反之输出队尾元素,它储存了当前区间的最值。
P1886 滑动窗口
/[模板] 单调队列
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 #include <bits/stdc++.h> #define N 1000010 using namespace std;typedef long long ll;struct Node { int idx; ll v; } nodes[N]; deque<Node> q; int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); cout.tie (nullptr ); int n, k; cin >> n >> k; for (int i = 1 ; i <= n; i++) { cin >> nodes[i].v; nodes[i].idx = i; } for (int i = 1 ; i <= n; i++) { while (!q.empty () && nodes[i].v < q.front ().v) q.pop_front (); q.push_front (nodes[i]); if (i - q.back ().idx == k) q.pop_back (); if (i >= k) cout << q.back ().v << ' ' ; } cout << endl; q.clear (); for (int i = 1 ; i <= n; i++) { while (!q.empty () && nodes[i].v > q.front ().v) q.pop_front (); q.push_front (nodes[i]); if (i - q.back ().idx == k) q.pop_back (); if (i >= k) cout << q.back ().v << ' ' ; } return 0 ; }
ST 表
ST 表Sparse Table 是一种能在 时间内预处理,
回答区间最值查询(不支持修改,若要修改请移步线段树)的一种数据结构。借用了动态规划的思想,兼以步长
的倍增。
对于一段静态序列(不变),为了求某段子列的最大值,先把这个子列分成两个更小的子序列。假设
表示在区间 内的最大值,对于单个点,即
时,显然有 (最大值就是自己)。否则,最大值即是左区间最大值和右区间最大值更大的那一个。
P3865 [模板] ST
表
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 #include <bits/stdc++.h> #define N 100010 using namespace std;int a[N];int logn[N];int f[N][22 ];int n;void init () { for (int i = 1 ; i <= n; i++) f[i][0 ] = a[i]; for (int j = 1 ; j <= logn[n]; j++) { for (int i = 1 ; i + (1 << j) - 1 <= n; i++) { f[i][j] = max (f[i][j - 1 ], f[i + (1 << (j - 1 ))][j - 1 ]); } } } void initLog () { logn[1 ] = 0 ; logn[2 ] = 1 ; for (int i = 3 ; i <= n + 100 ; i++) logn[i] = logn[i >> 1 ] + 1 ; } int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); cout.tie (nullptr ); int m; cin >> n >> m; for (int i = 1 ; i <= n; i++) cin >> a[i]; initLog (); init (); while (m--) { int l, r; cin >> l >> r; int tmp = logn[r - l + 1 ]; cout << max (f[l][tmp], f[r - (1 << tmp) + 1 ][tmp]) << endl; } return 0 ; }
个人感觉理解难度有点大,建议先背下来
警示后人:不要用 endl
,换成 \n
快了高达七倍不止!AC 、TLE 。
在 C++ 中,数组的访问是行优先的。因此如果把 ST表数组的第一维和第二维交换位置,那么程序的运行效率会变得更高
并查集
并查集Disjoint Set
Union ,顾名思义,它是一种能完成数据合并、查询的集合数据结构。在路径压缩优化下,它能在平均
的时间复杂度内完成一次操作(由 证明,他也是
的奠基人之一)。关于并查集时间复杂度的证明请看:并查集复杂度 - OI
Wiki 。
首先,我们需要一个数组来记录每个点的父节点。在查询时,我们采取从底部逐级向上递归搜索的方式,可以发现,我们在实际操作时并不会关心它到根节点路径上的其他非根节点,出于效率考虑,可以将沿途每个点的父节点都一次性设成全局父节点,而这一切都可以在递归查询的过程中完成。在合并时,无需将每个点都合并过去,只需要把某棵树的根节点并到另一棵树的根节点下面去就行了!
P3367 [模板]
并查集
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 #include <bits/stdc++.h> using namespace std;int p[10010 ];int find (int x) { if (p[x] != x) p[x] = find (p[x]); return p[x]; } void merge (int x, int y) { p[find (x)] = find (y); } bool query (int x, int y) { return find (x) == find (y); } int main () { int n, m; cin >> n >> m; for (int i = 1 ; i <= n; i++) p[i] = i; for (int i = 1 ; i <= m; i++) { int z, x, y; cin >> z >> x >> y; if (z == 1 ) merge (x, y); else { if (query (x, y)) cout << "Y" << endl; else cout << "N" << endl; } } return 0 ; }
关于带权并查集、种类并查集,请参阅这篇博客 。
哈希/散列函数
哈希Hash 或者称“散列”,其运作原理是把一个数据映射到一个较小(但不能太小)的值域里进行比较。通常来说,我们希望哈希满足如下两个性质:
两个对象哈希值不同,则两个对象一定不同
哈希值相同,则两个对象不一定相同
人们把第二种情况中可能出现的“对象不同却哈希相同”的现象叫做“哈希碰撞Hash
Clash ”。
对于一个字符串 ,一般采取的是多项式散列的方法。首先把字符串的每一位挑出来,类比成一个数字位,然后采取形如
的方法获取它的散列结果。举个例子,字符串
会被散列成 。一般来说,把
设定成一个较大质数,发生碰撞的概率是很低的阳寿碰撞就没办法了,多交几次吧。
当然,你也可以使用碰撞率更低的 MD5,但是 C++
并没有内置相关的库,你如果想用,那么就必须要完全背下来或者是现场复制。其实还不如自己选特殊值算。
按照上边的方法计算,复杂度是 。如果加上询问,那就是 。在多次询问子串哈希时,观察到有如下式子:
假设 代表原字符串区间
内的子串的哈希值,那么 。现在就可以最快用 的快速幂求解了。
P3370 [模板]
字符串哈希
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 #include <bits/stdc++.h> using namespace std;typedef long long ll;const int MOD = 1000000007 ;const int b = 2 ;map<string, int > mp; int qpow (int a, int b) { int res = 1 ; while (b) { if (a & 1 ) res = (ll) (res * a) % MOD; a = (ll) (a * a) % MOD; b >>= 1 ; } return res; } int hashStr (string s) { int res = 0 ; for (int i = 0 ; i < s.length (); i++) { res = (res + (s[i] - '0' ) * qpow (b, s.length () - i - 1 )) % MOD; } return res; } int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); cout.tie (nullptr ); int n; cin >> n; for (int i = 1 ; i <= n; i++) { string s; cin >> s; int h = hashStr (s); if (!mp.count (s)) mp[s] = h; } cout << mp.size () << endl; return 0 ; }