数据结构与算法学习笔记(Acwing 提高课)----动态规划·单调队列优化DP
数据结构与算法学习笔记----动态规划·单调队列优化DP
@@ author: 明月清了个风
@@ first publish time: 2025.6.27ps⭐️单调队列基本模型是滑动窗口中的最值问题,一般在窗口中是单调的。暴力的做法就是循环对应长度的区间进行检查,对其进行优化就是寻找这个窗口中的最优值,也就是排除一些一定不会被选中的值,将时间复杂度降为O(n),因为将窗口中的搜索降到了O(1),具体的可以复习基础课中的[单调队列](数据结构与算法学习笔记----单调队列_滑动窗口 单调 队列-CSDN博客)。
Acwing 135. 最大子序和
[原题链接](135. 最大子序和 - AcWing题库)
输入一个长度为 n n n的整数序列,从中找出一段长度不超过 m m m的连续子序列,使得子序列中所有数的和最大。
注意:子序列的长度至少为 1 1 1。
输入格式
第一行输入两个整数 n , m n,m n,m。
第二行输入 n n n个数,代表长度为 n n n的整数序列。
同一行数之间用空格隔开。
输出格式
输出一个整数,代表该序列的最大子序和。
数据范围
1 ≤ n , m ≤ 300000 1 \le n, m \le 300000 1≤n,m≤300000。
思路
因为是连续子序列,很容易想到用前缀和的思想进行计算。
这道题也符合在一个有限集合中求最值或者求个数的问题,因此可以使用动态规划进行解决。
对于一个连续子序列来说,可以枚举其结尾 k k k,那么这个问题就转化为计算 s [ k ] − s [ k − j ] s[k] - s[k - j] s[k]−s[k−j],其中 1 ≤ j ≤ m 1 \le j \le m 1≤j≤m,这样一个区间的前缀和的最值问题。那么当 k k k固定时,就相当于在前面长度为 m m m的区间内,找到一个最小的 s s s,这样就变成了一个区间最值问题。
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <limits.h>using namespace std;const int N = 300010;int n, m;
int a[N];
int s[N];
int q[N];
int res = INT_MIN;int main()
{cin >> n >> m;for(int i = 1; i <= n; i ++) cin >> a[i];for(int i = 1; i <= n; i ++) s[i] = s[i - 1] + a[i];int hh = 0, tt = 0;// 从前向后处理队列。// 队列里存的是数的下标,数都是向队尾加的,数的大小单调递增// 队尾的数总是最大的。// 这里遍历i相当于一边处理队列一边枚举结尾了for(int i = 1; i <= n; i ++){if(q[hh] < i - m) hh ++; // 判断是否已经超过m长度了。res = max(res, s[i] - s[q[hh]]);while(hh <= tt && s[q[tt]] >= s[i]) tt --; // 保持单调,弹出多余的数,也就是不可能用到的数。q[++ tt] = i;}cout << res << endl;return 0;
}
Acwing 1088. 旅行问题
[原题链接](1088. 旅行问题 - AcWing题库)
John打算驾驶一辆汽车周游一个环形公路。
公路上总共有 n n n个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。
John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。
在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。
任务:判断以每个车站为起点能否按条件成功周游一周。
输入格式
第一行是一个整数 n n n,表示环形公路上的车站数;
接下来 n n n行,每行两个整数 p i , d i p_i,d_i pi,di,分别表示第 i i i号车站的存油量和第 i i i号车站到顺时针方向下一站的距离。
输出格式
输出共 n n n行,如果从第 i i i号车站出发, 一直按顺时针(或逆时针)方向行驶,能够成功周游一圈,则在第 i i i行输出TAK,否则输出NIE。
数据范围
3 ≤ n ≤ 1 0 6 3 \le n \le 10^6 3≤n≤106,
0 ≤ p i ≤ 2 × 1 0 9 0 \le p_i \le 2 \times 10^9 0≤pi≤2×109,
0 ≤ d i ≤ 2 × 1 0 9 0 \le d_i \le 2 \times 10^9 0≤di≤2×109,
思路
首先对于环形问题,之前就在区间DP中说过可以转化为线性问题。那么可以将整个环形展开后,复制一份接在后面,那么就相当于任意选取一个点,向后行进 n n n个点是否能够成立。
对于每一个点来说会有两个值,这个车站的油量以及他到下一个车站的距离,因此可以在每个点上存这两个值的差值。那么判断是否能够完成一圈的等价问题就是判断从任意一点开始向后长度为 n n n的区间中,任意长度的前缀和是否都大于 0 0 0。那么只要最小值大于 0 0 0就可以了,因此转化为求最小值。
那么对于任意起点 i i i,其向后长度为 n n n的区间中的最小值点 j j j,应满足 s j − s i ≥ 0 s_j - s_i \ge 0 sj−si≥0。
(最后一个数据用cin
过不了,找了半天问题😢)。
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>using namespace std;typedef long long LL;const int N = 2e6 + 10;int n;
LL s[N];
int q[N];
int o[N], d[N];
bool st[N];int main()
{cin >> n;for(int i = 1; i <= n; i ++) scanf("%d%d", &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 <= n * 2; i ++) s[i] += s[i - 1];int hh = 0, tt = -1;// 顺时针// 起点固定,找到向后长度n以内最小的,因此从后向前枚举// 队列中存数的下标,相当于从后向前选择单调递增的序列,队头元素是最小且最远的。for(int i = n * 2; i >= 0; i --){//判断长度,起点i长度n的终点下标是i + n - 1,因为前缀和因此不能大于i + n;if(hh <= tt && q[hh] > i + n) hh ++;if(i <= n) // 起点在n以内才开始判断,前面都是处理队列{if(s[q[hh]] >= s[i]) st[i + 1] = true; // 注意含义}while(hh <= tt && s[q[tt]] >= s[i]) tt --; // 找后面最小的q[++ tt] = i;}// 逆时针,1号车站的下一站是n号车站。d[0] = d[n];for(int i = 1; i <= n; i ++) s[i] = s[i + n] = o[i] - d[i - 1];for(int i = 1; i <= n * 2; i ++) s[i] += s[i - 1];hh = 0, tt = -1;q[0] = 0;for(int i = 1; i <= n * 2; i ++){if(hh <= tt && q[hh] < i - n) hh ++;if(i > n){if(s[i] - s[q[hh]] >= 0) st[i - n] = true;}while(hh <= tt && s[q[tt]] <= s[i]) tt--;q[++ tt] = i;}for(int i = 1; i <= n; i++)if(st[i])puts("TAK");else puts("NIE");return 0;
}
Acwing 1089. 烽火传递
[原题链接](1089. 烽火传递 - AcWing题库)
烽火台是重要的军事防御设施,一般建在交通要道或险要处。
一旦有军情发生,则白天用浓烟,晚上有火光传递军情。
在某两个城市之间有 n n n座烽火台, 每个烽火台发出信号都有一定的代价。
为了使情报准确传递,在连续 m m m个烽火台中至少要有一个发出信号。
现在输入 n , m n,m n,m和每个烽火台的代价,请计算在两城市之间准确传递情报所需花费的总代价最少为多少。
输入格式
第一行输入两个整数 n , m n,m n,m,具体含义见题目描述;
第二行输入 n n n个数表示每个烽火台的代价 a i a_i ai。
同一行数之间用空格隔开。
输出格式
输出仅一个整数,表示最小代价。
数据范围
1 ≤ m ≤ n ≤ 2 × 1 0 5 1 \le m \le n \le 2 \times 10^5 1≤m≤n≤2×105,
0 ≤ a i ≤ 1000 0 \le a_i \le 1000 0≤ai≤1000
思路
在上面的题目中,其实DP的概念相对变弱了很多,因为都没有讨论到状态表示和状态转移的问题,在这题中,由于引入了额外的限制条件,也就是连续 m m m个烽火台中至少有一个被点亮,这意味着需要点亮哪一个需要做出选择,因此考虑使用DP数组来对一组状态集合进行表示。
使用 f [ i ] f[i] f[i]表示前 1 ∼ i 1 \sim i 1∼i个烽火台满足题意且第 i i i个烽火台被点亮的所有点法,要求的属性是最小值。
那么对于状态转移来说,根据上一个被点亮的烽火台是哪一个进行划分,根据题意可知,由于已知当前状态 f [ i ] f[i] f[i]中第 i i i个烽火台被点亮,那么上一个被点亮的烽火台的起始范围为 i − m ∼ i − 1 i - m \sim i - 1 i−m∼i−1,那么状态转移方程就是 f [ i ] = m i n ( f [ j ] ) + w [ i ] f[i] = min(f[j]) + w[i] f[i]=min(f[j])+w[i]。这里的 m i n min min就是在一个长度固定的区间里求最小值的问题。
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>using namespace std;const int N = 2e5 + 10;int n, m;
int q[N];
int w[N];
int f[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;}int res = 1e9;for(int i = n - m + 1; i <= n; i ++ ) res = min(res, f[i]);cout << res << endl;return 0;
}
Acwing 1090. 绿色通道
[原题链接](1090. 绿色通道 - AcWing题库)
高二数学《绿色通道》总共有 n n n道题目要抄,编号 1 , 2 , ⋯ , n 1,2,\cdots, n 1,2,⋯,n,抄第 i i i题要花 a i a_i ai分钟。
小Y决定只用不超过 t t t分钟抄这个,因此必然有空着的题。
每道题要么不写,要么抄完,不能写一半。
下标连续的一些空题称为一个空题段,它的长度就是所包含的题目数。
这样应付自然会引起马老师的愤怒,最长的空题段越长,马老师越生气。
现在,小Y想知道他在这 t t t分钟内写哪些题目才能尽量减轻马老师的怒火。
由于小Y很聪明,你只需要告诉他最长的空题段至少有多长就可以了,不需输出方案。
输入格式
第一行为两个整数 n , t n,t n,t。
第二行为 n n n个整数,依次为 a 1 , a 2 , ⋯ , a n a_1,a_2,\cdots,a_n a1,a2,⋯,an。
输出格式
输出一个整数,表示最长的空题段至少有多长。
数据范围
0 < n ≤ 5 × 1 0 4 0 < n \le 5 \times 10^4 0<n≤5×104,
0 < a i ≤ 3000 0 < a_i \le 3000 0<ai≤3000,
0 < t ≤ 1 0 8 0 < t \le 10^8 0<t≤108,
思路
这道题与上一题很相似,在限制条件 t t t分钟内选择合适的题目使剩下的题目的最长空段最小,但是于这一题的最值和上一题的最值有所不同,这意味着我们不同的选择会导致最长的空题段不同,由于总时间 t t t的限制,这个值会有一个最小值,考虑使用二分来寻找这个值。
在之前已经讲过,使用二分的情景在于它的性质能够二分,而不仅仅是数值上的大小,也就是一半能够满足性质,而另一半不能满足性质,那就可以二分。那么在这一题中,比答案更短的空题段一定不能满足时间 t t t的限制,而比答案更长的空题段一定能够满足时间 t t t的限制。
那么二分的判断过程其实就和上一题基本一致了,对于每一个二分的值 l i m i t limit limit,每一题可以看成一个烽火台,要求的就是在连续 l i m i t limit limit的题目中必须做一题,最少的时间是多少,如果这个时间小于 t t t,那么 l i m i t limit limit成立,反之不成立。
在check
函数中最后的判断逻辑注意是只要有一个能够满足当前limit
就是true
。
代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>using namespace std;const int N = 5e4 + 10;int n, m;
int a[N];
int f[N];
int q[N];bool check(int limit)
{int hh = 0, tt = 0;for(int i = 1; i <= n; i ++){if(q[hh] < i - limit - 1) hh ++; // 边界与上一题不同,空段长limit,意味着两端的题目做了,因此可以到i - limit - 1。f[i] = f[q[hh]] + a[i];while(hh <= tt && f[q[tt]] >= f[i]) tt--;q[++ tt] = i;}for(int i = n - limit; i <= n; i ++)if(f[i] <= m) return true;return false;
}int main()
{cin >> n >> m;for(int i = 1; i <= n; i ++) cin >> a[i];int l = 0, r = n;while(l < r){int mid = l + r >> 1;if(check(mid)) r = mid;else l = mid + 1;}cout << l << endl;return 0;
}
Acwing 1087. 修剪草坪
[原题链接](1087. 修剪草坪 - AcWing题库)
在一年前赢得了小镇的最佳草坪比赛后,FJ变得很懒,再也灭有修剪过草坪。
现在,新一轮的最佳草坪比赛又开始了,FJ 希望能够再次夺冠。
然而,FJ 的草坪非常脏乱,因此,FJ 只能够让他的奶牛来完成这项工作。
FJ有 N N N只排成一排的奶牛,编号为 1 ∼ N 1 \sim N 1∼N。
每只奶牛的效率是不同的,奶牛 i i i的效率为 E i E_i Ei。
编号相邻的奶牛们很熟悉,如果FJ安排超过 K K K只编号连续的奶牛,那么这些奶牛就会罢工去开派对。
因此,现在 FJ 需要你的帮助,找到最合理的安排方案并计算 FJ 可以得到的最大效率。
注意,方案需满足不能包含超过 K K K只连续编号的奶牛。
输入格式
第一行:空格隔开的两个整数 N N N和 K K K;
第二到 N + 1 N + 1 N+1行:第 i + 1 i + 1 i+1行有一个整数 E i E_i Ei。
输出格式
共一行,包含一个数值,表示 FJ 可以得到的最大的效率值。
数据范围
1 ≤ N ≤ 1 0 5 1 \le N \le 10^5 1≤N≤105,
0 ≤ E i ≤ 1 0 9 0 \le E_i \le 10^9 0≤Ei≤109,
思路
这题可以用典型的DP来做。
使用f[i]
表示从前 i i i头奶牛中选满足题意的方案,那么状态划分就是根据第 i i i头奶牛是否被选择了进行划分,如果第 i i i头奶牛没有被选择了,那么 f [ i ] = f [ i − 1 ] f[i] = f[i - 1] f[i]=f[i−1];如果第 i i i头奶牛被选择了,那么就要根据最后这段包含 i i i的长度是多少进行划分了,最后一段最长是 K K K,那么就要加上 E j ∼ E i E_j \sim E_i Ej∼Ei这段的效率, j j j的取值最小是 i − K + 1 i - K + 1 i−K+1,这段的效率可以使用前缀和来计算,状态转移方程就是 f [ i ] = f [ i − j − 1 ] + s i − s i − j f[i] = f[i - j - 1] + s_i - s_{i - j} f[i]=f[i−j−1]+si−si−j。
因为我们要求的是最大值,在上面的状态转移方程中 s i s_i si是固定的,因此只要 f [ i − j − 1 ] − s i − j f[i - j - 1] - s_{i - j} f[i−j−1]−si−j最大即可,而这个值的区间长度是 K K K,进而转化为滑动窗口求最值问题。可以将这个值即为 g [ j ] g[j] g[j],然后需要考虑边界问题—— g [ 0 ] = f [ − 1 ] − s 0 g[0] = f[-1] - s_0 g[0]=f[−1]−s0的含义,这个值出现意味着 i = = j i == j i==j,那就是对于以 i i i为结尾的这一段包含了 1 ∼ i 1 \sim i 1∼i中所有的奶牛,因此这里的 f [ − 1 ] f[-1] f[−1]可以看成是 f [ 0 ] = 0 f[0] = 0 f[0]=0。
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>using namespace std;typedef long long LL;const int N = 1e5 + 10;int n, m;
LL s[N];
LL f[N];
int q[N];LL g(int i)
{return f[i - 1] - s[i];
}int main()
{cin >> n >> m;for(int i = 1; i <= n; i ++){cin >> s[i];s[i] += s[i - 1];}int hh = 0, tt = 0;for(int i = 1; i <= n; i ++){if(q[hh] < i - m) hh ++; // 最长是m,由于要前缀和,因此就是i - m + 1的前一个i - m。f[i] = max(f[i - 1], g(q[hh]) + s[i]);while(hh <= tt && g(q[tt]) <= g(i)) tt --;q[++ tt] = i;}cout << f[n] << endl;return 0;
}
Acwing 1091. 理想的正方形
[原题链接](1091. 理想的正方形 - AcWing题库)
有一个 a × b a \times b a×b的整数组成的矩阵,现请你从中找出一个 n × n n \times n n×n的正方形区域,使得该区域所有数中的最大值和最小值的差最小
输入格式
第一行为三个整数,分别表示 a , b , n a,b,n a,b,n的值;
第二行至第 a + 1 a + 1 a+1行每行为 b b b个非负整数,表示矩阵中相应位置上的数
输出格式
输出仅一个整数,为 a × b a \times b a×b矩阵中所有 n × n n \times n n×n正方形区域中的最大整数和最小整数的差值的最小值。
数据范围
2 ≤ a , b ≤ 1000 2 \le a,b \le 1000 2≤a,b≤1000,
n ≤ a , n ≤ b , n ≤ 100 n \le a, n \le b, n \le 100 n≤a,n≤b,n≤100。
思路
这一题将整个窗口变成了二维的,可以考虑将问题重新转化为一维的。
先处理出所有一维长度为 n n n的区间的最大值,将每一个区间的最大值都存在区间右端点处,然后对于正方形来说,其 n n n行的最大值都被存在了最后一列上,对这一列进行一次滑动窗口的处理即可。
用相同的方法处理一遍最小值即可。
代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>using namespace std;const int N = 1010, INF = 1e9;int n, m, k;
int w[N][N];
int row_min[N][N], row_max[N][N];
int q[N];void get_min(int a[], int b[], int tot)
{int hh = 0, tt = -1;for (int i = 1; i <= tot; i ++ ){if (hh <= tt && q[hh] <= i - k) hh ++ ;while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;q[ ++ tt] = i;b[i] = a[q[hh]];}
}void get_max(int a[], int b[], int tot)
{int hh = 0, tt = -1;for (int i = 1; i <= tot; i ++ ){if (hh <= tt && q[hh] <= i - k) hh ++ ;while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;q[ ++ tt] = i;b[i] = a[q[hh]];}
}int main()
{cin >> n >> m >> k;for (int i = 1; i <= n; i ++ )for (int j = 1; j <= m; j ++ )scin >> w[i][j];for (int i = 1; i <= n; i ++ ){get_min(w[i], row_min[i], m);get_max(w[i], row_max[i], m);}int res = INF;int a[N], b[N], c[N];for (int i = k; i <= m; i ++ ){for(int j = 1; j <= n; j ++ ) a[j] = row_min[j][i];get_min(a, b, n);for (int j = 1; j <= n; j ++ ) a[j] = row_max[j][i];get_max(a, c, n);for (int j = k; j <= n; j ++ ) res = min(res, c[j] - b[j]);}printf("%d\n", res);return 0;
}