单调队列优化dp

输入一个长度为 n 的整数序列,从中找出一段长度不超过 m 的连续子序列,使得子序列中所有数的和最大。

注意: 子序列的长度至少是 1。

输入格式
第一行输入两个整数 n,m。

第二行输入 n 个数,代表长度为 n 的整数序列。

同一行数之间用空格隔开。

输出格式
输出一个整数,代表该序列的最大子序和。

数据范围
1≤n,m≤300000
输入样例:
6 4
1 -3 5 1 -2 3
输出样例:
7

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 300010, INF = 1e9;

int n, m;
int s[N];
int q[N];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]), s[i] += s[i - 1];

    int res = -INF;
    int hh = 0, tt = 0;

    for (int i = 1; i <= n; i ++ )
    {
        if (q[hh] < i - m) hh ++ ;
        res = max(res, s[i] - s[q[hh]]);
        while (hh <= tt && s[q[tt]] >= s[i]) tt -- ;
        q[ ++ tt] = i;
    }

    printf("%d\n", res);

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/128103/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在这里插入图片描述


John 打算驾驶一辆汽车周游一个环形公路。

公路上总共有 n 个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。

John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。

在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。

任务:判断以每个车站为起点能否按条件成功周游一周。

输入格式
第一行是一个整数 n,表示环形公路上的车站数;

接下来 n 行,每行两个整数 pi,di,分别表示表示第 i 号车站的存油量和第 i 号车站到 顺时针方向 下一站的距离。

输出格式
输出共 n 行,如果从第 i 号车站出发,一直按顺时针(或逆时针)方向行驶,能够成功周游一圈,则在第 i 行输出 TAK,否则输出 NIE。

数据范围
3≤n≤106,
0≤pi≤2×109,
0≤di≤2×109
输入样例:
5
3 1
1 2
5 2
0 1
5 4
输出样例:
TAK
NIE
TAK
NIE
TAK
难度:中等
时/空限制:1s / 64MB
总通过数:1380
总尝试数:3305
来源:《信息学奥赛一本通》 , POI2004
算法标签

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 2e6 + 10;

int n;
int oil[N], dist[N];
LL s[N];
int q[N];
bool ans[N];

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
    {
        scanf("%d%d", &oil[i], &dist[i]);
        s[i] = s[i + n] = oil[i] - dist[i];
    }
    for (int i = 1; i <= n * 2; i ++ ) s[i] += s[i - 1];

    int hh = 0, tt = 0;
    q[0] = n * 2 + 1;
    for (int i = n * 2; i >= 0; i -- )
    {
        if (q[hh] > i + n) hh ++ ;
        if (i < n)
        {
            if (s[i] <= s[q[hh]]) ans[i + 1] = true;
        }
        while (hh <= tt && s[q[tt]] >= s[i]) tt -- ;
        q[ ++ tt] = i;
    }

    dist[0] = dist[n];
    for (int i = 1; i <= n; i ++ ) s[i] = s[i + n] = oil[i] - dist[i - 1];
    for (int i = 1; i <= n * 2; i ++ ) s[i] += s[i - 1];

    hh = 0, tt = 0;
    q[0] = 0;
    for (int i = 1; i <= n * 2; i ++ )
    {
        if (q[hh] < i - n) hh ++ ;
        if (i > n)
        {
            if (s[i] >= s[q[hh]]) ans[i - n] = true;
        }
        while (hh <= tt && s[q[tt]] <= s[i]) tt -- ;
        q[ ++ tt] = i;
    }

    for (int i = 1; i <= n; i ++ )
        if (ans[i]) puts("TAK");
        else puts("NIE");

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/128104/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在一年前赢得了小镇的最佳草坪比赛后,FJ 变得很懒,再也没有修剪过草坪。

现在,新一轮的最佳草坪比赛又开始了,FJ 希望能够再次夺冠。

