Luogu P4767 [IOI2000] 邮局 加强版

1 篇文章 0 订阅

Luogu P4767 [IOI2000] 邮局 加强版题解(决策单调DP,四边形不等式优化)

题目描述

坐标轴上有n个村庄,你需要在一些村庄中设立邮局,使每个村庄与其最近的邮局之间的距离总和最小。已知村庄位置与邮局数量,计算每个村庄和最近的邮局之间所有距离的最小可能的总和。

村庄数量为 V ( n ) V(n) V(n),邮局数量为 P ( m ) P(m) P(m),村庄坐标记录在数组

样例输入 #1

10 5 
1 2 3 6 7 9 11 22 44 50

样例输出 #1

9

提示

对于 40 % 40\% 40% 的数据, V ≤ 300 V \leq 300 V300

对于 100 % 100\% 100% 的数据, 1 ≤ P ≤ 300 1 \leq P \leq 300 1P300 P ≤ V ≤ 3000 P \leq V \leq 3000 PV3000 1 ≤ 1 \leq 1 村庄位置 ≤ 10000 \leq 10000 10000

思路

我们首先考虑 40 40% 40数据的暴力写法,首先先将村庄坐标从小到大排序,设 f i , j f_{i,j} fi,j代表前 i i i个村庄中设立 j j j个邮局所得到的答案。我们可以先预处理出一个 w w w(也就是代码中的 d i s dis dis)数组。 w i , j w_{i,j} wi,j代表村庄 i i i j j j设立只一所邮局的消费。得

w i , j = w i , j − 1 + a j − a ( i + j ) / 2 w_{i,j}=w_{i,j-1}+a_j-a_{(i+j)/2} wi,j=wi,j1+aja(i+j)/2

预处理出 w w w数组后,我们就可以愉快的开始DP惹~

f i , j = m i n ( f i , j , f k , j − 1 + w k + 1 , i ) f_{i,j}=min(f_{i,j},f_{k,j-1}+w_{k+1,i}) fi,j=min(fi,j,fk,j1+wk+1,i)

最后输出 f n , m f_{n,m} fn,m
时间复杂度 O ( n m 2 ) O(nm^2) O(nm2)
代码如下

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=3005; 
int n,m,a[N*20],f[N][N],w[N][N];
signed main(){
	memset(f,0x3f,sizeof(f));
	scanf("%lld%lld",&n,&m);
	for (int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
	}
	sort(a+1,a+n+1);
    for(int i=1;i<=n;i++)
        for(int j=i;j<=n;j++)
            w[i][j]=w[i][j-1]+a[j]-a[(i+j)/2];
    f[0][0]=0;
	for (int j=1;j<=m;j++){
		for (int i=1;i<=n;i++){
			for (int k=0;k<i;k++){
				f[i][j]=min(f[i][j],f[k][j-1]+w[k+1][i]);
			}
		}
	}
	printf("%lld",f[n][m]);
	return 0;
}

然后就可以……

成功拿下60分
完结撒花

正解

基于上述暴力,我们可以很轻易的想到四边形不等式
w i , j = w i , j − 1 + a j − a ( i + j ) / 2 w_{i,j}=w_{i,j-1}+a_j-a_{(i+j)/2} wi,j=wi,j1+aja(i+j)/2

根据四边形不等式在 a ≤ b ≤ c ≤ d a \leq b \leq c \leq d abcd的情况下
w a , b + w c , d ≤ w a , d + w b , c w_{a,b}+w_{c,d} \leq w_{a,d}+w_{b,c} wa,b+wc,dwa,d+wb,c

很明显,我们的DP满足四边形不等式
证明的话后续有时间会更新的。

决策单调性嘛……虽然知道怎么用,但证明我暂时也没搞太明白

回归正题,我们在转移中记录转移的位置到pos,从而降低决策点枚举的数量。
根据决策单调性,我们可以知道 p o s i , j pos_{i,j} posi,j p o s i , j − 1 pos_{i,j-1} posi,j1 p o s i + 1 , j pos_{i+1,j} posi+1,j
最后输出与暴力的DP一样

最终AC代码

#include<bits/stdc++.h>
using namespace std;
const int N=3005; 
int n,m,a[N],pos[N][N],dis[N][N];
long long f[N][N];
signed main(){
	memset(f,0x3f,sizeof(f));
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
	}
	sort(a+1,a+n+1);
    for(int i=1;i<=n;i++)
        for(int j=i;j<=n;j++)
            dis[i][j]=dis[i][j-1]+a[j]-a[(i+j)/2];
    f[0][0]=0;
	for (int j=1;j<=m;j++){
		pos[n+1][j]=n;
		for (int i=n;i>=1;i--){
			for (int k=pos[i][j-1];k<=pos[i+1][j];k++){
				int v=f[k][j-1]+dis[k+1][i];
				if (v<f[i][j]){
					f[i][j]=v;
					pos[i][j]=k;
				}
			}
		}
	}
	printf("%lld",f[n][m]);
	return 0;
}

真正的完结撒花~~

有不足请各位大佬指正小蒟蒻,小蒟蒻没写过几篇题解

后记

另外在写代码中遇到了一个抽象的问题,再写一开始的暴力DP时,若是将我的 d i s dis dis数组的名字换成 w w w,就会多T掉一个点,所以代码中的 w w w换成了 d i s dis dis,若是有大佬知道原因可以在评论区告诉蒟蒻,

感谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值