DP好慢?那可能是你不懂得单调队列与斜率优化!!!

单调队列

初步讲解

见例题:

【题意】
给定一个n个数的数列,从左至右输出每个长度为m的数列段内的最大数。
比如8个数的数列[1 3 -1 -3 5 3 6 7],m=3,那么每连续3个最大值如下:

[1  3  -1] -3  5  3  6  7  
最大值:3 

 1 [3  -1  -3] 5  3  6  7  
最大值:3

 1  3 [-1  -3  5] 3  6  7  
最大值:5 

 1  3  -1 [-3  5  3] 6  7  
最大值:5 

 1  3  -1  -3 [5  3  6] 7  
最大值:6 

 1  3  -1  -3  5 [3  6  7] 
最大值:7 

【输入格式】
第一行两个整数n和m( 1<= n <= 20 0000,m<=n)。
下来给出n个整数。
【输出格式】
一行一个整数,表示每连续m个数的最大值。
【样例输入】
8 3
1 3 -1 -3 5 3 6 7
【样例输出】
3
3
5
5
6
7

那么怎么做这一道题目呢?

想到这个题目,一开始想到的肯定是暴力,但是 O ( n 2 ) O(n^2) O(n2)肯定过不了,线段树 O ( n l o g ) O(nlog) O(nlog)卡过?没错,这肯定是整解。

但是杀猪用牛刀就不好了,有没有既短又简单的代码?物理说过,鱼和熊掌不可兼得

单调队列便是最好的选择了。

考虑没有时间的限制,那么我们肯定是一直 m a x max max取最大值了,但是加个时间的限制怎么整?不可能说当最大值超出我目前的范围就暴力在目前范围在找一个吧(一个从小到大的队列便可以卡住了)。

接着我们的思路想,如果我们不是暴力找,而是就在找过的过程中找到一些比前面的最大值小的最大值(大的话直接代替)的数字并且存入队列中,那么当前面的最大值没用了的情况下就可以使用这个数字了。

在完全理解了下面代码的情况下就会发现其实单调队列就是维护队中数字降序排序。

#include<cstdio>
#include<cstring>
#define  N  210000
using  namespace  std;
int  a[N],list[N]/*队列*/,head/*队列头*/,tail/*队列尾*/,n,m;
int  main()
{
	scanf("%d%d",&n,&m);
	for(int  i=1;i<=n;i++)scanf("%d",&a[i]);
	head=1;
	for(int  i=1;i<=n;i++)
	{
		while(a[list[tail]]<=a[i]  &&  tail>=head)tail--;//我的这个数字比你们以前的大,而且时间比你们还新,就可以踢掉你们了
		while(i-list[head]+1>m  &&  tail>=head)head++;//时间到了,再见
		list[++tail]=i;//将自己添加进去
		if(i>=m)printf("%d\n",a[list[head]]);//输出最大的队头
	}
	printf("\n");
	return  0;
}

单调队列适用于优化什么DP呢?

单调队列可以将复杂度降一个档次,比如从 O ( n 2 ) O(n^2) O(n2)降到 O ( n ) O(n) O(n)

但是他比较适用于有一些限制条件(如本题的时间),用来踢出队头,如果没有这个,一般就可以直接设一个数字取最值了。

同时一般会取一些最大值或最小值,用来踢出队尾,当然,还有些情况的话,你只要保证当前情况踢出的数字对后面没有影响,应该也是可以用单调队列的。(做过一道这样的题目,忘了。)

进入实战

1

【题意】
    从N个数(N<=100000)中选若干个数,要求在连续M(M<=N)个数里至少要有一个数被选择。 求选出来数的最小总和。
 【输入格式】
    第一行两个整数 N,M;
    接下来N个数ai(ai<=100)表示第i个数。
 【输出格式】
    一个整数,最小总和。
 【样例输入】
5 3
 1
 2
 5
 6
 2
【样例输出】
4

f [ i ] f[i] f[i]代表在 1 1 1~ i i i中选数字并且选自己的最小值,不难想到DP方程: f [ i ] = m i n ( f [ j ] + a [ i ] ) f[i]=min(f[j]+a[i]) f[i]=min(f[j]+a[i]),而这里的 f [ j ] f[j] f[j]便可以用单调队列优化了。

#include<cstdio>
#include<cstring>
#define  N  210000
using  namespace  std;
int  a[N],list[N],head,tail,n,m,dp[N];
int  main()
{
	scanf("%d%d",&n,&m);
	for(int  i=1;i<=n;i++)scanf("%d",&a[i]);
	list[head=tail=1]=0;
	int  ans=999999999;
	for(int  i=1;i<=n;i++)
	{
		dp[i]=dp[list[head]]+a[i];//直接转移,然后将dp[i]塞入队列
		while(dp[list[tail]]>=dp[i]  &&  tail>=head)tail--;
		while(i-list[head]+1>m  &&  tail>=head)head++;
		list[++tail]=i;
		if(n-i+1<=m  &&  dp[i]<ans)ans=dp[i];
	}
	printf("%d\n",ans);
	return  0;
}

2

【题意】 
     一个数列有N(1 <= N <= 100,000)个数(0<=ai<=10^9),要求从中选若干个,使得选出来的数之和最大。不能选中超过连续M(M<=N)个。  
【输入格式】 
     第一行两个整数N和M;
     下来给出n个数。
 【输出格式】 
     一行一个整数,表示最大的和。 
【样例输入】 
5 2 
 1 
 2 
 3 
 4 
 5 
【样例输出】 
12 

看着很眼熟,实际是真的很眼熟。

不能连续选超过 m m m个,意思不就是每 m + 1 m+1 m+1个数中选一个数,使总和最小。

