《关于我们WA题太多导致皮卡丘看不下去把妙蛙种子电死于是我们现在叫不听不听,|

superPG

园龄:5年粉丝:2关注:10

2021-07-08 12:40阅读: 655评论: 1推荐: 1

单调队列优化DP

单调队列优化DP

单调栈单调队列都是借助单调性,及时排除不可能的决策,保持候选集合的高度有效性和秩序性。单调队列尤其适合优化决策取值范围的上、下界均单调变化,每个决策在候选集合中插入或删除至多一侧的问题。

先以最大子序和这道题理解单调队列优化DP的思想。

[AcWing135.最大子序和](135. 最大子序和 - AcWing题库)

  1. 求连续子序列的和,必然采用前缀和的思想。
  2. 所有连续子序列的最大值,肯定是至少以某一结点i为头/尾扫一遍整个序列。
  3. 但是对于某一个序列尾结点i,我们之前枚举尾结点的时候已经扫过i前面的数了,再扫一遍岂不是很丑陋的O(n^2)。
  4. 单调队列就是用来优化这种问题的方法。

对于从左到右扫描的 k < j < i ,如果S[j] <= S[k],对于右节点 i ,完全没必要再考虑k了。一是因为 同样的 S[i] 减去更小的 左端点前缀和 S[j] 得到的 序列和 肯定更大;二是因为 j 更靠右边,存活的时间更长。

因此,在我们从左到右扫描的过程中,采用一个队列来保存 扫描过的 结点 i ,要求S[i] 单调递增。

而在寻找最大连续子区间的时候,只需要找到队列首位(且满足序列长度小于等于M)的那个,就一定是左端点的最优解。由于每个元素只入队/出队一次,所以时间复杂度是O(N)。

单调队列的优化思想是:在O(n)的时间复杂度下,减少另一端点的枚举范围,使时间复杂度从O(n^2)降低到O(n),妙就妙在维护队列的单调性不需要太多多余操作,只需将队列元素和当前元素对比并判断是否出队即可。

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

int n,m;
const int N = 3e5+10;

int a[N];
int sum[N];
int q[N];

int main()
{
    scanf("%d %d",&n,&m);
    
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        sum[i] = sum[i-1] + a[i];
    }
    
    int hh = 0,tt = 0;
    int res = -0x3f3f3f3f;
    for(int i=1;i<=n;i++)
    {
        while(hh<=tt && (i-q[hh])>m) hh++;  //注意前缀和的性质!,sum[i] - sum[q[hh]]表示的是区间,[q[hh]+1,i],所以这里的闭区间算长度不加1!
        res = max(res, sum[i] - sum[q[hh]]);
        while(hh<=tt && sum[q[tt]] >= sum[i]) tt--;
        q[++tt] = i;
    }
    
    cout<<res<<endl;
    
    return 0;
}

[AcWing1087.修剪草坪](1087. 修剪草坪 - AcWing题库)

Tag:线性DP + 单调队列优化

状态f[i] 代表前 i 个中选取不超过连续 m 个的最大值。属性:MAX

状态转换:对于最后一个元素 i,有选和不选两种方式。

  1. 不选择第 i 个元素,则情况转化为 f[i-1] 。(无需考虑连续的问题)

  2. 选择第 i 个元素,则情况转化为, 对于选择的最后连续的一段, 选几个最好?

    设选择了 j 个,其中(1 <= j <= m),因为至少要选 i 号元素,至多不能超过m个。

    转移方程为:

    ​ f [ i ] = max{ s[ i ] - s[ i - j ] + f [ i - j - 1 ] } ( 1 <= j <= k)

    ​ = max{ f [ i - j - 1 ] - s [ i - j ] } + s [ i ]

    ​ 如果将max函数内部的运算看作 g( i - j ) 的话,相当于建立一个单调队列,维护 g(i - j )的递减序列, 由于 j 的限制是 (1 <= j <= k),则维护 i 之前 最多 k 个即可。

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

const int N = 1e5+10;
typedef long long LL;
LL a[N];
LL s[N];
LL f[N];
int q[N];
int n,m;

LL g(int i)
{
    return f[i-1] - s[i];
}

