四边形不等式是一个很强有力的武器,往往可以在原本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;
}
大意:
给定一串数字,将其分成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;
}
大意:
给定一串数,一串连续的数的贡献为其所有元素两两相乘的乘积之和,单个数没有贡献
现在可以将其分成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函数,看到两两相乘,我们不难想到完全平方式
这是相当好处理的,那么这么一道难题就又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;
}
从这题开始就是另一种递推式了
石子合并相信大家都做过,来看一道升级版
环形合并,规则跟石子合并一样
但是数据范围更大,得加入优化
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;
}
大意:
有一些左上到右下的点,
现在要用一颗树把它们连起来,要求树只有向左和向右的边,求其最小边权
思路:
跟石子合并一样,
考虑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;
}
大致就是这样,有新题目也许还会更新