然而,FJ 的草坪非常脏乱,因此,FJ 只能够让他的奶牛来完成这项工作。

FJ 有 N 只排成一排的奶牛,编号为 1 到 N。

每只奶牛的效率是不同的,奶牛 i 的效率为 Ei。

编号相邻的奶牛们很熟悉,如果 FJ 安排超过 K 只编号连续的奶牛,那么这些奶牛就会罢工去开派对。

因此,现在 FJ 需要你的帮助,找到最合理的安排方案并计算 FJ 可以得到的最大效率。

注意,方案需满足不能包含超过 K 只编号连续的奶牛。

输入格式
第一行:空格隔开的两个整数 N 和 K;

第二到 N+1 行:第 i+1 行有一个整数 Ei。

输出格式
共一行,包含一个数值,表示 FJ 可以得到的最大的效率值。

数据范围
1≤N≤105,
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。

#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
LL w[N];
int q[N];
int head, tail, n, k;

int main()
{
    cin >> n >> k;
    LL sum = 0;
    for (int i = 1; i <= n; ++i) 
    {
        cin >> w[i]; 
        sum += w[i];
    }

    head = tail = 0;
    for (int i = 1; i <= n; ++i)
    {
        w[i] += w[q[head]];
        if (q[head] < i - k) ++head;
        while (head <= tail && w[i] <= w[q[tail]]) --tail;
        q[++tail] = i;
    }

    cout << sum - w[q[head]] << endl;
    return 0;
}

作者:羽笙
链接:https://www.acwing.com/solution/content/5362/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

高二数学《绿色通道》总共有 n 道题目要抄,编号 1,2,…,n,抄第 i 题要花 ai 分钟。

小 Y 决定只用不超过 t 分钟抄这个,因此必然有空着的题。

每道题要么不写,要么抄完,不能写一半。

下标连续的一些空题称为一个空题段,它的长度就是所包含的题目数。

这样应付自然会引起马老师的愤怒,最长的空题段越长,马老师越生气。

现在,小 Y 想知道他在这 t 分钟内写哪些题,才能够尽量减轻马老师的怒火。

由于小 Y 很聪明,你只要告诉他最长的空题段至少有多长就可以了,不需输出方案。

输入格式
第一行为两个整数 n,t。

第二行为 n 个整数,依次为 a1,a2,…,an。

输出格式
输出一个整数,表示最长的空题段至少有多长。

数据范围
0<n≤5×104,
0<ai≤3000,
0<t≤108
输入样例:
17 11
6 4 5 2 5 3 4 5 2 3 4 5 2 3 6 3 5
输出样例:
3
二分+单调队列dp

#include<bits/stdc++.h>
using namespace std;
int n,t;
int a[50005];int f[50005];
int x[50005];int y[50005];
bool kkk(int m)
{
    memset(f,0,sizeof(f));//一定要初始化
    int l=0,r=1;//为什么 l=0 r=1  相当于直接初始化了一个特殊0点   区间是[l,r) (包含l,不包含r)
    for(int i=1;i<=n;i++)
    {
        while(r>l&&i-x[l]>m+1) l++;//维护单调队列
            f[i]=y[l]+a[i];
        while(r>l&&y[r-1]>=f[i]) r--;//维护单调队列
        x[r]=i;y[r++]=f[i];//添加
    }
    int MIN=100000001;
    for(int i=n;i>=n-m;i--)
        MIN=min(MIN,f[i]);//在最后的几个中找到最小值
    return MIN<=t; //最小值与 t 比较,返回
}
int main()
{
    int ans=100000001;
    cin>>n>>t;
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    int l=0,r=n,mid;
    while(l<=r)//二分答案
    {
        mid=(l+r)/2;
        if(kkk(mid))
        {
            ans=min(ans,mid);
            r=mid-1;
        }
        else
            l=mid+1;
    }
    cout<<ans;
    return 0;//再见了您嘞
}

作者:ZTEG
链接:https://www.acwing.com/solution/content/5854/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

