当前位置:   article > 正文

浅析单调队列优化

单调队列优化

什么是单调队列

模板题

先来个模板题压压惊(bushi)。

滑动窗口 /【模板】单调队列 - 洛谷

单调队列解释

单调队列分为“单调”和“队列”

  1. 单调:元素有序(递增or递减)
  2. 队列:可以在队头和队尾对队列进行维护(听起来很像双端队列)

思路

以上面模板题中的最大值为例,分析一下单调队列的维护思路:

本题中队列维护的是某区间的最大值。

区间意味着有头有尾,尾即目前遍历到的下标i,头可以用长度来计算:max(1,i-len)。跟不上区间移动的,即下标在目前有效区间头之前的,踢出队列。

最大值的处理则比较有趣了:出现在a[i]之前比a[i]小的数一定不可能是区间max了(因为它与a[i]共处同一区间且a[i]更大),踢出去;但出现在a[i]之后比a[i]小的数有可能是区间max(a[i]是更先出队的),得先放在队列里待后续评估。

为实现元素的唯一标识与区间长度的记录,队列中维护的是数组中元素的序号(下标+1)

实现

实现思路

不满足区间范围的,是从队头pop出去;

出现在a[i]之前比a[i]小的,从队尾pop出去;

根据前面的分析,无论如何a[i]都是要入队看看的。

细节:注意队列的初始化and队列的非空判定

实现手段

STL——deque

贴模板题代码

  1. #include <iostream>
  2. #include <deque>
  3. using namespace std;
  4. #define maxn 1000005
  5. deque<int> sq, bq;
  6. int n, k;
  7. int a[maxn];
  8. int main() {
  9. cin >> n >> k;
  10. for (int i = 1; i <= n; i++) {
  11. cin >> a[i];
  12. }
  13. sq.push_back(0);
  14. for (int i = 1; i <= n; i++) {
  15. while(!sq.empty() && sq.front() <= i - k){
  16. sq.pop_front();
  17. }
  18. while (!sq.empty() && a[i] <= a[sq.back()]) {
  19. //cout << "pop" << sq.back() << endl;
  20. sq.pop_back();
  21. }
  22. sq.push_back(i);
  23. //cout << "push" << i << endl;
  24. if (i >= k) {
  25. cout << a[sq.front()] << " ";
  26. }
  27. }
  28. cout << endl;
  29. bq.push_back(0);
  30. for (int i = 1; i <= n; i++) {
  31. while (!bq.empty() && a[i] >= a[bq.back()]) {
  32. //cout << "pop" << bq.back() << endl;
  33. bq.pop_back();
  34. }
  35. bq.push_back(i);
  36. //cout << "push" << i << endl;
  37. if (i >= k) {
  38. if (bq.front() <= i - k) {
  39. //cout << "pop" << bq.front() << endl;
  40. bq.pop_front();
  41. }
  42. cout << a[bq.front()] << " ";
  43. }
  44. }
  45. cout << endl;
  46. return 0;
  47. }
手搓数组
  1. #include <iostream>
  2. using namespace std;
  3. #define maxn 1000005
  4. int a[maxn];
  5. int q[maxn];
  6. int q2[maxn];
  7. int main() {
  8. int n, m;
  9. cin >> n >> m;
  10. for (int i = 1; i <= n; i++) {
  11. cin >> a[i];
  12. }
  13. int head = 0, tail = -1;
  14. q[0] = 0;
  15. for (int i = 1; i <= n; i++) {
  16. while (head <= tail && q[head] <= i - m)
  17. head++;//已经不在范围内要出队
  18. while (head <= tail && a[i] <= a[q[tail]])
  19. tail--;//维护单调递增的队列
  20. q[++tail] = i;
  21. if (i >= m)
  22. cout << a[q[head]] << " ";
  23. }
  24. cout << "\n";
  25. head = 0, tail = -1;
  26. q2[0] = 0;
  27. for (int i = 1; i <= n; i++) {
  28. while (head <= tail && q2[head] <= i - m)
  29. head++;//已经不在范围内要出队
  30. while (head <= tail && a[i] >= a[q2[tail]])
  31. tail--;//维护单调递增的队列
  32. q2[++tail] = i;
  33. if (i >= m)
  34. cout << a[q2[head]] << " ";
  35. }
  36. cout << "\n";
  37. return 0;
  38. }

单调队列可以优化啥东西

求极值但可以转化成区间最大/最小元素

题目1(同维度)

