浅谈单调队列优化DP

对于形如

fi=max(fLjR+wi)

的状态转移方程,也就是转移来自之前某个定长区间的最值,我们可以使用单调队列来维护区间最值,从而优化时间复杂度。

烽火传递

我们看到题目可以想到用 fi 表示考虑到 i 这个烽火台,点第 i 个的合法方案中的最小代价

那么可以想到,点了第 i 个烽火台,前面 [im,i1] 的区间内至少要点一个,然后我们的状态转移方程就是前面区间最小值加上自己所需的代价

边界 f0=0

答案 min(fnm+1n)

然后这一道题要求定长区间最小值,我们可以用单调队列优化

#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 里不选一个,使得不选的效率最小,然后用总效率减去不选的效率就可以得到答案

fi 表示考虑到 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 点加 oi 的油再消耗掉 di 的油所剩的油量,也就是 oidi

  1. 更新前缀和 si

  2. 从任意一点 i 出发,顺时针走一圈,我们要保证在过程中油量始终 0 ,也就是在 [i,i+n1] 中,对任意的 j(iji+n1) ,都要保证 sjsi10i 固定,找 sj 的最小值,也就是在区间内找 sj 最小值,然后与 si 比较

  3. 这里我们要进行反向遍历,从 n×21 (顺时针需要求出 后面 一段区间中的最值,只有从后往前做才能在处理到当前数的时候,把后面数的信息存下来),由于计算的时候会用到 i ,所以这里我们就先更新再求值。

逆时针

每个点 i 表示从 i 点加 oi 的油再消耗掉 di1 的油所剩的油量,也就是 oidi1 (因为是向后走)

更新 si 这里注意是用 si+si+1 后缀和,那么以 i 起点的后缀和为 sjsi+1

在任意情况下都有 sjsi+10

然后正向遍历维护 sj 的最小值

#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 道题目中至少要做一个

然后对于 fi 我们设为已经考虑到第 i 个,然后第 i 个做的合法方案中的最小值

转移就是从上一段区间的中找到最小值 fj 然后用它加上这个点要用的 wi 时间

fi=min(fj)+wi

边界 f0=0 第0题不需要时间

答案为我们得出来的值-1,因为,我们考虑的是每 m+1 题中至少做一个

二分边界:

m 的值越大,fi 就越小(不用做的题目多了),所以当 fit 时,要将 m 值缩小(让 fi 趋近于 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; }

琪露诺

这道题如果用这个点转移到区间内会很麻烦,所以我们考虑用区间转移到点的方式来做

fi 表示考虑到第 i 个数,选第 i 个数的合法方案中的最大收获

转移就是从上一段区间的中找到最小值 fj 然后用它加上这个点要用的 wi 价值

fi=max(fj)+wi

边界 fi=,f0=0

答案 max(fnr+1r)

那么这里提一下,这个从 nr+1 开始是因为这个区间内这些值是最后一个区间,去枚举所有值就行

接下来这道题的边界也有些麻烦

每一次放到队列里的值是 iL (因为是 [iR,iL] 的队列)

比较的值当然也是 iL

#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; }

__EOF__

本文作者ljfyyds
本文链接https://www.cnblogs.com/ljfyyds/p/17512283.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   ljfyyds  阅读(37)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示