20191010 专题:单调队列

总览:

两种实现:stl(deque),手写(双端队列)
stl之deque用法
模板:滑动窗口:https://www.luogu.org/problem/P1886
手写的单调队列
作用:维护一段定长区间的最大(小)值。

T1 烽火传递

烽火传递
题目描述
原题来自:NOIP 2010 提高组初赛 · 完善程序

烽火台是重要的军事防御设施,一般建在交通要道或险要处。一旦有军情发生,则白天用浓烟,晚上有火光传递军情。

在某两个城市之间有 座烽火台,每个烽火台发出信号都有一定的代价。为了使情报准确传递,在连续 个烽火台中至少要有一个发出信号。现在输入 和每个烽火台的代价,请计算总共最少的代价在两城市之间来准确传递情报。

输入格式
第一行是n,m,表示 n个烽火台和连续烽火台数m;

第二行 n个整数表示每个烽火台的代价 a i a_i ai

输出格式
输出仅一个整数,表示最小代价。

样例
样例输入
5 3
1 2 5 6 2
样例输出
4
样例说明
在第2,5号烽火台上发信号。

思路:dp点燃第i个的最小花费,此时[i-m,i-1]必有一个点燃(当然取最小的)(最小值用单调区间维护)

状态转移方程:
d p [ i ] = a [ i ] + m i n ( d p [ j ] ) ( j 属 于 [ i − m , i − 1 ] ) dp[i]=a[i]+min(dp[j]) (j属于[i-m,i-1]) dp[i]=a[i]+min(dp[j])(j[im,i1])

代码:

#include<bits/stdc++.h>
using namespace std;

const int A=2e5+5;

int n,m,a[A];
int dp[A];

deque <int> q;

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
	for(int i=1;i<=n+1;i++)
	{
		if(i<=m)	dp[i]=a[i];
		else	dp[i]=a[i]+dp[q.front()];
		while(!q.empty()&&q.front()<=i-m)
			q.pop_front();
		while(!q.empty()&&dp[q.back()]>=dp[i])
			q.pop_back();
		q.push_back(i);
	}
	printf("%d",dp[n+1]);
	return 0;
}

T2 道路游戏

(洛谷P1070)
题目描述
小新正在玩一个简单的电脑游戏。

游戏中有一条环形马路,马路上有 n n个机器人工厂,两个相邻机器人工厂之间由一小段马路连接。小新以某个机器人工厂为起点,按顺时针顺序依次将这 n n个机器人工厂编号为1-n1−n,因为马路是环形的,所以第 nn 个机器人工厂和第 1 1个机器人工厂是由一段马路连接在一起的。小新将连接机器人工厂的这 n 段马路也编号为 1-n1−n,并规定第 i i段马路连接第 i 个机器人工厂和第 i+1i+1 个机器人工厂(1≤i≤n-11≤i≤n−1),第 nn 段马路连接第 nn 个机器人工厂和第 11个机器人工厂。

游戏过程中,每个单位时间内,每段马路上都会出现一些金币,金币的数量会随着时间发生变化,即不同单位时间内同一段马路上出现的金币数量可能是不同的。小新需要机器人的帮助才能收集到马路上的金币。所需的机器人必须在机器人工厂用一些金币来购买,机器人一旦被购买,便会沿着环形马路按顺时针方向一直行走,在每个单位时间内行走一次,即从当前所在的机器人工厂到达相邻的下一个机器人工厂,并将经过的马路上的所有金币收集给小新,例如,小新在 ii(1≤i≤n1≤i≤n)号机器人工厂购买了一个机器人,这个机器人会从 ii 号机器人工厂开始,顺时针在马路上行走,第一次行走会经过 i i号马路,到达 i+1 i+1号机器人工厂(如果 i=ni=n,机器人会到达第 11 个机器人工厂),并将 ii 号马路上的所有金币收集给小新。 游戏中,环形马路上不能同时存在 2 2个或者 2 2个以上的机器人,并且每个机器人最多能够在环形马路上行走p p次。小新购买机器人的同时,需要给这个机器人设定行走次数,行走次数可以为 1~p1 p 之间的任意整数。当马路上的机器人行走完规定的次数之后会自动消失,小新必须立刻在任意一个机器人工厂中购买一个新的机器人,并给新的机器人设定新的行走次数。

以下是游戏的一些补充说明:

游戏从小新第一次购买机器人开始计时。

购买机器人和设定机器人的行走次数是瞬间完成的,不需要花费时间。

