【动态规划】CF213C Relay Race
前言
此乃小 Oler 的一篇算法随笔,从今日后,还会进行详细的修订。
Relay Race
题目
题面描述
福瑞克 (Furik) 和鲁比克 (Rubik) 参加接力赛。比赛将在一个大广场上举行,广场的一侧有 n n n 米。给定的正方形被划分为 n × n n \times n n×n 个单元格(表示为单位正方形),每个单元格都有一些数字。
比赛开始时, Furik 站在坐标为 ( 1 , 1 ) (1,1) (1,1) 的单元格中, Rubik 站在座标为 ( n , n ) (n,n) (n,n) 的单元中。在开始后, Furik 立即跑向 Rubik ,此外,如果 Furik 站在坐标为 ( i , j ) (i,j) (i,j) 的单元格处,则他可以移动到单元格 ( i + 1 , j ) (i+1,j) (i+1,j) 或 ( i , j + 1 ) (i,j+1) (i,j+1) 。 Furik 到达 Rubik 后, Rubik 开始从坐标为 ( n , n ) (n,n) (n,n) 的单元格运行到坐标为 ( 1 , 1 ) (1,1) (1,1) 的单元格。如果 Rubik 站在单元格 ( i , j ) (i,j) (i,j) 中,那么他可以移动到单元格 ( i − 1 , j ) (i-1,j) (i−1,j) 或 ( i , j − 1 ) (i,j-1) (i,j−1) 。 Furik 和 Rubik 都不允许超越领域的边界;如果一个球员越过边界,他将被取消比赛资格。
为了赢得比赛,福瑞克和鲁比克必须获得尽可能多的分数。点数是从 t t t 开始的数的总和。
输入格式
第一行包含单个整数 ( 1 ≤ n ≤ 300 ) (1 \le n \le 300) (1≤n≤300)。
接下来的
n
n
n 行各包含
n
n
n 整数:
第
i
i
i 行
a
i
,
j
a_{i,j}
ai,j 上的第
j
j
j 个数字
a
i
,
j
(
−
1000
≤
a
i
,
j
≤
1000
)
a_{i,j}(-1000 \le a_{i,j} \le 1000)
ai,j(−1000≤ai,j≤1000) 是在坐标为
(
i
,
j
)
(i,j)
(i,j) 的单元格中写入的数字。
输出格式
在一行上打印一个数字——这就是问题的答案。
样例 #1
样例输入 #1
1
5
样例输出 #1
5
样例 #2
样例输入 #2
2
11 14
16 12
样例输出 #2
53
样例 #3
样例输入 #3
3
25 16 25
12 18 19
11 13 8
样例输出 #3
136
提示
对第二个样例的评论:
Furik 的最佳路径是:
(
1
,
1
)
(1,1)
(1,1) ,
(
1
,
2
)
(1,2)
(1,2) ,
(
2
,
2
)
(2,2)
(2,2),而Rubik的最佳路径是:
(
2
,
2
)
(2,2)
(2,2) ,
(
2
,
1
)
(2,1)
(2,1) ,
(
1
,
1
)
(1,1)
(1,1)。
对第三个样例的评论:
Furik 的最佳路径为:
(
1
,
1
)
(1,1)
(1,1) ,
(
1
,
2
)
(1,2)
(1,2) ,
(
1
,
3
)
(1,3)
(1,3) ,
(
2
,
3
)
(2,3)
(2,3) ,
(
3
,
3
)
(3,3)
(3,3) ,对于Rubik:
(
3
,
3
)
(3,3)
(3,3) ,
(
3
,
2
)
(3,2)
(3,2) ,
(
2
,
2
)
(2,2)
(2,2) ,
(
2
,
1
)
(2,1)
(2,1) ,
(
1
,
1
)
(1,1)
(1,1) 。
样例的数字:
Furik 的路径用黄色标记, Rubik 的路径则用粉色标记。
题解
理解题意
输入一个 n × n n \times n n×n 的矩形,每个 a i , j a_{i,j} ai,j 是这个位置的价值。现在要从左上角走到右下角再返回,每个价值只被计算一次,求最大价值和。
分析思路
看完题目会马上想到和本题非常相似的两题 P1006 [NOIP2008 提高组] 传纸条 、 P1004 [NOIP2000 提高组] 方格取数 。
可是一看数据,麻了,
n
≤
300
n \le 300
n≤300 ,可是一时间想不到什么办法,只好先硬着头皮打暴力…
我们可以把一来一回看作两个人同时从起点出发,同时移动,所以可以定义
f
x
1
,
y
1
,
x
2
,
y
2
f_{x_1,y_1,x_2,y_2}
fx1,y1,x2,y2 为当 Furik 走到
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1) 的方格时, Rubik 走到
(
x
2
,
y
2
)
(x_2,y_2)
(x2,y2) 方格内的最大价值和。
则当前状态为
f
i
,
j
,
k
,
l
←
a
i
,
j
+
a
k
,
l
+
max
{
f
i
−
1
,
j
,
k
−
1
,
l
f
i
,
j
−
1
,
k
,
l
−
1
f
i
−
1
,
j
,
k
,
l
−
1
f
i
,
j
−
1
,
k
−
1
,
l
f_{i,j,k,l} \gets a_{i,j}+a_{k,l}+\max \begin{cases} f_{i-1,j,k-1,l}\\ f_{i,j-1,k,l-1}\\ f_{i-1,j,k,l-1}\\ f_{i,j-1,k-1,l} \end{cases}
fi,j,k,l←ai,j+ak,l+max⎩
⎨
⎧fi−1,j,k−1,lfi,j−1,k,l−1fi−1,j,k,l−1fi,j−1,k−1,l
若两个人走到同一个方格中,会计算重复,那我们应删去一个。
即当
i
=
=
k
i==k
i==k 并且
j
=
=
l
j==l
j==l 同时成立时,
f
i
,
j
,
k
,
l
−
=
a
i
,
j
f_{i,j,k,l}-=a_{i,j}
fi,j,k,l−=ai,j 。
最后取
f
n
,
n
,
n
,
n
f_{n,n,n,n}
fn,n,n,n 就是答案了。
但由于 n n n 可能达到 300 300 300 ,时间复杂度为 O ( n 4 ) O(n^4) O(n4) ,所以会出现爆空间和内存…
Code ( MLE+TLE , 40ps )
#include<bits/stdc++.h>
using namespace std;
const int N=101;
int n,x,y,z,a[N][N];
int f[N][N][N][N];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++)
scanf("%d",&a[i][j]);
}
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
for(int k=1;k<=n;k++) {
for(int l=1;l<=n;l++) {
f[i][j][k][l]=max(f[i-1][j][k-1][l],max(f[i][j-1][k][l-1],max(f[i-1][j][k][l-1],f[i][j-1][k-1][l])))+a[i][j]+a[k][l];
if(k==i&&l==j) f[i][j][k][l]-=a[i][j];
}
}
}
}
printf("%d\n",f[n][n][n][n]);
return 0;
}
经过一番思考,可以想到,在定义dp数组 f f f 时, Furik 和 Rubik 是同时出发的,所以他们两个人所走的步数也一样是相同的,再深入则引出了著名的哈曼顿距离,简单来说就是对于任意一个 ( x , y ) (x,y) (x,y) 方格,这个距离就为 x + y x+y x+y , 即某个点或方格的横纵坐标的和。
那么可以优化定义为 f i , x 1 . x 2 f_{i,x_1.x_2} fi,x1.x2 ,表示当两人走了 i i i 步时, Furik 的横坐标为 x 1 x_1 x1 , Rubik 的横坐标为 x 2 x_2 x2 ,知道了横坐标,必然不能少了纵坐标。
由刚刚的定理易得两条性质:
- { y 1 = i − x 1 y 2 = i − x 2 \begin{cases} y_1=i-x_1\\ y_2=i-x_2 \end{cases} {y1=i−x1y2=i−x2
- 若 x 1 = x 2 x_1=x_2 x1=x2 ,那么 y 1 = y 2 y_1=y_2 y1=y2 。
状态转移方程式为
f
i
,
j
,
k
=
a
i
−
j
,
j
+
a
i
−
k
,
k
+
max
{
f
i
−
1
,
j
,
k
f
i
−
1
,
j
−
1
,
k
f
i
−
1
,
j
,
k
−
1
f
i
−
1
,
j
−
1
,
k
−
1
f_{i,j,k} = a_{i-j,j}+a_{i-k,k}+\max \begin{cases} f_{i-1,j,k}\\ f_{i-1,j-1,k}\\ f_{i-1,j,k-1}\\ f_{i-1,j-1,k-1} \end{cases}
fi,j,k=ai−j,j+ai−k,k+max⎩
⎨
⎧fi−1,j,kfi−1,j−1,kfi−1,j,k−1fi−1,j−1,k−1
同样的我们也要消掉两人走到同一个方格时的价值。
时间复杂度为 O ( 2 × n 3 ) O(2 \times n^3) O(2×n3) ,并不会 TLE。
Code2 ( AC , 100ps )
#include<bits/stdc++.h>
using namespace std;
const int oo=0x3f3f3f3f;
const int N=301;
int n,a[N][N];
int f[N<<1][N][N];
int main() {
scanf("%d",&n);
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++)
scanf("%d",&a[i][j]);
}
memset(f,-oo,sizeof(f)); //存在负数,所以初始化设成-oo
f[2][1][1]=a[1][1];
for(int i=3;i<=(n<<1);i++) { //枚举步数
for(int j=1;j<=n&&j<i;j++) { //Furik的横坐标
for(int k=j;k<=n&&k<i;k++) { //Rubik的横坐标
int val=a[i-j][j];
if(j!=k) val+=a[i-k][k]; //若不在同一个格子内,加上
f[i][j][k]=max(f[i][j][k],f[i-1][j][k]+val);
f[i][j][k]=max(f[i][j][k],f[i-1][j-1][k]+val);
f[i][j][k]=max(f[i][j][k],f[i-1][j][k-1]+val);
f[i][j][k]=max(f[i][j][k],f[i-1][j-1][k-1]+val);
}
}
}
printf("%d\n",f[n<<1][n][n]);
return 0;
}
后记
此处为本人的错误小笔记,与原文无关。
1) 还原
一开始的思路是按照题目说的去做,先计算 Furik 的最优路径 $f_{n,n},并用 x , y x,y x,y 数组记录路径,然后再把最优路径上的方格都用 v i s vis vis 标记其访问过(也可修改其值为 0 0 0 ),然后再从 ( n , n ) (n,n) (n,n) 开始,找出 Rubik 到 ( 1 , 1 ) (1,1) (1,1) 的最大值,存入数组 g g g 中,最后直接把两个最优路径相加 f n , n + g 1 , 1 f_{n,n}+g_{1,1} fn,n+g1,1 完成。
2) 推翻
由于定义的数组 f , g f,g f,g 都是基于当前的最优路径价值,并不能直接将其相加使之成为全局的最大价值和。
由于两条路径可能出现重叠的部分,而在寻找第一个最优路径时,做法舍弃了其他方格上的数值,把本来只需用一次访问的方格重复访问了两次,浪费了步数,从而导致程序算不出最优解。
WA 程序答案路径:
正解的路径:
Code3 ( WA , 40ps )
浅浅记录一下错误代码…
#include<bits/stdc++.h>
using namespace std;
const int oo=0x3f3f3f3f;
const int N=520;
int n,a[N][N],f[N][N];
int g[N][N],x[N][N],y[N][N];
bool vis[N][N];
void dfs(int xx,int yy) {
if(!xx&&!yy) return ;
vis[xx][yy]=1;
dfs(x[xx][yy],y[xx][yy]);
}
int main() {
scanf("%d",&n);
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
scanf("%d",&a[i][j]);
f[i][j]=g[i][j]=-oo;
}
}
for(int i=1;i<=n;i++) {
for(int j=1;j<=n;j++) {
if(f[i][j-1]+a[i][j]>f[i][j]) {
f[i][j]=f[i][j-1]+a[i][j];
x[i][j]=i;
y[i][j]=j-1;
}
if(f[i-1][j]+a[i][j]>f[i][j]) {
f[i][j]=f[i-1][j]+a[i][j];
x[i][j]=i-1;
y[i][j]=j;
}
}
}
dfs(n,n);
for(int i=n;i>=1;i--) {
for(int j=n;j>=1;j--) {
g[i][j]=max(g[i+1][j],max(g[i][j+1],g[i][j]));
if(!vis[i][j])
g[i][j]+=a[i][j];
}
}
printf("%d\n",f[n][n]+g[1][1]);
return 0;
}