浅谈二维单调栈

有趣事实

你说得对,但是《二维单调栈》是由ltc在被电瓶车撞后在模拟赛中脑洞大开自主研发的一款全新神(唐)秘(氏)数据结构。故事发生在一个被称作“C++”的幻想世界,在这里,被神选中的人将被授予“单调性”,导引vector<pair<node ,vector<node> > >之力。你将扮演一位名为“node”的神秘角色,在自♂由的旅行中pop掉性格各异、能力独特的更劣解们,和他们一起击败难题,找回失散的rating——同时,逐步发掘“萨基雅尔维的波尔卡”的真相。

如你所见,通过运用这个算法,我在信友队模拟赛中死磕四个小时在“萨基雅尔维的波尔卡”这道题上,最后发现算法假掉导致爆零(joker)

期间运用vector<pair<node ,vector<node> > >这样的唐氏方法维护经过 传播后使二维单调栈成为了我们机房的名梗

真是别样的数据结构大战!

引入

问题1

如何在一个单调的序列中查询第一个比k大的数?

学过OI的都知道直接二分一下即可,非常弱智,复杂度 O(logN)

问题2

如何在一个序列(不一定单调!)中查询第一个比k大的数?

我们先来看下面这个序列
1 3 2 6 3 5 7
这个鬼东西貌似不怎么符合单调性,也就是我们不能二分
难道只能打O(N)的暴力?
这不现实,查询多了必然会TLE(悲
有一个办法,既然它不单调……

那我们就让它单调!
我们考虑如果有一个数j,它在i后面,但还比i小(或等于)
它必然选不到
因为如果 k 比 i 大,i 都不能满足它,小得可怜的 j 更不能满足
如果 k 比 i 小,既然 i 在前面,那肯定选 i 而不是 j
因此,如果有数比前面的数小(或等于),那它有没有都一样,删掉它不会影响答案
也就是说,我们可以把原序列中递减的部分全部剔除,让小的数统统滚蛋
上面的序列也就可以转化成这样:
1 3 6 7
既然它已经单调了,我们就可以愉快地跑二分了(喜
具体实现可以维护一个单调栈,从后往前加数,如果栈顶小于这个数就pop,最后把这个数放进去
预处理O(N), 查询O(logN)

正片开始

问题三

如何在一个序列(不一定单调)中找到第一个和第二个大于k的数?

如果我们还是如法炮制,直接维护一个单调栈
恭喜你,算法假了!
还是这个序列
1 3 2 6 3 5 7
维护单调栈:
1 3 6 7
如果我们要找到比4大的第一个和第二个数,在单调栈里是6和7
可是正确的应该是6和5欸
why?
因为单调栈只能保证最优解不会被剔除,但是次优解可能在最优解后面还比它小,但它仍然可能大于k

这时就需要隆重请出全新的神(唐)秘(氏)算法:

二维单调栈

首先我们发现次优解肯定在最优解和原始单调栈中最优解后面的那个数之间(当然也有可能就是后面的那个数)
而最优解和它后面一个数在原始序列中之间的数又形成了一个新的序列,一个包含次优解的序列
那么对于这个序列我们还可以再维护一个单调栈,用来存储可能的次优解
也就是说对于在原始单调栈中的每个元素,我们都维护一个单调栈来存储被这个元素pop掉的元素

具体实现:
每次放入元素前先将单调栈里比它小的数放进这个元素的单调栈(放进这个元素的单调栈时也要遵守单调栈的规则,也就是要pop掉比它小的元素),然后pop掉
需要注意的是,在将比他小的数放入自己单调栈的过程中,我们不需要关心这个元素的单调栈,只要将它本身放入,把它的单调栈全删了,因为这些元素连次优解都不可能是了(毕竟他自己都只可能是次优解了,它栈里的元素只能比它更劣,次优解都不是)
代码:

vector<pair<node ,vector<node> > > q;//这是我们维护的二维单调栈  
vector<node> q1; //这是缓存  
for (int i = n; i >= 1; i--)//从后往前放入单调栈  
{  
	q1.clear();//清空缓存  
	while (!q.empty() && q.back().first.val <= a[i])//放入第一维单调栈  
	{  
		while (!q1.empty() && q1.back().val <= q.back().first.val)//放入第二维单调栈  
		{  
			q1.pop_back();  
		}  
		q1.push_back(q.back().first);  
		q.pop_back();//将这个元素从第一维单调栈中取出并放入第二维单调栈  
	}  
	q.push_back(make_pair((node) {a[i], i}, q1));  
}  
//(关于查询就是两个二分,就不放了doge) 

预处理O(N),查询虽然是两个二分但是并列的,还是O(logN)的,事实上常数应该非常小(毕竟两数直接不会有多少数),应该跑得飞快

大log当小log,小log当没log
——KevinLikesCoding(蔡弈凡)大神

所以它复杂度应该是常数

结语

声明:这本来就是一个整活算法,可能没什么应用场景,喜欢用可以用,觉得鸡肋勿喷

很快NOIP2024要来了,祝大家RP++

我永远喜欢珂朵莉!

posted @   I_LOVE_MATH  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示