题解:P7685 [CEOI 2005] Mobile Service

· · 题解

本题解把 SP703 和洛谷 P7685 写在一起,便于循序渐进地做题。

题意

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

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

SP703:多测组数$T$,大小不详,较小;无输出编号需求。 洛谷P7685:$3\le m\le 200,1\le n\le 1000,64\rm MB$。 下文将提供洛谷 P7685 数据要求的题解,其他数据范围敬请自行修改。 ## 思路 最朴素的状态设法,设 $f_{i,x,y,z}$ 表示完成第 $i$ 个请求后,三个服务员各自在 $x,y,z$。 但是看到空间限制无比的小,因此状态下标那么多,肯定开不下。 我们发现,每个请求之后,三个服务员中,肯定有一个服务员在请求位置 $a_i$,因此我们尝试从这里下手,缩减状态。 令 $f_{i,x,y}$ 表示,三个服务员一个去到了 $a_i$ ,剩下两个在位置 $x,y$ 的最小代价,不妨令 $z=a_{i-1}$ ,那么显然的有: $$f_{i,x,y}=\min \{f_{i-1,x,y}+c(z,a_i)\}$$ $$f_{i,x,z}=\min \{f_{i-1,x,y}+c(y,a_i)\}$$ $$f_{i,y,z}=\min \{f_{i-1,x,y}+c(x,a_i)\}$$ 记得判断三个位置相互不同。 还想缩减空间,那就滚动数组吧。 ```cpp 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$ 两状态循环的时候,两个全枚举了 $1\sim n$,那么尝试缩减,通过保证两个状态 $x<y$ 可以实现缩减状态枚举量(虽然只是除以二)。具体实现见代码。 ## 代码(SP703) ```cpp #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 的时候作处理了。 我们记 $pos_{i,x,y}$ 表示,遍历到第 $i$ 个请求时,“处理请求 $a_i$ 的并非在位置 $x,y$ 的服务员”这一情况是否改变,如果不改变则为 $0$,否则就是前往 $a_i$ 的服务员的前继位置。 找最小代价的时候,把最终最小代价所处的状态 ${\rm ans}=f_{n,x,y}$ 的 $x,y$ 记为 $minx=x,miny=y$ ,即其中一个服务员处理完请求 $a_n$ 之后,其他两个服务员所在的位置。 令 $X_i,Y_i$ 表示处理第 $i$ 个请求时,其中一个服务员处理请求 $a_i$ ,其他两个服务员所在的位置。那就可以考 $\rm pos$ 数组反推了: 初始状态 $X_n=minx,Y_n=miny$,考虑倒序枚举 $i:n\rightarrow1$,每次更新$minx,miny$ 从而更新 $X_i,Y_i$。 如果上文的“情况”不改变,那么 $X_i,Y_i$ 相应不变,不更新 $minx,miny$;否则将等于 $a_{i-1}$ 的 $minx$ 或者 $miny$ 置为 $pos_{i,X_i,Y_i}$。 ```cpp 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$ 数组,就可以输出答案了。由于编号 $1\sim 3$ 有一个很好的性质,就是和为 $6$,知道了其中两个编号,那么另外一个就是唯一的。而我们正要通过 $X,Y$ 数组更新对应编号:即记 $A,B$ 表示当前没有前往请求 $a_i$ 的服务员编号,初始不妨设为 $A=1,B=2$ (反正是 Special Judge)。 具体及细节见代码。 ## 代码(洛谷P7685) ```cpp #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; } ```