单调队列优化dp

背景——引入单调队列

       滑动窗口

分析   

    对于这样一个问题,我们采用单调队列来解决。那么什么是单调队列。

如其名就是具有单调性的队列。维护这样一个队列的好处是,当我们想求最大值的时候,我们只需要保证队列是单调递增的,于是每次取出队头就是我们的最大值。比起直接遍历,极大提高了效率。接下来我们以滑动窗口这个例子来一起了解一下具体怎么实现。

单调队列优化dp_第1张图片

         首先我们定义一个数组q[i]用来存储元素的下标。之后第一步就是把第一个元素的下标存储进去,然后依次存储下一个元素的下标。这里我们先以滑动窗口的最小值为例子往下讲解。                        第一个元素下标进队列,此时队列为\left \{ 0 \right \},因为储存的是下标。对应的值为\left \{ 1\right \}                                  接下来第二个元素,为3,比队列中的上一个元素大,能维持从尾到头单调递减,入队              于是队列为\left \{ 0,1 \right \},对应的值为\left \{ 1,3 \right \}

        然后第三个元素,为-1,比上一个元素小,由于要维持单调递减,于是上一个元素就得出队         于是就变成了\left \{ 0 \right \},其对应的值也就变成了\left \{ 1 \right \},但是现在-1还不能着急入队,因为此时进去还是        不能满足单调性,于是我们需要把1(元素的值)出队,于是变成了\left \{ \right \},满足条件,入队,其列          应该变为\left \{ 2 \right \},对应的值为\left \{ -1\right \},此时由于滑动窗口已经有三个元素了,于是我们需要输出队        头元素-1

        接下来第四个元素,为-3,根据上述流程,-1>-3,为了维持单调性,-1出队,然后再           入队,于是队列为\left \{ 3 \right \},队中元素为\left \{ -3 \right \},同时我们需要输出-3

        后续依次类推即可。

细节处理        

           需要注意的是,我们需要将这个单调队列的大小维护在k这个大小,因为这个滑动窗口是不断移动的。如下图,

单调队列优化dp_第2张图片

        当我们滑动窗口的区间为数字[3,5]之间,根据上述单调性入队出队规则,队列中的元素应该为\left \{ 1,2,3 \right \},对应的值应该为\left \{ 3,4,5 \right \},接下来我们考虑滑动窗口后移动

单调队列优化dp_第3张图片

        根据上述规则,此时6应该入队,此时队列应该变为\left \{ 1,2,3,4 \right \},对应的值为\left \{ 3,4,5,6 \right \},输出队头元素对应的值3,是不是不太对了,3已经划走了,怎么还会输出3呢?

        他都已经划走了,那就不关他的事了,将他从队头丢出去,所以我们这个队列最多维护k=3个元素。此时队列变为\left \{ 2,3,4 \right \},对应的值为\left \{ 4,5,6 \right \},输出队首4,正确!

代码

#include
#include
using namespace std;
const int N = 1e6 + 10, M = 100010;
int a[N], q[M];
int n, k;
int main()
{
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i++)
        scanf("%d",&a[i]);
    int hh = 0, tt = 0;
    for (int i = 0; i < n; i++)
    {
        while (hh <= tt && i - k + 1 > q[hh])
            hh++;
        while (hh <= tt && a[i] < a[q[tt]])
            tt--;
        q[++tt] = i;
        if (i >= k - 1)
            cout << a[q[hh]] << " ";
    }
    puts(" ");
    //最大值即再维护依次一次单调性即可
    hh = 0, tt = 0;
    for (int i = 0; i < n; i++)
    {
        while (hh <= tt && i - k + 1 > q[hh])
            hh++;
        while (hh <= tt && a[i] > a[q[tt]])
            tt--;
        q[++tt] = i;
        if (i >= k - 1)
            cout << a[q[hh]] << " ";
    }
}

