原创 | 这道题codeforces的简单题差点做了我一下午,你能解出来吗?

时间:2022-07-24
本文章向大家介绍原创 | 这道题codeforces的简单题差点做了我一下午,你能解出来吗?,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

大家好,今天周末算法专题选的问题是codeforces contest1400中的B题,RPG Protagonist,翻译过来就是RPG游戏当中主人公。看起来这个标题就很唬人,其实这也是codeforces的题目风格。

这道题的链接是:https://codeforces.com/contest/1400/problem/B

按照道理来说div2比赛当中的B题都不难,大概和LeetCode当中的Easy差不多,有时候要简单一些,有时候要难一些。一般来说属于大部分人都能做出来的题。但今天选的这道题有一点意外,它的通过人数比同一场的C题还要少。其实并不是它难,而是这题当中藏着一个思维陷阱,我觉得挺有意思的,所以今天的文章选了它。

题意

codeforces和LeetCode题目很大的不同点在于codeforces当中的许多题目会有一个短的背景故事,而不是很枯燥地直接告诉你这道题是怎样怎样的你去算去吧。所以相比之下会有意思一些,当然在比赛的时候这也很考验选手的读题(英语)水平,你能不能过滤掉这些背景故事的干扰,迅速提炼出题干。

这题的背景故事是说你是一个RPG游戏当中的主人公,你和一个随从一起去铁匠铺买装备。你的目的是买尽可能多的斧子和剑,然后把这些武器融了获得铁锭。你和随从各有一个背包,你背包的容量是p,随从背包的容量是f。其中一把剑占的体积是s,一把战斧(war axe)占的体积是w。铁匠铺里一共有cnts把剑,cntw把斧子。

然后题目规定一把剑和一把斧子融了都可以获得一个铁锭,请问你最多可以获得多少铁锭?

其中

1 le p, f, s, w le 10^9

1 le cnts, cntw le 2 * 10^5

样例

首先输入t,表示测试数据的组数。

1 leq t leq 10^4

接着输入p和f,第二行输入cnts和cntw,最后一行输入s和w。

题解

这题有意思的地方是看着挺像是个裸的背包问题,但是我们看看数据范围就知道不可能用背包问题来解。并且只有两种商品,其实也没有什么背包的必要。仔细分析一下会发现不管买剑还是买斧子,得到的收益是一样的,都是1。所以很自然地会往贪心算法上来想。

贪心法推演

这道题的贪心策略很明显也很简单,既然只有两种商品,并且两种商品的收益一样,那么当然优先拿体积小的那个商品了。但很明显这样可能会造成体积的浪费,我们假设s的体积小于w,我们优先拿s。当我们拿完了s之后,发现剩下的体积拿不了w,但是又有空余,这就造成了浪费。

比如第一个例子当中,p和f是33和27。s和w是5和6,假设我们33先拿,我们优先拿5,拿了6个刚好拿完,这时候的用了30的体积,剩下的3点体积就浪费了。这时候27拿6的话就只能拿4个,于是总共只能拿6+4=10个铁锭。但实际上我们33可以拿3个5和3个6,这样刚好可以把33体积全部用完,27就拿剩下的3个5以及2个6,也可以把体积拿完,这样我们实际上一共可以拿6 + 5 = 11个铁锭,比上面的做法更优。

但是这个问题似乎并不是不可以解决的,比如刚才,我们拿了6个5之后发现有多余,我们可以先拿6个5,发现多余了3。为了把空间尽可能用起来,我们可以把已经拿的5去换6。6和5相差一点体积,我们多了3点体积,所以可以把3个5换成6。这样刚好就可以得到答案那个结果。

稍微想了一下,好像没有反例。于是写了代码提交,错了。我找了一下错误的数据是这个:

51 30
7 16
8 2

按照我们刚才的方法来看,我们51先拿的话,直接可以把16个2全部拿完,之后剩下了19点空间,我们可以拿2个8。剩下的30拿3个8,于是可以一共可以拿16 + 2 + 3 = 21个铁锭。

但是这个答案是错的,因为51和30这两个背包都没有用完。51浪费了5,30浪费了6,这非常可惜。如果我们只拿13个2,然后拿3个8,这样就可以把51全部装满。之后30这个包可以装1个2和3个8,这样我们可以拿22个铁锭。

感觉好像又只是一种特殊情况,如果我们可以想一个机制来解决这种情况是不是就可以AC这道题了?

很遗憾,这种特殊情况很难解决,因为对于s商品可以一次性全部拿完的情况,我们很难判断每一个包究竟拿多少才可以达到最优。也就是说贪心法从根本上就是不可行的,这是出题人留下的诱饵,给你一种好像可以解,你再多努努力就可以了的错觉

回头用枚举

发现了贪心法不可解之后当然要回头了,把贪心这个方法堵死了之后,其实这道题的解法就很明显了。非常简单,就是枚举

我们捋一下刚才发现的信息,首先只有两种商品,也只有两个背包,两个商品的价值一样,我们希望最后受益最大。那么没的说,我们肯定是希望能尽可能得多装商品。我们假设p这个包装了x个s,那么它会装多少个w?答案是能装多少装多少

同样我们已经知道了p装了x个s和y个w,f这个包怎么装?当然是优先装体积更小的s,能装多少装多少。如果还有空余再装w,再能装多少装多少……所以我们会发现,p这个包拿s的数量确定了之后,整个拿取的方案也就确定了。我们枚举这个x就行了。

惊不惊喜意不意外,这题就是这么简单。但是如果一开始走错了路走到贪心法上去了,你可能做一下午也做不出来。而且这题也很阴险,给了

10^4

组测试数据,要枚举p包装了s的数量x,这个x的数量级是

10^5

,两个相乘算法的数量级要

10^9

了。但这题给了2秒,并且枚举的常数很小,Python跑完都只用了500毫秒,实际上是不会超时的,但很多人看到这个数据范围就被吓破胆了,连我一开始也被唬了一下。

但其实这也是codeforces的常规套路,让正解的时间复杂度看起来在超时边缘,吓你一波,让你不敢写。所以有经验的选手在比赛的时候,先不管三七二十一,想到算法先写出来。之后有时间多再来想优化。

只要没有被唬住,其实是很简单的题,我们一个枚举就结束了。

t = int(input())

for _ in range(t):
    p, f = list(map(int, input().split(' ')))
    cnts, cntw = list(map(int, input().split(' ')))
    s, w = list(map(int, input().split(' ')))

    # 我们默认让s是体积小的商品,这样方便编码
    if s > w:
        s, w = w, s
        cnts, cntw = cntw, cnts

    # 最多可以拿s的上限
    most_can_take = min(cnts, p // s)
    ret = 0
    # 枚举p包拿了多少个s
    for i in range(most_can_take+1):
        # 剩下的就是能装多少装多少
        # i开头表示p这个包装的数量,f表示随从装的数量
        i_take_w = min(cntw, (p - i * s) // w)
        s_left = cnts - i
        w_left = cntw - i_take_w
        f_take_s = min(s_left, f // s)
        f_take_w = min(w_left, (f - f_take_s * s) // w)
        ret = max(ret, i + i_take_w + f_take_s + f_take_w)

    print(ret)

今天的文章到这里就结束了,如果喜欢本文的话,请来一波三连,给我一点支持吧(关注、在看、点赞)。

- END -