单调队列优化DP
最大子序列和
来源: https://www.acwing.com/problem/content/137/
题目描述
输入一个长度为 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≤n,m≤300000 1≤n,m≤300000
输入样例:
6 4
1 -3 5 1 -2 3
输出样例:
7
思路分析
使用滑动窗口优化DP
该题让求长度不超过 m 的最大子序列和,我们知道一段子序列的和可以用前缀和来快速求解,如下图所示
表达式包含两个变量,双重循环时间复杂度为 n 2 n^2 n2 ,显然不可接受。
求区间和最大值的常用线性方法是滑动窗口,可以固定区间左端点或右端点,然后另一个端点通过滑动窗口从区间内求得,枚举固定端点即可遍历整个序列
做法1
我们可以固定右端点 j
,因为 j
在 i
的右边,因此枚举到 j
的时候,s[i-1]
的最值已经计算出,正序遍历即可
然后明确滑动窗口的一些关键点:
- 区间长度: 不超过
m
m
m , 即
[i-1, j-1]
- 滑动窗口边界:
队首下标 q[hh] < j - m
- 滑动窗口求最小值
- 边界情况:当枚举到第一个元素时,
i-1
为 0,应该提前加入到队列中
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 300010;
int a[N];
ll s[N];
ll q[N];
int n, m;
ll res = -2e9;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) scanf("%d", &a[i]), s[i] = s[i-1] + a[i];
int hh = 0, tt = -1;
// i 表示上面分析中的 j
for (int i = 0; i <= n; i ++)
{
if (hh <= tt && q[hh] < i - m) hh ++;
if (i) res = max(res, s[i] - s[q[hh]]); // 处理边界,第一次枚举时将 s[0] 加入队列中
while(hh <= tt && s[q[tt]] > s[i]) tt --;
q[++ tt] = i;
}
printf("%d\n", res);
return 0;
}
做法2
固定左端点 i
,因为 i
在 j
的右边,所以枚举 i-1
时,需要用到 s[j]
,但 s[j]
并未求出,因此倒序枚举就比较合适
然后明确滑动窗口的一些关键点:
- 区间长度: 不超过
m
m
m , 即
[i-1, j-1]
- 滑动窗口边界:
队首下标 q[hh] > j
- 滑动窗口求最小值
- 边界情况:实际枚举时
i
会从n-1
开始, 但此时j
没有被添加进队列,所以需要从n
开始,但这一步不计算答案,也可以在队列初始化时进行
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 300010;
int a[N];
ll s[N];
ll q[N];
int n, m;
ll res = -2e9;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) scanf("%d", &a[i]), s[i] = s[i-1] + a[i];
int hh = 0, tt = -1;
// 枚举时,用 i 表示上面分析的 i - 1
for (int i = n; i >= 0; i --)
{
if (hh <= tt && q[hh] > i + m) hh ++;
if (i < n) res = max(s[q[hh]] - s[i], res);
while(hh <= tt && s[q[tt]] < s[i]) tt --;
q[++ tt] = i;
}
printf("%d\n", res);
return 0;
}
旅行问题
来源: https://www.acwing.com/problem/content/description/1090/
题目描述
John 打算驾驶一辆汽车周游一个环形公路。
公路上总共有 n n n 个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。
John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。
在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。
任务:判断以每个车站为起点能否按条件成功周游一周。
输入格式
第一行是一个整数 n n n,表示环形公路上的车站数;
接下来 n n n 行,每行两个整数 pi,dipi,di,分别表示表示第 i i i 号车站的存油量和第 i i i 号车站到 顺时针方向 下一站的距离。
输出格式
输出共 n n n 行,如果从第 i i i 号车站出发,一直按顺时针(或逆时针)方向行驶,能够成功周游一圈,则在第 ii 行输出 TAK,否则输出 NIE。
数据范围
3
≤
n
≤
1
0
6
3≤n≤10^6
3≤n≤106,
0
≤
p
i
≤
2
×
1
0
9
0≤pi≤2×10^9
0≤pi≤2×109,
0
≤
d
i
≤
2
×
1
0
9
0≤di≤2×10^9
0≤di≤2×109
输入样例:
5
3 1
1 2
5 2
0 1
5 4
输出样例:
TAK
NIE
TAK
NIE
TAK
思路分析
通过分析题意,需要对进行拆换成链。
然后先考虑顺时针,从
1
1
1 号点出发,
1
1
1 号点是否到达
2
2
2 号点等价于 oil[1] - dist[1] >= 0
,到达
3
3
3 号点等价于 1 能到达 2 且 oil[1] + oil[2] - (dist[1] + dist[2])
,我们能够发现需要统计 oil[i]-dist[i]
的前缀和。复制两遍 oil[i]-dist[i]
,展成链状后求前缀和。依次类推,从1号点走一圈就等价于 s[1] >= s[0], s[2] >= s[0], ..., s[1+n]
等价于 min{s[j] - s[k]} >= 0
k = i-1, i, …, j-1
和上一题基本类似,但也稍有不同,固定值应选择 s[k]
,即枚举左端点,使用滑动窗口求得 s[j]
的最小值,此时可以选择倒序枚举。
为什么倒序呢?
按照上题的解释,正序枚举由于 j
在右边,在遍历
i
−
1
i-1
i−1 的时候,得不到右边区间内
j
j
j 的最小值,所以需要倒序遍历。但是对于本体而言,可以正序也可以倒序。但是此题中正序和倒序在本质上是相同的,因为多复制了一遍,正序遍历的前
n
n
n 个元素计算区间
j
j
j 的最值,后面在统计答案
为什么选择枚举 s[i-1]
呢?
因为如果选择枚举 s[j]
后,s[i-1]
需要通过滑动窗口求最大值,但是我们的区间定义的是从 i
开始到 j
,即起点是 i
, min{s[j] - s[k]} >= 0
k = i-1, i, …, j-1, 固定左端点没有意义,但是对于逆时针来说需要这样枚举,二者是正好相反的
考虑逆时针,先令 dist[n] = dist[0]
, 求前缀和或后缀和都可以,本质上和上一题基本一致,只不过需要反向考虑
以第二段为起点,向左延伸,即可得到反向的情况,适合枚举右端点,此时可采用正序遍历
做法一:
第一遍:
- 区间长度: 不超过
m
m
m , 即
[i-1, j-1]
- 滑动窗口边界:
队首下标 q[hh] < j - m
- 滑动窗口求最小值
- 边界情况:当枚举到第一个元素时,
i-1
为 0,应该提前加入到队列中
第二遍:
- 区间长度: 不超过
m
m
m , 即
[i-1, j-1]
- 滑动窗口边界:
队首下标 q[hh] > j
- 滑动窗口求最小值
- 边界情况:实际枚举时
i
会从n-1
开始, 但此时j
没有被添加进队列,所以需要从n
开始,但这一步不计算答案,也可以在队列初始化时进行
顺时针正序遍历:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10, M = 2 * N;
int n;
int w[N], o[N];
ll s[M];
ll q[M];
bool res[N];
int hh, tt;
int read() {
int t;
scanf("%d", &t);
return t;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++) w[i] = read(), o[i] = read(), s[i] = s[i+n] = w[i] - o[i];
for (int i = 1; i <= 2*n; i ++) s[i] += s[i-1];
hh = 0, tt = -1;
for (int i = 0; i <= 2*n; i ++) // 和倒序本质上是相同的
{
if (hh <= tt && q[hh] < i - n) hh ++;
while(hh <= tt && s[q[tt]] > s[i]) tt --;
q[++ tt] = i;
if (i > n)
{
if (s[q[hh]] >= s[i-n-1]) res[i-n] = 1;
}
}
o[0] = o[n];
for (int i = 1; i <= n; i ++) s[i] = s[i+n] = w[i] - o[i-1];
for (int i = 1; i <= 2*n; i ++) s[i] += s[i-1];
hh = 0, tt = -1;
for (int i = 0; i <= 2*n; i ++)
{
if (hh <= tt && q[hh] < i - n) hh ++;
if (i > n)
{
if (s[i] - s[q[hh]] >= 0) res[i-n] = true;
}
while(hh <= tt && s[q[hh]] < s[i]) tt --;
q[++ tt] = i;
}
for (int i = 1; i <= n; i ++)
if (res[i]) puts("TAK");
else puts("NIE");
return 0;
}
顺时针倒序遍历:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e6 + 10, M = 2 * N;
int n;
int w[N], o[N];
ll s[M];
ll q[M];
bool res[N];
int hh, tt;
int read() {
int t;
scanf("%d", &t);
return t;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++) w[i] = read(), o[i] = read(), s[i] = s[i+n] = w[i] - o[i];
for (int i = 1; i <= 2*n; i ++) s[i] += s[i-1];
hh = 0, tt = -1;
for (int i = 2*n; i >= 0; i --)
{
if (hh <= tt && q[hh] > i + n) hh ++;
if (i < n)
{
if (s[q[hh]] - s[i] >= 0) res[i+1] = true;
}
while(hh <= tt && s[q[tt]] > s[i]) tt --;
q[++ tt] = i;
}
o[0] = o[n];
for (int i = 1; i <= n; i ++) s[i] = s[i+n] = w[i] - o[i-1];
for (int i = 1; i <= 2*n; i ++) s[i] += s[i-1];
hh = 0, tt = -1;
for (int i = 0; i <= 2*n; i ++)
{
if (hh <= tt && q[hh] < i - n) hh ++;
if (i > n)
{
if (s[i] - s[q[hh]] >= 0) res[i-n] = true;
}
while(hh <= tt && s[q[hh]] < s[i]) tt --;
q[++ tt] = i;
}
for (int i = 1; i <= n; i ++)
if (res[i]) puts("TAK");
else puts("NIE");
return 0;
}
烽火传递
来源:https://www.acwing.com/problem/content/1091/
题目描述
烽火台是重要的军事防御设施,一般建在交通要道或险要处。
一旦有军情发生,则白天用浓烟,晚上有火光传递军情。
在某两个城市之间有 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≤m≤n≤2×10^5
1≤m≤n≤2×105,
0
≤
a
i
≤
1000
0≤ai≤1000
0≤ai≤1000
输入样例:
5 3
1 2 5 6 2
输出样例:
4
思路分析
典型的使用滑动窗口优化DP问题的案例
状态表示:f[i]
表示从
1
∼
n
1\sim n
1∼n 符合要求且第
i
i
i 个点燃的所有方案
属性:f[i]
存储的是
1
∼
n
1\sim n
1∼n 符合要求的所有方案中代价的最小值
集合划分:题目要求最长连续不点燃的区间长度必须小于
m
m
m 因此对于 f[i]
来说,考虑区间内的即可 [i-m+1, i-1]
,f[i] = w[i] + min{f[k]} (k=i-m+1, ..., i-1)
。f[i]
由区间内最小的 f[k]
转移过来。
答案:我们只需要在最后一个区间长度不超过 m-1
的区间内寻找最小值即可。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 2e5+10;
int a[N], f[N];
int n, m;
int q[N];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++)
{
if (hh <= tt && q[hh] < i - m) hh ++;
f[i] = f[q[hh]] + a[i];
while(hh <= tt && f[q[tt]] > f[i]) tt --;
q[++tt] = i;
}
int res = 2e9;
for (int i = n; i >= n-m+1; i --) res = min(res, f[i]);
printf("%d\n", res);
return 0;
}
修剪草坪
来源: https://www.acwing.com/problem/content/1089/
题目描述
在一年前赢得了小镇的最佳草坪比赛后,FJ 变得很懒,再也没有修剪过草坪。
现在,新一轮的最佳草坪比赛又开始了,FJ 希望能够再次夺冠。
然而,FJ 的草坪非常脏乱,因此,FJ 只能够让他的奶牛来完成这项工作。
FJ 有 N N N 只排成一排的奶牛,编号为 1 1 1 到 N N N。
每只奶牛的效率是不同的,奶牛 i i i 的效率为 E i E_i Ei。
编号相邻的奶牛们很熟悉,如果 FJ 安排超过 K K K 只编号连续的奶牛,那么这些奶牛就会罢工去开派对。
因此,现在 FJ 需要你的帮助,找到最合理的安排方案并计算 FJ 可以得到的最大效率。
注意,方案需满足不能包含超过 KK 只编号连续的奶牛。
输入格式
第一行:空格隔开的两个整数 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≤N≤10^5
1≤N≤105,
0
≤
E
i
≤
1
0
9
0≤E_i≤10^9
0≤Ei≤109
输入样例:
5 2
1
2
3
4
5
输出样例:
12
样例解释
FJ 有 5 只奶牛,效率分别为 1、2、3、4、5。
FJ 希望选取的奶牛效率总和最大,但是他不能选取超过 2 只连续的奶牛。
因此可以选择第三只以外的其他奶牛,总的效率为 1 + 2 + 4 + 5 = 12。
思路分析
状态表示
f[i]
表示考虑前 i
头牛的所有合法方案的最大效率
状态计算
对应集合划分,对第 i
头牛可以划分为选或不选
- 不选第
i
头牛,f[i] = f[i-1]
- 选第
i
头牛,依照前边选了多少连续的牛,设为j
,第i-j
头牛一定不选,以此划分成固定部分和变动部分:前i-j-1
头牛的划分情况是变动的,后面i-j+1
到i
的牛是必选的。f[i] = max{f[i-j-1] + s[i] - s[i-j]}
(1 <= j <= k)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
int n, m;
ll s[N];
int q[N];
ll f[N];
ll g(int i)
{
if (!i) return 0;
return f[i-1] - s[i];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++) scanf("%lld", &s[i]), s[i] += s[i-1];
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++)
{
if (hh <= tt && q[hh] < i-m) hh ++;
f[i] = max(f[i-1], g(q[hh]) + s[i]);
while(hh <= tt && g(q[tt]) < g(i)) -- tt;
q[++ tt] = i;
}
printf("%lld\n", f[n]);
return 0;
}
理想的正方形
来源: https://www.acwing.com/problem/content/1093/
思路分析
横着做一遍求区间内的最值,再竖着做一遍求区间内的最值,遍历最后的小区间即可得到答案
有点类似与神经网络中的池化操作,得到小尺寸的图片
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;
int w[N][N];
int n, m, k;
int q[N];
int row_max[N][N], row_min[N][N];
void get_max(int a[], int b[], int len)
{
int hh = 0, tt = -1;
for (int i = 1; i <= len; 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_min(int a[], int b[], int len)
{
int hh = 0, tt = -1;
for (int i = 1; i <= len; 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()
{
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
scanf("%d", &w[i][j]);
for (int i = 1; i <= n; i ++)
{
get_max(w[i], row_max[i], m);
get_min(w[i], row_min[i], m);
}
int a[N], b[N], c[N];
int res = 2e9;
for (int i = k; i <= m; i ++)
{
for (int j = 1; j <= n; j ++) a[j] = row_max[j][i];
get_max(a, b, n);
for (int j = 1; j <= n; j ++) a[j] = row_min[j][i];
get_min(a, c, n);
for (int j = k; j <= n; j ++)
res = min(res, b[j] - c[j]);
}
printf("%d\n", res);
return 0;
}