学习笔记:斜率优化dp

上讲习题

AcWing 1087

本题可以想到,设 f i f_i fi为前 i i i头牛合法选择的最大效益。首先可以不选, f i = f i − 1 f_i=f_{i-1} fi=fi1。考虑选择第 i i i头牛,可以枚举一个 j j j表示 i i i开始往前连续的牛的数量,保证 1 ≤ j ≤ k 1\le j\le k 1jk即可。则这一段牛就是 ∑ x = i − j + 1 i \displaystyle\sum_{x=i-j+1}^i x=ij+1i,为了保证不选第 i − j i-j ij头牛,那么就是用 f i − j − 1 f_{i-j-1} fij1。于是有状态转移方程 f i = max ⁡ ( f i − 1 , max ⁡ ( f i − j − 1 + ∑ x = i − j + 1 i , 1 ≤ j ≤ k ) ) f_i=\max(f_{i-1},\max(f_{i-j-1}+\displaystyle\sum_{x=i-j+1}^i,1\le j\le k)) fi=max(fi1,max(fij1+x=ij+1i,1jk))。有了状态转移方程,发现有一个求区间和的过程,用前缀和。发现中间的 j j j求的是一个 max ⁡ \max max,可以用单调队列维护。

#include<bits/stdc++.h>
using namespace std;
const int NN=1e5+4;
long long s[NN],f[NN];
int q[NN];
int main()
{
    
    
	int n,k;
	scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++)
    {
    
    
    	scanf("%lld",&s[i]);
		s[i]+=s[i-1];
	}
	int h=0,t=0;
    for(int i=1;i<=n;i++)
	{
    
    
	    if(q[h]<i-k)
			h++;
    	f[i]=max(f[i-1],f[q[h]-1]+s[i]-s[q[h]]);
	    while(h<=t&&f[q[t]-1]-s[q[t]]<=f[i-1]-s[i])
			t--;
	    q[++t]=i;
	}
    printf("%lld",f[n]);
    return 0;
}

AcWing 1090

本题和上讲AcWing 1089非常像。考虑如何转换成那一题,发现本题中缺少的就是连续不选择的限制,这也刚好是本题要求的,那么就二分答案。然后有了这个限制,按那一题的做法计算最少用的代价,看看能不能有一种方法在 t t t的时间内完成,这就是二分答案的 c h e c k check check函数。