#include<cstdio>
#include<cstring>
#define  N  210000
using  namespace  std;
typedef  long  long  LL;
LL  a[N],list[N],dp[N];
int  head,tail,n,m;
int  main()
{
	LL  sum=0;
    scanf("%d%d",&n,&m);m++;
    for(int  i=1;i<=n;i++){scanf("%lld",&a[i]);sum+=a[i];}
    list[head=tail=1]=0;
    LL  ans=LL(99999999999999);
    for(int  i=1;i<=n;i++)
    {
        dp[i]=dp[list[head]]+a[i];
        while(dp[list[tail]]>=dp[i]  &&  tail>=head)tail--;
        while(i-list[head]+1>m  &&  tail>=head)head++;
        list[++tail]=i;
        if(n-i+1<=m  &&  dp[i]<ans)ans=dp[i];
    }
    printf("%lld\n",sum-ans);//最后减出答案。
    return  0;
}

3

题目描述

【题意】
    一个猴子要吃香蕉,一共N棵香蕉树排列在一条直线上,它一开始在第一棵树上。
    每棵树上有不同数量的香蕉,猴子每次最多的跳跃距离为D,而且最多只能跳M次,问猴子最多能吃到多少香蕉?
 【输入格式】
    第一行 三个整数 N,D,M (M<N<=5000,D<=10000);
    下面N行 每行两个整数 ai,bi (ai,bi<=1000000,ai为正整数) 分别表示每棵树上的香蕉数目,以及每棵树的位置(树的位置是递增的)。
    数据保证没有两棵香蕉树在同一位置,以及b[1]=0。
 【输出格式】
    一个整数,表示猴子最多吃到的香蕉数。
 【样例输入】
5 5 2
 6 0
 8 3
 4 5
 6 7
 9 10
【样例输出】
20

这道题目还是比较恶心的,我们设 f f f数组 f [ i ] [ j ] f[i][j] f[i][j]表示在 j j j步之内跳到 i i i所能摘到香蕉的最大值。

我们知道猴子一直往右边跳才是更优的,回头就不会更优了

不难想出一个 O ( m n 2 ) O(mn^2) O(mn2)的做法。

f [ i ] [ k ] = m i n ( f [ j ] [ k − 1 ] + a [ i ] ) f[i][k]=min(f[j][k-1]+a[i]) f[i][k]=min(f[j][k1]+a[i])

不能想到,用单调队列维护 f [ j ] [ k − 1 ] f[j][k-1] f[j][k1]的最大值,消掉 j j j

O ( n m ) O(nm) O(nm)的时间可以过了,同时将单调队列设成结构体,将 f [ ] [ ] f[][] f[][]滚掉一维。

再卡一些毒瘤的优化就可以跑的飞快了

#include<cstdio>
#include<cstring>
#define  N  5100
using  namespace  std;
struct  node
{
	int  x/*位置*/,p/*能摘到的香蕉数*/;
}list[N];int  head,tail;
int  n,m,q,a[N],b[N],dp[N];
int  main()
{
	scanf("%d%d%d",&n,&q,&m);
	for(int  i=1;i<=n;i++){scanf("%d%d",&a[i],&b[i]),dp[i]=-1;}
	dp[1]=a[1];
	for(int  i=1;i<=m;i++)
	{
		head=1;tail=1;list[head].p=b[i];list[head].x=dp[i];//dp[1]~dp[i]中,最大的肯定是dp[i]
		for(int  j=i+1/*dp[1]~dp[i]肯定都不变了了*/;j<=n;j++)
		{
			while(head<=tail  &&  b[j]-list[head].p>q)head++;
			if(head>tail)break;//后面的跳不到了
			int  x=list[head].x;
			if(dp[j]!=-1)//在更新之前提前把dp[j]塞进去
			{
				while(head<=tail  &&  dp[j]>=list[tail].x)tail--;
				list[++tail].p=b[j];list[tail].x=dp[j];
			}
			dp[j]=x+a[j];
		}
		if(dp[i+1]==-1)break;//后面的都跑不到了,不加会WA,不信试试,调试一下就知道为什么了。
	}
	int  ans=0;
	for(int  i=1;i<=n;i++)
	{
		if(dp[i]>ans)ans=dp[i];
	}
	printf("%d\n",ans);
	return  0;
}

4

【题意】
    给一个N*M的数矩阵。求一个子矩阵,要求子矩阵中最大值与最小值的差<=C,而且子矩阵的宽(横)不超过100,长(竖)没有限制。
    求子矩阵的最大面积。
 【输入格式】
    第一行两个整数 M(左右方向),N(上下方向)和 C  (N,M<=500 0<=C<= 10 );
    接下来 N行 每行M个整数(每个数的范围为-30000至30000)。
 【输出格式】
    子矩阵的最大面积(长*宽)。
 【样例输入】
10 15 4
 41 40 41 38 39 39 40 42 40 40
 39 40 43 40 36 37 35 39 42 42
 44 41 39 40 38 40 41 38 35 37
 38 38 33 39 36 37 32 36 38 40
 39 40 39 39 39 40 40 41 43 41
 39 40 41 38 39 38 39 39 39 42
 36 39 39 39 39 40 39 41 40 41
 31 37 36 41 41 40 39 41 40 40
 40 40 40 42 41 40 39 39 39 39
 42 40 44 40 38 40 39 39 37 41
 41 41 40 39 39 40 41 40 39 40
 47 45 49 43 43 41 41 40 39 42
 42 41 41 39 40 39 42 40 42 42
 41 44 49 43 46 41 42 41 42 42
 45 40 42 42 46 42 44 40 42 41
【样例输出】
35

先给出题人寄刀片

这道题目的话难就难在DP方程,同时单调队列的运用也十分的巧妙。