有一个 a×b 的整数组成的矩阵,现请你从中找出一个 n×n 的正方形区域,使得该区域所有数中的最大值和最小值的差最小。

输入格式
第一行为三个整数,分别表示 a,b,n 的值;

第二行至第 a+1 行每行为 b 个非负整数,表示矩阵中相应位置上的数。

输出格式
输出仅一个整数,为 a×b 矩阵中所有“n×n 正方形区域中的最大整数和最小整数的差值”的最小值。

数据范围
2≤a,b≤1000,
n≤a,n≤b,n≤100,
矩阵中的所有数都不超过 109。

输入样例:
5 4 2
1 2 5 6
0 17 16 0
16 17 2 1
2 10 2 1
1 2 2 2
输出样例:
1

题目描述
有一个 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
题目分析
这道题一看,发现很像滑动窗口,只不过变成了二维的.
所以就是一个二维滑动窗口.

前置知识
既然是二维的滑动窗口,那么我们就必须先会一维.也就是一维单调队列.
那么就在此简单的讲一下单调队列的实现:
以最大值为例,既然我们想要保证队列开头为答案,那么我们就要保证每次更新使最大值一直放在队列。
当前队头就是目前最大值,那么如果我们要求进入一个新的值的时候,应该怎么办呢?
1.可以想,我们的区间长度是不超过k的那么如果当前队列里和我目前将要进队的这个值的位置相差超过了k那么就得出队。
2.还有一种情况,因为队列是从大到小排序的,最新的值又是从队尾入的,所以我们现在要保持单调性的话,就必须把对尾的所有小于现在数给踢掉.

参考代码
int k;
int a[N],q[N];
int head,tail;
head=tail=0;
for(int i=1;i<=n;++i){
    while(head<=tail&&i-q[head]>k) head++;
    while(head<=tail&&a[i]>=a[q[tail]]) tail--;
    q[++tail]=i;
}
ok!
既然知道了一维的,那么二维的就迎刃而解了.
同时处理行和列是不可能的,那么我们就先单独对列处理.用两个队列和f[i][j]和g[i][j]分别存储以i,j结尾的大小为k的矩阵的最大与最小值,然后通过一维的滑动窗口处理出每一列的极值,然后在对每一行进行处理.
最后统计答案.
完结散花✿✿ヽ(°▽°)ノ✿

#include<bits/stdc++.h>
using namespace std;
const int INF=1e9;
const int N=1e3+50;
int n,m,k,ans;
int head1,tail1,head2,tail2;
int a[N][N],f[N][N],g[N][N];
struct data {
    int x,s;
} q1[N],q2[N];
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",&a[i][j]);
    for(int i=1; i<=n; i++) {
        head1=tail1=1;
        head2=tail2=1;
        q1[1].x=1;
        q1[1].s=a[i][1];
        q2[1].x=1;
        q2[1].s=a[i][1];
        for(int j=1; j<=m; j++) {
            while(head1<=tail1&&q1[head1].x<=j-k) head1++;
            while(head2<=tail2&&q2[head2].x<=j-k) head2++;
            f[i][j]=q1[head1].s;
            g[i][j]=q2[head2].s;
            while(head1<=tail1&&q1[tail1].s<=a[i][j+1]) tail1--;
            while(head2<=tail2&&q2[tail2].s>=a[i][j+1]) tail2--;
            q1[++tail1].x=j+1;
            q1[tail1].s=a[i][j+1];
            q2[++tail2].x=j+1;
            q2[tail2].s=a[i][j+1];
        }
    }
    ans=INF<<1;
    for(int j=k; j<=m; j++) {
        head1=tail1=1;
        head2=tail2=1;
        q1[1].x=1;
        q1[1].s=f[1][j];
        q2[1].x=1;
        q2[1].s=g[1][j];
        for(int i=1; i<=n; i++) {
            while(head1<=tail1&&q1[head1].x<=i-k) head1++;
            while(head2<=tail2&&q2[head2].x<=i-k) head2++;
            if(i>=k) ans=min(ans,q1[head1].s-q2[head2].s);
            while(head1<=tail1&&q1[tail1].s<=f[i+1][j]) tail1--;
            while(head2<=tail2&&q2[tail2].s>=g[i+1][j]) tail2--;
            q1[++tail1].x=i+1;
            q1[tail1].s=f[i+1][j];
            q2[++tail2].x=i+1;
            q2[tail2].s=g[i+1][j];
        }
    }
    printf("%d",ans);
    return 0;
}

