题目描述
一个公司有三个移动服务员,最初分别在位置 1 , 2 , 3 1,2,3 1,2,3 处。
如果某个位置(用一个整数表示)有一个请求,那么公司必须指派某名员工赶到那个地方去。
某一时刻只有一个员工能移动,且不允许在同样的位置出现两个员工。
从 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。
给出 N N N 个请求,请求发生的位置分别为 p 1 ∼ p N p_1∼p_N p1∼pN。
公司必须按顺序依次满足所有请求,且过程中不能去其他额外的位置,目标是最小化公司花费,请你帮忙计算这个最小花费。
输入格式
第 1 1 1 行有两个整数 L , N L,N L,N,其中 L L L 是位置数量, N N N 是请求数量,每个位置从 1 1 1 到 L L L 编号。
第 2 2 2 至 L + 1 L+1 L+1 行每行包含 L L L 个非负整数,第 i + 1 i+1 i+1 行的第 j j j 个数表示 c ( i , j ) c(i,j) c(i,j),并且它小于 2000 2000 2000。
最后一行包含 N N N 个整数,是请求列表。
一开始三个服务员分别在位置 1 , 2 , 3 1,2,3 1,2,3。
输出格式
输出一个整数 M M M,表示最小花费。
数据范围
3 ≤ L ≤ 200 3≤L≤200 3≤L≤200,
1 ≤ N ≤ 1000 1≤N≤1000 1≤N≤1000
输入样例
5 9
0 1 1 1 1
1 0 2 3 2
1 1 0 4 1
2 1 5 0 1
4 2 3 4 0
4 2 4 1 5 4 3 2 1
输出样例
5
题目分析
这道题就是在 AcWing 271.杨老师的照相排列 中所说的,通过“一个状态应该更新哪些后续阶段的未知状态”得到状态转移方程的,这种方法比“如何计算出一个状态”思考起来更加自然、简便。
状态表示:设 f [ i , x , y , z ] f[i,x,y,z] f[i,x,y,z] 表示当前完成了第 i i i 个请求,且三位员工分别在 x , y , z x,y,z x,y,z 的所有途径中公司花费的最小值。
状态计算:(设第 i + 1 i+1 i+1 个请求在 u u u 处) (注意要判断是否有两个员工所处位置相同)
- 在 x x x 位置的员工去完成第 i + 1 i+1 i+1 个请求, f [ i + 1 , u , y , z ] = m i n { f [ i + 1 , u , y , z ] , f [ i , x , y , z ] + c [ x , u ] } f[i+1,u,y,z]=min\{f[i+1,u,y,z], f[i,x,y,z]+c[x,u]\} f[i+1,u,y,z]=min{ f[i+1,u,y,z],f[i,x,y,z]+c[x,u]};
- 在 y y y 位置的员工去完成第 i + 1 i+1 i+1 个请求, f [ i + 1 , x , u , z ] = m i n { f [ i + 1 , x , u , z ] , f [ i , x , y , z ] + c [ y , u ] } f[i+1,x,u,z]=min\{f[i+1,x,u,z], f[i,x,y,z]+c[y,u]\} f[i+1,x,u,z]=min{ f[i+1,x,u,z],f[i,x,y,z]+c[y,u]};
- 在 z z z 位置的员工去完成第 i + 1 i+1 i+1 个请求, f [ i + 1 , x , y , u ] = m i n { f [ i + 1 , x , y , u ] , f [ i , x , y , z ] + c [ z , u ] } f[i+1,x,y,u]=min\{f[i+1,x,y,u], f[i,x,y,z]+c[z,u]\} f[i+1,x,y,u]=min{ f[i+1,x,y,u],f[i,x,y,z]+c[z,u]}。
但是,上述 f f f 数组的空间复杂度为 O ( 1000 × 20 0 3 ) = O ( 8 × 1 0 9 ) O(1000×200^3)=O(8×10^9) O(1000×2003)=O(8×109),难以接受。
发现在完成第 i + 1 i+1 i+1 个请求时,总有一个员工在 u u u 处,因此可以不必表示这个员工所处位置,只需表示另外两个员工的位置即可。
状态表示 2 2 2:设 f [ i , x , y ] f[i,x,y] f[i,x,y] 表示当前完成了第 i i i 个请求,且一位员工在 p [ i ] p[i] p[i],另两位员工分别在 x , y x,y x,y 的所有途径中公司花费的最小值。
状态计算 2 2 2:(注意要判断是否有两个员工所处位置相同)
- 在 x x x 位置的员工去完成第 i + 1 i+1 i+1 个请求, f [ i + 1 , p [ i ] , y ] = m i n { f [ i + 1 , p [ i ] , y ] , f [ i , x , y ] + c [ x , p [ i + 1 ] } f[i+1,p[i],y]=min\{f[i+1,p[i],y], f[i,x,y]+c[x,p[i+1]\} f[i+1,p[i],y]=min{ f[i+1,p[i],y],f[i,x,y]+c[x,p[i+1]};
- 在 y y y 位置的员工去完成第 i + 1 i+1 i+1 个请求, f [ i + 1 , x , p [ i ] ] = m i n { f [ i + 1 , x , p [ i ] ] , f [ i , x , y ] + c [ y , p [ i + 1 ] ] } f[i+1,x,p[i]]=min\{f[i+1,x,p[i]], f[i,x,y]+c[y,p[i+1]]\} f[i+1,x,p[i]]=min{ f[i+1,x,p[i]],f[i,x,y]+c[y,p[i+1]]};
- 在 p [ i ] p[i] p[i] 位置的员工去完成第 i + 1 i+1 i+1 个请求, f [ i + 1 , x , y ] = m i n { f [ i + 1 , x , y ] , f [ i , x , y ] + c [ p [ i ] , p [ i + 1 ] ] } f[i+1,x,y]=min\{f[i+1,x,y], f[i,x,y]+c[p[i],p[i+1]]\} f[i+1,x,y]=min{ f[i+1,x,y],f[i,x,y]+c[p[i],p[i+1]]}。
状态初始化: f [ i , x , y ] = + ∞ , f [ 0 , 1 , 2 ] = 0 f[i,x,y]=+\infty,f[0,1,2]=0 f[i,x,y]=+∞,f[0,1,2]=0。
代码:
#include <iostream>
#include <cstring>
using namespace std;
int l, n, res = 0x3f3f3f3f;
int c[210][210], p[1010], f[1010][210][210];
int main(){
cin >> l >> n;
for (int i = 1; i <= l; i ++)
for (int j = 1; j <= l; j ++)
cin >> c[i][j];
for (int i = 1; i <= n; i ++) cin >> p[i];
memset(f, 0x3f, sizeof(f));
p[0] = 3; //因为开始时三个员工分别在1,2,3位置,不妨设第0个请求在3位置
f[0][1][2] = 0; //初始状态为另两个员工在1,2位置
for (int i = 0; i < n; i ++)
for (int x = 1; x <= l; x ++)
for (int y = 1; y <= l; y ++){
int z = p[i], u = p[i + 1], v = f[i][x][y];
if(x == y || x == z || y == z) continue;
f[i + 1][x][y] = min(f[i + 1][x][y], v + c[z][u]);
f[i + 1][x][z] = min(f[i + 1][x][z], v + c[y][u]);
f[i + 1][y][z] = min(f[i + 1][y][z], v + c[x][u]);
}
for (int x = 1; x <= l; x ++)
for (int y = 1; y <= l; y ++)
res = min(res, f[n][x][y]);
cout << res;
return 0;
}
当然, f f f 数组的空间复杂度还是很大,虽然在大多数情况下已经可以AC,但这里还要介绍以下滚动数组:
- 滚动数组技巧: d p [ i dp[i dp[i& 1 ] = d p [ ( i − 1 ) 1]=dp[(i-1) 1]=dp[(i−1)& 1 ] + ? 1]+? 1]+?;
- 滚动数组需要及时清空不需要的位置信息;
- 滚动数组适用范围:某一状态 d p [ i ] dp[i] dp[i] 只与 d p [ i − 1 ] dp[i-1] dp[i−1] 有关。
因此, f f f 数组的空间复杂度只要 O ( 2 × 20 0 2 ) O(2×200^2) O(2×2002) 即可 (时间复杂度也会大大降低,实测表明不用滚动数组总时间约 3 s 3s 3s,用滚动数组只要约 1.8 s 1.8s 1.8s)。
代码 (滚动数组):
#include <iostream>
#include <cstring>
using namespace std;
int l, n, res = 0x3f3f3f3f;
int c[210][210], p[1010], f[2][210][210];
int main(){
cin >> l >> n;
for (int i = 1; i <= l; i ++)
for (int j = 1; j <= l; j ++)
cin >> c[i][j];
for (int i = 1; i <= n; i ++) cin >> p[i];
memset(f, 0x3f, sizeof(f));
p[0] = 3;
f[0][1][2] = 0;
for (int i = 0; i < n; i ++)
for (int x = 1; x <= l; x ++)
for (int y = 1; y <= l; y ++){
int z = p[i], u = p[i + 1], &v = f[i & 1][x][y];
if(x != y && x != z && y != z){
f[(i + 1) & 1][x][y] = min(f[(i + 1) & 1][x][y], v + c[z][u]);
f[(i + 1) & 1][x][z] = min(f[(i + 1) & 1][x][z], v + c[y][u]);
f[(i + 1) & 1][y][z] = min(f[(i + 1) & 1][y][z], v + c[x][u]);
}
v = 0x3f3f3f3f; //注意每次用完都要初始化,所以上面不能用continue了
}
for (int x = 1; x <= l; x ++)
for (int y = 1; y <= l; y ++)
res = min(res, f[n & 1][x][y]);
cout << res;
return 0;
}