T2960 全民健身【思维Dp,预处理,差分优化】

Online Judge:YCJSOI

Label:Dp,思维题,预处理,滚动优化

题目描述

乐乐现在掌管一个大公司,办公楼共有n层。为了增加员工的身体素质,他决定在每层楼都建立一个活动室,活动室分乒乓球和排球两种。

已知每层楼喜欢乒乓球和排球的人数。

每个人的行走楼层数是他去自己喜欢的运动室的楼层数。

请你帮乐乐算算,每层楼应该建乒乓球还是排球,使得所有人行走楼层数总和最小。

输入

第一行一个整数n,表示楼层数量。

接下来n行 ,每行两个整数a和b,表示喜欢乒乓球和排球的人数。

输出

输出一个整数,表示所有人行走楼层总和的最小值。

样例

Input#1

2
10 5
4 3

Output#1

9

Input#2

15
68 42
1 35
25 70
59 79
65 63
46 6
28 82
92 62
43 96
37 28
5 92
54 3
83 93
17 22
96 19

Output#2

506

Hint

第一层建乒乓球室,第二层建排球室。行走楼层为5+4=9

对于30%的数据,n的范围\([2,20]\);;
对于80%的数据,n的范围\([2,500]\);
对于100%的数据,n的范围\([2,4000]\),每层楼喜欢乒乓球和排球的人数范围\([1,10^5]\)

题解

看数据范围应该是个Dp。

Ⅰ状态定义

一开始看题,不难定义出这样一个状态\(dp[co=0/1][i][j]\),意义是,已经决策完了前i层建什么,且\([j+1,i]\)这些层建的都是\(co\),也就是\(j\)这一层建的是\(co\)^\(1\),此时的最小代价。

但有一个问题,\([j+1,i]\)这些层喜欢\(co\)^\(1\)的人,他们一定是去\(j\)这层吗?不一定,可能还可以去i后面的某点,所以我们无法在当前状态中得知那个最小代价,我们能求得的只有\([1,j]\)这些层所有人的代价+\([j+1,i]\)这些层中喜欢\(co\)的人的代价(为0)。

说人话就是,如果我在\([j+1,i]\)这些层都建乒乓球室,1.那么\([j+1,i]\)这些层中打乒乓球的人都不用移动在本层打就好了(代价为0),2.在\([1,j]\)这些层的人不论他喜欢乒乓球还是排球都可以在i之前做出决策,3.在\([j+1,i]\)这些层中喜欢排球的人无法在当前层状态中做出决策,因为他们可以选择去第\(j\)层打排球,但如果楼上还有更近的排球管呢?我们在当前状态中无法预知。

所以改一下意义,它表示的是[1,j]这些层所有人的代价+[j+1,i]这些层中喜欢co​的人的代价


Ⅱ转移

初始化如下

memset(dp,-1,sizeof(dp));//相当于赋一个极大值	
dp[0][1][0]=dp[1][1][0]=0;

转移采用填表方式,更新最小值。

for(i=1;i<n;++i)for(j=0;j<i;++j)for(x=0;x<=1;++x)if(~dp[x][i][j]){
//枚举当前层i,[j+1,i]都建x
		for(y=0;y<=1;++y){//枚举第i+1层建什么
			if(x==y)Do(dp[y][i+1][j],dp[x][i][j]);//如果建的和i相同
			else{
				ll val=calc(y,j+1,i);
				if(~val)Do(dp[y][i+1][i],dp[x][i][j]+val);
			}
		}
}		

主要看到\(x!=y\)时的情况,我们此时在中间\([j+1,i]\)建了\(x\),在两端\(j,i+1\)建了\(y(!x)\),那个\(calc(co,l,r)\)函数,求的就是\([l,r]\)这些层中喜欢\(co\)的人,分别去到\(l-1,r+1\)的最小代价。

很明显以中点为边界,左半边的去\(l-1\),右半边的去\(r+1\)最优吧。

最low的求法应该就是下面这种\(O(N)\)的:

虽然非常暴力,但一定要注意下面几处特判。

inline ll calc(int co,int l,int r){
    ll sum=0;
    register int i,mid=(l+r)>>1;
    if(l==1&&r==n)return 1e9;//左右都不能去
    if(l==1){//左边不能去
        for(i=l;i<=r;++i)sum+=(r-i+1)*a[co][i];
        return sum;
    }
    if(r==n){//右边不能去
        for(i=l;i<=r;i++)sum+=(i-l+1)*a[co][i];
        return sum;
    }   
    for(i=l;i<=mid;++i)sum+=(i-l+1)*a[co][i];//左半边的去l-1层
    for(i=mid+1;i<=r;++i)sum+=(r-i+1)*a[co][i];//右半边的去r+1层
    return sum;
}

最后的统计答案如下。别忘了还要加上最后一段代价。

