洛谷 P1430 序列取数 【区间dp+挖掘性质优化】

文章讲述了如何使用区间动态规划解决A和B在整数序列中轮流取数的问题,通过定义状态并进行有效状态转移,优化时间复杂度,以求得A的最高得分。
摘要由CSDN通过智能技术生成

原题链接:https://www.luogu.com.cn/problem/P1430

题目描述

给定一个长为 n 的整数序列 (n≤1000),由 A 和 B 轮流取数(A 先取)。每个人可从序列的左端或右端取若干个数(至少一个),但不能两端都取。所有数都被取走后,两人分别统计所取数的和作为各自的得分。假设 A 和 B 都足够聪明,都使自己得分尽量高,求 A 的最终得分。

输入格式

第一行,一个正整数 T,表示有 T 组数据。(T≤100)

接着 T 行,每行第一个数为 n,接着 n 个整数表示给定的序列。

输出格式

输出T行,每行一个整数,表示 A 的得分。

输入输出样例

输入 #1

2
1 -1
2 1 2

输出 #1

-1
3

解题思路:

对于一个区间内的数,A分得一部分的数,B分得剩下那部分的数。从A先开始A和B轮流取,每次只能从剩下的数的左边取一段或者右边取一段,有点像石子合并那个题目,根据这个操作我们就可以看出来是一个区间dp问题,下面考虑怎么定义状态来处理呢。

我们先考虑一下石子合并那个解法,那么就需要第一维枚举区间长度,第二维枚举区间左端点,第三维考虑区间中点进行分割,如果按照这个方法处理的话时间复杂度O(n^3),n=1000,那么时间复杂度就到了1e9,这显然过不了,我们可以接着看看有没有什么性质来优化一下。

对于题目给定的操作是每次从左侧或者右侧取一段数,那么我们来看看有没有什么等价的操作,我们发现可以将一次取一段数改为一次取一个数,一次取可以进行多次操作,通过连续取这一段数的长度,也就取得了这一段数,所以这俩种操作实际上是可以等价转换的,然后我们考虑每次取一个数设计状态。

我们可以定义如下状态

  • f[l][r][0]表示先手取这个区间左侧的数a[l]能取得的最大值
  • f[l][r][1]表示先手取这个区间右侧的数a[r]能取得的最大值

为了优化状态转移,我们还定义g[l][r]表示后手在这个区间内能取得的最大值

但是g[l][r]应该怎么进行计算呢,首先根据状态定义g[l][r]表示后手能在区间[l,r]取得的最大值

那么g[l][r]=区间[l,r]所有的元素和-先手能在区间[l,r]取得的最大值

也就是g[l][r]=(s[r]-s[l-1])-max(f[l][r][0],f[l][r][1]);

这样需要的一些式子的计算方式就知道了,就可以考虑状态转移了

状态转移

先手取这个区间左边的那个数

f[l+1][r][0]表示先手接着取左边。g[l+1][r]表示先手不取了,轮到后手取了。

  • f[l][r][0]=a[l]+max(f[l+1][r][0],g[l+1][r]);

先手取这个区间右边的这个数

f[l][r-1][1]表示后手接着取右边。g[l][r-1]表示先手不取了,轮到后手取了

  • f[l][r][1]=a[r]+max(f[l][r-1][1],g[l][r-1]);

然后每次还要计算一下g[l][r],方便后续状态转移

  • g[l][r]=(s[r]-s[l-1])-max(f[l][r][[0],f[l][r][1])

根据上述优化,我们就将时间复杂度优化到了O(n^2),n=1000,这样时间就是1e6了,T=100,最多有100组数据,所以最终时间复杂度为O((n^2)*T),时间复杂度为1e8,题目给了3s的时间,所以是可以过的。

最终答案

由于最开始取时,可以取左边的数也可以取右边的数,所以答案就是max(f[1][n][0],f[1][n][1])

cpp代码如下

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;
typedef long long LL;
const int N = 1010;

int T, n;
int a[N];
LL s[N], f[N][N][2], g[N][N];
int main()
{
    cin >> T;
    while (T--)
    {
        cin >> n;
        for (int i = 1; i <= n; i++)
        {
            scanf("%d", &a[i]);
            s[i] = s[i - 1] + a[i];
        }

        for (int len = 1; len <= n; len++)
            for (int l = 1; l + len - 1 <= n; l++)
            {
                int r = l + len - 1;
                if (len == 1)  //区间长度为1,只有一个数,这个数肯定是先手的
                    f[l][r][0] = f[l][r][1] = a[l], g[l][r] = 0;
                else
                {
                    //f[l+1][r][0]表示先手接着取左边。g[l+1][r]表示先手不取了,轮到后手取了。
                    f[l][r][0] = a[l] + max(f[l + 1][r][0], g[l + 1][r]);
                    //f[l][r-1][1]表示后手接着取右边。g[l][r-1]表示先手不取了,轮到后手取了。
                    f[l][r][1] = a[r] + max(f[l][r - 1][1], g[l][r - 1]);
                    //然后每次还要计算一下g[l][r],方便后续状态转移
                    g[l][r] = s[r] - s[l - 1] - max(f[l][r][0], f[l][r][1]);
                }
            }
        // 由于最开始取时,可以取左边的数也可以取右边的数,所以答案就是max(f[1][n][0],f[1][n][1])
        printf("%lld\n", max(f[1][n][0], f[1][n][1]));
    }
    return 0;
}
  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值