算法基础-(数据结构)

1.单调栈

1. 什么是单调栈?

单调栈,顾名思义,就是 具有单调性的栈 。它依旧是⼀个 栈结构 ,只不过⾥⾯存储的数据是递增或者 递减的。这种结构是很容易实现的(如下⾯的代码),但重点是维护⼀个单调栈的意义是什么?

cpp 复制代码
#include <iostream>
#include <stack>
using namespace std;

const int N = 3e6 + 10;
int a[N], n;

// 单调递增栈
void test1() {
    stack<int> st;
    for (int i = 1; i <= n; i++) {
        while (st.size() && st.top() >= a[i]) st.pop();
        st.push(a[i]);
    }
    // 输出栈内元素(递增)
    cout << "单调递增栈结果:";
    while (!st.empty()) {
        cout << st.top() << " ";
        st.pop();
    }
    cout << endl;
}

// 单调递减栈
void test2() {
    stack<int> st;
    for (int i = 1; i <= n; i++) {
        while (st.size() && st.top() <= a[i]) st.pop();
        st.push(a[i]);
    }
    // 输出栈内元素(递减)
    cout << "单调递减栈结果:";
    while (!st.empty()) {
        cout << st.top() << " ";
        st.pop();
    }
    cout << endl;
}

int main() {
    // 测试用例:n=5,数组a=[3,1,4,2,5]
    n = 5;
    a[1] = 3; a[2] = 1; a[3] = 4; a[4] = 2; a[5] = 5;
    
    test1(); // 递增栈结果:1 2 5 
    test2(); // 递减栈结果:5 4 3 
    return 0;
}

2. 单调栈解决的问题

单调栈能帮助我们解决以下四个问题:
• 寻找当前元素左侧,离它最近,并且⽐它⼤的元素在哪;
• 寻找当前元素左侧,离它最近,并且⽐它⼩的元素在哪;
• 寻找当前元素右侧,离它最近,并且⽐它⼤的元素在哪;
• 寻找当前元素右侧,离它最近,并且⽐它⼩的元素在哪。
虽然是四个问题,但是原理是⼀致的。因此,只要解决⼀个,举⼀反三就可以解决剩下的⼏个。

3. 寻找当前元素左侧,离它最近,并且⽐它⼤的元素在哪

从左往右遍历元素,构造⼀个单调递减的栈。插⼊当前位置的元素的时:
• 如果栈为空,则左侧不存在⽐当前元素⼤的元素;
• 如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈⾥⾯存的是每个元素的下标。

cpp 复制代码
#include <iostream>
#include <stack>
using namespace std;

const int N = 3e6 + 10;
int a[N], n;
int ret[N];

void test() {
    stack<int> st; // 单调递增栈:存储下标
    for (int i = 1; i <= n; i++) ret[i] = 0;

    // 逆序遍历:找右侧第一个更小的元素
    for (int i = n; i >= 1; i--) {
        // 弹出栈中≥当前值的下标
        while (st.size() && a[st.top()] >= a[i]) {
            st.pop();
        }
        // 栈非空:右侧第一个更小元素的值
        if (st.size()) {
            ret[i] = a[st.top()];
        }
        st.push(i);
    }

    for (int i = 1; i <= n; i++) {
        cout << ret[i] << " ";
    }
    cout << endl;
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    test();
    return 0;
}

4. 寻找当前元素左侧,离它最近,并且⽐它⼩的元素在哪

从左往右遍历元素,构造⼀个单调递增的栈。插⼊当前位置的元素的时:
• 如果栈为空,则左侧不存在⽐当前元素⼩的元素;
• 如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈⾥⾯存的是每个元素的下标。

cpp 复制代码
#include <iostream>   // 输入输出头文件:用来输入数字、输出结果
#include <stack>      // 栈的头文件:用来使用"栈"这个工具
using namespace std;

// 定义数组最大长度,3e6+10表示足够装下大量数字(不用改)
const int N = 3e6 + 10;
int a[N], n;  // a[N]:装输入的数字;n:装数字的总个数
int ret[N];   // ret[N]:装最终结果(每个数左边最近更小的数的下标)