int main()
{
    scanf("%d %d",&n,&m);
    
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
        s[i] = s[i-1] + a[i];
    }
    
    int hh=0, tt=0;
    LL res = 0;
    for(int i=1;i<=n;i++)
    {
        if(hh<=tt && q[hh] < i-m) hh++;
        f[i] = max(f[i-1],s[i] + g(q[hh]));     //注意利用闫氏DP分析法时,分成了选i和不选i两种,而单调队列是对选i进行了优化,还有和不选i
        //即f[i-1]进行比较
        while(hh<=tt && g(q[tt]) <= g(i)) tt--; //维护递减子序列
        q[++tt] = i;
    }
    
    cout<<f[n]<<endl;
    
    return 0;
}

[AcWing1088.旅行问题](1088. 旅行问题 - AcWing题库)

Tag:单调队列DP + 环形问题

单调队列一般用来处理区间最小值问题

本题几个特殊的点:

  1. 把 p[ i ] 和 d[ i ] 综合处理,即每个点保存 p[i] - d[i] ,sum[i] 也保存这个点的 p[ i ] - d[ i ] 的前缀和。

  2. 破环为链。环形问题的基本操作。

  3. 顺时针和逆时针的顺序问题。考察的是每个点的可行性,肯定是顺时针走一圈逆时针走一圈,两种里面有任意一种能走都会设置为1。

    和前面两个题目的不同点是:

    • 这个题是确定起点,去找终点;而不是像之前的两题,已经有了一个终点,问从哪一个起点走来最好。
    • 所以自第一遍顺时针的时候需要逆向处理,滑动窗口中保存的是,距离不超过n的,逆序的递增序列。所以,滑动窗口的队首才是 [i, i+n] 之间前缀和的最小值。如果最小值可行,那整个序列都可行。
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;

typedef long long LL;

const int N=2e6+10;

int n;
int o[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",&o[i],&dist[i]);
        s[i]=s[i+n]=o[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) //逆向的时候在i<n的时候转一圈
        {//事实上,这里应该判断s[q[hh]]-s[i]>=0 更好理解
            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]=o[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;
}

要铭记前缀和的性质if(s[i] <= s[q[hh]]) ans[i+1] = true;,其中s[ i ]只能确定 ans[i+1]是否成立,就是因为区间 [i+1 ~ i+n] 是由 s[i + n] 和 **s[ i ] **确定的。

[AcWing1089.烽火传递](1089. 烽火传递 - AcWing题库)

和前面几个题目不同,前面几个题目利用的前缀和处理区间和问题,本题直接利用单调队列的性质,裸单调队列DP。

确定好状态划分之后,手撕还是很容易的。

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


int n,m;
const int N = 2e5+10;
int a[N];
int q[N];
int f[N];
//f[i] 表示 以 i 结尾的选择方案中的最小值。 

int main()
{
	scanf("%d %d",&n,&m);
	
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	
	int hh = 0, tt = 0;
	f[0] = 0;
	for(int i = 1;i<=n;i++)
	{
		while(hh<=tt && q[hh]<i-m) hh++;
        //每一次优化当前的结果,直接加队首的最小值就一定是最优解。
		f[i] = f[q[hh]] + a[i];
		while(hh<=tt && f[q[tt]]>=f[i]) tt--;
        //维护的是 递增序列, 队首为最小值。
		q[++tt] = i;
	}
	
	int res = 0x3f3f3f3f;
	for(int i=n-m+1;i<=n;i++)
	{
		res = min(res, f[i]);
	}
	
	printf("%d\n",res);
	
	return 0;
}

[AcWing.1090绿色通道](AcWing 1090. 绿色通道 - AcWing)

单调队列一定要确定滑动窗口的大小,所以,可以采用二分的方法来确定,滑动窗口(即空题段)的大小。

注意本题的要求是空题段。在处理边界问题时要尤其注意。当 q[hh] < i - limit - 1 才不符合题意,这个自己画个图就很容易理解了。

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

const int N = 5e4+10;

int n,t;
int w[N];
int f[N];
int q[N];

bool check(int limit)
{
	memset(f,0,sizeof f);
	
	int hh = 0, tt = 0;
	for(int i = 1;i<=n;i++)
	{
	    //注意limit的定义是 空题段
		while(hh<=tt && 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]<=t)
		return true;
	}
	return false;
}

int main()
{
	scanf("%d %d",&n,&t);
	for(int i=1;i<=n;i++) scanf("%d",&w[i]);
	
	int l = 0, r = n;
	while(l<r)
	{
		int mid = (l+r)>>1;
		if(check(mid)) r = mid;
		else l = mid +1;
	}
	cout<<l<<endl;
	
	return 0;
}

[AcWing1091.理想的正方形](1091. 理想的正方形 - AcWing题库)

image-20210708120627012

