P4677 山区建小学(区间dp)

luogu 链接:https://www.luogu.com.cn/problem/P4677

题目分析:第一反应是区间dp,题目涉及到在不固定的区间中建立学校,从而得到距离的最小值。

那么我们怎么考虑这个问题呢?观察一下数据范围:n最大为500,三层for循环可以a

区间dp的套路都是比较固定的,阶段-状态-决策一共三层循环:由于在枚举区间的时候是不需要管左边界的所以只需要一层循环,第二层循环枚举的是建立学校的数目,第三层循环枚举的自然是在区间内建立学校的位置。

这样大致可以分析出来集合的二维表示:f[i][j]表示前i个村镇中建立j所学校的所需路程的最小值

我们继续分析,既然要用k来划分区间,则最后距离的表达应当是划分之后的子状态加上划分之后的区间中的距离之和。

于是可以写出状态转移方程:f[i][j] = f[k][j-1] + min(dis[k+1][i]);

其中min(dis(k+1,i))的含义是在k+1到i区间建一所学校距离的最小值

这个距离怎么求?这里可以用一点贪心,在固定区间中要使得距离最小则学校必然建在中间位置

我第一次考虑这里的时候有一点疑惑,若是(区间左边界+区间右边界)等于一个奇数,那么除以二之后必然是中间两个点中靠前的那一个,那能保证是最优解吗?

举个例子:这里直接写出四个村镇的坐标,0,2,10,19,易知左边界是1,右边界是4,中点得到的是第二个点,那么第二个点和第三个点的情况一样吗?

一样的,把第二个点当做学校时其他村镇到这个点的距离之和设为s2,同理可设出s3

那么s2 = x2-x1+x3-x2+(x4-x3+x3-x2),s3 = x3-x2+(x3-x2+x2-x1)+x4-x3

可知若有两个中点,那不论选哪个结果都是一样的,那么我们放心的就把区间中点建学校当做最小值来使用啦!

但是我们若想在三层for循环中直接用dis[k+1][i]就需要先初始化

也就是分别枚举左边界和右边界和中点,然后累加算出dis[k+1][i]

    cin>>n>>m; 

	for(int i=2;i<=n;i++) //初始化前缀和数组,由于题目给的是距离,那么默认第一个点坐标为0,从第二个点开始累加 
	{
		cin>>a[i];
		a[i]+=a[i-1];
	}
	
	for(int l=1;l<=n;l++)           //枚举左端点 
		for(int r=l;r<=n;r++)       //枚举右端点 
		{
			int mid=(l+r)>>1;       //用位运算较快 
			for(int k=l;k<=r;k++)   //枚举从左至右每个村镇,来累加距离 
			dis[l][r]+=abs(a[mid]-a[k]);
		}

然后根据状态转移方程分析核心代码

dp[i][j]=min(dp[i][j],dp[k][j-1]+dis[k+1][i]);  //含义是前i个村镇中建j个小学的路程最短的方案

很明显k就是枚举的建立小学的位置,由于dp数组中i<j是没有意义的(含义是村镇比小学还少),因此k的最小值应当从j-1开始,那么j从哪里开始循环呢?

由于方程中出现了j-1,因此j至少应当从1开始。若i从1开始循环的话,那么dis[k+1][i]在k = 0并且i=0的情况下就为0,是合法的,但是dp[0][0]呢?一定也是0

等一等!我们忽略了一件事,由于结果是要求最小值,所以dp数组是不能初始化为0的(放在全局就相当于初始化为0了),应该初始化为正无穷

所以在写核心代码之前还要进行初始化,但是dp[0][0]是一定要为0的

    memset(dp,0x3f3f3f3f,sizeof dp);
	
	dp[0][0]=0;
	
	for(int i=1;i<=n;i++)     //f[i][j]是前i个村庄中建j个小学,k枚举的是位置 
		for(int j=1;j<=m;j++)
			if(j<=i)
			for(int k=j-1;k<=i-1;k++)dp[i][j]=min(dp[i][j],dp[k][j-1]+dis[k+1][i]);

 最后呈上完整代码~

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
using namespace std;
const int N=510;
int n,m,a[N],dis[N][N],dp[N][N];
int main()
{
	cin>>n>>m; 
	for(int i=2;i<=n;i++) //初始化前缀和数组,由于题目给的是距离,那么默认第一个点坐标为0,从第二个点开始累加 
	{
		cin>>a[i];
		a[i]+=a[i-1];
	}
	
	for(int l=1;l<=n;l++)           //枚举左端点 
		for(int r=l;r<=n;r++)       //枚举右端点 
		{
			int mid=(l+r)>>1;       //用位运算较快 
			for(int k=l;k<=r;k++)   //枚举从左至右每个村镇,来累加距离 
			dis[l][r]+=abs(a[mid]-a[k]);
		}
		
	memset(dp,0x3f3f3f3f,sizeof dp);
	
	dp[0][0]=0;
	
	for(int i=1;i<=n;i++)     //f[i][j]是前i个村庄中建j个小学,k枚举的是位置 
		for(int j=1;j<=m;j++)
			if(j<=i)
			for(int k=j-1;k<=i-1;k++)dp[i][j]=min(dp[i][j],dp[k][j-1]+dis[k+1][i]);

		
	printf("%d\n",dp[n][m]);
	return 0;
}

 

 

 

上一篇:结对编程


下一篇:【类和模块】JavaScript中Java式的类继承