数据结构入门

单调栈与单调队列 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(\text{st}[L][j], \text{st}[R - 2^j + 1][j]) \quad \text{ 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; }


心路历程



posted @ 2019-04-11 17:13  SuuTTT  阅读(231)  评论(0编辑  收藏  举报