作者:叁秋.
链接:https://www.acwing.com/solution/content/5855/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

二维滑动窗口!!!
学到了学到了


John 打算驾驶一辆汽车周游一个环形公路。

公路上总共有 n 个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。

John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。

在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。

任务:判断以每个车站为起点能否按条件成功周游一周。

输入格式
第一行是一个整数 n,表示环形公路上的车站数;

接下来 n 行,每行两个整数 pi,di,分别表示表示第 i 号车站的存油量和第 i 号车站到 顺时针方向 下一站的距离。

输出格式
输出共 n 行,如果从第 i 号车站出发,一直按顺时针(或逆时针)方向行驶,能够成功周游一圈,则在第 i 行输出 TAK,否则输出 NIE。

数据范围
3≤n≤106,
0≤pi≤2×109,
0≤di≤2×109
输入样例:
5
3 1
1 2
5 2
0 1
5 4
输出样例:
TAK
NIE
TAK
NIE
TAK
难度:中等
时/空限制:1s / 64MB
总通过数:1380
总尝试数:3305
来源:《信息学奥赛一本通》 , POI2004
算法标签

算法分析
单调队列

顺时针

每个点i表示从i点加oil[i]的油再耗dist[i]的油所剩的油量,即oil[i] - dist[i]

1、计算出油量的前缀和
2、从某个点i出发,顺时针走一圈,在过程中油量始终 >= 0,等价于在[i,i + n - 1]中,对任意的j,i <= j <= i + n - 1,均有s[j] - s[i - 1] >= 0,即i固定,找s[j]的最小值,即从[i,i + n - 1]中找到滑动窗口的最小值
3、由于2操作,需要对i进行反向遍历,即从n * 2遍历到1,又由于i <= j <= i + n - 1,遍历到i时需要用到i位置的值,因此找[i,i + n - 1]区间最小值时需要在while后面的语句找
逆时针

每个点i表示从i点加oil[i]的油再耗dist[i - 1]的油所剩的油量,即oil[i] - dist[i - 1],其中1号点浩的是dist[n]的油,因此需要初始化dist[0] = dist[n]

1、计算出油量的后缀和
2、从某个点i出发,逆时针走一圈,在过程中油量始终 >= 0,等价于在[i - n + 1,i]中,对任意的j,i - n + 1 <= j <= i,均有s[j] - s[i + 1] >= 0,即i固定,找s[j]的最小值,即从[i - n + 1,i]中找到滑动窗口的最小值
3、由于2操作,需要对i进行正向遍历,即从1遍历到n * 2,又由于i - n + 1 <= j <= i,遍历到i时需要用到i位置的值,因此找[i - n + 1,i]区间最小值时需要在while后面的语句找
时间复杂度 O(n)O(n)
参考文献
算法提高课

Java 代码
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

