单调队列 & 单调栈 简单练习




HDU-3410 Passing the Message

HDU-3410 Passing the Message

题意

n n n 个小孩要传递信息,传递信息只能从高个子的传递给矮个子的
问第 i i i 个小孩可以传递到最左边的哪个小孩,最右边的哪个小孩

思路

要算个数,可以正向计算左边的,逆向计算右边的
维护单调递减的序列
每次弹出的时候即使可以将信息传递到的小孩

Code

#include <algorithm>
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn = 5e4 + 5;

// 维护单调递减的栈
int res[2][maxn];
int st[maxn];
int a[maxn];

void solve(int n) {
    int ans, top;
    top = 0;
    for(int i = 1; i <= n; ++i) {
        ans = 0;
        while(top > 0 && a[st[top - 1]] <= a[i])
            top--, ans = st[top];
        st[top++] = i, res[0][i] = ans;
    }
    top = 0;
    for(int i = n; i >= 1; --i) {
        ans = 0;
        while(top > 0 && a[st[top - 1]] <= a[i])
            top--, ans = st[top];
        st[top++] = i, res[1][i] = ans;
    }
}

int main() {
    int T;
    scanf("%d", &T);
    for(int t = 1; t <= T; ++t) {
        int n;
        scanf("%d", &n);
        for(int i = 1; i <= n; ++i)
            scanf("%d", &a[i]);
        printf("Case %d:\n", t);
        solve(n);
        for(int i = 1; i <= n; ++i)
            printf("%d %d\n", res[0][i], res[1][i]);
    }
    return 0;
}


HDU-4252 A Famous City

HDU-4252 A Famous City

题意

n n n 个建筑的图片,每个图片有一个高度,高度可以为 0 0 0
问从左到右,至少可以表示多少个建筑
举个栗子

在这里插入图片描述
1 3 6 3 4
上面这个至少就有 4 4 4

思路

可以发现每弹出一个,后面的连续建筑和前面的连续建筑会分割开,则建筑数量会增加 1 1 1
利用单调栈,维护一个单调递增的栈
最后栈中剩下的个数是高度不同的,有几个不同高度就要有几个建筑
再加上弹出的建筑数 a n s ans ans,就可以得到最终结果

Code

#include <algorithm>
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 5;

// 维护单调递增栈
int st[maxn];
int a[maxn];

void solve(int n) {
    int top = 0, ans = 0;
    for(int i = 1; i <= n; ++i) {
        while(top > 0 && a[st[top]] > a[i])
            top--, ans++;
        if(a[i] && (top == 0 || a[i] > a[st[top]]))
            st[++top] = i;
    }
    printf("%d\n", top + ans);
}

int main() {
    int T = 0, n;
    while(~scanf("%d", &n)) {
        for(int i = 1; i <= n; ++i)
            scanf("%d", &a[i]);
        printf("Case %d: ", ++T);
        solve(n);
    }
    return 0;
}


HDU-1506 Largest Rectangle in a Histogram

HDU-1506 Largest Rectangle in a Histogram

题意

求最大连续矩形面积

思路

维护单调递增的栈
若弹出则计算此块矩形的面积,并记录弹出的矩形宽度和
则当前要弹入栈顶的是 弹出的宽度和+要弹入的矩形的宽度
这里循环的时候要多加一个空的循环
为了计算最后剩余在栈内的矩形的面积

Code

#include <algorithm>
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 5;

// 维护单调递增的栈
ll st[maxn];
ll width[maxn];
ll h[maxn];

void solve(int n) {
    ll ans = 0; int top = 0; h[n + 1] = 0;
    for(int i = 1; i <= n + 1; ++i) {
        if(h[i] > st[top]) {
            st[++top] = h[i], width[top] = 1;
        }else {
            ll wSum = 0;
            while(st[top] > h[i]) {
                wSum += width[top];
                ans = max(ans, wSum * st[top]);
                top -= 1;
            }
            st[++top] = h[i], width[top] = wSum + 1;
        }
    }
    printf("%lld\n", ans);
}