切蛋糕 - 洛谷

思路(基础+优化)

抽象一下这个题,其实是在求:不定长度且长度有大小限制(1<=len<=maxL)的最大子段和


插播一段关于“前缀和”:可以简单理解成数列前n项的和,不妨记为sum数组。那么子序列闭区间[i,j]的和则为 sum[j]-sum[i-1] 

可参见题目:最大子段和 - 洛谷

 插播结束。


即使子段和我们采用前缀和来计算,仍需一个二重循环里面遍历j的取值。

进一步地我们注意到,对于每个i来说sum[i]都是固定的,于是我们可以做如下图的数学推导

至此,我们已把问题转化为找到sum数组指定区间中的最小值,单调队列可以闪亮登场了。

代码及注意事项说明

本题是不允许子段的长度为0的,应注意出队的边界。

  1. #include <cstdio>
  2. #include <algorithm>
  3. #include <queue>
  4. using namespace std;
  5. #define maxn 500005
  6. int a[maxn];
  7. int sum[maxn];
  8. int ans = -1 << 30;
  9. deque<int>sq;
  10. int main() {
  11. int n, m;
  12. scanf("%d %d", &n, &m);
  13. for (int i = 1; i <= n; i++) {
  14. scanf("%d", &a[i]);
  15. }
  16. for (int i = 1 ; i <= n; i++) {
  17. sum[i] = sum[i - 1] + a[i];
  18. }
  19. sq.push_back(0);
  20. for (int i = 1; i <= n; i++) {
  21. while (!sq.empty() && sq.front() < i - m) {
  22. sq.pop_front();
  23. }
  24. ans = max(ans, sum[i] - sum[sq.front()]);
  25. while (!sq.empty() && sum[i] <= sum[sq.back()]) {
  26. //cout << "pop" << sq.back() << endl;
  27. sq.pop_back();
  28. }
  29. sq.push_back(i);
  30. }
  31. printf("%d\n", ans);
  32. return 0;
  33. }

题目2(跨维度)

Watching Fireworks is Fun - 洛谷

思路(基础+优化)

f_{i,j}表示在放第i个烟花时,所处位置为j所能获得的最大值。

根据题意可以列出第一行的式子,但注意到b_i是常数可以提出去,于是可以得到第二行的式子。

对于固定的i和j来说,a_i和j都是常量,可以提出去,于是可以得到第三行的表达式。

根据第三行的式子不难看出,我们需要维护一个关于f_{i-1,k}的优先队列,来辅助更快地计算出f_{i,j}

代码及注意事项说明

由于计算第i维时,只需要用到第i-1维的数据,所以我们的dp只需要两行就够了,轮流当第i-1维和第i维。

又由于行数分别为0和1,可以通过异或的操作来互换。

  1. #include <iostream>
  2. #include <algorithm>
  3. #include <cmath>
  4. #include <deque>
  5. using namespace std;
  6. #define maxn 150005
  7. #define maxm 305
  8. typedef long long ll;
  9. ll a[maxn], b[maxn], t[maxn];
  10. //ll dp[maxm][maxn];
  11. ll dp[2][maxn];int fl=1;
  12. int q[maxn];
  13. ll ans = -1 << 30;
  14. int main() {
  15. int n, m, d;
  16. cin >> n >> m >> d;
  17. for (int i = 1; i <= m; i++) {
  18. cin >> a[i] >> b[i] >> t[i];
  19. }
  20. for (int i = 1; i <= m; i++) {
  21. int l=1,r=0,k=1;
  22. for (int j = 1; j <= n; j++) {
  23. for (; k <= min(n*1ll, j + (t[i] - t[i - 1])*d); k++) {
  24. while(l<=r && dp[fl^1][q[r]]<=dp[fl^1][k]) r--;
  25. q[++r]=k;
  26. }
  27. while(l<=r&&q[l]<max(1ll,j-(t[i]-t[i-1])*d)) l++;
  28. dp[fl][j]=dp[fl^1][q[l]]-abs(a[i]-j)+b[i];
  29. //dp[i][j] = max(dp[i - 1][k] - abs(a[i] - j) + b[i], dp[i][j]);
  30. }
  31. fl^=1;
  32. }
  33. for(int j = 1;j<=n;j++) ans=max(ans,dp[fl^1][j]);
  34. cout << ans << "\n";
  35. return 0;
  36. }

参考资料:

OI-wiki

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/993878
推荐阅读
相关标签
  

闽ICP备14008679号