【试炼场】矩阵取数游戏 【区间DP】

传送门

题目大意

给出一个N行M列的矩阵,矩阵中每个格子上有一个非负整数,要求取数M次,每次取数取走每一行的第一个数或者最后一个数,若值为a的数在第K次取数中被取到,能够得到的分值是 2k × \times a。现在求M次取数之后能够得到的最大的分值。

分析

显而易见的是,每一行都有一个最佳的取数方案,且这些方案之间互不影响(也就是说,你对第K行的取数方案再怎么变化,对于任意的另一行的方案都没有影响)

既然如此,我们就分开考虑,把每一行的最佳方案找到,最后再加回去就可以了。

数据范围N,M均 \leq 80,较小的数据范围很容易让人想到动态规划。接下来就是构思状态的定义以及转移方程了。

为了方便表达,我用val[i]表示当前行第i个数的值。

  1. 状态的定义

每一次取数只能拿走行首或者行末,因而每一次取数过后留下的一定是连续的一段数。对于这种情况,我们通常都是使用区间dp来表示的。

既然是区间dp,那么通过你多年的经验状态就是f[i][j],存储当前剩下的一段数是从第i个到第j个时能取得的最大价值。

  1. 状态的转移

理论上说应该是有两种转移方案,

一种是由外到内,从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个的取物方案。两种都得好好熟练运用啊~

猜你喜欢

转载自blog.csdn.net/Cyan_rose/article/details/83020933
今日推荐