int main() {
    int n;
    while(~scanf("%d", &n) && n) {
        for(int i = 1; i <= n; ++i)
            scanf("%lld", &h[i]);
        solve(n);
    }
    return 0;
}


ZOJ-2642 Feel Good

ZOJ-2642 Feel Good

题意

求最大连续矩形面积,每个矩形的面积为 最矮的矩形高度 × \times × 这块连续矩形的高度和
(其实我也没看过题目,看样例猜的题意)
最后输出矩形面积以及矩形的左右端点
如果有多个输出任意一个即可

思路

跟上一题很像
直接设置每个矩形的宽度为他们的高度
剩下的就是套用最大连续矩形面积的模板就好了
在计算最大面积的时候,可以记录此时的右端点
要输出的时候向前找到左端点

Code

#include <algorithm>
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 5;

// 维护单调递增的栈
ll st[maxn];
ll width[maxn];
ll h[maxn];

void solve(int n) {
    ll ans = 0, tmp; 
    int top = 0, l, r; h[n + 1] = 0;
    for(int i = 1; i <= n + 1; ++i) {
        if(h[i] > h[st[top]]) {
            st[++top] = i, width[top] = h[i];
        }else {
            ll wSum = 0; int tr = st[top];
            while(h[st[top]] > h[i]) {
                wSum += width[top];
                if(wSum * h[st[top]] > ans)
                    tmp = wSum, r = tr, ans = wSum * h[st[top]];
                top -= 1;
            }
            st[++top] = i, width[top] = wSum + h[i];
        }
    }
    l = r;
    while(tmp > 0)    tmp -= h[l], l--;
    printf("%lld\n%d %d\n", ans, l + 1, r);
}

int main() {
    int n;
    while(~scanf("%d", &n) && n) {
        for(int i = 1; i <= n; ++i)
            scanf("%lld", &h[i]);
        solve(n);
    }
    return 0;
}


HDU-6319 Problem A. Ascending Rating ★

HDU-6319 Problem A. Ascending Rating

题意

长度为 n n n 的数组 a [ ] a[] a[]
给出前 k k k a [ i ] a[i] a[i]
剩下的数由公式 a [ i ] = ( a [ i − 1 ] × p + q × i + r ) m o d    M O D a[i] = (a[i -1] \times p + q \times i + r) \mod MOD a[i]=(a[i1]×p+q×i+r)modMOD 来计算
A = ∑ k ≤ i ≤ n ( m a x ( a i , a i + 1 , . . . , a i + k − 1 ) A = \sum_{k \leq i \leq n}(max(a_i, a_{i + 1}, ... , a_{i + k - 1}) A=kin(max(ai,ai+1,...,ai+k1) ^ i ) i) i)
B = ∑ k ≤ i ≤ n ( c o u n t B = \sum_{k \leq i \leq n}(count B=kin(count ^ i ) i) i)
其中 c o u n t count count 表示该子区间 [ i , i + k − 1 ] [i, i + k - 1] [i,i+k1] 从左到右最大值修改的次数

思路

A A A 的计算可以直接维护单调递减的队列来求
而计算修改次数,可以发现对于后面的数如果最大值更改了,对前面的数是不会有影响的
反之如果前面的最大值有所修改,对后面的子区间修改次数是有可能产生影响的
因此可以考虑逆序,然后求修改次数
修改次数即为当前队列中元素的个数

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e7 + 5;

// 维护单调递减的队列
int qu[maxn];
int a[maxn];

void solve(int n, int m) {
    ll A = 0, B = 0;
    int head = 1, tail = 0;
    for(int i = n; i >= 1; --i) {
        while(tail >= head && a[qu[tail]] <= a[i])
            tail--;
        qu[++tail] = i;
        if(n - i + 1 < m)   continue;
        while(tail >= head && qu[head] >= i + m)
            head++;
        A += (a[qu[head]] ^ i) * 1ll;
        B += ((tail - head + 1) ^ i) * 1ll;
    }
    printf("%lld %lld\n", A, B);
}