#include<bits/stdc++.h>
using namespace std;
const int NN=5*1e4+4;
int n,t,a[NN],f[NN],q[NN];
bool check(int mid)
{
    
    
	int head=0,tail=0;
	for(int i=1;i<=n;i++)
	{
    
    
		if(q[head]+mid+1<i)
			head++;
		f[i]=f[q[head]]+a[i];
		while(f[q[tail]]>=f[i]&&head<=tail)
			tail--;
		q[++tail]=i;
	}
	for(int i=n-mid;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",&a[i]);
	int l=1,r=n;
	while(l<r)
	{
    
    
		int mid=l+(r-l)/2;
		if(check(mid))
			r=mid;
		else
			l=mid+1;
	}
	printf("%d",r);
	return 0;
}

AcWing 1091

本题是一个二维的滑动窗口。可以先竖着求一遍在 n n n行内的最小值和最大值,这样就相当于把 n n n行压缩成一个数,这样就压缩了一维。然后再横着求一边一维的滑动窗口即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=1004;
int g[NN][NN],maxn[NN][NN],minn[NN][NN],q1[NN],q2[NN];
int main()
{
    
    
	int a,b,n,ans=1e9,h1,h2,t1,t2;
	scanf("%d%d%d",&a,&b,&n);
	for(int i=1;i<=a;i++)
		for(int j=1;j<=b;j++)
			scanf("%d",&g[i][j]);
	for(int i=1;i<=a;i++)
	{
    
    
		h1=1;
		h2=1;
		t1=0;
		t2=0;
		for(int j=1;j<=b;j++)
		{
    
    
			if(h1<=t1&&q1[h1]<=j-n)
				h1++;
			if(h2<=t2&&q2[h2]<=j-n)
				h2++;
			while(h1<=t1&&g[i][q1[t1]]<=g[i][j])
				t1--;
			while(h2<=t2&&g[i][q2[t2]]>=g[i][j])
				t2--;
			q1[++t1]=q2[++t2]=j;
			if(j>=n)
			{
    
    
				maxn[i][j-n+1]=g[i][q1[h1]];
				minn[i][j-n+1]=g[i][q2[h2]];
			}
		}
	}
	for(int i=1;i<=b-n+1;i++)
	{
    
    
		h1=1;
		h2=1;
		t1=0;
		t2=0;
		for(int j=1;j<=a;j++)
		{
    
    
			if(h1<=t1&&q1[h1]<=j-n)
				h1++;
			if(h2<=t2&&q2[h2]<=j-n)
				h2++;
			while(h1<=t1&&maxn[q1[t1]][i]<=maxn[j][i])
				t1--;
			while(h2<=t2&&minn[q2[t2]][i]>=minn[j][i])
				t2--;
			q1[++t1]=q2[++t2]=j;
			if(j>=n)
				ans=min(ans,maxn[q1[h1]][i]-minn[q2[h2]][i]);
		}
	}
	printf("%d",ans);
	return 0;
}

概念

斜率优化,指的是一些形如 y = k x + b y=kx+b y=kx+b的状态转移方程,利用 k k k的单调性的优化。其中, k k k b b b是常量。

方法

一般来说都会是和单调队列优化并用。首先,常量就是不变的数,而用于迭代的是找的最小的,所以不能放在 k k k b b b中,只能放在 x x x y y y中。得到了这样一个方程,每次加入一个点 i i i(横坐标 x x x,纵坐标 y y y)就说明 i i i可以作为以后的 j j j。在斜率相邻的两点之间连一条边。想找一个使 f i f_i fi最小的 j j j,那么就是这些边中,第一个斜率比 f i f_i fi的函数的斜率大的边的左端点,这个可以自己画图试一试,可以用二分来找。然后,判断自己是否在点内部,如果在外部就可以加入这个点集,看看从哪里连边斜率才单调递增并删掉其他的点,这些点都是会被包含在内的点。求最大值同理。

例题

AcWing 300

请注意,本题只是后面两题的铺垫,并不需要斜率优化。考虑状态转移,设 f i f_i fi表示完成前 i i i个任务所需的最小代价。枚举一个划分两批任务的分界点 j j j,则: f i = min ⁡ ( f j + ∑ k = j + 1 i c k × ∑ k = 1 i t k + s × ∑ k = j + 1 n c k , 0 ≤ j < i ) f_i=\min(f_j+\displaystyle\sum_{k=j+1}^i c_k\times \displaystyle\sum_{k=1}^i t_k+s\times \displaystyle\sum_{k=j+1}^n c_k,0\le j<i) fi=min(fj+k=j+1ick×k=1itk+s×k=j+1nck,0j<i)因为有求一段区间的和的过程,所以要用前缀和优化。

#include<bits/stdc++.h>
using namespace std;
const int NN=5004;
int t[NN],c[NN],f[NN];
int main()
{
    
    
    int n,s;
    scanf("%d%d",&n,&s);
    for(int i=1;i<=n;i++)
    {
    
    
        scanf("%d%d",&t[i],&c[i]);
        t[i]+=t[i-1];
        c[i]+=c[i-1];
    }
    memset(f,0x3f,sizeof(f));
    f[0]=0;
    for(int i=1;i<=n;i++)
        for(int j=0;j<i;j++)
            f[i]=min(f[i],f[j]+(c[i]-c[j])*t[i]+(c[n]-c[j])*s);
    printf("%d",f[n]);
    return 0;
}

AcWing 301

本题就需要按上述方法去斜率优化了。先计算状态转移方程,原方程: f i = f j + ( c i − c j ) × t k + s × ( c n − c j ) f_i=f_j+(c_i-c_j)\times t_k+s\times (c_n-c_j) fi=fj+(cicj)×tk+s×(cncj) f j = ( s + t i ) × c j + f i − t i × c i − s × c n f_j=(s+t_i)\times c_j+f_i-t_i\times c_i-s\times c_n fj=(s+ti)×cj+fiti×cis×cn于是,我们成功转换成上述形式。我们发现,本题中横坐标 x x x是单调递增的,所以新加的点一定在最右侧,所以一定不会包含在内,可以直接去删掉斜率较高的。我们又发现,斜率 s s s是不变的,而且 t t t是单调递增的,所以如果一条边比当前的斜率小,那么一定也比以后的斜率小,直接删除即可。最后,如何计算两个点连的边的斜率呢?纵坐标之差除以横坐标之差即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=3e5+4;
long long c[NN],t[NN],f[NN];
int q[NN];
double doit(int x,int y)
{
    
    
	return 1.0*(f[x]-f[y])/(c[x]-c[y]);
}
int main()
{
    
    
	int n,s;
	scanf("%d%d",&n,&s);
	for(int i=1;i<=n;i++)
	{
    
    
		scanf("%lld%lld",&t[i],&c[i]);
		t[i]+=t[i-1];
		c[i]+=c[i-1];
	}
	int head=0,tail=0;
	for(int i=1;i<=n;i++)
	{
    
    
		while(head<tail&&doit(q[head+1],q[head])<=s+t[i])
			head++;
		f[i]=f[q[head]]+t[i]*(c[i]-c[q[head]])+s*(c[n]-c[q[head]]);
		while(head<tail&&doit(q[tail],q[tail-1])>=doit(i,q[tail-1]))
			tail--;
		q[++tail]=i;
	}
	printf("%lld",f[n]);
	return 0;
}

AcWing 302

这个题目 t t t可能是负数,所以不保证斜率是单调递增的,所以不能删除斜率小的边,而且要二分。本题的精度特别变态,所以这里改成用乘法。

#include<bits/stdc++.h>
using namespace std;
const int NN=3e5+4;
int q[NN];
long long f[NN],t[NN],c[NN];
int main()
{
    
    
	int n,s;
    scanf("%d%d",&n,&s);
    for(int i=1;i<=n;i++)
    {
    
    
		scanf("%lld%lld",&t[i],&c[i]);
		t[i]+=t[i-1];
		c[i]+=c[i-1];
	}
	int head=0,tail=0;
    for(int i=1;i<=n;i++)
    {
    
    
		int l=head,r=tail;
		while(l<r)
		{
    
    
			int mid=l+r>>1;
			if(f[q[mid+1]]-f[q[mid]]>(s+t[i])*(c[q[mid+1]]-c[q[mid]]))
				r=mid;
			else
				l=mid+1;
		}
		int j=q[r];
		f[i]=f[j]-(s+t[i])*c[j]+t[i]*c[i]+s*c[n];
		while(head<tail&&(double)(f[q[tail]]-f[q[tail-1]])*(c[i]-c[q[tail]])>=(double)(f[i]-f[q[tail]])*(c[q[tail]]-c[q[tail-1]]))
			tail--;
		q[++tail]=i;
	}
	printf("%lld",f[n]);
	return 0;
}

习题

AcWing 303

解析和代码在下一篇博客——最短路给出

猜你喜欢

转载自blog.csdn.net/weixin_44043668/article/details/108947589
今日推荐