【做题】CSA49F - Card Collecting Game——思维&dp

原文链接 https://www.cnblogs.com/cly-none/p/CSA49F.html

题意:Alice和Bob在玩游戏。有n种卡牌,每种卡牌有bi张,保证bi为偶数。现在,Alice要把所有卡牌任意平分为2份(仅要求每份卡牌数为bi2),并对每份分别进行一次游戏。第一次游戏由Alice先手,第二次由Bob先手。

每次游戏中,Alice和Bob会轮流取走一张卡牌直到取尽。设最后Alice有ni张第i种牌,那么她会得到niaici的分数。一次游戏的得分是Alice从每种牌得到的分数总和。

现在,Alice想要最大化两次游戏的得分总和,Bob则想最小化。求出在两人都采取最优决策时的得分总和。

n2×103, ai2×103, bi5×105, ci0

先考虑如何计算一次游戏的得分。

首先,因为是轮流取,故对于每2ai张卡牌i,都能产生ci的分数。因此,我们可以先考虑这一部分的贡献,然后将所有ni2ai取模。

接下来,考虑如果ni<2ai1,那么只要Bob跟着Alice取,Alice就得不到这个ci。当ni=2ai1时,先手就能恰好取到ai张牌,但先后手顺序会交换。

因此,对于剩下来的卡牌,我们忽略ni<2ai1的,并对剩下的按ci从大到小排序。那么,若Alice先手,就能得到2ici的分数;否则就是2|ici

那么,我们就能得到一个dp的做法。以ci为关键字排序后,设dp[i,j,a,b]表示当前处理了前i种卡牌,第一份已经有j张卡牌,且第一份有ani2ai取模后是2ai1的卡牌,第二份有b种。注意到只要记录ab的奇偶性就可以了。暴力转移,则这个dp的复杂度是O((bi)2)

但这样还不足以解决本题。考虑ai比较小,故我们要从这个角度来优化dp。

先注意到两点,一是我们在转移时,产生的贡献只和放到第一份的数量对2ai取模的值有关;二是dp状态中最庞大的j,最后只是用来确定第一份的数量等于bi2的。于是我们考虑对j进行优化,目的是在转移时,只用枚举放在第一份的数量对2ai取模的值。

于是我们把第一份卡牌分为两个部分,一部分是所有ni2ai取模后的结果,则另一部分的卡牌总数就是i2aiki的形式。要保证第一份的卡牌总数为一个固定值,我们就要求出i2aiki的能表示出哪些数。

先考虑ki的取值范围。设nimod2ai=ri,那么ki就是在[0,biri2ai]之间的整数。但biri2ai有两种取值:在ribimod2ai时,为bi2ai;否则是bi2ai1。于是我们不妨就令ki的上界为bi2ai1,当ribi的时候,把多出来的那个2ai算在第一部分里就可以了。

剩下就是一个背包问题。注意到我们可以把ai相等的数放在一起计算,而ai只有$ \sqrt {\sum a_i}O(\sqrt {\sum a_i} (\sum b_i))$的。

总结一下,第一部分的处理和O((bi)2)的算法差不多,但这一部分的复杂度是O((ai)2)的。第二部分的背包,复杂度为O(ai(bi))

于是时间复杂度为O((ai)2+ai(bi))

Copy
#include <bits/stdc++.h> using namespace std; const int A = 2010, B = 500010, N = 2010; int dp[2][A << 2][2][2], bag[B], n, num[A], p, sa, sb, ans; struct data { int a,b,c; bool operator < (const data& x) const { return c > x.c; } } dat[N]; inline void ckmx(int& x,int y) { x = x < y ? y : x; } int main() { scanf("%d",&n); for (int i = 1 ; i <= n ; ++ i) scanf("%d%d%d",&dat[i].a, &dat[i].b, &dat[i].c); sort(dat+1,dat+n+1); for (int i = 1 ; i <= n ; ++ i) { if (dat[i].b / (dat[i].a << 1) - 1 > 0) num[dat[i].a] += dat[i].b / (dat[i].a << 1) - 1; sa += dat[i].a; sb += dat[i].b; } p = 1; memset(dp,-1,sizeof dp); dp[0][0][0][0] = 0; for (int i = 1, sum = 0 ; i <= n ; ++ i, p ^= 1) { memset(dp[p],-1,sizeof dp[p]); for (int j = 0 ; j <= (sum << 2) ; ++ j) for (int a = 0 ; a < 2 ; ++ a) for (int b = 0 ; b < 2 ; ++ b) { if (dp[p^1][j][a][b] == -1) continue; for (int k = 0 ; k < 2 * dat[i].a && k <= dat[i].b ; ++ k) { int tmp = (dat[i].b - k) / (dat[i].a << 1), na = a, nb = b; if (k == 2 * dat[i].a - 1) { if (!na) tmp ++; na ^= 1; } if ((dat[i].b - k) % (dat[i].a << 1) == (dat[i].a << 1) - 1) { if (nb) tmp ++; nb ^= 1; } tmp = tmp * dat[i].c; ckmx(dp[p][j + k][na][nb], dp[p^1][j][a][b] + tmp); if (dat[i].b >= 2 * dat[i].a && k <= dat[i].b % (dat[i].a << 1)) ckmx(dp[p][j + k + 2 * dat[i].a][na][nb], dp[p^1][j][a][b] + tmp); } } sum += dat[i].a; } bag[0] = 1; for (int i = 1 ; i < A ; ++ i) { if (!num[i]) continue; for (int j = 0 ; j <= sb ; ++ j) { if (bag[j]) bag[j] = 0; else { bag[j] = -1; if (j >= 2 * i) { if (bag[j - 2 * i] != -1) bag[j] = bag[j - 2 * i] + 1; } } } for (int j = 0 ; j <= sb ; ++ j) if (bag[j] == -1) bag[j] = 0; else if (bag[j] <= num[i]) bag[j] = 1; else bag[j] = 0; } p ^= 1; for (int i = 0 ; i <= (sa << 2) && i <= (sb >> 1) ; ++ i) for (int a = 0 ; a < 2 ; ++ a) for (int b = 0 ; b < 2 ; ++ b) { if (dp[p][i][a][b] == -1) continue; if (bag[(sb >> 1) - i]) ans = max(ans, dp[p][i][a][b]); } printf("%d\n",ans); return 0; }

小结:这个问题相当有难度。得到O((bi)2)的dp已经偏难,而后一部分的优化对思维能力和细节处理能力的要求,是在博主目前能力之上的,也体现了算法优化的一些重要思路。

posted @   莫名其妙的aaa  阅读(425)  评论(0编辑  收藏  举报
编辑推荐:
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· ASP.NET Core 模型验证消息的本地化新姿势
阅读排行:
· ThreeJs-16智慧城市项目(重磅以及未来发展ai)
· .NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想
· Ai满嘴顺口溜,想考研?浪费我几个小时
· Browser-use 详细介绍&使用文档
· 软件产品开发中常见的10个问题及处理方法
点击右上角即可分享
微信分享提示