原题链接: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;
}