第一章动态规划(十三)

单调队列优化的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]];
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值