void test() {
    // 定义一个栈,栈里存的是数字的"下标"(抽屉编号),维护单调递增的栈
    stack<int> st;

    // 从第1个数字到第n个数字,逐个处理
    for(int i = 1; i <= n; i++) {
        // 核心逻辑:把栈里≥当前数字的下标都弹出(保证栈是递增的)
        // st.size():判断栈里有没有东西;a[st.top()]:栈顶下标对应的数字
        while(st.size() && a[st.top()] >= a[i]) {
            st.pop();  // 弹出栈顶(扔掉不符合的下标)
        }

        // 如果栈里还有东西,栈顶就是"左边最近更小的数的下标"
        if(st.size()) {
            ret[i] = st.top();
        } else {
            ret[i] = 0;  // 栈空=没找到,填0
        }

        // 把当前数字的下标放进栈里,维护栈的递增性
        st.push(i);
    }

    // 输出结果:从第1个到第n个,逐个打印ret里的数
    for(int i = 1; i <= n; i++) {
        cout << ret[i] << " ";
    }
    cout << endl;
}

int main() {
    // 第一步:输入数字的总个数(测试用例里是9)
    cin >> n;
    // 第二步:输入n个数字,依次放进a[1]到a[n](抽屉1到抽屉n)
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    // 第三步:执行找"左边最近更小数下标"的逻辑
    test();

    return 0;
}

5. 寻找当前元素右侧,离它最近,并且⽐它⼤的元素在哪

从右往左遍历元素,构造⼀个单调递减的栈。插⼊当前位置的元素的时:
• 如果栈为空,则左侧不存在⽐当前元素⼤的元素;
• 如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈⾥⾯存的是每个元素的下标。

cpp 复制代码
#include <iostream>   // 输入输出工具:能输入数字、输出结果
#include <stack>      // 栈工具:用来找"右侧最近更大的数"
using namespace std;

const int N = 3e6 + 10; // 准备足够多的"抽屉"(不用改,够装数字就行)
int a[N], n;  // a抽屉:装输入的9个数字;n:记数字的总个数(比如9)
int ret[N];   // ret抽屉:装最终结果(每个数的答案)

void test() {
    stack<int> st; // 拿出空的"栈盒子"(维护单调递减,只存下标)

    // 核心:从最后一个数往第一个数遍历(右→左)
    for(int i = n; i >= 1; i--) {
        // 第一步:把栈里≤当前数的下标都扔掉(保证栈是递减的)
        // st.size():盒子里有东西吗?a[st.top()]:栈顶下标对应的数字
        while(st.size() && a[st.top()] <= a[i]) {
            st.pop(); // 扔掉不符合的下标
        }

        // 第二步:填结果
        if(st.size()) {
            ret[i] = st.top(); // 栈非空→栈顶是"右侧最近更大数的下标"
        } else {
            ret[i] = 0; // 栈空→没找到,填0
        }

        // 第三步:把当前数的下标放进栈里
        st.push(i);
    }

    // 输出结果:从第1个到第9个,逐个打印ret抽屉里的数
    for(int i = 1; i <= n; i++) {
        cout << ret[i] << " ";
    }
    cout << endl;
}

int main() {
    cin >> n; // 输入9(告诉程序有9个数)
    // 把1、4、10、6、3、3、15、21、8依次放进a[1]~a[9]
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    test(); // 执行找"右侧最近更大数下标"的逻辑
    return 0;
}

6. 寻找当前元素右侧,离它最近,并且⽐它⼩的元素在哪

从右往左遍历元素,构造⼀个单调递增的栈。插⼊当前位置的元素的时:
• 如果栈为空,则左侧不存在⽐当前元素⼩的元素;
• 如果栈⾮空,插⼊当前位置元素时的栈顶元素就是所找的元素。
注意,因为我们要找的是最终结果的位置。因此,栈⾥⾯存的是每个元素的下标。

cpp 复制代码
#include <iostream>   // 输入输出工具:输入数字、输出结果
#include <stack>      // 栈工具:找右侧最近更小的数
using namespace std;

const int N = 3e6 + 10; // 足够多的"抽屉",装数字和结果
int a[N], n;  // a:装输入的数字;n:数字总个数
int ret[N];   // ret:装最终结果(每个数的答案)

void test() {
    stack<int> st; // 空栈,维护单调递增(只存下标)
    
    // 从最后一个数往第一个数遍历(右→左)
    for(int i = n; i >= 1; i--) {
        // 核心:弹出栈里≥当前数的下标(保证栈递增)
        // st.size():栈里有东西吗?a[st.top()]:栈顶下标对应的数字
        while(st.size() && a[st.top()] >= a[i]) {
            st.pop(); // 扔掉不符合的下标
        }

        // 填结果:栈非空=找到答案,栈空=填0
        if(st.size()) {
            ret[i] = st.top();
        } else {
            ret[i] = 0; // 补充初始化,避免随机值
        }

        // 把当前数的下标放进栈,维护递增性
        st.push(i);
    }

    // 输出结果:从第一个数到最后一个数打印
    for(int i = 1; i <= n; i++) {
        cout << ret[i] << " ";
    }
    cout << endl;
}

