[luogu P1006]传纸条
这题做的我怀疑人生。。这题应该属于看起来简单,做起来费劲那种类型的。
介绍三种做法,复杂度各不相同, \(O(n^5)\) \(O(n^4)\) 到 \(O(n^3)\) 都有(五次方的好像是一个新做法)
题意:给出\(n\)行\(m\)列的矩阵,以左上角为起点,以右下角为终点,找到两条不相交路径,经过的权值和最大是多少。
\(O(n^5)\)做法
把最终的路径划分为 \(n\) 行,设 \(f_{i,j,k}\) 表示两张纸条传到第 \(i\) 行,第一张传到第 \(j\) 列,第二张传到第 \(k\) 列,能得到的最大好心值。
转移方程很好写,从上一行合法的位置转移过来即可。这种方法容易确保路径不相交,而且跑不满,最慢的点只有300ms,开了O2甚至比 \(n^4\) 做法还要快。
代码:
#include<cstdio>
#include<cctype>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
int a[81][81],qz[81][81];int n,m;
int f[81][81][81];
int main(void){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)scanf("%d",&a[i][j]),qz[i][j]=qz[i][j-1]+a[i][j];
memset(f,0xc1,sizeof(f));//-1044266559
//赋初值为无穷小,可以自动过滤掉不合法状态
for(int i=1;i<=m;++i)f[1][1][i]=qz[1][i];
for(int i=2;i<=n;++i){
for(int j=1;j<m;++j)for(int k=j+1;k<=m;++k){
for(int jj=1;jj<=j;++jj)for(int kk=j+1;kk<=k;++kk)
f[i][j][k]=max(f[i][j][k],f[i-1][jj][kk]+qz[i][j]-qz[i][jj-1]+qz[i][k]-qz[i][kk-1]);
}
}
printf("%d\n",f[n][m-1][m]);
return 0;
}
\(O(n^4)\)做法
其他题解里介绍的很多了。用 \(f_{x1,y1,x2,y2}\) 表示两个纸条分别传到\((x1,y1)(x2,y2)\)的最大值。
这种做法看起来不容易确保路径不相交,其实是可以的,就是有些麻烦。
想象一个方格图,左上角是(1,1),右下角是(n,m),想一想如果一个纸条在另一个纸条左上方,里面的那个纸条就动不了了,因为我们不知道右下的纸条经过了哪些格。
如果两个纸条在同一行或者同一列,或者一个在另一个右上,却可以进行转移。(原来这么简单)
具体实现呢?分成同一行或者同一列,还有右上两种情况考虑。本来以为边界值不太好写,后来发现想复杂了(我太菜了,老是搞不明白这种细节)。此代码可以进一步简化。
#include<cstdio>
#include<cctype>
#include<cstdlib>
#include<cstring>
#include<algorithm>
using namespace std;
int a[81][81];int n,m;
int f[81][81][81][81];
//x1>=x2 y1<=y2
int main(void){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)scanf("%d",&a[i][j]);//,qz[i][j]=qz[i][j-1]+a[i][j];
memset(f,0xc1,sizeof(f));//-1044266559
f[1][1][1][1]=0;
for(int x1=1;x1<=n;++x1)for(int x2=1;x2<=x1;++x2)
for(int y2=1;y2<=m;++y2)for(int y1=1;y1<=y2;++y1){
//x1 y1 x2 y2 可以用作局部变量
if(x1==x2&&y1==y2)continue;
int &dp=f[x1][y1][x2][y2];
if(x1==x2){
dp=max(dp,f[x1][y1][x2][y2-1]+a[x2][y2]);
dp=max(dp,f[x1][y1][x2-1][y2]+a[x2][y2]);
}
else if(y1==y2){
dp=max(dp,f[x1-1][y1][x2][y2]+a[x1][y1]);
dp=max(dp,f[x1][y1-1][x2][y2]+a[x1][y1]);
}
else {
dp=max(dp,f[x1][y1][x2][y2-1]+a[x2][y2]);
dp=max(dp,f[x1][y1][x2-1][y2]+a[x2][y2]);
dp=max(dp,f[x1-1][y1][x2][y2]+a[x1][y1]);
dp=max(dp,f[x1][y1-1][x2][y2]+a[x1][y1]);
}
}
printf("%d\n",f[n][m-1][n-1][m]);
return 0;
}
}
\(O(n^3)\)做法
最快的一种做法,别的题解也介绍过了。
两个纸条一起走,这个走一步那个也走一步。是不是巧妙地避免了路径相交?
\(f_{i,j,k}\) 表示两个纸条分别走了 \(i\) 步,一个走到 \(j\) 行一个走到 \(k\) 行。步数行数都知道了,可以算列数。转移方程:\(f_{i,j,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})+a_{j,i+2-j}+a_{k,i+2-k}\)
具体实现需要注意,每一步最少/最多走到多少行,比如你走了很多步不可能还在第一行。
代码
#include<cstdio>
#include<cctype>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#define Max(a,b,c,d) max(max(a,b),max(c,d))
using namespace std;
int n,m;
int a[81][81],f[161][81][81];
int main(void){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i)for(int j=1;j<=m;++j)scanf("%d",&a[i][j]);
memset(f,0xc0,sizeof(f));
f[0][1][1]=0;
for(int i=1;i<n+m-2;++i){
const int liml=max(1,i-m+2),limr=min(n,i+1);
//limit_left limit_right
for(int j=liml;j<=limr;++j)for(int k=liml;k<=limr;++k)if(k!=j)
f[i][j][k]=max(f[i][j][k], Max(f[i-1][j][k],f[i-1][j-1][k],f[i-1][j-1][k-1],f[i-1][j][k-1]) + a[j][i+2-j]+a[k][i+2-k] );
}
printf("%d\n",f[n+m-3][n-1][n]);
return 0;
}