区间最大/小值求法:

  • 在行方向上设置n长的滑动窗口,然后每一次都在最右边保存它和它左边n-1个元素的最大值。
  • image-20210708120811203
  • 然后再对每一列,求出其中的最大值,即为当前 n*n 的方阵的最大值。
  • 最小值同里。

利用单调队列优化,可以在处理最大值时,行方向 O(a*b),列方向O(a * b),求最小值统里,故总得时间复杂度就是O(a * b)。(在整个数据矩阵是 a * b 的情况下)。

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

typedef long long LL;

const int N = 1010, INF = 1e9;
int a[N];
int w[N][N];
int n,m,k;
int row_min[N][N],row_max[N][N];
int q[N];
//get_min()或get_max()函数,都是对数组a进行单调队列求最小(大)值,并将值放入数组b中保存。
void get_min(int a[],int b[],int tot)
{
	int hh = 0, tt = -1;
	for(int i=1;i<=tot;i++)
	{
		while(hh<=tt && q[hh]<=i-k) hh++;
		while(hh<=tt && a[q[tt]]>=a[i]) tt--;
		q[++tt] = i;
		b[i] = a[q[hh]];
	}
}

void get_max(int a[],int b[],int tot)
{
	int hh=0,tt=-1;
	for(int i=1;i<=tot;i++)
	{
		while(hh<=tt && q[hh]<=i-k) hh++;
		while(hh<=tt && a[q[tt]]<=a[i]) tt--;
		q[++tt] = i;
		b[i] = a[q[hh]];
	}
}

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",&w[i][j]);
    }
    
    for(int i=1;i<=n;i++)
    {
        get_min(w[i],row_min[i],m);
        get_max(w[i],row_max[i],m);
    }
	
    int res=INF;
    int a[N],b[N],c[N];
    for(int i=k;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=min(res,c[j]-b[j]);
    }
    
    printf("%d\n",res);
    
    return 0;
}

总得来说,单调队列可以用来求范围内的最值维题。在遍历数据的过程中,它抱持了队列中的单调性,无论是队列是根据 前缀和 亦或是 dp值 保存的,队首都能返回 符合约束条件的 最大 / 最小值。我认为,单调队列一般是把 需要重复遍历的 O(n^2) 时间复杂度降低到 O(n) (每个元入队 / 出队一次)的时间复杂度。

298. 围栏 - AcWing题库

Tag :单调队列优化DP

和前面练手的几道题不同,这道题很有意思。

状态表示:f[ i ] [ j ] 表示 前 i 个工匠刷前 j 个木板得到报酬的最大值

状态计算:(根据最后一个状态来看状态转移

  1. 不用第 i 个工匠: f [ i ] [ j ] = f[ i - 1] [ j ]

  2. 使用第 i 个工匠 但是 不刷第 j 面墙: f[ i ] [ j ] = f [ i ] [ j - 1 ]

  3. 使用第 i 个工匠 并且 刷第 j 面墙:

    这里注意, 根据题干, 刷墙一定是连续的,所以刷墙的范围是包含 j 的 连续的 一段。

    假设 刷墙的 范围是 [ k , j ],那么 这里的状态计算可以写成:

    f [ i ] [ j ] = f [ i - 1 ] [ k ] + ( j - k ) * p[ i ] (记为 @)

    那么问题转化成 对于区间 [ k , j ],固定 j(在我们遍历木板的过程中 j 肯定是要遍历一遍的) 的情况下, 寻找 符合条件 的 k (区间起点) ,使表达式 @ 的值最大

    右端点不点,左端点向右走,要求区间方案的极值,这是正是单调队列求极值的适用条件,这样就可以优化掉 暴力 寻找左端点k 这一维的时间复杂度。

    注意 :这里是对区间左端点 k 进行 单调队列优化, 而不是 整个区间。

    那么 k 又有什么约束条件呢?

    1. k < si ,作为区间左端点,为了使区间 包含 si ,必须要满足这个条件。

    2. k >= j - Li ,题目要求连续一段的长度不能超过 Li ,也就是要求左端点 k >= j - Li。

      其他问题见代码注释

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

const int N = 16000 + 10, M = 110;

int n,m;
int q[N];
int f[M][N];

struct Carpenter{
	int l,p,s;
	bool operator < (const Carpenter & t)
	{
		return s < t.s;
	}
}car[M];

int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=m;i++) scanf("%d %d %d",&car[i].l, &car[i].p,&car[i].s);
	sort(car+1,car+m+1);
	
	for(int i=1;i<=m;i++)
	{
		int hh = 0, tt = -1;
		for(int j = 0;j<=n;j++)
		{
			f[i][j] = f[i-1][j];	//不用第 i 个工匠的情况
			if(j) f[i][j] = max(f[i][j],f[i][j-1]);	//不刷第 j 快木板的情况
			
			int l = car[i].l, p = car[i].p, s = car[i].s;
			if(hh<=tt && q[hh] < j - l) hh++; //枚举的 k 的左端点 
			// j >= s 的时候 说明区间 [ k , j ] 是可行的,更新一遍答案。 
			if(j>=s && hh<=tt)	//s是枚举 k 的右端点,必须包含s 
			{
				int k = q[hh];
				f[i][j] = max(f[i][j],f[i-1][k] + p * (j - k));
			}
			// j < s 的时候, j 也是有用的 ,这时候 j 可能是一个起点,
			// 通过在队列中维护  f[i][j] 的第三种来源 的最大值。 
			if(j<s)
			{
				while(hh<=tt && f[i-1][q[tt]] - q[tt] * p <= f[i-1][j] - j*p) tt--;
                //这里对 第三种表达式进行了化简,因为现在枚举的 j 是用来做起点的了,所以表达式中( j - k ) * p(i) 中的 j 就没用了,(大家在队列里都是被用作起点的),只考虑 k 即可。
				q[++tt] = j;
			}
		}
	}	
	
	printf("%d\n",f[m][n]);
	
	return 0;
}