int main() {
    cin >> n; // 输入数字个数(比如9)
    // 把数字依次放进a[1]~a[n]
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    test(); // 执行找数逻辑
    return 0;
}

💡 总结:

• 找左侧,正遍历;找右侧,逆遍历;
• ⽐它⼤,单调减;⽐它⼩,单调增。


1.1 【模板】单调栈

题⽬来源: 洛⾕
题⽬链接: P5788 【模板】单调栈
难度系数: ★★

题目背景

模板题,无背景。

2019.12.12 更新数据,放宽时限,现在不再卡常了。

题目描述

给出项数为 n 的整数数列 a1...n​。

定义函数 f(i) 代表数列中第 i 个元素之后第一个大于 ai​ 的元素的下标,即 f(i)=mini<j≤n,aj​>ai​​{j}。若不存在,则 f(i)=0。

试求出 f(1...n)。

输入格式

第一行一个正整数 n。

第二行 n 个正整数 a1...n​。

输出格式

一行 n 个整数表示 f(1),f(2),...,f(n) 的值。

输入输出样例

输入 #1复制

复制代码
5
1 4 2 3 5

输出 #1复制

复制代码
2 5 4 5 0

说明/提示

【数据规模与约定】

对于 30% 的数据,n≤100;

对于 60% 的数据,n≤5×103 ;

对于 100% 的数据,1≤n≤3×106,1≤ai​≤109。


【解法】

右侧离它最近并且⽐它⼤的元素:
• 逆序遍历数组;
• 构造⼀个单调递减的栈;
• 进栈时,栈顶元素就是最终结果。

【参考代码】

cpp 复制代码
#include <iostream>   // 输入输出工具:输入数字、输出结果
#include <stack>      // 栈工具:找右侧第一个更大的数
using namespace std;

const int N = 3e6 + 10; // 足够多的"抽屉",装数字和结果(适配3e6的大数据)
int n;          // 数列的长度(比如示例里的5)
int a[N];       // a抽屉:装输入的数列(a[1]=1, a[2]=4...)
int ret[N];     // ret抽屉:装每个数的答案(右边第一个更大数的下标,没找到填0)

int main() {
    // 第一步:输入数列长度和数列本身
    cin >> n;
    for(int i = 1; i <= n; i++) {
        cin >> a[i]; // 把数字依次放进a[1]到a[n]
    }

    stack<int> st; // 空栈:维护单调递减,只存"下标"(抽屉号)

    // 第二步:逆序遍历(从最后一个数到第一个数)
    for(int i = n; i >= 1; i--) {
        // 核心:弹出栈里≤当前数的下标(保证栈是递减的)
        // st.size():栈里有东西吗?a[st.top()]:栈顶下标对应的数字
        while(st.size() && a[st.top()] <= a[i]) {
            st.pop(); // 扔掉不符合的下标
        }

        // 第三步:填答案
        if(st.size()) {
            ret[i] = st.top(); // 栈非空→栈顶是"右边第一个更大数的下标"
        } else {
            ret[i] = 0; // 栈空→没找到,填0
        }

        // 第四步:把当前数的下标放进栈,维护递减性
        st.push(i);
    }

    // 第五步:输出结果(如果要输出"值",把ret[i]改成a[ret[i]]即可)
    for(int i = 1; i <= n; i++) {
        // 示例输出是下标,所以输出ret[i];若要输出值,改成cout << a[ret[i]] << " ";
        cout << ret[i] << " ";
    }
    cout << endl;

    return 0;
}

1.2 发射站

题⽬来源: 洛⾕
题⽬链接: P1901 发射站
难度系数: ★★


题目描述

某地有 N 个能量发射站排成一行,每个发射站 i 都有不相同的高度 Hi​,并能向两边(两端的发射站只能向一边)同时发射能量值为 Vi​ 的能量,发出的能量只被两边最近的且比它高的发射站接收。显然,每个发射站发来的能量有可能被 0 或 1 或 2 个其他发射站所接受。

请计算出接收最多能量的发射站接收的能量是多少。

输入格式

第 1 行一个整数 N。

