四边形不等式优化区间dp

31 篇文章 0 订阅

四边形不等式是一个很强有力的武器,往往可以在原本dp推导式中优化掉一个n的时间复杂度

区间dp里,假如dp[i,j]和cost函数满足四边形不等式的话:

dp[i][j]+dp[i+1][j+1]<=dp[i+1][j]+dp[i][j+1]

cost[i][j]+cost[i+1][j+1]<=cost[i][j+1]+cost[i+1][j]

一般能够证明其决策具有单调性

若令s[i,j]表示dp[i,j]取得最优解时的点,则此时有:

s[i][j-1]<=s[i][j]<=s[i+1][j]

利用这个,就可以大大简化时间复杂度 

在常见的dp问题中,有两种非常经典的递推式

dp[i,j]=dp[k,j-1]+cost(k+1,i)   0<=k<i

dp[i,j]=dp[i,k]+dp[k+1,j]+cost(i,j)     i<=k<=j

下面将会对其进行总结

邮局

大意:

n个原始点,在数轴上面放m个新点,求每一个原始点到任意一个新点的最短距离的最小值

思路:

首先得把dp转移式推出来

令dp[i,j]表示前i个原始点里插入j个新点时前i个点的最小答案,

则dp[i,j]=dp[k,j-1]+cost(k+1,i);    0<=k<i

k就是枚举i之前放j-1个点的位置,再加上k+1到i之间的点的答案即可

下面考虑cost函数,这里cost(i,j)函数代表:

在i和j之间只放一个新点时,i和j之间的点到它的最小距离,那这就是一个最简单的一维最短曼哈顿距离问题了,只要放在i,j的中位数的位置即可

这样我们能确保整个式子可以递推了

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3010;
ll n,m;
ll mas[N];
ll dp[N][310];//前i个村庄里放j个邮局 
ll f[N][N];//i与j之间放一个邮局时中间几个村庄到邮局的最近距离 
int main()
{
	cin>>n>>m;
	memset(dp,0x3f,sizeof dp);
	dp[0][0]=0;
	for(int i=1;i<=n;++i) cin>>mas[i];
	sort(mas+1,mas+1+n);
	for(int i=1;i<=n;++i)
	{
		for(int j=i+1;j<=n;++j)
		{
			f[i][j]=f[i][j-1]+mas[j]-mas[(i+j)/2]; 
		}
	}
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=m;++j)
		{
			for(int k=0;k<i;++k)
			{
			    dp[i][j]=min(dp[i][j],dp[k][j-1]+f[k+1][i]);	 
			} 
		}
	}
	cout<<dp[n][m]<<endl;
	return 0;
} 

考虑一下时间复杂度,求cost可以在n^2时间内解决掉,这中间可以同时枚举i,再加上枚举j的复杂度,是O(m*n^2),也就是差不多2e10的水平。。。

你不超时谁超时

那么四边形不等式就可以拿出来优化了(证明我不会,也许你可以打表验证)

只要在枚举k的时候,把左右界换成之前提到的最优决策数组s即可,这里我写成了d数组

最后的问题就是d的递推了。

这里i要逆序枚举,才能保证d[i+1,j]在这之前有更新,j要正序枚举,来保证d[i,j-1]在这之前有更新

且j要放在i之前枚举,