购买机器人和机器人行走是两个独立的过程,机器人行走时不能购买机器人,购买完机器人并且设定机器人行走次数之后机器人才能行走。

在同一个机器人工厂购买机器人的花费是相同的,但是在不同机器人工厂购买机器人的花费不一定相同。

购买机器人花费的金币,在游戏结束时再从小新收集的金币中扣除,所以在游戏过程中小新不用担心因金币不足,无法购买机器人而导致游戏无法进行。也因为如此,游戏结束后,收集的金币数量可能为负。

现在已知每段马路上每个单位时间内出现的金币数量和在每个机器人工厂购买机器人需要的花费,请你告诉小新,经过 mm 个单位时间后,扣除购买机器人的花费,小新最多能收集到多少金币。

输入格式
第一行 33 个正整数n,m,pn,m,p,意义如题目所述。

接下来的 n n行,每行有 mm 个正整数,每两个整数之间用一个空格隔开,其中第 i 行描

述了 ii 号马路上每个单位时间内出现的金币数量(1≤1≤金币数量≤100≤100),即第 i i行的第 jj(1≤j≤m1≤j≤m)个数表示第 jj 个单位时间内 i 号马路上出现的金币数量。

最后一行,有 nn 个整数,每两个整数之间用一个空格隔开,其中第 ii 个数表示在 i i号机器人工厂购买机器人需要花费的金币数量(1≤1≤金币数量≤100≤100)。

输出格式
共一行,包含 1 1个整数,表示在 mm 个单位时间内,扣除购买机器人

花费的金币之后,小新最多能收集到多少金币。

输入输出样例
输入 #1
2 3 2
1 2 3
2 3 4
1 2
输出 #1
5
说明/提示
【数据范围】

对于 40%的数据,2≤n≤40,1≤m≤402≤n≤40,1≤m≤40。

对于 90%的数据,2≤n≤200,1≤m≤2002≤n≤200,1≤m≤200。

对于 100%的数据,2≤n≤1000,1≤m≤1000,1≤p≤m2≤n≤1000,1≤m≤1000,1≤p≤m。

NOIP 2009 普及组 第四题

思路:先求前缀和(二维),方便求区间和。再三重循环(机器人死掉的时间,地点,死前的步数)。
注:用mod搞掉回环。

状态转移方程:
f [ i ] = m a x ( f [ i − k ] + s u m [ i ] [ j ] − s u m [ i − k ] [ j − k ] − c o s t [ j − k ] ) f[i]=max(f[i−k]+sum[i][j]−sum[i-k][j-k]−cost[j−k]) f[i]=max(f[ik]+sum[i][j]sum[ik][jk]cost[jk])

