一个动态规划的经典问题
本题是在牛客网上的讨论区发现的字节跳动的笔试题,感觉很有意思。原作者写的思路我看了很久才看明白。在这里记录一下个人理解。
原题意思大概如下:
A和B两个人在一组牌中各自选取几张。每张牌上有两个数字,一个代表得到该牌后的个人分,另一个代表团体分。要求最后A,B两人个人得分相同,团体得分最大。
输入:
第一行一个整数,N代表牌的数量,
接下来N行,每行两个数字。
输出:
最大团体得分
示例:
输入:
5
1 1
1 2
3 4
5 6
2 3
输出:
13
无效思路:
最优化问题通常都是使用动态规划去解。我刚开始的想法是对每一张牌都有两种情况:选,不选。
不选的情况下opt(i) = opt(i-1),选了的话就要判断选这张牌的情况下剩余的牌的最优情况。是有后效性的,十分复杂。这让我一度怀疑这题能不能使用动态规划。在后面的学习中才认识,对于同一问题既可以存在有后效性的问题定义,也可以有无后效性的问题定义。要使用动态规划解题,就得找出问题的无后效性定义,从而写出其状态转移方程。
正确思路:
使用动态规划是毫无疑问的了。难就难在对该问题给出一个无后效性的定义。
记X(A), X(B)分别为玩家A,B的个人得分,sumx为所有卡片的个人得分之和,sumy为所有卡片的团体得分之和。
A和B的个人得分之差为X(A) - X(B),当X(A) <X(B),那么差值为负数,但是数组下标不能为负数。因此建立长度为 2*sumx+1的数组存储,以其中间位置的元素下标sumx处作为X(A)==X(B)时的差值。0 - (sumx - 1) 的部分就是X(A)<X(B)的情况了。
f[i][j]表示考虑第0张~第i张卡片的情况下,差值为j时的最大团体分.
当面对第i张牌时,有三种可能情况:A,B都不要,A要,B要。对于每种可能的差值j,当第一种情况好理解,现在来看第二种情况当A要的时候,此时要得到当前差值的团体分,就要去找差值为j-x[i]处的团体分加上y[i].第三种情况同理。则有如下状态转移方程
![]()
记N为卡片总张数,则 f[N - 1][0] 即为所求。
显然,f为一个 N * (sum * 2 + 1)维的数组。
由于卡片张数N,最大有100,每一张卡片的个人得分x[i]最大能达到1000,所以 sum可以达b到10W,数组f的空间最大可达 100 * 10W * 2 = 2000W 个元素,有超过空间限制的危险,注意到只需保留f[i - 1],即可求出f[i],因此采用滚动数组。
为处理方便,特别地,记f[-1]为玩家A,B均未拿取任何卡片的情况,显然,仅有f[-1][sum] = 0,f[-1][0] ~ f[-1][sum - 1] 以及 f[-1][sum + 1] ~ f[-1][2 * sum] 均为不合法的状态 (A,B分差为0时取得的团体分只有可能为0)。
将这些不合法的状态初始化为一个小于团体得分之和小于 -sumy 的值,以保证后序步骤中,从这些不合法状态得来的结果均小于0,已达到排除不合法状态的目的。
代码如下:
#include <cstdio>
#include <iostream>
#include <vector>
#include <queue>
#include <map>
#include <numeric>
using namespace std;
int main()
{
int N;
while (cin >> N)
{
//读入数据
vector<int> x(N), y(N);
for (int i = 0; i < N; ++i)
cin >> x[i] >> y[i];
int sumx = accumulate(x.begin(), x.end(), 0);
int sumy = accumulate(y.begin(), y.end(), 0);
//小于sumy的值,用于赋给不合法的状态
int INF = -sumy * 4;
//滚动数组f[i][j], 并将所有值初始化为INF
vector<vector<int>> f(2, vector<int>(2 * sumx + 1, INF));
int pre = 0, cur = 1;
//此时cur代表-1,将f[-1][0]赋为0
f[cur][sumx] = 0;
for (int i = 0; i < x.size(); ++i)
{
swap(pre, cur);
//此时cur代表i,pre代表i - 1, f[pre]为上一轮已求解完的状态,f[cur]为接下来要求解的状态
for (int j = 0; j < f[cur].size(); ++j)
f[cur][j] = f[pre][j]; // f[i][j] = f[i - 1][j]
for (int j = 0; j < f[0].size(); ++j)
{
if (j + x[i] < f[0].size())) f[cur][j] = max(f[cur][j], f[pre][j + x[i]] + y[i]); //f[i][j] = max(f[i][j], f[i - 1][j + x[i]] + y[i]
if (j - x[i] >= 0) f[cur][j] = max(f[cur][j], f[pre][j - x[i]] + y[i]);//f[i][j] = max(f[i][j], f[i - 1][j - x[i]] + y[i]
}
}
cout << f[cur][sumx] << endl;
}
}
再总结一下动态规划:
找出无后效性定义,写出状态转移方程,根据状态转移方程求解。本题中还应注意到一点:以数组下标表示了A,B之间的个人得分关系。不同数组代表了处理的不同卡片时的所有得分关系的状态。
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的。
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到
这个性质叫做最优子结构;
而不管之前这个状态是如何得到的
这个性质叫做无后效性。
再废话两句:
一开始的无效思路,对于i-1选或者不选,会影响到选择i后还能不能保证差值为0。因此是存在后效性。而有效方案则将个人得分差值情况与团体得分情况分开了,其计算了所有差值情况的团体分,得到整体最优再选择了差值0的最优解。
原牛客网链接:
https://www.nowcoder.com/discuss/93316?type=2&order=0&pos=6&page=1
本文介绍了一道典型的动态规划题目,通过合理的状态定义与状态转移方程,解决了两个玩家在有限牌堆中选取以获得最大团队分数的问题。文章详细解释了如何避免后效性,并通过滚动数组优化空间复杂度。

5159

被折叠的 条评论
为什么被折叠?



