单调队列是什么:
单调队列,顾名思义,一个是单调一个是队列。首先是单调,可以是单调递增单调递减,也可以是某种特殊数据的组合的比较方式,是可以自定义>号的这种(我想应该可以写成运算符重载,或者函数引用,不过我还不会qwq)。其次是队列,队列其实就是封装好的,只可以从队首队尾进行操作的数据结构。不过由于队首队尾都需要推入和弹出操作,所以用std::deque很适合。
单调队列如何能优化dp过程:
思考这样一个问题,给定一组数,从a0一直到an-1,需要求某段连续区间 [l, r]内的最值,其中r-l==k固定,应当怎么求。
可能用stl的multiset很好求,时间复杂度为O(nlogn),其实这个复杂度也是可以接受的。但是单调队列能够在O(n)的复杂度求出答案。
以求最大值为例,我们设有一个优先队列deque<pair<int,int>>dq,第一个数表示位置,第二个数表示数值大小。优先队列内部相邻的两个元素i和j(i更靠近队首)满足条件:i.first < j.first && i.second > j.second
假设我们已经考虑到了[0,k-1]的范围,现在需要考虑a[k]同时需要舍弃a[0],再假设a[i]为[0,k-1]内最大值。1.·我们需要先把队尾的所有的值(也就是second)比a[i]小的弹出,然后在推入(i,a[i])。2.我们需要看队首的元素的坐标(也就是first)是否是0(即是即将被舍弃元素的坐标),若是,则弹出。
上述过程大概就是单调队列维护的过程。在这个过程中,我们会发现,如果i,j两个坐标,i<j而且a[i]<a[j],我们发现在舍弃i时,对区间最大值完全没影响的,如果改成a[i]>a[j],则很有可能在i被舍弃后,a[j]成为最大的值。
总而言之,单调队列能够在O(n)内维护区间最大值。
题目
例题1:P2698 [USACO12MAR] Flowerpot S - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目翻译和简介洛谷已经给了,直接说思路。
需要维护最大最小的两个单调队列,而且一旦满足极值差的要求,需要左侧指针的移动。
然后从左到右遍历一遍就好,单调队列复杂度O(n)
ac代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
/*以维护区间最大值的递增序列为例,区间整体是向右侧移动的。
队首是区间最大值,队尾是相对的最小值。如果说新的点,大于所有区间内的最大值,则队列清空,只剩这个点
如果说新的点小于区间内最大值,则从队尾查起,如果有比它小的,都弹出。
这样的功能是什么,是能够保证队列从队首到队尾,依次递减。
*/
int ab(int x)
{
if (x < 0)
return -x;
else
return x;
}
struct node
{
int x, y;
};
int main(void)
{
std::ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, d;
cin >> n >> d;
vector<struct node> a(n);
for (int i = 0; i < n; i++)
{
cin >> a[i].x >> a[i].y;
}
sort(a.begin(), a.end(), [&](struct node i, struct node j)
{ return i.x < j.x; });//按照坐标排序
int l, r, ans = -1;
l = r = a[0].x;//最初从最左侧雨滴的坐标开始
deque<struct node> dq_mx, dq_mn;//单调队列,分别维护当前区间的最大值和最小值
dq_mx.push_back(a[0]), dq_mn.push_back(a[0]);
for (int i = 1; i < n; i++)
{
while (!dq_mx.empty() && dq_mx.back().y <= a[i].y)
{
dq_mx.pop_back();
}
dq_mx.push_back(a[i]);//队尾检查
while (!dq_mn.empty() && dq_mn.back().y > a[i].y)
{
dq_mn.pop_back();
}
dq_mn.push_back(a[i]);
if (dq_mx.front().y - dq_mn.front().y >= d)
{
int temp = ab(dq_mx.front().x - dq_mn.front().x);
if (ans == -1)
{
ans = temp;
}
else
ans = min(ans, temp);
temp = min(dq_mx.front().x, dq_mn.front().x);
while (!dq_mx.empty() && temp == dq_mx.front().x)
{
dq_mx.pop_front();
}
while (!dq_mn.empty() && temp == dq_mn.front().x)
{
dq_mn.pop_front();
}//队首检查,看需不需要把队首踢出去
}
}
cout << ans << "\n";
}
例题2:Problem - E - Codeforces
题目简介(翻译):输入给定四个正整数n,m,k,d。有一条河,河流宽度为m,河岸宽度为n,形成一个n*m的格子区域,每个格子有对应的水深(两岸水深为0)。
建一座桥,桥有立柱,要求相邻立柱之间的距离不能超过d,且最左和最右侧必须加立柱,加一个立柱的代价是a[i][j]+1(a[i][j]为水深),需要得到建桥的最小代价。
最后题目又套了一层,需要连续建k个桥(当然前面的问题能解决了后面的就很轻松了)
(第2行截面图,黑色表示桥的立柱)
(图来自codeforce)
思路:比如我们即将考虑到此行的第i个位置,dp[i]表示在i处建立柱,且只考虑i之前位置的最小值。那么我们需要和后d个数中最小的进行状态转移,也就是dp[i]=dp[j]+cost(代价)。如果说对后面每一个数都去考虑,则复杂度为O(md),其实就是O(m2)的复杂度,绝对会tle。
所以只需要单调队列去维护后d个数字的最小值就可以了。复杂度为O(n)
ac代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 998244353;
int n, m, k, d;
ll judge(vector<ll> &num) // 对每一行都判别,注意加引用,否则会无故增加时间
{
ll siz = num.size();
vector<ll> dp(siz, 0);
dp[0] = 1;
ll l = 0, r = 0;
deque<pair<ll, ll>> q; // 单调队列
q.push_back(pair<ll, ll>(0, 1)); // first表示坐标,second表示代价
for (ll i = 1; i < siz; i++)
{
r = i;
l = max(l, r - d - 1);
while (!q.empty() && q.front().first < l)
{
q.pop_front();
}
dp[r] = q.front().second + num[r];
while (!q.empty() && dp[r] < q.back().second)
{
q.pop_back();
}
q.push_back(pair<ll, ll>(r, dp[r]));
}
return dp[siz - 1];
}
void slv(void)
{
cin >> n >> m >> k >> d;
vector<vector<ll>> a(n, vector<ll>(m));
vector<ll> res;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
cin >> a[i][j];
a[i][j]++; // 因为要+1,先加上方便些
}
res.push_back(judge(a[i]));
}
ll ans = 0;
for (int i = 0; i < k; i++)
ans += res[i];
ll temp = ans;
for (int i = k; i < n; i++)
{
temp -= res[i - k];
temp += res[i];
ans = min(temp, ans);
}
cout << ans << "\n";
}
int main(void)
{
std::ios::sync_with_stdio(false);
cin.tie(NULL);
int t;
cin >> t;
while (t--)
{
slv();
}
}