单调栈与单调队列 Minimum stack / Minimum queue
问题1:如何O(1)地访问一个栈的最小值?
Ans:我们给栈里的数附上第二个域,代表在这个数之前(包含这个数)的最小值。
stack<pair<int, int>> st;
void push(int new_elem){
int new_min = st.empty() ? new_elem : min(new_elem, st.top().second);
st.push({new_elem, new_min});
}
我们单独观察second元素,发现是一个非递增的单调递减数列,所以叫它单调栈。
刷题时,常常不用存本来的数据(first域),也不需要栈,只用一个数组储存第二个域。
问题2:如何O(1)地访问一个队列的最小值?
另一个表述是你有一个长度为n的数组,如何O(n)第从左到右询问所有长度为m的子数组的最小值?
Ans:
实现1:单调栈不能从头部pop,如果强行增加pop函数,那么pop掉第一个数会需要O(n)地更新后面的数的second。
我们考虑只维护一个单调递减序列。
具体实现如下:
q.front()存的是队列最小值。
push时将队列尾部所有大于new_element的数都pop掉。
pop时先判一下要删的数,如果已经被删掉了就不操作。否则pop掉当前的头。
deque<int> q;
void push(int new_element){
while (!q.empty() && q.back() > new_element)
q.pop_back();
q.push_back(new_element);
}
void pop(int remove_element){
if (!q.empty() && q.front() == remove_element)
q.pop_front();
}
但是上面的pop操作需要读入一个数,很不自然。
我们可以通过增加一个储存下标的second域来解决,
deque<pair<int, int>> q;
int cnt_added = 0;
int cnt_removed = 0;
void push(int new_element){
while (!q.empty() && q.back().first > new_element)
q.pop_back();
q.push_back({new_element, cnt_added});
cnt_added++;
}
void pop(){
if (!q.empty() && q.front().second == cnt_removed)
q.pop_front();
cnt_removed++;
}
实现2:
上面的单调队列都把除了单调递减的数列的数删完了,如何保存所有元素呢?
可以直接用两个单调栈来模拟。
往s1里push新元素。
从s2里pop。 如果s2空了,那么就把s1全都放到s2里面。
这样就通过s2可以访问栈的开头元素了。
stack<pair<int, int>> s1, s2;
int query(){
int minimum=0;
if (s1.empty() || s2.empty())
minimum = s1.empty() ? s2.top().second : s1.top().second;
else
minimum = min(s1.top().second, s2.top().second);
return minimum;
}
void push(int new_element){
int minimum = s1.empty() ? new_element : min(new_element, s1.top().second);
s1.push({new_element, minimum});
}
void pop(){
if (s2.empty()) {
while (!s1.empty()) {
int element = s1.top().first;
s1.pop();
int minimum = s2.empty() ? element : min(element, s2.top().second);
s2.push({element, minimum});
}
}
int remove_element = s2.top().first;
s2.pop();
}
ST表
intruduction
ST表,O(nlogn)时间预处理,O(nlogn)空间,O(logn)地查询。
思想是倍增地去查询,二分地预处理。
记st[i][j]存的是[i,i+2^j)区间的信息,对于类似max,min,sum的f(),我们有递推方程:
$ st[i][j] = f(st[i][j-1], st[i +2^{j - 1})][j - 1]);$
实现
const int MAXN=1e7;
const int K=25;
int st[MAXN][K + 1];
int a[MAXN];
int N;
int f(int a,int b){
return min(a,b);
}
void init(){
for (int i = 0; i < N; i++)
st[i][0] = a[i];
for (int j = 1; j <= K; j++)
for (int i = 0; i + (1 << j) <= N; i++)
st[i][j] = f(st[i][j-1], st[i + (1 << (j - 1))][j - 1]);
}
int query(int L,int R){
int ret=1e9;//change when query max or sum
//long long sum = 0;
for (int j = K; j >= 0; j--) {
if ((1 << j) <= R - L + 1) {
ret=f(ret,st[L][j]);
L += 1 << j;
}
}
return ret;
}
ST最强大的地方在于对于Idempotence的函数(类似max,min这样“同一个元素被运算多次不会产生影响的函数”)可以做到O(1)查询。
我们考虑将区间分为两段前后有重叠部分的区间:
(min( ext{st}[L][j], ext{st}[R - 2^j + 1][j]) quad ext{ where } j = log_2(R - L + 1))
具体的实现方法就是
int log[MAXN+1];
void initlog(){
log[1] = 0;
for (int i = 2; i <= MAXN; i++)
log[i] = log[i/2] + 1;
}
int query(int L,int R){
int j = log[R - L + 1];
int minimum = min(st[L][j], st[R - (1 << j) + 1][j]);
}
并查集Disjoint Set Union/ Union Find
问题
n个集合,支持O(1)合并两个集合与查询某元素所属集合操作。
intro
思想就是对每个集合建一棵树。对每个元素维护一个father数组,指向其父亲结点。
于是查找就可以用O(n)地暴力找到跟结点。
合并就是将两个根节点并到一起。
实现
void make_set(int v) {
parent[v] = v;
}
int find_set(int v) {
if (v == parent[v])
return v;
return find_set(parent[v]);
}
void union_sets(int a, int b) {
a = find_set(a);
b = find_set(b);
if (a != b)
parent[b] = a;
}
路径压缩+压行
//int n;int fa[maxn];
void init() {for (int i = 1; i <= n; i++) fa[i] = i;}
int find(int x) {if (fa[x] == x) {return x;}return fa[x] = find(fa[x]);}
void un(int x, int y) { int xx = find(x), yy = find(y); fa[yy] = xx; }
心路历程