单调队列优化dp

     对于一个优化问题,我们首先可以先想一下他的暴力做法,然后在看一下是否可以优化。

     对于单调队列优化的 dp 问题,一般满足在一个可动区间求最大值或者最小值。如果暴力枚举那个区间,一般会超时,于是我们可以选择将最大值放入队头的最大值或者最小值,通过这样维护我们可以减去暴力枚举的过程,优化了时间复杂度。接下来我们看来两个例题来熟悉一下。

   琪露诺

分析

定义状态 f[i] 表示从 i 这个点开始往后跳获得的最大冰冻指数

f[i]=max(f[j])+a[i]   此时i+l<=j<=i+r ;

f[i+1]=max(f[j])+a[i+1] ,此时a+l+1<=j<=i+r+1

我们先想到暴力的做法,就是枚举 j 的取值范围,依次记录下最大值,但是对于这样一个滑动区间的最大值,

由此可以利用单调队列的优化,对于这样的问题我们可以把我们最大的元素放在队头

 代码
//定义状态f[i]表示从i这个点开始往后跳获得的最大冰冻指数
//f[i]=max(f[j])+a[i];此时i+l<=j<=i+r;
//f[i+1]=max(f[j])+a[i],此时a+l+1<=j<=i+r+1
//由此可以利用单调队列的优化,我们储存最大的元素放在队头
#include
#include
#include
using namespace std;
const int N=2e5+10;
int a[N],q[N],f[N];
int hh,tt,n,r,l,ans=-1e9;