for(int j=1;j<=m;++j)
    {
        d[n+1][j]=n;
        for(int i=n;i;--i)
        {
            ll tmp=0x3f3f3f3f3f3f3f3f;
            ll p;
            for(int k=d[i][j-1];k<=d[i+1][j];++k)
            {

code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=3010;
ll n,m;
ll mas[N];
ll dp[N][310];//前i个村庄里放j个邮局 
ll f[N][N];//i与j之间放一个邮局时中间几个村庄到邮局的最近距离 
ll d[N][N];//最优决策位置 
int main()
{
	cin>>n>>m;
	memset(dp,0x3f,sizeof dp);
	dp[0][0]=0;
	for(int i=1;i<=n;++i) cin>>mas[i];
	sort(mas+1,mas+1+n);
	for(int i=1;i<=n;++i)
	{
		for(int j=i+1;j<=n;++j)
		{
			f[i][j]=f[i][j-1]+mas[j]-mas[(i+j)/2]; 
		}
	}
	for(int j=1;j<=m;++j)
	{
		d[n+1][j]=n;
		for(int i=n;i;--i)
		{
			ll tmp=0x3f3f3f3f3f3f3f3f;
			ll p;
			for(int k=d[i][j-1];k<=d[i+1][j];++k)
			{
			    if(tmp>dp[k][j-1]+f[k+1][i])
				{
					tmp=dp[k][j-1]+f[k+1][i];
					p=k;
				}	
			} 
			dp[i][j]=tmp;
			d[i][j]=p;
//			for(int k=0;k<i;++k)
//			{
//			    dp[i][j]=min(dp[i][j],dp[k][j-1]+f[k+1][i]);	 
//			} 
		}
	}
	cout<<dp[n][m]<<endl;
	return 0;
} 

hdu division 

大意:

给定一串数字,将其分成m个集合,使每一个集合的贡献之和最小,

贡献=(集合最大值-集合最小值)^2

思路:

先sort(这个不用解释的吧)

同样考虑区间dp

dp[i,j]表示前i个数分成j个集合的最小贡献和

则dp[i,j]=dp[k,j-1]+cost(k+1,i);    0<=k<i

这里cost(i,j)表示(a[j]-a[i])^2,也就是从i到j的元素放到一个集合里的贡献,因为我们排过序了,所以可以直接用一个前缀和来解决

然后你就惊喜地发现,这道题和上一道其实大同小异,连递推式都一样,只要加上四边形不等式优化即可

code(数组有点大,别开long long)

#include<bits/stdc++.h>
using namespace std;
#define ll int
const ll N=10005;
ll t,n,m;
ll dp[N][5010];
ll mas[N];
ll f(ll l,ll r)
{
	return (mas[r]-mas[l])*(mas[r]-mas[l]);
} 
ll d[N][5010];
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>t;
	for(int s=1;s<=t;++s)
	{
		cin>>n>>m;
		for(int i=1;i<=n;++i) cin>>mas[i];
		sort(mas+1,mas+1+n);
		memset(dp,0x3f,sizeof dp);
		dp[0][0]=0;
		for(int j=1;j<=m;++j)
		{
			d[n+1][j]=n;
			for(int i=n;i;--i)
			{
				ll tmp=0x3f3f3f3f;
				ll p;
				for(int k=d[i][j-1];k<=d[i+1][j];++k)
				{
					if(tmp>dp[k][j-1]+f(k+1,i))
					{
						tmp=dp[k][j-1]+f(k+1,i);
						p=k;
					}
				}
				dp[i][j]=tmp;
				d[i][j]=p;
			}
		}
		cout<<"Case "<<s<<": "<<dp[n][m]<<endl;
	}
	return 0;
} 

Lawrence 

大意:
给定一串数,一串连续的数的贡献为其所有元素两两相乘的乘积之和,单个数没有贡献

现在可以将其分成k+1段,求整串数的最小贡献

思路:
别说了,区间dp

dp[i,j]=dp[k,j-1]+cost(k+1,i);    0<=k<i

cost(i,j)表示i到j的连续的数的两两乘积之和

先不说cost数组怎么算,这个递推式是不是已经见怪不怪了。。。

一般来说这种递推式就是专门用来解决前i个数与j个操作的问题

然后对于这个cost函数,看到两两相乘,我们不难想到完全平方式

cost(i,j)=(sum(j)-sum(i-1))^2-\sum_{k=i}^{j}k^2

这是相当好处理的,那么这么一道难题就又a掉了

不过中间变量会爆int,所以要开long long...

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1010;
ll n,m;
ll mas[N];
ll dp[N][N];//前i截车里炸j次
//dp[i,j]=min{dp[k,j-1]+cost(k+1,i)}; 0<=k<i
//cost(i,j)=(sum(i)^2-sum(i^2))/2
ll sum[N];
ll pf[N];
ll sd(ll l,ll r)
{
	return ((sum[r]-sum[l-1])*(sum[r]-sum[l-1])-(pf[r]-pf[l-1]))/2;
}
ll d[N][N];
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	while(cin>>n>>m&&n&&m)
	{
		m++;
		memset(d,0,sizeof d);
		memset(sum,0,sizeof sum);
		memset(pf,0,sizeof pf);
		for(int i=1;i<=n;++i) cin>>mas[i];
		memset(dp,0x3f,sizeof dp);
		for(int i=1;i<=n;++i) sum[i]=sum[i-1]+mas[i];
		for(int i=1;i<=n;++i) pf[i]=pf[i-1]+mas[i]*mas[i];
		memset(dp,0x3f,sizeof dp);
		dp[0][0]=0;
		for(int j=1;j<=m;++j)
		{
			d[n+1][j]=n; 
			for(int i=n;i;--i)
			{
				ll tmp=0x3f3f3f3f;
				ll p;
				for(int k=d[i][j-1];k<=d[i+1][j];++k)
				{
					if(tmp>dp[k][j-1]+sd(k+1,i))
					{
						tmp=dp[k][j-1]+sd(k+1,i);
						p=k;
					}	
					//dp[i][j]=min(dp[i][j],dp[k][j-1]+sd(k+1,i));
				}
				dp[i][j]=tmp;
				d[i][j]=p;
			}
		}
		cout<<dp[n][m]<<endl;
	}
	return 0;
} 

从这题开始就是另一种递推式了

石子合并相信大家都做过,来看一道升级版

猴子party 

环形合并,规则跟石子合并一样

但是数据范围更大,得加入优化

dp[i,j]=dp[i,k]+dp[k+1,j]+cost(i,j)     i<=k<=j

cost就用前缀和解决

这里因为是环形合并,所以考虑枚举区间长度和左端点

这里因为是先枚举短的区间,所以枚举到i,j的时候,最优决策数组d[i+1][j]是在之前更新过的,所以这里i不用逆序枚举

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=2020;
ll n;
ll mas[N];
ll dp[N][N];
ll s[N][N];
ll sum[N];
ll w(ll l,ll r)
{
	return sum[r]-sum[l-1];
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	while(cin>>n)
	{
		for(int i=1;i<=n;++i) cin>>mas[i];
		for(int i=1+n;i<=n+n;++i) mas[i]=mas[i-n];
	//	for(int i=1;i<=n;++i) cout<<mas[i]<<' ';
	//	cout<<endl;
		for(int i=1;i<=n+n;++i) sum[i]=sum[i-1]+mas[i];
		memset(dp,0x3f,sizeof dp);
		for(int i=0;i<=n+n;++i) dp[i][i]=0,s[i][i]=i;
		for(int len=2;len<=n;len++){
	            for(int i=1;i<=2*n-len+1;i++){
	                int j=i+len-1;
	                for(int k=s[i][j-1];k<=s[i+1][j];k++){
	                    if(dp[i][j]>dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]){
	                        dp[i][j]=dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1];
	                        s[i][j]=k;
	                    }
	                }
	            }
	        }
		ll ans=1e17;
		for(int l=1;l<=n;++l)
		{
			ans=min(ans,dp[l][l+n-1]);
		}
		cout<<ans<<endl;
	}
	return 0;
} 

