传送门
题目大意
给出一个N行M列的矩阵,矩阵中每个格子上有一个非负整数,要求取数M次,每次取数取走每一行的第一个数或者最后一个数,若值为a的数在第K次取数中被取到,能够得到的分值是 2k × \times ×a。现在求M次取数之后能够得到的最大的分值。
分析
首先我们可以看出,每一行都有一个最佳的取数方案,且这些方案之间互不影响(显而易见 )
既然如此,我们就分开考虑,把每一行的最佳方案找到,最后再加回去就可以了。
数据范围N,M均 ≤ \leq ≤ 80,较小的数据范围很容易让人想到动态规划。接下来就是构思状态的定义以及转移方程了。
为了方便表达,我用val[i]表示当前行第i个数的值。
1.状态的定义
每一次取数只能拿走行首或者行末,因而每一次取数过后留下的一定是连续的一段数。对于这种情况,我们通常都是使用区间dp来表示的。
既然是区间dp,那么通过你多年的经验状态就是f[i][j],存储当前剩下的一段数是从第i个到第j个时能取得的最大价值。
2.状态的转移
理论上说应该是有两种转移方案,
一种是由外到内,从f[1][M]开始dp,答案则是在f[k][k] (k ∈ \in ∈[1,M])中取最大值,再加上2M × \times ×k点对应的值。
仅限口胡没试过,等哪天有空了搞一搞
另一种则是反过来,从每一个f[k][k]开始,最终答案就是f[1][M];
博主采用的是第二种。
由内而外转移的话,我们每次转移要么扩张区间的左侧,要么扩张区间的右侧,所以能够转移到f[i][j]的状态只有f[i+1][j]和f[i][j-1]。
转移的时候还要注意,题目里所说的分值的定义与取数的顺序有关,因此在进行转移的时候,相当于先取了边上的i或者j,再按照原来f[i-1][j]和f[i][j-1]的最优方案进行取数
这样的后果就是:原方案中第i次取第k个数的代价 k × \times × 2i 会变为 k × \times × 2i+1。
所以在转移的时候,要先把原来的最优方案的代价 × \times × 2,再加上val[i] × \times × 2或者val[j] × \times × 2
说了这么多了,转移方程很清晰了:
f[i][j]=max(f[i+1][j]*2+val[i]*2,f[i][j-1] * 2+val[j]*2)
别的问题
除了dp,此题还有个难点,就是各行的答案会超过long long的上界,这就强制要求我们写个高精……
但我会屈服吗?当然不会!在翻遍题解区之后,发现了一个新东西:__int128。
__int128,可以理解为是两个long long(即__int64)拼起来的,因此能够表达的数就会长出一倍,可以很方便的过掉这道题。不过,在本机调试是用不了的,貌似是因为编译器不支持这种类型,调试的话可以去洛谷的IDE上面用。
另外就是注意一下__int128不能直接输出,需要自己手写一个输出函数~
当然,如果你是个有毅力的人,写个高精也可以当是练习一下。不过话说回来,近年来的题目基本上都在取模运算,高精貌似考的越来越少了……这就看个人喜好吧~
还有一些小细节就看代码吧:
#include<bits/stdc++.h>
#define rint register int
#define ivoid inline void
#define iint inline int
#define ull unsigned long long
#define ll __int128
using namespace std;
const int N=10e5+5;
const int M=2000;
ll a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;
ll ans;
ll b0[M][M],dp[M][M];
ll rad()//快读
{
ll ret=0,f=1;
char c;
for(c=getchar();(c<'0'||c>'9')&&c!='-';c=getchar())
if(c=='-')
f=-1,c=getchar();
for(;c>='0'&&c<='9';c=getchar())
ret=(ret<<3)+(ret<<1)+c-'0';
return ret*f;
}
void print(ll x)//输出答案
{
if(!x) return;
if(x) print(x/10);
putchar(x%10+'0');
}
ivoid dp1(ll x)//区间dp
{
for(rint k=0;k<=m;k++)
for(rint i=1,j=i+k;i<=m&&j<=m;i++,j=i+k)
{
dp[i][j]=max((dp[i+1][j]+b0[x][i])<<1,(dp[i][j-1]+b0[x][j])<<1);
//这里的方程换了一种写法,实质上是一样的,主要是为了位运算快一点
}
ans+=dp[1][m];
}
#define read(x) x=rad()
int main()
{
read(n);read(m);
for(rint i=1;i<=n;i++)//读入矩阵
for(rint j=1;j<=m;j++)
read(b0[i][j]);
for(rint i=1;i<=n;i++)//按行dp
dp1(i);
if(ans!=0)
print(ans);
else
cout<<0;
return 0;
}
总结一下
区间dp算是很经典的一类动态规划问题了,用得最多的一种是这样从每一个子状态合并推出全局状态,还有一种就是表示前i个取j个的取物方案。两种都得好好熟练运用啊~