好题解:
(https://www.luogu.org/blog/Fenian/p1070-dao-lu-you-hu-ti-xie)
P1070道路游戏题解
一道非常具有思考性的dp。
用f[i]表示第i个时间最大利益
cost[i]表示第i个工厂机器人的雇佣费
r[j][i]表示第i个时刻到达第j个工厂的价值
sum[i][j]表示价值的前缀和
首先,我们将第一个工厂编号为0( It is important 一定要理解这一操作)。
因为第i条路指向第i+1个工厂,所以我们可以将第i条路的价值变成第i+1个工厂的价值,于是我们得到如下输入代码(因为从0开始编号,所以第n条路的价值转移给第0号工厂,因此对n取模)

for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            scanf("%d",&r[i%n][j];

同时,我们对价值进行前缀和。因为价值存在工厂和时间两个约束,所以我们使用二维前缀和。

for(int i=1;i<=m;i++)
        for(int j=0;j<n;j++)
            sum[i][j]=sum[i-1][(j-1+n)%n]+r[j][i];

即sum值由对角线传递
*我们从O(n3)的解法讲起
我们很容易得到这个转移方程
f [ i ] = m a x ( f [ i − k ] + s u m [ i ] [ j ] − s u m [ i − k ] [ j − k ] − c o s t [ j − k ] ) f[i]=max(f[i−k]+sum[i][j]−sum[i-k][j-k]−cost[j−k]) f[i]=max(f[ik]+sum[i][j]sum[ik][jk]cost[jk])
意思是机器人从j-k号工厂出发,走k次在i时刻到达j号工厂,并对这一过程取最优值。
如果是以这道题考察普及组的初衷,那么到这里就结束,但是如果数据稍稍加强,对于1000的数据,O(n3)是远不够得。
*那么,我想办法进行压缩
我们对上面的方程稍加处理得到
f [ i ] = m a x ( f [ i − k ] − s u m [ i − k ] [ j − k ] − c o s t [ j − k ] ) + s u m [ i ] [ j ] f[i]=max(f[i−k]−sum[i-k][j-k]−cost[j−k])+sum[i][j] f[i]=max(f[ik]sum[ik][jk]cost[jk])+sum[i][j]
就是将不需要枚举k步数的项提出来。
我们用 r e c [ i ] [ j rec[i][j rec[i][j]来记录 f [ i ] − s u m [ i ] [ j ] − c o s t [ j ] f[i]−sum[i][j]−cost[j] f[i]sum[i][j]cost[j]
得到
f [ i ] = m a x ( r e c [ i − k ] [ j − k ] ) + s u m [ i ] [ j ] f[i]=max(rec[i-k][j-k])+sum[i][j] f[i]=max(rec[ik][jk])+sum[i][j]
因为rec的i和j两项同时减去k,所以实际上f[i]是对对角线上的rec进行取最大值。我们用优先队列对这一过程进行优化,将同一对角线的rec放入同一个优先队列。
详细的细节,还是看看代码吧(第二个)

程序(三重循环 O( n 3 n^3 n3)):

#include<bits/stdc++.h>
using namespace std;

const int A=1005;

int n,m,p;
int mon[A][A],pay[A];
int sum[A][A];
int r[A];

int main()
{
	scanf("%d%d%d",&n,&m,&p);
	fill(r+1,r+1+m,-1e8);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			scanf("%d",&mon[j][(i+1)%n]);
	for(int i=1;i<=n;i++)
		scanf("%d",&pay[i%n]);
	for(int i=1;i<=m;i++)
		for(int j=0;j<n;j++)
			sum[i][j]=sum[i-1][(j-1+n)%n]+mon[i][j];
	for(int i=1;i<=m;i++)
		for(int j=0;j<n;j++)
			for(int k=1;k<=min(i,p);k++)
				r[i]=max(r[i],r[i-k]+sum[i][j]-sum[i-k][(j-k+n)%n]-pay[(j-k+n)%n]);
	printf("%d",r[m]);
	return 0;
}

单调队列没 写出来。。。(:逃
标程(优先队列):
(https://www.luogu.org/problemnew/solution/P1070)

#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;
inline int read()
{
    int x=0;
    char c=getchar();
    while(c<'0'||c>'9') c=getchar();
    while(c<='9'&&c>='0')
    {
        x=x*10+c-'0';
        c=getchar();
    }
    return x;
}

int n,m,p;
int r[1005][1005];
int cost[1005];
int sum[1005][1005];
int rec[1005][1005];
int dp[1005];
int pre[1005][1005];
int maxn=-99999999;

struct node{
    int data,step;
    bool operator<(node a)const{
        return a.data>data;
    }
};

priority_queue<node>q[1100];

void update(int si,int now)
{
    node tmp=q[si].top();
    while(now-tmp.step>=p)
    {
        q[si].pop();
        tmp=q[si].top();
    }
}

int main()
{
    freopen("roadgame.txt","r",stdin);
    n=read();m=read();p=read();
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            r[i%n][j]=read();
    for(int i=1;i<=m;i++)
        for(int j=0;j<n;j++)
            sum[i][j]=sum[i-1][(j-1+n)%n]+r[j][i];
    for(int i=0;i<n;i++)
        cost[i]=read();
    for(int i=0;i<n;i++)
        pre[0][(i-1+n)%n]=i;
    for(int i=0;i<n;i++)
    {
        rec[0][i]=-cost[i];
        node tmp;
        tmp.data=-cost[i];
        tmp.step=0;
        q[pre[0][i]].push(tmp);
    }
    for(int i=1;i<=m;i++)
    {
        int t=-9999999;
        for(int j=0;j<n;j++)
        {
            dp[i]=max(dp[i],rec[i-1][(j-1+n)%n]+sum[i][j]);
            maxn=max(maxn,dp[i]);
            t=max(t,dp[i]);
        }
        for(int j=0;j<n;j++)
        {
            pre[i][j]=pre[i-1][(j-1+n)%n];
            node tmp;
            tmp.data=t-sum[i][j]-cost[j];
            tmp.step=i;
            q[pre[i][j]].push(tmp);
            update(pre[i][j],i);
            rec[i][j]=q[pre[i][j]].top().data;
        }
    }
    cout<<maxn;
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值