单调队列优化的DP
例题:135. 最大子序和
单调队列
1、需要求某一个区间的连续子段和,因此需要用到前缀和s[ ]
2、对于任意的 s[k] ,求以 k 结尾的区间长度不超过 m 的最大连续和,等价于求 s[k] - s[k-j],其中 1<= j <= m,因为 a[k] 是固定的,所以即要求 s[k-j]的最小值即可。(单调队列存的是下标)
import java.util.Scanner;
public class Main {
static int N = 300010;
static int[] s = new int[N];
static int hh = 0;
static int tt = -1;
static int[] q = new int[N];
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
for (int i = 1; i <= n; i++) {
s[i] = scan.nextInt();
s[i] += s[i - 1];
}
int res = -0x3f3f3f3f;
for (int i = 1; i <= n; i++) {
if (q[hh] < i - m) hh++;
res = Math.max(res, s[i] - s[q[hh]]);
while (hh <= tt && s[q[tt]] >= s[i]) tt--;
q[++tt] = i;
}
System.out.println(res);
}
}
【小根堆实现】
import java.util.PriorityQueue;
import java.util.Scanner;
public class Main {
static int N = 300010;
static PriorityQueue<Pair> q = new PriorityQueue<Pair>((x, y) -> x.val - y.val);
static int[] s = new int[N];
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
for (int i = 1; i <= n; i++) {
s[i] = scan.nextInt();
s[i] += s[i - 1];
}
int res = -0x3f3f3f3f;
for (int i = 1; i <= n; i++) {
int t = s[i] - s[i - 1];
while (!q.isEmpty() && i - m > q.peek().idx) q.poll();
if (!q.isEmpty()) t = Math.max(t, s[i] - q.peek().val);
res = Math.max(res, t);
q.add(new Pair(s[i], i));
}
System.out.println(res);
}
}
class Pair {
int val, idx;
Pair(int val, int idx) {
this.val = val;
this.idx = idx;
}
}
例题:旅行问题
【题目描述】
原题来自:POI 2004
John 打算驾驶一辆汽车周游一个环形公路。公路上总共有 n车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。
任务:判断以每个车站为起点能否按条件成功周游一周。
【输入】
第一行是一个整数 n,表示环形公路上的车站数;
接下来 n行,每行两个整数 pi,di ,分别表示表示第 i 号车站的存油量和第 i号车站到下一站的距离。
【输出】
输出共 n行,如果从第 i 号车站出发,一直按顺时针(或逆时针)方向行驶,能够成功周游一圈,则在第 i 行输出 TAK,否则输出 NIE。
【输入样例】
5
3 1
1 2
5 2
0 1
5 4
【输出样例】
TAK
NIE
TAK
NIE
TAK
【提示】
数据范围与提示:
对于全部数据,3≤n≤106,0≤pi≤2×109,0<di≤2×109。
————————————————————————————————————————————————————
【破环成链】
所有的环形问题都可以转为线性问题,将环拆成一条链,再复制一次加到链的后边,
比如环形的 5 个车站,编号 1—5,拆成链就是:
我们用数组存储每个车站的油量与距离的差值,这样判断从某个车站能否周游一圈就只需要判断从 1-----1 (也就是长度为 n 的区间内)的每个前缀和是否出现负数。也就是求前缀和数组,长度为 n 的区间最小值。
前缀和 s[ ]:从 1 出发
s[1] >= 0,说明可以到 2
s[2] < 0,说明不可以到 3
顺时针算一遍,将能够周游一圈的车站标记一下。
逆时针算一遍,将能够周游一圈的车站标记一下。
import java.util.Scanner;
public class Main {
static int N = (int) (2e6 + 10);//破环成链需要两倍空间
static int n;
static int[] d = new int[N];
static int[] o = new int[N];
static long[] s = new long[N];
static int[] q = new int[N];
static boolean[] st = new boolean[N];//车站i是否能周游一圈
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
for (int i = 1; i <= n; i++) {
o[i] = sc.nextInt();
d[i] = sc.nextInt();
}
//顺时针做
for (int i = 1; i <= n; i++) {
s[i] = o[i] - d[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;
for (int i = n * 2; i > 0; i--) {
if (hh <= tt && q[hh] >= i + n) hh++;//删掉队头
while (hh <= tt && s[q[tt]] >= s[i]) tt--;//包含s[i],先更新再求
q[++tt] = i;
if (i <= n) {
if (s[q[hh]] >= s[i - 1]) st[i] = true;
}
}
//逆时针做
d[0] = d[n];
for (int i = 1; i <= n; i++) {
s[i] = o[i] - d[i - 1];
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;
for (int i = 1; i <= n * 2; i++) {
if (hh <= tt && q[hh] < i - n) hh++;
if (i > n) {
if (s[q[hh]] <= s[i]) st[i - n] = true;
}
while (hh <= tt && s[q[tt]] <= s[i]) tt--;//不包含s[i],先求再更新
q[++tt] = i;
}
//输出答案
for (int i = 1; i <= n; i++) {
if (st[i]) {
System.out.println("TAK");
}else {
System.out.println("NIE");
}
}
}
}
例题:烽火传递
【题目描述】
原题来自:NOIP 2010 提高组初赛 · 完善程序
烽火台是重要的军事防御设施,一般建在交通要道或险要处。一旦有军情发生,则白天用浓烟,晚上有火光传递军情。
在某两个城市之间有 n座烽火台,每个烽火台发出信号都有一定的代价。为了使情报准确传递,在连续 m 个烽火台中至少要有一个发出信号。现在输入 n,m和每个烽火台的代价,请计算总共最少的代价在两城市之间来准确传递情报。
【输入】
第一行是 n,m,表示 n 个烽火台和连续烽火台数 m;
第二行 n个整数表示每个烽火台的代价 ai。
【输出】
输出仅一个整数,表示最小代价。
【输入样例】
5 3
1 2 5 6 2
【输出样例】
4
【提示】
样例说明
在第 2,5号烽火台上发信号。
数据范围与提示:
对于全部数据,1≤n,m≤2×105,1≤ai≤1000。
——————————————————————————————————————————————————
import java.util.Scanner;
public class Main {
static int N = 200010;
static int n;
static int m;
static int[] w = new int[N];
static int[] f = new int[N];
static int[] q = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
for (int i = 1; i <= n; i++) {
w[i] = sc.nextInt();
}
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 = Integer.MAX_VALUE >> 1;
for (int i = n - m + 1; i <= n; i++) {
res = Math.min(res, f[i]);
}
System.out.println(res);
}
}
例题:绿色通道
【题目描述】
高二数学《绿色通道》总共有 n道题目要抄,编号 1…n,抄第 i 题要花 ai 分钟。小 Y 决定只用不超过 t分钟抄这个,因此必然有空着的题。每道题要么不写,要么抄完,不能写一半。下标连续的一些空题称为一个空题段,它的长度就是所包含的题目数。这样应付自然会引起马老师的愤怒,最长的空题段越长,马老师越生气。
现在,小 Y 想知道他在这 t分钟内写哪些题,才能够尽量减轻马老师的怒火。由于小 Y 很聪明,你只要告诉他最长的空题段至少有多长就可以了,不需输出方案。
【输入】
第一行为两个整数 n,t。
第二行为 n个整数,依次为 a1,a2,…,an。
【输出】
输出一个整数,表示最长的空题段至少有多长。
【输入样例】
17 11
6 4 5 2 5 3 4 5 2 3 4 5 2 3 6 3 5
【输出样例】
3
【提示】
样例说明:
数据范围与提示:
对于 60% 数据,n≤2000;
对于所有数据,0<n≤5×104,0<ai≤3000,0<t≤108。
————————————————————————————————————————————————————
import java.util.Scanner;
public class Main {
static int N = 50010;
static int n;
static int m;
static int[] w = new int[N];
static int[] f = new int[N];
static int[] q = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
for (int i = 1; i <= n; i++) {
w[i] = sc.nextInt();
}
int l = 0, r = n;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
System.out.println(l);
}
private static boolean check(int limit) {
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++) {
if (q[hh] < i - limit - 1) hh++;
f[i] = f[q[hh]] + w[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;
}
}
例题:修剪草坪
【题目描述】
原题来自:USACO 2011 Open Gold
在一年前赢得了小镇的最佳草坪比赛后,FJ 变得很懒,再也没有修剪过草坪。现在,新一轮的最佳草坪比赛又开始了,FJ 希望能够再次夺冠。
然而,FJ 的草坪非常脏乱,因此,FJ 只能够让他的奶牛来完成这项工作。FJ 有 N只排成一排的奶牛,编号为 1 到 N。每只奶牛的效率是不同的,奶牛 i 的效率为 Ei。
靠近的奶牛们很熟悉,如果 FJ 安排超过 K只连续的奶牛,那么这些奶牛就会罢工去开派对。因此,现在 FJ 需要你的帮助,计算 FJ 可以得到的最大效率,并且该方案中没有连续的超过 K只奶牛。
【输入】
第一行:空格隔开的两个整数 N和 K;
第二到 N+1行:第 i+1 行有一个整数 Ei。
【输出】
一行一个值,表示 FJ 可以得到的最大的效率值。
【输入样例】
5 2
1
2
3
4
5
【输出样例】
12
【提示】
样例说明:
FJ 有 5只奶牛,他们的效率为 1,2,3,4,5。他们希望选取效率总和最大的奶牛,但是他不能选取超过 2 只连续的奶牛。FJ 选择出了第三只以外的其他奶牛,总的效率为 1+2+4+5=12。
数据范围与提示:
对于全部数据,1≤N≤105,0≤Ei≤109。
——————————————————————————————————————————————————
数组越界了…感觉和y总代码一样为什么他的C++代码没越界…
import java.util.Scanner;
public class Main {
static int N = (int) (1e5 + 10);
static int n;
static int m;
static long[] s = new long[N];
static long[] f = new long[N];
static int[] q = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
for (int i = 1; i <= n; i++) {
s[i] = sc.nextLong();
s[i] += s[i - 1];
}
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++) {
if (q[hh] < i - m) hh++;
f[i] = Math.max(f[i - 1], g(q[hh]) + s[i]);
while (hh <= tt && g(q[tt]) <= g(i)) tt--;
q[++tt] = i;
}
System.out.println(f[n]);
}
private static long g(int i) {
return f[i - 1] - s[i];
}
}
例题:理想的正方形
【题目描述】
原题来自:HAOI 2007
有一个 a×b的整数组成的矩阵,现请你从中找出一个 n×n的正方形区域,使得该区域所有数中的最大值和最小值的差最小。
【输入】
第一行为三个整数,分别表示 a,b,n的值;
第二行至第 a+1行每行为 b个非负整数,表示矩阵中相应位置上的数。
【输出】
输出仅一个整数,为 a×b矩阵中所有「n×n正方形区域中的最大整数和最小整数的差值」的最小值。
【输入样例】
5 4 2
1 2 5 6
0 17 16 0
16 17 2 1
2 10 2 1
1 2 2 2
【输出样例】
1
【提示】
数据范围与提示:
对于 20% 的数据 2≤a,b≤100,n≤10;
对于 100% 的数据 2≤a,b≤1000,n≤a,n≤b,n≤100,矩阵中的所有数都不超过 109。
————————————————————————————————————————————————————
有错…
import java.util.Scanner;
public class Main {
static int N = 1010;
static int n;
static int m;
static int k;
static int[][] w = new int[N][N];
static int[][] row_max = new int[N][N];
static int[][] row_min = new int[N][N];
static int[] q = new int[N];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
k = sc.nextInt();
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
w[i][j] = sc.nextInt();
}
}
for (int i = 1; i <= n; i++) {
get_min(w[i], row_min[i], m);
get_max(w[i], row_min[i], m);
}
int res = Integer.MAX_VALUE >> 1;
int[] a = new int[N];
int[] b = new int[N];
int[] c = new int[N];
for (int i = 1; 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 = Math.min(res, c[j] - b[j]);
}
}
System.out.println(res);
}
private static 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]];
}
}
private static 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]];
}
}
}