浅谈单调队列优化DP
对于形如
的状态转移方程,也就是转移来自之前某个定长区间的最值,我们可以使用单调队列来维护区间最值,从而优化时间复杂度。
烽火传递
我们看到题目可以想到用 \(f_i\) 表示考虑到 \(i\) 这个烽火台,点第 \(i\) 个的合法方案中的最小代价
那么可以想到,点了第 \(i\) 个烽火台,前面 \([i-m,i-1]\) 的区间内至少要点一个,然后我们的状态转移方程就是前面区间最小值加上自己所需的代价
边界 \(f_0=0\)
答案 \(\min(f_{n-m+1 \to n})\)
然后这一道题要求定长区间最小值,我们可以用单调队列优化
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 10;
int n, m, w[N], f[N], q[N], ans = N;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ )
cin >> w[i];
int hh = 0, tt = 0;
for(int i = 1; i <= n; i ++ )
{
if(q[hh] < i - m) hh ++ ;//队头滑出
f[i] = f[q[hh]] + w[i];
while(hh <= tt && f[q[tt]] >= f[i]) tt -- ;//将比当前元素更差的扔出去
q[++ tt] = i;//添加元素
if(i > n - m) ans = min(ans, f[i]);//更新答案
}
cout << ans << endl;
return 0;
}
修剪草坪
这道题目有两种解法,一种是直接计算(未懂),另一种是一种转化的思路
题目让我们最多连续选 \(k\) 个使得效率最大,我们可以看成在 \(k+1\) 里不选一个,使得不选的效率最小,然后用总效率减去不选的效率就可以得到答案
\(f_i\) 表示考虑到 \(i\) 这个奶牛,不选第 \(i\) 个的的最小不选代价
然后同样从前 \(k + 1\) 个转移
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
LL w[N], q[N], h, t, n, k;
int main()
{
cin >> n >> k;
LL sum = 0;
for(int i = 1; i <= n; i ++ )
{
cin >> w[i];
sum += w[i];
}
for(int i = 1; i <= n; i ++ )
{
w[i] += w[q[h]];//这里放在前面是因为我们是k+1个不选一个,如果先把队头弹出答案
if(q[h] < i - k) h ++ ;
while(h <= t && w[q[t]] >= w[i]) t -- ;
q[++ t] = i;
}
cout << sum - w[q[h]];
return 0;
}
旅行问题
环形问题我们会想到化环为链 ,那么很容易想到将原数组复制两次,这样原问题就转化成了
在新数组中是否存在长度为 \(n + 1\) 的合法区间
接下来我们考虑顺时针的问题
顺时针
每个点 \(i\) 表示从 \(i\) 点加 \(o_i\) 的油再消耗掉 \(d_i\) 的油所剩的油量,也就是 \(o_i-d_i\)
-
更新前缀和 \(s_i\)
-
从任意一点 \(i\) 出发,顺时针走一圈,我们要保证在过程中油量始终 \(≥0\) ,也就是在 \([i,i+n-1]\) 中,对任意的 \(j(i≤j≤i+n-1)\) ,都要保证 \(s_j-s_{i-1}≥0\), \(i\) 固定,找 \(s_j\) 的最小值,也就是在区间内找 \(s_j\) 最小值,然后与 \(s_i\) 比较
-
这里我们要进行反向遍历,从 \(n\times2\) 到 \(1\) (顺时针需要求出 后面 一段区间中的最值,只有从后往前做才能在处理到当前数的时候,把后面数的信息存下来),由于计算的时候会用到 \(i\) ,所以这里我们就先更新再求值。
逆时针
每个点 \(i\) 表示从 \(i\) 点加 \(o_i\) 的油再消耗掉 \(d_{i-1}\) 的油所剩的油量,也就是 \(o_i-d_{i-1}\) (因为是向后走)
更新 \(s_i\) 这里注意是用 \(s_i+s_{i+1}\) 后缀和,那么以 \(i\) 起点的后缀和为 \(s_j-s_{i+1}\)
在任意情况下都有 \(s_j-s_{i+1} ≥ 0\)
然后正向遍历维护 \(s_j\) 的最小值
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2e6 + 10;
typedef long long LL;
int n;
LL s[N];//前缀和
int o[N], d[N];//油量,距离
int q[N];
bool ans[N];//每个点是否能走到
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++ )
cin >> o[i] >> d[i];
for(int i = 1; i <= n; i ++ )
s[i] = s[i + n] = o[i] - d[i];
for(int i =1; i <= 2 * n; i ++ )
s[i] += s[i - 1];
int h = 1, t = 0;
for(int i = 2 * n; i >= 1; i -- )
{
while(h <= t && s[q[t]] >= s[i]) t -- ;
q[++ t] = i;
if(q[h] > i + n - 1) h ++ ;
if(i <= n && s[q[h]] - s[i - 1] >= 0)
ans[i] = true;
}
d[0] = d[n];
for(int i = n; i; i -- )
s[i] = s[i + n] = o[i] - d[i - 1];
for(int i = 2 * n; i; i -- )
s[i] += s[i + 1];
h = 1, t = 0;
for(int i = 1; i <= 2 * n; i ++ )
{
while(h <= t && s[q[t]] >= s[i]) t -- ;
q[++ t] = i;
if(q[h] < i - n + 1) h ++ ;
if(i > n && s[q[h]] - s[i + 1] >= 0)
ans[i - n] = true;
}
for(int i = 1; i <= n; i ++ )
{
if(ans[i])
puts("TAK");
else
puts("NIE");
}
return 0;
}
绿色通道
这道题目我们要使得最大长度最小,所以考虑二分
我们设最大空题子段长度为 \(m\), 可以知道每 \(m +1\) 道题目中至少要做一个
然后对于 \(f_i\) 我们设为已经考虑到第 \(i\) 个,然后第 \(i\) 个做的合法方案中的最小值
转移就是从上一段区间的中找到最小值 \(f_j\) 然后用它加上这个点要用的 \(w_i\) 时间
边界 \(f_0=0\) 第0题不需要时间
答案为我们得出来的值-1,因为,我们考虑的是每 \(m+1\) 题中至少做一个
二分边界:
\(m\) 的值越大,\(f_i\) 就越小(不用做的题目多了),所以当 \(f_i≤t\) 时,要将 \(m\) 值缩小(让 \(f_i\) 趋近于 \(t\))
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 10;
int n, tim, w[N], f[N], q[N];
bool check(int m)
{
int h = 1, t = 0;
for (int i = 1; i <= n; i++)
{
while (h <= t && f[q[t]] >= f[i - 1])
t--;
q[++t] = i - 1;
if (q[h] < i - m)
h++;
f[i] = f[q[h]] + w[i];
if (i > n - m && f[i] <= tim)
return 1;
}
return 0;
}
int main()
{
cin >> n >> tim;
for (int i = 1; i <= n; i++)
cin >> w[i];
int l = -1, r = n + 1;
while (l + 1 < r)
{
int mid = (l + r) >> 1;
if (check(mid))
r = mid;
else
l = mid;
}
cout << r - 1;
return 0;
}
琪露诺
这道题如果用这个点转移到区间内会很麻烦,所以我们考虑用区间转移到点的方式来做
\(f_i\) 表示考虑到第 \(i\) 个数,选第 \(i\) 个数的合法方案中的最大收获
转移就是从上一段区间的中找到最小值 \(f_j\) 然后用它加上这个点要用的 \(w_i\) 价值
边界 \(f_i=-\infty,f_0=0\)
答案 \(\max(f_{n-r+1\to r})\)
那么这里提一下,这个从 \(n-r+1\) 开始是因为这个区间内这些值是最后一个区间,去枚举所有值就行
接下来这道题的边界也有些麻烦
每一次放到队列里的值是 \(i-L\) (因为是 \([i-R,i-L]\) 的队列)
比较的值当然也是 \(i-L\)
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10, INF = -0x3f3f3f3f;
int n, f[N], q[N], a[N], l, r;
int main()
{
cin >> n >> l >> r;
for (int i = 0; i <= n; i++)
cin >> a[i];
memset(f, -0x3f, sizeof(f));
f[0] = 0;
int h = 1, t = 0;
int ans = -2e9;
for (int i = l; i <= n; i++)
{
while (h <= t && f[q[t]] <= f[i - l])
t--;
q[++t] = i - l;
if (q[h] < i - r)
h++;
f[i] = f[q[h]] + a[i];
if (i > n - r)
ans = max(ans, f[i]);
}
cout << ans;
return 0;
}

浙公网安备 33010602011771号