第 2 到 N+1 行,第 i+1 行有两个整数 Hi​ 和 Vi​,表示第 i 个发射站的高度和发射的能量值。

输出格式

输出仅一行,表示接收最多能量的发射站接收到的能量值。答案不超过 32 位带符号整数的表示范围。

输入输出样例

输入 #1复制

复制代码
3
4 2 
3 5 
6 10

输出 #1复制

复制代码
7

说明/提示

对于 40% 的数据,1≤N≤5000,1≤Hi​≤105,1≤Vi​≤104。

对于 70% 的数据,1≤N≤105,1≤Hi​≤2×109,1≤Vi​≤104。

对于 100% 的数据,1≤N≤106,1≤Hi​≤2×109,1≤Vi​≤104。

【解法】

有了单调栈之后,这道题就变成模拟题了......


【参考代码】

cpp 复制代码
#include <iostream>   // 输入输出工具:输入发射站信息、输出结果
#include <stack>      // 栈工具:找"最近更高的站"
using namespace std;

typedef long long LL; // 防止能量值太大溢出(比如多个能量相加超过int范围)
const int N = 1e6 + 10; // 足够多的"抽屉",装1e6个发射站的信息
int n;          // 发射站的总数(比如示例里的3)
LL h[N], v[N];  // h:装每个站的高度;v:装每个站的能量值
LL sum[N];      // sum:装每个站接收的总能量(初始都是0)

int main() {
    // 第一步:输入发射站数量和每个站的高度、能量
    cin >> n;
    for(int i = 1; i <= n; i++) {
        cin >> h[i] >> v[i]; // 站1的h=4、v=2;站2的h=3、v=5;站3的h=6、v=10
    }

    // 第二步:找每个站"左边最近更高的站",并把能量加给这个站
    stack<int> st; // 空栈:维护单调递减(只存站的下标)
    for(int i = 1; i <= n; i++) { // 从左到右遍历每个站
        // 核心:弹出栈里≤当前站高度的下标(保证栈是递减的)
        while(st.size() && h[st.top()] <= h[i]) {
            st.pop();
        }
        // 栈非空:栈顶就是"左边最近更高的站",接收当前站的能量
        if(st.size()) {
            sum[st.top()] += v[i]; // 比如站2的左边更高是站1,sum[1] +=5
        }
        // 把当前站的下标放进栈,维护递减性
        st.push(i);
    }

    // 第三步:找每个站"右边最近更高的站",并把能量加给这个站
    while(st.size()) st.pop(); // 清空之前的栈
    for(int i = n; i >= 1; i--) { // 从右到左遍历每个站
        // 核心:弹出栈里≤当前站高度的下标(保证栈是递减的)
        while(st.size() && h[st.top()] <= h[i]) {
            st.pop();
        }
        // 栈非空:栈顶就是"右边最近更高的站",接收当前站的能量
        if(st.size()) {
            sum[st.top()] += v[i]; // 比如站1的右边更高是站3,sum[3] +=2
        }
        // 把当前站的下标放进栈,维护递减性
        st.push(i);
    }

    // 第四步:找接收能量最多的站
    LL ret = 0; // 存最大能量值(初始0)
    for(int i = 1; i <= n; i++) {
        ret = max(ret, sum[i]); // 逐个比较,保留最大的
    }
    cout << ret << endl; // 输出7(示例)

    return 0;
}
相关推荐
玩转数据库管理工具FOR DBLENS6 小时前
DBLens:开启数据库管理新纪元——永久免费,智能高效的国产化开发利器
数据结构·数据库·测试工具·数据库开发
@小码农6 小时前
2025年全国青少年信息素养大赛 Gandi编程 小低组初赛真题
数据结构·人工智能·算法·蓝桥杯
六毛的毛6 小时前
重排链表问题
数据结构·链表
Fine姐7 小时前
数据结构——02队列
数据结构
仰泳的熊猫7 小时前
1176 The Closest Fibonacci Number
数据结构·c++·算法·pat考试
一条大祥脚7 小时前
Cuda Rudece算子实现(附4090/h100测试)
java·数据结构·算法
2401_841495648 小时前
【LeetCode刷题】跳跃游戏
数据结构·python·算法·leetcode·游戏·贪心算法·数组
_w_z_j_9 小时前
全排列问题(包含重复数字与不可包含重复数字)
数据结构·算法·leetcode
@小码农9 小时前
LMCC大模型认证 青少年组 第一轮模拟样题
数据结构·人工智能·算法·蓝桥杯