我们找到两个列的编号 l , r ( l < = r ) l,r(l<=r) l,r(l<=r),然后在 l − > r l->r l>r的这边区域内找到两个行,构成一个矩形,怎么找到两行,也暴力找,不,先把每行在 l − > r l->r l>r区域内的最大值与最小值处理出来,然后枚举一行,在他上面用单调队列找是或否有合法的行是的最大值减最小值小于等于 c c c

当然,用了两个单调队列。

时间复杂度 O ( 100 n m ) O(100nm) O(100nm)

贴贴代码理解:

#include<cstdio>
#include<cstring>
#define  N  510
using  namespace  std;
inline  int  mymin(int  x,int  y){return  x<y?x:y;}
inline  int  mymax(int  x,int  y){return  x>y?x:y;}
int  mmin[N],mmax[N],n,m,c,a[N][N];
struct  node
{
	int  list[N],head,tail;
}mx,mn;
int  main()
{
	scanf("%d%d%d",&m,&n,&c);
	for(int  i=1;i<=n;i++)
	{
		for(int  j=1;j<=m;j++)scanf("%d",&a[i][j]);
	}
	int  ans=1;
	for(int  l=1;l<=m;l++)
	{
		for(int  i=1;i<=n;i++)mmin[i]=mmax[i]=a[i][l];
		int  ed=mymin(l+99,m);//不超过100列
		for(int  r=l;r<=ed;r++)
		{
			for(int  i=1;i<=n;i++)mmin[i]=mymin(mmin[i],a[i][r]),mmax[i]=mymax(mmax[i],a[i][r]);//更新最大最小值
			mx.head=mn.head=1;mx.tail=mn.tail=0;
			int  j=1;//目前我们选的上面的行
			for(int  i=1;i<=n;i++)//枚举下面的行
			{
				while(mx.head<=mx.tail  &&  mmax[mx.list[mx.tail]]<mmax[i])mx.tail--;//假设我的最大值大于你的最大值,把你踢出去
				while(mn.head<=mn.tail  &&  mmin[mn.list[mn.tail]]>mmin[i])mn.tail--;
				mx.list[++mx.tail]=i;mn.list[++mn.tail]=i;//假设我的最小值小于你的最大值,把你踢出去
				for(;mmax[mx.list[mx.head]]-mmin[mn.list[mn.head]]>c  &&  j<=i;j++)
				{
					while(mx.head<=mx.tail  &&  mx.list[mx.head]<=j)mx.head++;
					while(mn.head<=mn.tail  &&  mn.list[mn.head]<=j)mn.head++;
					//for循环先执行第一句,判断第二句进去循环,结尾第三句,然后再第二句,所以是<=j
				}
				ans=mymax(ans,(i-j+1)*(r-l+1));
			}
		}
	}
	printf("%d\n",ans);
	return  0;
}

大家肯定很好奇为什么我的最大值大于你要把你踢出去。

首先考虑一下。

假设一个行的最大值小于下一行的,最小值大于下一行的,那么是不是选下一行时可以直接选这一行?

同理,这里单调队列里面的最大值不仅代表了自己,还代表了前面最大值比自己小的行。最小值也差不多,同时,用 j + + j++ j++不断放宽可以淘汰的行数。

同时与下一行构成的矩形肯定没 j j j小,所以 j + + j++ j++了对后面没有影响。

不然为什么说这里的单调队列很妙。

斜率优化

有时候有些问题单调队列也用不了,为什么?因为有时候可能这些数值收到了 i i i的影响,导致有些数字有时会大于有些数字,有时又小于某些数字。

但是斜率优化,能利用单调队列精准的找到一些绝对没用的状态。

例题

例题