299. 裁剪序列 - AcWing题库

寄了,这题啃不动了。下次一定 ,先去打场 Div2 醒醒脑。

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

typedef long long LL;

const int N = 1e5+10;

int n;
LL m;
int a[N], q[N];
LL f[N];

multiset<LL> S;

void remove(LL x)
{
	auto it = S.find(x);
	S.erase(it);
}

int main()
{
	scanf("%d %lld",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);	
		if(a[i] > m)	//当有某一个元素a[i]大于m时,不满足题设条件。 
		{
			puts("-1");
			return 0;
		}
	}	
	
	int hh = 0, tt = 0;
	LL sum = 0;
	for(int i=1, j=0;i<=n;i++)
	{
		sum += a[i];
		while(sum>m) sum -= a[++j];
		
		while(hh <= tt && q[hh] <= j)
		{
			if(hh<tt) remove(f[q[hh]] + a[q[hh+1]]);
			hh++;
		}
		int tail = tt;
		while(hh <= tt && a[q[tt]] <= a[i])
		{
			if(tt != tail) remove(f[q[tt]] + a[q[tt+1]]);
			tt--;
		}
		if(hh <= tt && tt != tail) remove(f[q[tt]] + a[q[tt+1]]);
		q[++tt] = i;
		if(hh<tt) S.insert(f[q[tt-1]] + a[q[tt]]);
		
		f[i] = f[j] + a[q[hh]];
		if(S.size()) f[i] = min(f[i], *S.begin());	
	}
	
	printf("%lld\n",f[n]);
	
	return 0;
}

本文作者:superPG

本文链接:https://www.cnblogs.com/superPG/p/14985576.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   superPG  阅读(655)  评论(1编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起
  1. 1 いつも何度でも 伊藤サチコ
いつも何度でも - 伊藤サチコ
00:00 / 00:00
An audio error has occurred.

呼んでいる 胸のどこか奥で

いつも心踊る 夢を見たい

かなしみは 数えきれないけれど

その向こうできっと あなたに会える

繰り返すあやまちの そのたび ひとは

繰り返すあやまちの そのたび ひとは

ただ青い空の 青さを知る

果てしなく 道は続いて見えるけれど

この両手は 光を抱ける

さよならのときの 静かな胸

さよならのときの 静かな胸

ゼロになるからだが 耳をすませる

生きている不思議 死んでいく不思議

花も風も街も みんなおなじ

nananan lalala lululu

nananan lalala lululu

呼んでいる 胸のどこか奥で

いつも何度でも 夢を描こう

かなしみの数を 言い尽くすより

同じくちびるで そっとうたおう

閉じていく思い出の そのなかにいつも

閉じていく思い出の そのなかにいつも

忘れたくない ささやきを聞く

こなごなに砕かれた 鏡の上にも

新しい景色が 映される

はじまりの朝の静かな窓

はじまりの朝の静かな窓

ゼロになるからだ 充たされてゆけ

海の彼方には もう探さない

輝くものは いつもここに

わたしのなかに 見つけられたから

nananan lalala lululu

nananan lalala lululu