for(x=0;x<=1;++x)for(j=1;j<n;++j){
	if(~dp[x][n][j])Do(ans,dp[x][n][j]+calc(x^1,j+1,n));
}

这样,大致的算法流程就结束了。但是也许在上面代码中你就发现了,空间、时间对于100%数据都承受不了(时间复杂度为\(O(4\cdot N^3)\)),只能过掉80%数据。


Ⅲ优化·空间

空间的话,主要是原来的\(dp[2][N][N]\)数组内存好像有点玄,所以求稳优化一波。由于每个\(i\)只用到上一层状态,所以直接滚动就好了,变成dp[2][2][N]

Ⅲ优化·时间

时间呢,感觉那个calc(co,l,r)函数非常可优化的样子。在草稿纸上列了一下,发现可以利用差分思想,\(O(N)\)预处理前缀/后缀,然后\(O(1)\)计算出结果。

举个例子:对于求\([l,r]\)到它\(l-1\)的代价

比如\(l=4,r=7\)的情况:

列式为\(cost=a[4]*1+a[5]*2+a[6]*3+a[7]*4\)

我们预处理一个前缀值:isum[i]=\((a[1]*1+a[2]*2+a[3]*3+..a[i]*i)\)

然后再处理一个普通的前缀和:s[i]=(a[1]+a[2]+a[3]+..a[i])

那么上式\(cost=isum[7]-(s[7]-s[3])*3\)

推广一下可以得到:

inline ll getL(int co,int l,int r){//求[l,r]这些层中喜欢co的人跑到第l-1层的代价
	ll sum=0;
	sum=(isum[0][co][r]-isum[0][co][l-1])-1ll*(l-1)*(s[co][r]-s[co][l-1]);
	return sum;
}

类似的,去右端的情况,预处理一个后缀值即可,下面不再赘述。

综上,时间复杂度为\(O(N^2)\)

完整代码如下:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4002;
ll ans=-1;
int a[2][N],n;
inline int read(){
    int x=0;char c=getchar();
    while(c<'0'||c>'9')c=getchar();
    while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x;
}
inline void Do(ll &x,ll y){if(x==-1||x>y)x=y;}
ll s[2][N],isum[2][2][N];//isum:sum of i*val
void pre(){
	for(int i=1;i<=n;i++){
		for(int x=0;x<=1;x++){
			s[x][i]=s[x][i-1]+a[x][i];
			isum[0][x][i]=isum[0][x][i-1]+1ll*a[x][i]*i;
		}
	}
	for(int i=n;i>=1;i--){
		for(int x=0;x<=1;x++){
			isum[1][x][i]=isum[1][x][i+1]+1ll*a[x][i]*(n-i+1);
		}
	}
}
inline ll getL(int co,int l,int r){
	ll sum=0;
	sum=(isum[0][co][r]-isum[0][co][l-1])-1ll*(l-1)*(s[co][r]-s[co][l-1]);
	return sum;
}
inline ll getR(int co,int l,int r){
	ll sum=0;int id=n-r;
	sum=(isum[1][co][l]-isum[1][co][r+1])-1ll*id*(s[co][r]-s[co][l-1]);
	return sum;
}
inline ll calc(int co,int l,int r){//[l,r]可以去l-1,r+1
    register int i,mid=(l+r)>>1;
    if(l==1&&r==n)return -1;
    if(l==1)return getR(co,l,r);
    if(r==n)return getL(co,l,r);
	return getL(co,l,mid)+getR(co,mid+1,r);
}
ll dp[2][2][N];
namespace p100{
	void solve(){
		pre();
		memset(dp,-1,sizeof(dp));
		register int i,j,x,y,g=0;
		memset(dp[g],-1,sizeof(dp[g]));	
		dp[0][g][0]=dp[1][g][0]=0;
		for(i=1;i<n;++i){
			g^=1;
			memset(dp[0][g],-1,sizeof(dp[0][g]));
			memset(dp[1][g],-1,sizeof(dp[1][g]));
			for(j=0;j<i;++j)for(x=0;x<=1;++x)if(~dp[x][g^1][j]){
				for(y=0;y<=1;++y){
					if(x==y)Do(dp[y][g][j],dp[x][g^1][j]);
					else{
						ll val=calc(y,j+1,i);
						if(~val)Do(dp[y][g][i],dp[x][g^1][j]+val);
					}
				}
			}
		}		
		for(x=0;x<=1;++x)for(j=1;j<n;++j){
			if(~dp[x][g][j])Do(ans,dp[x][g][j]+calc(x^1,j+1,n));
		}
		printf("%lld\n",ans);
	}
}
int main(){
	n=read();
	for(int i=1;i<=n;++i)a[0][i]=read(),a[1][i]=read();
	p100::solve();
}

upd:由于上面代码比较丑,所以在博客文件里还放了几位学长的解析。

posted @ 2019-10-06 22:09  real_lyb  阅读(275)  评论(0编辑  收藏  举报