public class Main {
    static int N = 100000 * 2 + 10;
    static int[] oil = new int[N];
    static int[] dist = 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) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
        int n = Integer.parseInt(br.readLine());
        for(int i = 1;i <= n;i ++)
        {
            String[] s1 = br.readLine().split(" ");
            oil[i] = Integer.parseInt(s1[0]);
            dist[i] = Integer.parseInt(s1[1]);
        }
        //顺时针
        for(int i = 1;i <= n ;i ++) s[i] = s[i + n] = oil[i] - dist[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 >= 1;i --)
        {
            if(hh <= tt && q[hh] > i + n - 1) hh ++;
            while(hh <= tt && s[q[tt]] >= s[i]) tt --;
            q[++ tt] = i;

            if(i <= n && s[q[hh]] - s[i - 1] >= 0) st[i] = true;
        }

        //逆时针
        dist[0] = dist[n];
        for(int i = n;i >= 1;i --) s[i] = s[i + n] = oil[i] - dist[i - 1];
        for(int i = n * 2;i >= 1;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 + 1) hh ++;
            while(hh <= tt && s[q[tt]] >= s[i]) tt --;
            q[++ tt] = i;

            if(i >= n + 1 && s[q[hh]] - s[i + 1] >= 0) st[i - n] = true;
        }

        for(int i = 1;i <= n;i ++)
        {
            if(st[i]) log.write("TAK\n");
            else log.write("NIE\n");
        }
        log.flush();
    }
}

作者:小呆呆
链接:https://www.acwing.com/solution/content/12796/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单调队列优化DP是一种常用的优化方法,可以将时间复杂度从 $O(n^2)$ 降低到 $O(n)$ 或者 $O(n \log n)$。以下是一道利用单调队列优化DP的典型题目: 题目描述: 给定一个长度为 $n$ 的序列 $a_i$,定义 $f(i)$ 为 $a_i$ 到 $a_n$ 中的最小值,即 $f(i) = \min\limits_{j=i}^n a_j$。现在定义 $g(i)$ 为满足 $f(j) \ge a_i$ 的最小下标 $j$,即 $g(i) = \min\{j \mid j > i, f(j) \ge a_i\}$。如果不存在这样的下标 $j$,则 $g(i) = n+1$。 现在请你计算出 $1 \le i \le n$ 的所有 $g(i)$ 的值。 输入格式: 第一行包含一个整数 $n$。 第二行包含 $n$ 个整数 $a_1,a_2,\cdots,a_n$。 输出格式: 输出 $n$ 行,第 $i$ 行输出 $g(i)$ 的值。 输入样例: 5 3 1 2 4 5 输出样例: 2 5 5 5 6 解题思路: 设 $dp(i)$ 表示 $g(i)$,那么 $dp(i)$ 与 $dp(i+1)$ 的转移关系可以表示为: $$dp(i)=\begin{cases}i+1, &\text{if}\ f(i+1)\ge a_i \\dp(i+1), &\text{else}\end{cases}$$ 这个转移方程可以使用暴力 DP 解决,时间复杂度为 $O(n^2)$。但是,我们可以使用单调队列优化 DP,将时间复杂度降为 $O(n)$。 我们定义一个单调队列 $q$,存储下标。队列 $q$ 中的元素满足: - 队列中的元素是单调递减的,即 $q_1 < q_2 < \cdots < q_k$; - 对于任意的 $i\in [1,k]$,有 $f(q_i) \ge f(q_{i+1})$。 队列 $q$ 的作用是维护一个长度为 $k$ 的区间 $[i+1,q_k]$,满足这个区间中的所有 $j$ 都满足 $f(j) < f(i+1)$。 根据定义,当我们要求 $dp(i)$ 时,只需要查找队列 $q$ 中第一个满足 $f(q_j) \ge a_i$ 的位置 $q_j$,那么 $g(i) = q_j$,如果队列 $q$ 中不存在这样的位置,则 $g(i) = n+1$。 那么如何维护单调队列 $q$ 呢?我们可以在每次 DP 的过程中,将 $i$ 加入队尾。然后判断队首元素 $q_1$ 是否满足 $f(q_1) \ge a_i$,如果满足则弹出队首元素,直到队首元素不满足条件为止。 由于每个元素最多被加入队列一次,并且最多被弹出一次,因此时间复杂度为 $O(n)$。具体实现细节可以参考下面的代码实现:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值