不难想到DP方程( s u m sum sum数组为前缀和数组):
f [ i ] = m i n ( f [ j ] + ( L − ( s u m [ i ] − s u m [ j ] + ( i − j − 1 ) ) ) 2 ) f[i]=min(f[j]+(L-(sum[i]-sum[j]+(i-j-1)))^2) f[i]=min(f[j]+(L(sum[i]sum[j]+(ij1)))2)
f [ i ] = m i n ( f [ j ] + ( L − s u m [ i ] + s u m [ j ] − i + j + 1 ) 2 ) f[i]=min(f[j]+(L-sum[i]+sum[j]-i+j+1)^2) f[i]=min(f[j]+(Lsum[i]+sum[j]i+j+1)2)
f [ i ] = m i n ( f [ j ] + ( L + 1 − s u m [ i ] − i + s u m [ j ] + j ) 2 ) f[i]=min(f[j]+(L+1-sum[i]-i+sum[j]+j)^2) f[i]=min(f[j]+(L+1sum[i]i+sum[j]+j)2)
L ′ = L + 1 , s [ i ] = s u m [ i ] + i L'=L+1,s[i]=sum[i]+i L=L+1,s[i]=sum[i]+i
f [ i ] = m i n ( f [ j ] + ( L ′ − s [ i ] + s [ j ] ) 2 ) f[i]=min(f[j]+(L'-s[i]+s[j])^2) f[i]=min(f[j]+(Ls[i]+s[j])2)
f [ i ] = m i n ( f [ j ] + ( L ′ − s [ i ] + s [ j ] ) 2 ) f[i]=min(f[j]+(L'-s[i]+s[j])^2) f[i]=min(f[j]+(Ls[i]+s[j])2)

那么怎么用斜率优化呢?我们考虑两种方法,我比较习惯第二种。

  1. 公式法

假设存在 q < p < i q<p<i q<p<i,同时

f [ q ] + ( L ′ − s [ i ] + s [ q ] ) 2 > f [ p ] + ( L ′ − s [ i ] + s [ p ] ) 2 f[q]+(L'-s[i]+s[q])^2>f[p]+(L'-s[i]+s[p])^2 f[q]+(Ls[i]+s[q])2>f[p]+(Ls[i]+s[p])2
f [ q ] + ( s [ i ] − s [ q ] − L ′ ) 2 > f [ p ] + ( s [ i ] − s [ p ] − L ′ ) 2 f[q]+(s[i]-s[q]-L')^2>f[p]+(s[i]-s[p]-L')^2 f[q]+(s[i]s[q]L)2>f[p]+(s[i]s[p]L)2

那么 q q q未来有没有可能小于 p p p

那么设 s [ k ] s[k] s[k],那么无非 s [ k ] = s [ i ] + v s[k]=s[i]+v s[k]=s[i]+v

那么就是证:
f [ q ] + ( s [ i ] + v − s [ q ] − L ′ ) 2 > f [ p ] + ( s [ i ] + v − s [ p ] − L ′ ) 2 f[q]+(s[i]+v-s[q]-L')^2>f[p]+(s[i]+v-s[p]-L')^2 f[q]+(s[i]+vs[q]L)2>f[p]+(s[i]+vs[p]L)2
f [ q ] + v 2 + 2 ∗ v ( s [ i ] − s [ q ] − L ′ ) + ( s [ i ] − s [ q ] − L ′ ) 2 > f [ p ] + v 2 + 2 ∗ v ( s [ i ] − s [ p ] − L ′ ) + ( s [ i ] − s [ p ] − L ′ ) 2 f[q]+v^{2}+2*v(s[i]-s[q]-L')+(s[i]-s[q]-L')^2>f[p]+v^{2}+2*v(s[i]-s[p]-L')+(s[i]-s[p]-L')^2 f[q]+v2+2v(s[i]s[q]L)+(s[i]s[q]L)2>f[p]+v2+2v(s[i]s[p]L)+(s[i]s[p]L)2
f [ q ] + ( s [ i ] − s [ q ] − L ′ ) 2 + 2 ∗ v ( s [ i ] − s [ q ] − L ′ ) > f [ p ] + ( s [ i ] − s [ p ] − L ′ ) 2 + 2 ∗ v ( s [ i ] − s [ p ] − L ′ ) f[q]+(s[i]-s[q]-L')^2+2*v(s[i]-s[q]-L')>f[p]+(s[i]-s[p]-L')^2+2*v(s[i]-s[p]-L') f[q]+(s[i]s[q]L)2+2v(s[i]s[q]L)>f[p]+(s[i]s[p]L)2+2v(s[i]s[p]L)

所以只需要证 2 ∗ v ( s [ i ] − s [ q ] − L ′ ) > = 2 ∗ v ( s [ i ] − s [ p ] − L ′ ) 2*v(s[i]-s[q]-L')>=2*v(s[i]-s[p]-L') 2v(s[i]s[q]L)>=2v(s[i]s[p]L)就行了

2 ∗ v ( s [ i ] − s [ q ] − L ′ ) − 2 ∗ v ( s [ i ] − s [ p ] − L ′ ) = s [ i ] − s [ q ] − L ′ − s [ i ] + s [ p ] − L ′ = s [ p ] − s [ q ] 2*v(s[i]-s[q]-L')-2*v(s[i]-s[p]-L')=s[i]-s[q]-L'-s[i]+s[p]-L'=s[p]-s[q] 2v(s[i]s[q]L)2v(s[i]s[p]L)=s[i]s[q]Ls[i]+s[p]L=s[p]s[q]

因为 p > q p>q p>q,所以 s [ p ] > = s [ q ] s[p]>=s[q] s[p]>=s[q],所以 s [ p ] − s [ q ] > = 0 s[p]-s[q]>=0 s[p]s[q]>=0,所以 2 ∗ v ( s [ i ] − s [ q ] − L ′ ) > = 2 ∗ v ( s [ i ] − s [ p ] − L ′ ) 2*v(s[i]-s[q]-L')>=2*v(s[i]-s[p]-L') 2v(s[i]s[q]L)>=2v(s[i]s[p]L),得证。

那么我们现在就可以把这些数字存进一个单调队列里面了。

再推一波斜率方程(也就是 q < p q<p q<p什么情况下可以淘汰 q q q

f [ q ] + ( s [ i ] − s [ q ] − L ′ ) 2 > f [ p ] + ( s [ i ] − s [ p ] − L ′ ) 2 f[q]+(s[i]-s[q]-L')^2>f[p]+(s[i]-s[p]-L')^2 f[q]+(s[i]s[q]L)2>f[p]+(s[i]s[p]L)2
f [ p ] + ( s [ i ] − s [ p ] − L ′ ) 2 < f [ q ] + ( s [ i ] − s [ q ] − L ′ ) 2 f[p]+(s[i]-s[p]-L')^2<f[q]+(s[i]-s[q]-L')^2 f[p]+(s[i]s[p]L)2<f[q]+(s[i]s[q]L)2
f [ p ] + s [ p ] 2 + ( s [ i ] − L ′ ) 2 − 2 ∗ s [ p ] ( s [ i ] − L ′ ) < f [ q ] + s [ q ] 2 + ( s [ i ] − L ′ ) 2 − 2 ∗ s [ q ] ( s [ i ] − L ′ ) f[p]+s[p]^{2}+(s[i]-L')^{2}-2*s[p](s[i]-L')<f[q]+s[q]^{2}+(s[i]-L')^{2}-2*s[q](s[i]-L') f[p]+s[p]2+(s[i]L)22s[p](s[i]L)<f[q]+s[q]2+(s[i]L)22s[q](s[i]L)
f [ p ] + s [ p ] 2 − 2 ∗ s [ p ] ( s [ i ] − L ′ ) < f [ q ] + s [ q ] 2 − 2 ∗ s [ q ] ( s [ i ] − L ′ ) f[p]+s[p]^{2}-2*s[p](s[i]-L')<f[q]+s[q]^{2}-2*s[q](s[i]-L') f[p]+s[p]22s[p](s[i]L)<f[q]+s[q]22s[q](s[i]L)
( f [ p ] + s [ p ] 2 ) − ( f [ q ] + s [ q ] 2 ) < 2 ( s [ i ] − L ′ ) ∗ ( s [ p ] − s [ q ] ) (f[p]+s[p]^{2})-(f[q]+s[q]^{2})<2(s[i]-L')*(s[p]-s[q]) (f[p]+s[p]2)(f[q]+s[q]2)<2(s[i]L)(s[p]s[q])
( f [ p ] + s [ p ] 2 ) − ( f [ q ] + s [ q ] 2 ) ( s [ p ] − s [ q ] ) < 2 ( s [ i ] − L ′ ) \frac{(f[p]+s[p]^{2})-(f[q]+s[q]^{2})}{(s[p]-s[q])}<2(s[i]-L') (s[p]s[q])(f[p]+s[p]2)(f[q]+s[q]2)<2(s[i]L)

就推完了,你没有听错,推完了。

那么我们不难发现设 i i i点的坐标为 ( s [ i ] , f [ i ] + s [ i ] 2 ) (s[i],f[i]+s[i]^{2}) (s[i],f[i]+s[i]2),那么 ( f [ p ] + s [ p ] 2 ) − ( f [ q ] + s [ q ] 2 ) ( s [ p ] − s [ q ] ) \frac{(f[p]+s[p]^{2})-(f[q]+s[q]^{2})}{(s[p]-s[q])} (s[p]s[q])(f[p]+s[p]2)(f[q]+s[q]2)就是斜率。

一般右边是由 i i i决定的数值,而左边的数值是固定的,因为 2 ( s [ i ] − L ′ ) 2(s[i]-L') 2(s[i]L)是递增的,所以就可以心安理得的用这个条件用来踢队头


但是队尾怎么踢?

如果新进来的一个点与 l i s t [ t a i l ] list[tail] list[tail]的斜率小于 l i s t [ t a i l ] list[tail] list[tail] l i s t [ t a i l − 1 ] list[tail-1] list[tail1]的斜率。

那么就踢出 l i s t [ t a i l ] list[tail] list[tail],同时继续踢,直到斜率保持升序。

为什么可以踢 l i s t [ t a i l ] list[tail] list[tail]?我们证一下,设新点与 l i s t [ t a i l ] list[tail] list[tail]的斜率为 a a a l i s t [ t a i l ] list[tail] list[tail] l i s t [ t a i l − 1 ] list[tail-1] list[tail1]的斜率为 b b b,当 2 ( s [ i ] − L ′ ) > a 2(s[i]-L')>a 2(s[i]L)>a时,选 l i s t [ t a i l ] list[tail] list[tail],但因为 a > b a>b a>b,所以 2 ( s [ i ] − L ′ ) > b 2(s[i]-L')>b 2(s[i]L)>b,所以选新点更优,所以就没 l i s t [ t a i l ] list[tail] list[tail]什么事了。

但同时我们也需要证一下 b < b< b<新点与 l i s t [ t a i l − 1 ] list[tail-1] list[tail1]的斜率 < a <a <a(当 a = b a=b a=b时新点与 l i s t [ t a i l − 1 ] list[tail-1] list[tail1]的斜率 = a =a =a)。

也就是说新点与 l i s t [ t a i l ] 、 l i s t [ t a i l − 1 ] 、 l i s t [ t a i l − 2 ] . . . list[tail]、list[tail-1]、list[tail-2]... list[tail]list[tail1]list[tail2]...构成的斜率递增或不变。

反证一下(机房大佬教的,同时 " = " "=" "="的情况仅当 b = c b=c b=c时成立,在此不多做赘述):

设单调队列中有四个点 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4 1 , 2 1,2 1,2斜率为 a a a 2 , 3 2,3 2,3斜率为 b b b 3 , 4 3,4 3,4斜率为 c c c 2 , 4 2,4 2,4斜率为 d d d

满足 a < b < c a<b<c a<b<c,同时 a > d a>d a>d

那么当 a < 2 ( s [ i ] − L ′ ) < b a<2(s[i]-L')<b a<2(s[i]L)<b时,按照规矩,从 a < b < c a<b<c a<b<c中看出是选 2 2 2号点,当 d < b d<b d<b时,那么存在一个 2 ( s [ i ] − L ′ ) 2(s[i]-L') 2(s[i]L)使得 d < 2 ( s [ i ] − L ′ ) d<2(s[i]-L') d<2(s[i]L),所以应该选 4 4 4号点,但是又不符合规律了,所以 d > b d>b d>b

那么当 2 ( s [ i ] − L ′ ) > c 2(s[i]-L')>c 2(s[i]L)>c时,按照规矩,从 a < b < c a<b<c a<b<c中看出是选 4 4 4号点,当 d > c d>c d>c的话,存在一个 2 ( s [ i ] − L ′ ) 2(s[i]-L') 2(s[i]L)使得 d > 2 ( s [ i ] − L ′ ) > c d>2(s[i]-L')>c d>2(s[i]L)>c,所以这是又要选 2 2 2号点,矛盾,所以 d < c d<c d<c

所以 b < d < c b<d<c b<d<c

#include<cstdio>
#include<cstring>
using  namespace  std;
typedef  long  long  ll;
int  list[51000],head,tail;
ll  f[51000],s[51000],L,n;
inline  double  X(int  x){return  double(s[x]);}
inline  double  Y(int  x){return  double(f[x]+s[x]*s[x]);}
inline  double  xielv(int  x,int  y){return  (Y(y)-Y(x))/(X(y)-X(x));}
int  main()
{
	scanf("%lld%lld",&n,&L);L++;
	for(int  i=1;i<=n;i++)
	{
		ll  x;scanf("%lld",&x);
		s[i]=s[i-1]+x+1;
	}
	head=tail=1;list[1]=0;
	for(int  i=1;i<=n;i++)
	{
		while(head<tail  &&  xielv(list[head],list[head+1])<=2*(s[i]-L))head++;//队头不行了,踢队头
		int  j=list[head];
		f[i]=f[j]+(s[i]-s[j]-L)*(s[i]-s[j]-L);
		while(head<tail  &&  xielv(list[tail-1],list[tail])>=xielv(list[tail-1],i))tail--;//维持斜率升序
		list[++tail]=i;
	}
	printf("%lld",f[n]);
	return  0;
}
  1. 图像法

这是一个特别好用的方法,适合那些比较懒的人QAQ。

首先,先把DP方程化成一个样子。

f [ i ] = f [ j ] + ( L ′ − s [ i ] + s [ j ] ) 2 f[i]=f[j]+(L'-s[i]+s[j])^2 f[i]=f[j]+(Ls[i]+s[j])2
f [ i ] = f [ j ] + s [ i ] 2 − 2 ∗ s [ i ] ( L ′ + s [ j ] ) + ( L ′ + s [ j ] ) 2 f[i]=f[j]+s[i]^{2}-2*s[i](L'+s[j])+(L'+s[j])^2 f[i]=f[j]+s[i]22s[i](L+s[j])+(L+s[j])2
f [ i ] = f [ j ] − 2 ∗ s [ i ] ( L ′ + s [ j ] ) + ( L ′ + s [ j ] ) 2 f[i]=f[j]-2*s[i](L'+s[j])+(L'+s[j])^2 f[i]=f[j]2s[i](L+s[j])+(L+s[j])2(删掉由 i i i决定的定值 s [ i ] 2 s[i]^{2} s[i]2一般不会有影响)
f [ j ] + ( L ′ + s [ j ] ) 2 = f [ i ] + 2 ∗ s [ i ] ( L ′ + s [ j ] ) f[j]+(L'+s[j])^2=f[i]+2*s[i](L'+s[j]) f[j]+(L+s[j])2=f[i]+2s[i](L+s[j])
f [ j ] + ( L ′ + s [ j ] ) 2 = 2 ∗ s [ i ] ( L ′ + s [ j ] ) + f [ i ] f[j]+(L'+s[j])^2=2*s[i](L'+s[j])+f[i] f[j]+(L+s[j])2=2s[i](L+s[j])+f[i]

那么我们设 f [ j ] + ( L ′ + s [ j ] ) 2 = y , x = L ′ + s [ i ] , k = 2 ∗ s [ i ] , b = f [ i ] f[j]+(L'+s[j])^2=y,x=L'+s[i],k=2*s[i],b=f[i] f[j]+(L+s[j])2=y,x=L+s[i],k=2s[i],b=f[i],那么我们的目标就是让 b b b最小,而这条式子中不仅代表了一个 j j j一个点,也代表了一个 j j j对于一个 i i i是一条直线。


构造此时的方法:

一般在式子中一些与 j j j无关的定值以及由 i i i决定的定值一般是可以去除的,推久了就有这种感觉了。

同时只与 j j j有关的单项式通通移到左边,也就是说 i i i不管怎么换,对于一个 j j j的话 y y y是不变的。

同时对于同一个 j j j x x x也是不变的,但是 k k k i i i的变化而变化,而 b b b一般就是我们要求的东西。

同时,我们一般会保证 x x x递增, k k k递增或递减,然后手动维护 y y y的递增或递减。

比如这题用公式法的单调队列维护的是斜率递增,那么仔细想想,其实也就是符合某种规律的 y y y递增。

当然,公式法和图像法本质是相同的,思考方式不同罢了。

等学会图像法以后会发现构造方程的方式是有异曲同工之妙的。


在这道题, s [ i ] s[i] s[i] i i i增大而增大,所以 x x x是递增的, k k k也是递增的。

那么我们通过画图来模拟当两个点 A , B ( x A < x B ) A,B(x_{A}<x_{B}) A,B(xA<xB) k k k的增加时选A、B的变化。

设A、B两点构成的斜率为 d d d,当 k < b k<b k<b时,很明显 A A A点都会更加优秀。

在这里插入图片描述

但是当 k = d k=d k=d时,就都一样了。

在这里插入图片描述

一旦 k > d k>d k>d的话,就是 B B B点将会更加优秀,那么很明显就是踢 A A A点了
在这里插入图片描述
于是就可以不断的去踢队头了。

这也是为什么要维持单调队列斜率升序的原因。

那么如果队尾插入的一个数字 i i i,与 l i s t [ t a i l ] list[tail] list[tail]的斜率小于 l i s t [ t a i l ] 与 l i s t [ t a i l − 1 ] list[tail]与list[tail-1] list[tail]list[tail1]的斜率怎么办?

在这里插入图片描述

可以看出,当斜率大于等于 l i s t [ t a i l ] list[tail] list[tail] l i s t [ t a i l − 1 ] list[tail-1] list[tail1]的话,那么也大于 l i s t [ t a i l − 1 ] list[tail-1] list[tail1] i i i的斜率,所以 i i i会更优秀,所以 l i s t [ t a i l ] list[tail] list[tail]就废了。

以此为条件踢 l i s t [ t a i l ] list[tail] list[tail]

#include<cstdio>
#include<cstring>
using  namespace  std;
typedef  long  long  ll;
int  list[51000],head,tail;
ll  f[51000],s[51000],L,n;
inline  double  X(int  x){return  double(s[x]);}
inline  double  Y(int  x){return  double(f[x]+s[x]*s[x]);}
inline  double  xielv(int  x,int  y){return  (Y(y)-Y(x))/(X(y)-X(x));}
int  main()
{
	scanf("%lld%lld",&n,&L);L++;
	for(int  i=1;i<=n;i++)
	{
		ll  x;scanf("%lld",&x);
		s[i]=s[i-1]+x+1;
	}
	head=tail=1;list[1]=0;
	for(int  i=1;i<=n;i++)
	{
		while(head<tail  &&  xielv(list[head],list[head+1])<=2*(s[i]-L))head++;//队头不行了,踢队头
		int  j=list[head];
		f[i]=f[j]+(s[i]-s[j]-L)*(s[i]-s[j]-L);
		while(head<tail  &&  xielv(list[tail-1],list[tail])>=xielv(list[tail-1],i))tail--;//维持斜率升序
		list[++tail]=i;
	}
	printf("%lld",f[n]);
	return  0;
}

实战练习

1

1

这道题目比例题还简单。

f [ i ] f[i] f[i]表示在 i i i建仓库前面的最小值。

t [ i ] t[i] t[i]表示将 1 − ( i − 1 ) 1-(i-1) 1(i1)的物品运到 i i i的费用, s [ i ] s[i] s[i]表示 1 − i 1-i 1i p p p值相加, q w q [ i ] = s [ i ] ∗ x [ i + 1 ] qwq[i]=s[i]*x[i+1] qwq[i]=s[i]x[i+1]

f [ i ] = m i n ( f [ j ] + ( t [ i ] − t [ j + 1 ] ) − s [ j ] ∗ ( x [ i ] − x [ j + 1 ] ) + c [ i ] ) f[i]=min(f[j]+(t[i]-t[j+1])-s[j]*(x[i]-x[j+1])+c[i]) f[i]=min(f[j]+(t[i]t[j+1])s[j](x[i]x[j+1])+c[i])
f [ i ] = f [ j ] + ( t [ i ] − t [ j + 1 ] ) − s [ j ] ∗ x [ i ] + s [ j ] ∗ x [ j + 1 ] + c [ i ] f[i]=f[j]+(t[i]-t[j+1])-s[j]*x[i]+s[j]*x[j+1]+c[i] f[i]=f[j]+(t[i]t[j+1])s[j]x[i]+s[j]x[j+1]+c[i]
f [ i ] = f [ j ] − t [ j + 1 ] − s [ j ] ∗ x [ i ] + q w q [ j ] f[i]=f[j]-t[j+1]-s[j]*x[i]+qwq[j] f[i]=f[j]t[j+1]s[j]x[i]+qwq[j]
f [ j ] − t [ j + 1 ] + q w q [ j ] = x [ i ] ∗ s [ j ] + f [ i ] f[j]-t[j+1]+qwq[j]=x[i]*s[j]+f[i] f[j]t[j+1]+qwq[j]=x[i]s[j]+f[i]

正常的斜率优化。

#include<cstdio>
#include<cstring>
#define  N  1100000
using  namespace  std;
typedef  long  long  LL;
int  n;
LL  x[N],p[N],c[N],t[N],qwq[N],s[N],f[N],mmin;
int  head,tail,list[N];
inline  double  X(int  x){return  double(s[x]);}
inline  double  Y(int  x){return  double(f[x]-t[x+1]+qwq[x]);}
inline  double  sl(int  x,int  y){return  (Y(x)-Y(y))/(X(x)-X(y));}
inline  LL  mymin(LL  x,LL  y){return  x<y?x:y;}
int  main()
{
	scanf("%d",&n);
	for(int  i=1;i<=n;i++)scanf("%lld%lld%lld",&x[i],&p[i],&c[i]);
	for(int  i=1;i<=n;i++)
	{
		s[i]=s[i-1]+p[i];
		t[i]=t[i-1]+s[i-1]*(x[i]-x[i-1]);
		qwq[i]=s[i]*x[i+1];
	}
	head=tail=1;list[1]=0;
	for(int  i=1;i<=n;i++)
	{
		while(head<tail  &&  sl(list[head],list[head+1])<double(x[i]))head++;
		int  now=list[head];
		f[i]=f[now]+(t[i]-t[now+1])-s[now]*(x[i]-x[now+1])+c[i];
		while(head<tail  &&  sl(list[tail-1],list[tail])>sl(list[tail],i))tail--;
		list[++tail]=i;
	}
	printf("%lld",f[n]);
	return  0;
}

2

毒瘤题

首先,如果一个土地的长宽都小于另外一块土地,这块土地就废了,然后就可以得到一个 a a a递增(第一个数字), b b b递减(第二个数字)。

所以 f [ i ] = f [ j ] + ( a [ i ] ∗ b [ j + 1 ] ) f[i]=f[j]+(a[i]*b[j+1]) f[i]=f[j]+(a[i]b[j+1])

所以 f [ j ] = a [ i ] ∗ ( − b [ j + 1 ] ) + f [ i ] f[j]=a[i]*(-b[j+1])+f[i] f[j]=a[i](b[j+1])+f[i]

仍然是 x = ( − b [ j + 1 ] ) x=(-b[j+1]) x=(b[j+1]),并且可以知道 x x x递增。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define  N  51000
using  namespace  std;
typedef  long  long  LL;
struct  node
{
	LL  a,b;
}t1[N],t2[N];
LL  sta,f[N];
bool  cmp(node  x,node  y){return  x.a==y.a?x.b<y.b:x.a<y.a;}
int  n,now,head,tail,list[N];
inline  double  X(int  x){return  double(-t2[x+1].b);}
inline  double  Y(int  x){return  double(f[x]);}
inline  double  sl(int  x,int  y){return  (Y(x)-Y(y))/(X(x)-X(y));}
int  main()
{
	scanf("%d",&n);
	for(int  i=1;i<=n;i++)scanf("%lld%lld",&t1[i].a,&t1[i].b);
	sort(t1+1,t1+1+n,cmp);
	for(int  i=n;i>=1;i--)
	{
		if(t1[i].b>sta)
		{
			sta=t1[i].b;
			t2[++now]=t1[i];
		}
	}//剔除一些不用找的土地
	n=now;
	sort(t2+1,t2+n+1,cmp);
	head=1;tail=1;list[1]=0;
	for(int  i=1;i<=n;i++)
	{
		while(head<tail  &&  sl(list[head],list[head+1])<t2[i].a)head++;
		now=list[head];
		f[i]=f[now]+t2[i].a*t2[now+1].b;
		while(head<tail  &&  sl(list[tail],list[tail-1])>sl(list[tail],i))tail--;
		list[++tail]=i;
	}
	printf("%lld\n",f[n]);
	return  0;
}

3

【题意】 
     有n个数,分成连续的若干段,每段的分数为a*x2+b*x+c(a,b,c是给出的常数),其中x为该段的各个数的和。
    求如何分才能使得各个段的分数的总和最大。
 【输入格式】 
     第1行:1个整数N (1 <= N <= 1000000)。
    第2行:3个整数a,b,c(-5<=a<=-1,b<=10000000,|c|<=10000000
     下来N个整数,每个数的范围为[1,100]。 
【输出格式】 
     一个整数,各段分数总和的值最大。 
【样例输入】
4
 -1 10 -20
 2 2 3 4
【样例输出】
9

裸题。

s s s数组为前缀和

f [ i ] = f [ j ] + a ∗ ( s [ i ] − s [ j ] ) 2 + b ∗ ( s [ i ] − s [ j ] ) + c f[i]=f[j]+a*(s[i]-s[j])^2+b*(s[i]-s[j])+c f[i]=f[j]+a(s[i]s[j])2+b(s[i]s[j])+c
f [ i ] = f [ j ] + a ∗ ( s [ i ] − s [ j ] ) 2 + b ∗ ( − s [ j ] ) f[i]=f[j]+a*(s[i]-s[j])^2+b*(-s[j]) f[i]=f[j]+a(s[i]s[j])2+b(s[j])
f [ i ] = f [ j ] + a ∗ ( s [ i ] 2 − 2 ∗ s [ i ] ∗ s [ j ] + s [ j ] 2 ) + b ∗ ( − s [ j ] ) f[i]=f[j]+a*(s[i]^2-2*s[i]*s[j]+s[j]^2)+b*(-s[j]) f[i]=f[j]+a(s[i]22s[i]s[j]+s[j]2)+b(s[j])
f [ i ] = f [ j ] + a ( s [ j ] 2 − 2 ∗ s [ i ] ∗ s [ j ] ) − b ∗ s [ j ] ; f[i]=f[j]+a(s[j]^2-2*s[i]*s[j])-b*s[j]; f[i]=f[j]+a(s[j]22s[i]s[j])bs[j];
f [ i ] = f [ j ] + a ∗ s [ j ] 2 − ( 2 ∗ a ∗ s [ i ] + b ) ∗ s [ j ] f[i]=f[j]+a*s[j]^2-(2*a*s[i]+b)*s[j] f[i]=f[j]+as[j]2(2as[i]+b)s[j]
f [ j ] + a ∗ s [ j ] 2 = f [ i ] + ( 2 ∗ a ∗ s [ i ] + b ) ∗ s [ j ] f[j]+a*s[j]^2=f[i]+(2*a*s[i]+b)*s[j] f[j]+as[j]2=f[i]+(2as[i]+b)s[j]

我们会发现 k = ( 2 ∗ a ∗ s [ i ] + b ) k=(2*a*s[i]+b) k=(2as[i]+b)是递减的,且是找最大值,用图像法的思路想想就知道该怎么踢队头队尾了。

记得用long long

题目的名字叫特别行动队APOI2010

#include<cstdio>
#include<cstring>
#define  N  2100000
using  namespace  std;
typedef  long  long  LL;
int  n;
LL  a,b,c,tt[N],f[N],s[N];
inline  double  X(int  x){return  double(s[x]);}
inline  double  Y(int  x){return  double(f[x]+a*s[x]*s[x]);}
inline  double  sl(int  x,int  y){return  (Y(x)-Y(y))/(X(x)-X(y));}
int  head,tail,list[N];
int  main()
{
	scanf("%d",&n);
	scanf("%lld%lld%lld",&a,&b,&c);
	for(int  i=1;i<=n;i++)
	{
		scanf("%lld",&tt[i]);
		s[i]=s[i-1]+tt[i];
	}
	head=1;tail=1;list[1]=0;
	for(int  i=1;i<=n;i++)
	{
		while(head<tail  &&  sl(list[head],list[head+1])>=(2*a*s[i]+b))head++;
		int  now=list[head];
		f[i]=f[now]+a*(s[i]-s[now])*(s[i]-s[now])+b*(s[i]-s[now])+c;
		while(head<tail  &&  sl(list[tail],list[tail-1])<=sl(list[tail],i))tail--;//维护斜率递减
		list[++tail]=i;
	}
	printf("%lld\n",f[n]);
	return  0;
}

小结

如果DP方程能化成只跟 i i i有关的式子和只跟 j j j有关的式子相加,那么大概率是单调队列。

如果DP方程能化成只跟 i i i有关的一个变量乘以只跟 j j j有关的一个变量,那么大概率是斜率优化。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值