(状态优化)SMOJ 中国移动/洛谷 P7685 SPOJ703/5638 CEOI2005 Mobile Service系列 题解

题意

一个公司有三个移动服务员。如果某个地方有一个请求,某个服务员必须赶到那个地方去(那个地方没有其他服务员),某一时刻只有一个服务员能移动。被请求后,他才能移动,不允许在同样的位置出现两个服务员。从 p p p q q q 移动一个服务员,需要花费 c ( p , q ) c(p,q) c(p,q) c ( p , p ) = 0 c(p,p)=0 c(p,p)=0。公司必须满足所有的请求,目标是最小化公司花费,并输出每次请求提供服务的服务员编号 1 , 2 , 3 1,2,3 1,2,3

共有 m m m 个地方, n n n 个请求。

∀ c ( p , q ) ∈ [ 0 , 2000 ] \forall c(p,q)\in[0,2000] c(p,q)[0,2000]

SMOJ: 3 ≤ m ≤ 200 , 1 ≤ n ≤ 1000 3\le m\le 200,1\le n\le 1000 3m200,1n1000;无输出编号需求。

SP703:多测组数 T T T,大小不详,较小;无输出编号需求。

SP5638: 3 ≤ m ≤ 300 , 1 ≤ n ≤ 3000 , 1.46 G B 3\le m\le 300,1\le n\le 3000,1.46\rm GB 3m300,1n3000,1.46GB

洛谷P7685: 3 ≤ m ≤ 200 , 1 ≤ n ≤ 1000 , 64 M B 3\le m\le 200,1\le n\le 1000,64\rm MB 3m200,1n1000,64MB

下文将提供洛谷 P7685 数据要求的题解,其他数据范围敬请自行修改。

思路

最朴素的状态设法,设 f i , x , y , z f_{i,x,y,z} fi,x,y,z 表示完成第 i i i 个请求后,三个服务员各自在 x , y , z x,y,z x,y,z

但是看到空间限制无比的小,因此状态下标那么多,肯定开不下。

我们发现,每个请求之后,三个服务员中,肯定有一个服务员在请求位置 a i a_i ai,因此我们尝试从这里下手,缩减状态。

f i , x , y f_{i,x,y} fi,x,y 表示,三个服务员一个去到了 a i a_i ai ,剩下两个在位置 x , y x,y x,y 的最小代价,不妨令 z = a i − 1 z=a_{i-1} z=ai1 ,那么显然的有:
f i , x , y = min ⁡ { f i − 1 , x , y + c ( z , a i ) } f_{i,x,y}=\min \{f_{i-1,x,y}+c(z,a_i)\} fi,x,y=min{fi1,x,y+c(z,ai)}

f i , x , z = min ⁡ { f i − 1 , x , y + c ( y , a i ) } f_{i,x,z}=\min \{f_{i-1,x,y}+c(y,a_i)\} fi,x,z=min{fi1,x,y+c(y,ai)}

f i , y , z = min ⁡ { f i − 1 , x , y + c ( x , a i ) } f_{i,y,z}=\min \{f_{i-1,x,y}+c(x,a_i)\} fi,y,z=min{fi1,x,y+c(x,ai)}

记得判断三个位置相互不同。

还想缩减空间,那就滚动数组吧。

memset(f,inf,sizeof(f));
f[0][1][2]=0;
a[0]=3;
for(int i=1;i<=n;i++)
{
	ll now=i&1,las=now^1;
	memset(f[now],inf,sizeof(f[now]));
	for(int x=1;x<=m;x++)
	{
		for(int y=1;y<=m;y++)
		{
			ll z=a[i-1];
			if(x==y||y==z||z==x)continue;
			if(!(x==y||y==a[i]||a[i]==x))f[now][x][y]=min(f[now][x][y],f[las][x][y]+c[z][a[i]]);
			if(!(x==z||z==a[i]||a[i]==x))f[now][x][z]=min(f[now][x][z],f[las][x][y]+c[y][a[i]]);
			if(!(y==z||z==a[i]||a[i]==y))f[now][y][z]=min(f[now][y][z],f[las][x][y]+c[x][a[i]]);
		}
	}
}

不过写完代码之后,发现交在 SP703 被无脑卡常。我的原因是对 x , y x,y x,y 两状态循环的时候,两个全枚举了 1 ∼ n 1\sim n 1n,那么尝试缩减,通过保证两个状态 x < y x<y x<y 可以实现缩减状态枚举量(虽然只是除以二)。具体实现见代码。

代码(SP703)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int M=202,N=1002,inf=0x3f3f3f3f;
int T,m,n,c[M][M],a[N];
ll f[2][M][M];
int main()
{
	scanf("%d",&T);
	while(T--)
	{
		scanf("%d%d",&m,&n);
		for(int i=1;i<=m;i++)
		for(int j=1;j<=m;j++)
		scanf("%d",&c[i][j]);
		for(int i=1;i<=n;i++)
		scanf("%d",&a[i]);
		memset(f,inf,sizeof(f));
		f[0][1][2]=0;
		a[0]=3;
		for(int i=1;i<=n;i++)
		{
			int now=i&1,las=now^1;
			memset(f[now],inf,sizeof(f[now]));
			for(int x=1;x<=m;x++)
			{
				for(int y=x+1;y<=m;y++)
				{
					int z=a[i-1];
					if(x==y||y==z||z==x)continue;
					if(!(x==y||y==a[i]||a[i]==x))f[now][x][y]=min(f[now][x][y],f[las][x][y]+c[z][a[i]]);
					if(!(x==z||z==a[i]||a[i]==x))f[now][min(x,z)][max(x,z)]=min(f[now][min(x,z)][max(x,z)],f[las][x][y]+c[y][a[i]]);
					if(!(y==z||z==a[i]||a[i]==y))f[now][min(y,z)][max(y,z)]=min(f[now][min(y,z)][max(y,z)],f[las][x][y]+c[x][a[i]]);
				}
			}
		}
		ll ret=inf;
		for(int x=1;x<=m;x++)
		{
			for(int y=1;y<=m;y++)
			{
				if(x==y)continue;
				ret=min(ret,f[n&1][x][y]);
			}
		}
		printf("%lld\n",ret);
	}
	return 0;
}