int main()
{
	cin>>n>>l>>r;
	for(int i=0;i<=n;i++)
		cin>>a[i];
	memset(f,0xcf,sizeof f);
	f[0]=0;
	hh=0,tt=0;
	for(int i=0;i<=n-l;i++)
	{
		while(q[hh]<=i-(r-l+1)&&hh<=tt)//维护滑动窗口
			hh++;
		while(hh<=tt&&f[q[tt]]

切蛋糕

       分析

        同样的我们先来思考一下暴力做法,实际上我们要求的是[l,r]这个区间和的最大值,于是我们可以利用前缀和的知识,我们先求出sum[r],再求出sum[l-1]这样只需要两个式相减,即sum[r]-sum[l-1],我们便得到了区间的和,然后我们依次枚举l,r,记录下其中的最大值即可。这样的时间复杂度是O(nm)的。于是我们接下来对他进行优化。

        我们选择将其中一个数字固定,当 r确定的时候,sum[r]就也是一个常数。于是接下来就是要维护[r-l,r-1]中最小的 sum[i] 这样得到的答案一定是在r一定时最优的,就省去了枚举端点的时间,时间复杂度也就降到了O(n)

单调队列优化dp_第4张图片

代码 
#include
#include
#include
using namespace std;
const int N=5e5+10;
int a[N],q[N],sum[N];//q[]储存下标
int hh,tt,ans=-1e9;

int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>a[i],sum[i]=sum[i-1]+a[i];//预处理出前缀和
	hh=0,tt=0;
	for(int i=1;i<=n;i++)
	{
		while(hh<=tt&&i-q[hh]>m)//即最大区间只能是m,超过了就要往后滑动了
			hh++;
		while(hh<=tt&&sum[q[tt]]>sum[i])//这里要维护一个单调递减的队列,下式说明了是减去
			tt--;					
		ans=max(ans,sum[i]-sum[q[hh]]);
		q[++tt]=i;//下标入队
	}
	cout<

例题



Mowing the Lawn G

分析 

        由于不能连续选择超过m的牛,于是我们可以定义状态f[i][0/1]表示第i头牛选还是不选
        f[i][0]=max(f[i-1],f[i-1][1])      转移前一个选还是不选的最大值
        f[i][1]=max(f[j][0]-sum[j]+sum[i])     从ji区间,sum[i]-sum[j-1]表示ji区间选了的,但由于我们的j是不选的,于是就变成了sum[j] 此时j的取值范围是[i-k,i)
       f[i][1]=max(f[j][0]-sum[j])+sum[i];于是我们只需要用单调递增的队列维护max中的值

代码 

#include
#include
#include
using namespace std;
typedef long long ll;
const int N=1e5+10;
int a[N],q[N];
ll sum[N],f[N][2];

int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>a[i],sum[i]=sum[i-1]+a[i];
	int hh=0,tt=0;
	
	for(int i=1;i<=n;i++)
	{
		f[i][0]=max(f[i-1][0],f[i-1][1]);
		while(hh<=tt&&i-q[hh]>m)
			hh++;
		f[i][1]=f[q[hh]][0]-sum[q[hh]]+sum[i];
		while(hh<=tt&&f[i][0]-sum[i]>f[q[tt]][0]-sum[q[tt]])
			tt--;
		
		
		q[++tt]=i;
	}
	cout<

PTA-Little Bird

分析 

        题目大体意思,如果飞到的树的高度小于当前树的高度,不管中间有多高,都不会增加劳累值,否则每次加 1

        假设现在在 i 位置上,想要飞到 j 位置上,定义f[i] 为劳累度

        于是f[i]=(aj>ai)?f[j]:f[j]+1 ,ji-mi-1

        劳累值越小越有利,所以我们需要维护一个f[i] 单调递减的队列

        除此之外,当f[i]和队尾相同时,也就是不增加劳累度的时候,如下图,

        如上图,可以跳到 3−7 这个区间,跳到 3 和 4 的时候,都是消耗劳累度的,但是我们选择哪个更好呢,当然是越大的越好,因为高度越高,下一次越有利,如上图,当我们跳到 4 ,下一次跳到 3 的时候就不增加劳累度。但是当我们第一次跳到 3 的时候(紫色部分),再跳到后面的 3 增加劳累度,于是当 f[i]==f[q[tt]] 时候我们还需维护 d 的单调增加找出最大值。 a[q[tt]]<=a[i]

代码

#include
#include
#include
using namespace std;
const int N=1e6+10;
int a[N],q[N],f[N];

int main()
{
	int n;cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	int t;cin>>t;
	while(t--)
	{
		int m;cin>>m;
		int hh=1,tt=1;q[tt]=1;//由于是从1开始的,所以得先把1给初始化进去
		for(int i=2;i<=n;i++)
		{
			while(hh<=tt&&i-q[hh]>m)
				hh++;
			f[i]=a[q[hh]]>a[i]?f[q[hh]]:f[q[hh]]+1;
			while(hh<=tt&&(f[i]

WIL

分析

 代码

//选择不超过d大小区间将其全部置为0,于是我们首先想到贪心,我们只要选择这样的一个大小为d且区间和最大的区间即可。于是我们可以通过枚举算出sum[x]-sum[x-d]的最大值,我们需要用队列维护这个最大值
#include
#include
#include
using namespace std;
typedef long long ll;
const int N=2e6+10;
int n,d,l,hh=0,tt=0,q[N];
ll p,sum[N],a[N];
int main()
{
	cin>>n>>p>>d;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		sum[i]=sum[i-1]+a[i];
	}
	int ans=d;q[tt]=d;l=1;//初始化最长区间为d

	for(int i=d+1;i<=n;i++)
	{
		 // while (hh <= tt && i - d > q[hh]) {
            // hh++;
        // }
		while(hh<=tt&&sum[i]-sum[i-d]>sum[q[tt]]-sum[q[tt]-d])
			tt--;//维护单调递增的队列
		q[++tt]=i;
		//然后维护这个不超过p的最大区间,这里i就代表了r
		while(hh<=tt&&sum[i]-sum[l-1]-sum[q[hh]]+sum[q[hh]-d]>p)//因为队头是最大值
		{
			l++;//不符合了区间太大了,于是缩小
			while(hh<=tt&&l-q[hh]+1>d)
				++hh;//维护单调队列的长度			
		}
		ans=max(ans,i-l+1);
	}
	cout<

你可能感兴趣的:(c++,算法,开发语言)