题解:[RMI 2020] 秘鲁 / Peru

题目传送门

Upd 2026/2/26:修了代码,放了一份封装完善的;加入了 DP 推导部分。

本篇题解主要介绍本题线性做法所利用的数据结构。当时第一次见到这种东西,没想到不那么冷门……

题意分析

DP 是显然的:

\[f_i=\min_{j=i-k}^{i-1}\left(f_j+\max_{l=j+1}^is_l\right) \]

维护不难做到 \(\mathcal O(n\log n)\)。然而发现没什么意义的交互和 200ms 的时限,显然是想卡时间。

那么我们担心带 \(\log\) 会过不去 \(2.5\times10^6\),因此想线性做法。

观察发现 \(f\) 单调不降。证明也很简单,考虑假设 \(i<j,f_i>f_j\),那么你完全可以用干掉 \(1\sim j\) 的方式干掉 \(1\sim i\),从而有 \(f_i=f_j\),因此不成立。

则真正有用的决策点 \(j\) 其实不多:

  • \(j\)\(s_j,s_{j+1},s_{j+2},\cdots,s_i\) 的严格最大值。

    否则你发现 \(j-1\) 不劣于 \(j\)

  • \(j=i-k\)。因为 \(j-1\) 不可能存在。

    这是很容易维护的。

那么我们就开一个单调队列 \(q\) 维护 \([i-k+1,i]\) 的严格最大值的下标 \(j\)。每一个点 \(j\) 的贡献就是 \(f_j+s_i\)\(s_i\) 为严格后缀最大值。

\(f_j+s_i\) 丢入一个双端队列 \(Q\) 维护即可,需要每次查询最小值。

双栈模拟队列

其实这道题目需要用到一个黑科技:双栈模拟队列

这道题目 DP 之后便是要维护一个数据结构,实现:

  • 双端插入、删除。
  • 查询最小值。

双端插入、删除使用双端队列即可,但是双端队列不能查询最小值。因为最小值不可差分,也就是说将最小值弹出后无法更新最小值。

于是考虑使用其他数据结构。

考虑到,前/后缀最小值是很好维护的。弹出元素时,可以利用其他前/后缀的信息来维护,插入时同样如此。

那么就考虑将我们需要维护的数据结构拆为两个能够维护前后缀最值的数据结构,从而维护最值。

如图:

004

图中的线就是表示维护一个前/后缀最值。那么查询最值时,左右两端取最小即可。

如何实现双端插入、删除呢?这两个数据结构都需要实现这种功能。但是发现它们靠在一起的一端不会需要这种操作,因此“单端操作”,即

那么思路就很清晰了,维护双栈模拟双端队列,同时维护前/后缀最值构成的区间最值。


但还是有一个小问题:要是在某一刻,一个栈中的元素弹空了,还要弹出怎么办?

这在普通数据结构的正常情况下是不会出现的,但是在这里会。因为有一些元素在另一个栈里。

这种时候,我们直接 \(\mathcal O(\textit{size})\) 暴力重构即可,其中 \(\textit{size}\) 表示此时双栈中元素的个数。

假定某一次 \(\mathcal O(\textit{size}_0)\) 重构后,双栈中的元素个数为 \(\textit{size}_0\),那么左右两个栈里元素个数均为 \(\mathcal O\left(\dfrac{\textit{size}_0}2\right)\)

而在下一次重构前,至少需要弹出 \(\mathcal O\left(\dfrac{\textit{size}_0}2\right)\) 个元素(弹空)。

那么就可以将重构操作的复杂度均摊到弹出操作上,两个操作的复杂度均摊下来都是 \(\mathcal O(1)\) 的。

因此总复杂度仍为线性。

AC 代码

//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=2.5e6,P=1e9+7;
template<typename T>
class Deque{
	private:
		vector<pair<T,T>>l,r;
		
		void build(int eps){
			vector<T>s;
			for(int i=l.size()-1;0<=i;i--){
				s.push_back(l[i].first);
			}
			for(int i=0;i<r.size();i++){
				s.push_back(r[i].first);
			}
			l.resize(0);r.resize(0);
			int sizeL=(s.size()>>1)+eps;
			for(int i=sizeL-1;0<=i;i--){
				push_front(s[i]);
			}
			for(int i=sizeL;i<s.size();i++){
				push_back(s[i]);
			}
		}
	public:
		void push_back(T x){
			T y=x;
			if(r.size()){
				y=min(y,r.back().second);
			}
			r.push_back({x,y});
		}
		void push_front(T x){
			T y=x;
			if(l.size()){
				y=min(y,l.back().second);
			}
			l.push_back({x,y});
		}
		void pop_back(){
			if(!r.size()){
				build(0);
			}
			r.pop_back();
		}
		void pop_front(){
			if(!l.size()){
				build(1);
			}
			l.pop_back();
		}
		int size(){
			return l.size()+r.size();
		}
		bool empty(){
			return !l.size()&&!r.size();
		}
		T front(){
			if(!l.size()){
				build(1);
			}
			return l.back().first;
		}
		T back(){
			if(!r.size()){
				build(0);
			}
			return r.back().first;
		}
		T query(){
			if(l.size()&&r.size()){
				return min(l.back().second,r.back().second);
			}else if(l.size()){
				return l.back().second;
			}else if(r.size()){
				return r.back().second; 
			}
		}
};
int solve(int n,int k,int s[]){
	for(int i=n;1<=i;i--){
		s[i]=s[i-1];
	}
	s[0]=0;
	int ans=0;
	deque<int>q;
	Deque<ll>Q;
	static ll f[N+1];
	for(int i=1;i<=n;i++){
		while(q.size()&&s[q.back()]<=s[i]){
			q.pop_back();
			if(q.size()){
				Q.pop_back();
			}
		}
		while(q.size()&&q.front()<i-k){
			q.pop_front();
			if(q.size()){
				Q.pop_front();
			}
		}
		if(q.size()){
			Q.push_back(f[q.back()]+s[i]);
		}
		q.push_back(i);
		if(Q.size()){
			f[i]=min(Q.query(),s[q.front()]+f[max(i-k,0)]);
		}else{
			f[i]=s[q.front()]+f[max(i-k,0)];
		}
		ans=(23ll*ans+f[i])%P;
	}
	return ans;
}
posted @ 2025-07-22 18:51  TH911  阅读(11)  评论(0)    收藏  举报