Tree Construction 

大意:
有一些左上到右下的点,

 现在要用一颗树把它们连起来,要求树只有向左和向右的边,求其最小边权

思路:

跟石子合并一样,

考虑dp[i,j]表示把i和j之间的点连起来的对应边权(中间只有一个断点)

dp[i,j]=dp[i,k]+dp[k+1,j]+cost(i,j)     i<=k<=j

这里cost(i,j)=mas[k].y-mas[j].y+mas[k+1].x-mas[i].x,可以看图理解

然后就跟合并石子是一个道理了

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1010;
ll n;
struct ty{
	ll x,y;
}mas[N];
ll dp[N][N];
ll f(ll a,ll b,ll c,ll d)
{
    return mas[b].y-mas[d].y+mas[c].x-mas[a].x;	
} 
ll s[N][N];
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	while(cin>>n)
	{
		for(int i=1;i<=n;++i) cin>>mas[i].x>>mas[i].y;
		memset(dp,0x3f,sizeof dp);
		for(int i=0;i<=n;++i) dp[i][i]=0,s[i][i]=i;
		for(int i=n;i;--i)
		{
			for(int j=i+1;j<=n;++j)
			{
				ll tmp=0x3f3f3f3f3f3f3f3f;
				ll p;
				for(int k=s[i][j-1];k<=s[i+1][j];++k)
				{
					if(tmp>dp[i][k]+dp[k+1][j]+f(i,k,k+1,j))
					{
						tmp=dp[i][k]+dp[k+1][j]+f(i,k,k+1,j);
						p=k;
					}
				//	dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+f(i,k,k+1,j));
				}
				dp[i][j]=tmp;
				s[i][j]=p;
			}
		}
		cout<<dp[1][n]<<endl;
	}
	return 0;
} 

大致就是这样,有新题目也许还会更新 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值