那么怎么才能找到前往服务的服务员编号呢?那就要在 dp 的时候作处理了。

我们记 p o s i , x , y pos_{i,x,y} posi,x,y 表示,遍历到第 i i i 个请求时,“处理请求 a i a_i ai 的并非在位置 x , y x,y x,y 的服务员”这一情况是否改变,如果不改变则为 0 0 0,否则就是前往 a i a_i ai 的服务员的前继位置。

找最小代价的时候,把最终最小代价所处的状态 a n s = f n , x , y {\rm ans}=f_{n,x,y} ans=fn,x,y x , y x,y x,y 记为 m i n x = x , m i n y = y minx=x,miny=y minx=x,miny=y ,即其中一个服务员处理完请求 a n a_n an 之后,其他两个服务员所在的位置。

X i , Y i X_i,Y_i Xi,Yi 表示处理第 i i i 个请求时,其中一个服务员处理请求 a i a_i ai ,其他两个服务员所在的位置。那就可以考 p o s \rm pos pos 数组反推了:

初始状态 X n = m i n x , Y n = m i n y X_n=minx,Y_n=miny Xn=minx,Yn=miny,考虑倒序枚举 i : n → 1 i:n\rightarrow1 i:n1,每次更新 m i n x , m i n y minx,miny minx,miny 从而更新 X i , Y i X_i,Y_i Xi,Yi

如果上文的“情况”不改变,那么 X i , Y i X_i,Y_i Xi,Yi 相应不变,不更新 m i n x , m i n y minx,miny minx,miny;否则将等于 a i − 1 a_{i-1} ai1 m i n x minx minx 或者 m i n y miny miny 置为 p o s i , X i , Y i pos_{i,X_i,Y_i} posi,Xi,Yi

for(int i=n;i>=1;i--)
{
	x[i]=minx,y[i]=miny;
	int tem=pos[i][x[i]][y[i]];
	if(tem)
	{
		if(minx==a[i-1])minx=tem;
		else miny=tem;
	}
}

得到了 X , Y X,Y X,Y 数组,就可以输出答案了。由于编号 1 ∼ 3 1\sim 3 13 有一个很好的性质,就是和为 6 6 6,知道了其中两个编号,那么另外一个就是唯一的。而我们正要通过 X , Y X,Y X,Y 数组更新对应编号:即记 A , B A,B A,B 表示当前没有前往请求 a i a_i ai 的服务员编号,初始不妨设为 A = 1 , B = 2 A=1,B=2 A=1,B=2 (反正是 Special Judge)。

具体及细节见代码。

代码(洛谷P7685)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define uc unsigned char
const int M=202,N=1002,inf=0x3f3f3f3f;
int m,n,c[M][M],a[N];
ll f[2][M][M];
int x[N],y[N];
uc pos[N][M][M];
int main()
{
	scanf("%d%d",&m,&n);
	for(int i=1;i<=m;i++)
	for(int j=1;j<=m;j++)
	scanf("%d",&c[i][j]);
	for(int i=1;i<=n;i++)
	scanf("%d",&a[i]);
	memset(f,inf,sizeof(f));
	f[0][1][2]=0;
	a[0]=3;
	x[0]=1,y[0]=2;
	for(int i=1;i<=n;i++)
	{
		int now=i&1,las=now^1;
		memset(f[now],inf,sizeof(f[now]));
		for(int x=1;x<=m;x++)
		{
			for(int y=1;y<=m;y++)
			{
				int z=a[i-1];
				if(x==y)continue;
				if(!(x==a[i]||y==a[i]))
				{
					ll tem=f[las][x][y]+c[z][a[i]];
					if(tem<f[now][x][y])
					{
						f[now][x][y]=tem;
						pos[i][x][y]=0;
					}
				}
				if(!(x==a[i]||z==a[i]))
				{
					ll tem=f[las][x][y]+c[y][a[i]];
					if(tem<f[now][x][z])
					{
						f[now][x][z]=tem;
						pos[i][x][z]=y;
					}
				}
				if(!(y==a[i]||z==a[i]))
				{
					ll tem=f[las][x][y]+c[x][a[i]];
					if(tem<f[now][z][y])//y上下对应 
					{
						f[now][z][y]=tem;
						pos[i][z][y]=x;
					}
				}
			}
		}
	}
	ll ret=inf;
	int minx=0,miny=0,A=1,B=2;
	for(int x=1;x<=m;x++)
	{
		for(int y=1;y<=m;y++)
		{
			if(f[n&1][x][y]<ret)
			{
				ret=f[n&1][x][y];
				minx=x,miny=y;
			}
		}
	}
	printf("%lld\n",ret);
	for(int i=n;i>=1;i--)
	{
		x[i]=minx,y[i]=miny;
		int tem=pos[i][x[i]][y[i]];
		if(tem)
		{
			if(minx==a[i-1])minx=tem;
			else miny=tem;
		}
	}
	for(int i=1;i<=n;i++)
	{
		if(x[i]==a[i-1])A=6-A-B;//不变即继承
		else if(y[i]==a[i-1])B=6-A-B;
		printf("%d ",6-A-B);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值