int main() {
    int T;
    scanf("%d", &T);
    while(T--) {
        int n, m, k, p, q, r, mod;
        scanf("%d%d%d%d%d%d%d", &n, &m, &k, &p, &q, &r, &mod);
        for(int i = 1; i <= k; ++i)
            scanf("%d", &a[i]);
        for(int i = k + 1; i <= n; ++i)
            a[i] = (a[i - 1] * 1ll * p + q * 1ll * i + r * 1ll) % mod;
        solve(n, m);
    }
    return 0;
}


HDU-3530 Subsequence ★

HDU-3530 Subsequence

题意

求一个长度最大的区间,该区间的 m ≤ m \leq m (最大值 - 最小值) ≤ k \leq k k

思路

维护两个单调队列,一个维护最大值,一个维护最小值
对于不满足 (最大值 - 最小值) ≤ k \leq k k 的,将队列所存的位置从前往后依次弹出,位置较前面的先弹出
如果差值也满足 > = m >= m >=m ,就取最大值

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e7 + 5;

int q1[maxn]; // 最大值 递减
int q2[maxn]; // 最小值 递增
int a[maxn];

void solve(int n, int m, int k) {
    int h1 = 1, t1 = 0, h2 = 1, t2 = 0;
    int ans = 0, pos = 0;
    for(int i = 1; i <= n; ++i) {
        // pos = 0;
        while(h1 <= t1 && a[q1[t1]] <= a[i])    t1--;
        while(h2 <= t2 && a[q2[t2]] >= a[i])    t2--;
        q1[++t1] = i, q2[++t2] = i;
        while(h1 <= t1 && h2 <= t2 && a[q1[h1]] - a[q2[h2]] > k) {
            if(q1[h1] < q2[h2]) pos = q1[h1++];
            else                pos = q2[h2++];
        }
        if(h1 <= t1 && h2 <= t2 && a[q1[h1]] - a[q2[h2]] >= m)
            ans = max(ans, i - pos);
    }
    printf("%d\n", ans);
}

int main() {
    int n, m, k;
    while(~scanf("%d%d%d", &n, &m, &k)) {
        for(int i = 1; i <= n; ++i)
            scanf("%d", &a[i]);
        solve(n, m, k);
    }
    return 0;
}


POJ-2823 Sliding Window

POJ-2823 Sliding Window

题意

求区间长度为 m m m 的最大值和最小值

思路

求最大值维护单调递减的队列
最小值维护单调递增的队列

Code

#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 5;

int q[maxn]; // 最大值 递减 // 最小值 递增
int a[maxn];
int ans[maxn];

void getMax(int n, int m) {
    int head = 1, tail = 0;
    for(int i = 1; i <= n; ++i) {
        while(head <= tail && a[q[tail]] <= a[i])   tail--;
        q[++tail] = i;
        while(head <= tail && q[head] + m <= i)     head++;
        ans[i] = a[q[head]];
    }
    for(int i = m; i <= n; ++i) 
        printf("%d%c", ans[i], i == n ? '\n' : ' ');
}

void getMin(int n, int m) {
    int head = 1, tail = 0;
    for(int i = 1; i <= n; ++i) {
        while(head <= tail && a[q[tail]] >= a[i])   tail--;
        q[++tail] = i;
        while(head <= tail && q[head] + m <= i)     head++;
        ans[i] = a[q[head]];
    }
    for(int i = m; i <= n; ++i) 
        printf("%d%c", ans[i], i == n ? '\n' : ' ');
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    getMin(n, m);
    getMax(n, m);
    return 0;
}


POJ-3250 Bad Hair Day [思维] ★

POJ-3250 Bad Hair Day

题意

长度为 n n n 的数组 a [ ] a[] a[],第 i i i 只牛可以看到右边连续比他矮的几只牛
(即如果出现一只比他高的,右边的牛也都看不到)
问总共可以看到的牛的次数和

思路

换一个角度也就是求每只牛被看到的次数和
每只牛都是被比他高的牛看到的
因此维护一个牛的高度的最大值,单调递减的栈
每次栈中的元素数量就是第 i i i 头牛被看到的次数

