POJ 1160邮局问题以及用四边形不等式优化

本文详细介绍了如何使用动态规划解决邮局问题,通过分析输入格式和输出格式,逐步阐述了状态转移方程和算法复杂度分析。此外,文章还提供了一段经过优化的代码实现,最终达到在极短时间内解决问题的目的。

邮局问题(点击去原题)

描述

一些村庄建在一条笔直的高速公路边上,我们用一条坐标轴来描述这条公路,每个村庄的坐标都是整数,没有两个村庄的坐标相同。两个村庄的距离定义为坐标之差的绝对值。我们需要在某些村庄建立邮局。使每个村庄使用与它距离最近的邮局,建立邮局的原则是:所有村庄到各自使用的邮局的距离总和最小。

数据规模:1<=村庄数<=300, 1<=邮局数<=30, 1<=村庄坐标<=10000

格式

输入格式

2行

第一行:n m {表示有n个村庄,建立m个邮局}
第二行:a1 a2 a3 .. an {表示n个村庄的坐标}

输出格式

1行

第一行:l {l表示最小距离总和}

样例1

样例输入1

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

样例输出1

9

题目分析

首先,这是一道区间的动态规划,就像矩阵连乘那样,d[ i ][ j ]表示 i 到 j的区间上的最优矩阵划分。

影响子问题的因素至少有两个,一个是村庄的区间,另一个是邮局数量。

故动态规划的子问题是d[ i ][ j ]表示的是: 在前 j 个村庄建立 i 个邮局的距离的最小值。


状态转移的思想是: 想一下,当只有一个邮局的时候,我们当然在中间的那个村庄去建一个邮局。(要注意的是:这个中间值要分奇偶数)

状态转移方程: d[ i ][ j ] = MIN{ d[ i-1 ][ k ] + w[ k+1 ][ j ] } ; 其中 i-1=<k<j, w[ k+1 ][ j ]表示的是在k+1到j的村庄建立一个邮局的最小花费( w[ k+1 ][ j ] = ∑ | a[L] - a[ mid ] | (k+1=<L<=j)  && mid =(k+1+j)/2或mid=(k+2+j)/2 ) 


算法复杂度分析: 上面子问题共n*p个,每次转移最大为O(n),所以总的复杂度是O( n^2*p )。我在POJ上提交的时候,用了35Ms的时间。


下面简单地说下优化,用的是四边形不等式。 这次在POJ上提交用了0Ms

我就不证明了,我只是说下思路,首先要证明的是w[i][j]要满足四边形不等式,然后用数学归纳法去证明d[i][j]也是满足四边形不等式的,最后再用s[i][j]表示的是记录d[i][j]的最优分割位置,然后证明就出来了。

最终我们得到的决策是:s[ i-1 ][ j ]=<s[ i ][ j ] <= s[ i ][ j+1 ]

具体证明点击这里


附上跑了0MM的代码:

最后说下,代码比较乱的部分是w[ i ][ j ]的计算,我用的技巧可以在O(1)时间内计算一个状态。

/*
	邮局问题

	d[ i, j ]表示前j个村庄建立i个邮局所需要的最少总路程

	d[ i, j ] = min{ d[ i-1, k ] + w[ k+1, j] } ( i-1=< k < j )
*/

#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>
using namespace std;

const int maxn = 300 + 5;
int d[ maxn ][ maxn ];
int a[ maxn ];
int sum[ maxn ];
int s[ maxn ][ maxn ];

const int INF = 1<<20;

void dp( int n, int p );
int min1( int a, int b )
{
	return a < b ? a : b;
}

int main()
{
	int v, p;
	scanf( "%d%d", &v, &p );

	for( int i=0; i<v; i++ )
	{
		scanf( "%d", &a[ i ] );
		if( i != 0 )
			sum[ i ] = sum[ i-1 ] + a[ i ];
		else
			sum[ i ] = a[ i ];
	}

	dp( v, p );

	return 0;
}

//dp
void dp( int n, int p )
{
	memset( d, 0, sizeof( d ) );

	int i, j, k;
	int lowerMid, upperMid;

	//initilize	d[ i, j ]表示前j个村庄建立i个邮局所需要的最少总路程
	for( i=0; i<n; i++ )
	{
		lowerMid = i/2;
		upperMid = (i+1)/2;
		int tmp = min1( sum[ i ] - 2*sum[ lowerMid ] + a[ lowerMid ], sum[ i ] - 2*sum[ upperMid ] + 2*a[ upperMid ] );
		
		d[ 0 ][ i ] = tmp;
		s[ 0 ][ i ] = 0;
		
		d[ i ][ i ] = 0;
		s[ i ][ i ] = i - 1;
		d[ i ][ n ] = n-2;
	}

	for( i=1; i<p; i++ )
	{
		for( j=n-1; j>i; j-- )
		{
			d[ i ][ j ] = INF;

			//优化在这里
			int end;
			if( j == n-1 )
				end = n-2;
			else
				end = s[i][j+1];

			for( k=s[ i-1 ][ j ]; k<=end; k++ )
			{
				lowerMid = (k+1+j) / 2;
				upperMid = (k+2+j) / 2;
				//注意: 计算lowerMid 与 upperMid两个数的tmp是不同的!!! 1个半小时的调试
				int tmp = min1( sum[ j ] + sum[ k ] - 2*sum[ lowerMid ] + a[ lowerMid ], sum[ j ] + sum[ k ] - 2*sum[ upperMid ] + 2*a[ upperMid ] );

				if( d[ i ][ j ] > d[ i-1 ][ k ] + tmp )
				{
					d[ i ][ j ] = d[ i-1 ][ k ] + tmp;
					s[ i ][ j ] = k;
				}
			}
		}
	}

	printf( "%d\n", d[ p-1 ][ n-1 ] );
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值