Code

#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn = 8e4 + 5;

int st[maxn]; // 最大值 递减

int main() {
    int n, x, top = 0; ll ans = 0;
    scanf("%d", &n);
    for(int i = 1; i <= n; ++i) {
        scanf("%d", &x);
        while(top > 0 && st[top] <= x)  top--;
        ans += top * 1ll;
        st[++top] = x;
    }
    printf("%lld\n", ans);
    return 0;
}


HDU-6444 Neko’s loop [单调队列][裴蜀定理][最大连续子段和] ★★★

HDU-6444 Neko’s loop
参考博客:
hdu 6444 网络赛 Neko’s loop(单调队列 + 裴蜀定理)题解
HDU 6444 Neko’s loop(思维+长度不超过L的最大子段和)

题意

一个长度为 n n n 的数组 a [ ] a[] a[],首尾相连循环, a [ i ] a[i] a[i] 表示这个位置上的价值(可能为负值)
现在开始取一些值,开始的位置可以任意选择
选择后下一次的位置为 ( i + k ) % m (i + k) \% m (i+k)%m,已经走过的位置再次走即再次获得它的价值
现在有 m m m 点能量值,即相当于最多可以移动 m m m 次,问要得到 ≥ s \ge s s 的价值,最初给定的价值需要多少(这个需要为非负数)

思路

由于是首尾相连的循环,且每次增加的位移是固定的,因此必然存在循环节。
根据裴蜀定理, n x + k y = t nx + ky = t nx+ky=t,可以知道循环节的数量应该为 g = g c d ( n , k ) g = gcd(n, k) g=gcd(n,k),则循环节的长度应该为 l e n = n / g c d ( n , k ) len = n / gcd(n, k) len=n/gcd(n,k)
(其实我一直不是很能理解这个地方,看了很多篇博客都没有详细说的。。_(:з」∠)_大概只是我太菜了吧)
分完组之后可以对每一组寻找可以取到的最大值

对于每个循环节,取到最大值是有两种情况的( s u m sum sum 为该循环节的和)

  1. m % l e n ! = 0 m \% len != 0 m%len!=0,这个时候应该找长度 ≤ ( m % l e n ) \leq (m \% len) (m%len) 所能得到的最大价值,再加上 ( m / l e n ) × s u m (m / len) \times sum (m/len)×sum
    这个其实存在一种情况会取不到最优的
  2. m % l e n = = 0 m \% len == 0 m%len==0,这个时候应该找长度 ≤ l e n \leq len len 所能得到的最大价值,再加上 ( m / l e n − 1 ) × s u m (m / len - 1) \times sum (m/len1)×sum

其实相当于先求出长度 ≤ \leq 当前确定的最大长度的最大字段和,再加上 ( m / l e n ) × s u m (m / len) \times sum (m/len)×sum 就是可以取到的最大值
当然后面加上循环节的和必须要是循环节的和 s u m > 0 sum > 0 sum>0 的情况下,不然越加越少

接下来是求最大连续子段和,利用单调队列
这里需要两倍的循环节,这样才能计算出首尾相连的最大连续子段和
维护以 i i i 为子段右端点, q [ h e a d ] q[head] q[head] 为子段左端点的最大连续子段
s u m [ k ] < s u m [ j ] & & k > j sum[k] < sum[j] \& \& k > j sum[k]<sum[j]&&k>j,这个时候应该选择 k k k 做左端点可以使子段和更优(忘记哪个博客看到的了)
因此单调队列维护 s u m [ j ] sum[j] sum[j] 的最小值,单调递增队列

Code

#include <iostream>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int inf = 0x3f3f3f3f;
const int maxn = 8e4 + 5;

ll a[maxn];
ll b[maxn]; // 同一组内的值 
ll q[maxn]; // 维护左端点
ll sum[maxn]; // 前缀和
bool vis[maxn]; // 标记是否算过

ll cal(int sz, int len) { // 最大子段和模板
    ll ans = 0;
    int head = 1, tail = 0;
    for(int i = 1; i <= len * 2; ++i) { 
        // 以 i 为右端点, q[head] 为左端点
        // 若 k > j && sum[k] < sum[j] -> k 做左端点更优
        // 因此维护单调递增队列
        sum[i] = b[i] + (i == 1 ? 0 : sum[i - 1]);
        if(i <= sz)
            ans = max(ans, sum[i]);
        while(head <= tail && q[head] + sz < i) 
            head++;
        if(head <= tail)
            ans = max(ans, sum[i] - sum[q[head]]);
        while(head <= tail && sum[q[tail]] >= sum[i])
            tail--;
        q[++tail] = i;
    }
    return ans;
}

ll solve(int m, int len) {
    ll ans = 0, ans1, ans2;
    for(int i = 1; i <= len; ++i)   ans += b[i]; // 该循环节的和
    ans1 = cal(m % len, len), ans2 = cal(len, len);
    if(ans > 0) {
        if(m / len >= 1)    ans1 += ans * (m / len * 1ll);
        if(m / len >= 2)    ans2 += ans * (m / len - 1) * 1ll;
    }
    return ans1 > ans2 ? ans1 : ans2;
}

int main() {
    int T;
    scanf("%d", &T);
    for(int t = 1; t <= T; ++t) {
        ll n, s, m, k;
        scanf("%lld%lld%lld%lld", &n, &s, &m, &k); // m 点能量
        for(int i = 0; i < n; ++i) {
            scanf("%lld", &a[i]); vis[i] = false;
        }
        int num = __gcd(n, k), len = n / num; // 循环节个数, 循环节长度
        ll res = -INF;
        for(int i = 0, g = 1; g <= num && i < n; ++i) {
            if(vis[i])  continue;
            vis[i] = true;
            for(int j = 1, pos = i; j <= len; ++j) {
                b[j] = b[j + len] = a[pos]; // 两倍长
                pos = (pos + k) % n, vis[pos] = true;
            }
            res = max(res, solve(m, len)), g += 1;
        }
        printf("Case #%d: %lld\n", t, res >= s ? 0 : s - res);
    }
    return 0;
}


P2216 [HAOI2007]理想的正方形 [二维单调队列]

P2216 [HAOI2007]理想的正方形
参考博客
BZOJ 1047 [HAOI2007]理想的正方形

题意

原矩阵大小为 a × b a \times b a×b
求子矩阵 n × n n \times n n×n m i n ( 最 大 值 − 最 小 值 ) min(最大值 - 最小值) min()

思路

二维单调队列模板
先维护 1 行 n 列的矩阵最大/小值
再维护 n 行 n 列的矩阵最大/小值

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e3 + 5;

int re[2][maxn][maxn];
int a[maxn][maxn];
int g[maxn][maxn];
int q[maxn];
int n, m, k;    

void solve(int f) {
    for(int i = 1; i <= n; ++i) {
       int L = 1, R = 0;
        for(int j = 1; j <= m; ++j) {
            while(L <= R && j - q[L] >= k)  L++;
            if(f)   while(L <= R && a[i][q[R]] >= a[i][j])  R--;
            else    while(L <= R && a[i][q[R]] <= a[i][j])  R--;
            q[++R] = j, g[i][j] = a[i][q[L]];
        }
    }
    for(int j = k; j <= m; ++j) {
        int L = 1, R = 0;
        for(int i = 1; i <= n; ++i) {
            while(L <= R && i - q[L] >= k)  L++;
            if(f)   while(L <= R && g[q[R]][j] >= g[i][j])  R--;
            else    while(L <= R && g[q[R]][j] <= g[i][j])  R--;
            q[++R] = i;
            re[f][i][j] = g[q[L]][j];
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &k);
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= m; ++j)
            scanf("%d", &a[i][j]);
    solve(0);
    solve(1);
    int ans = 1<<30;
    for(int i = k; i <= n; ++i)
        for(int j = k; j <= m; ++j)
            ans = min(ans, re[0][i][j] - re[1][i][j]);
    printf("%d\n", ans);
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值