codeforces 1425E,一万种情况的简单题

大家好,欢迎阅读codeforces专题。

我们今天选中的是codeforces 1425场比赛的E题,这是一场印尼多校联合的ICPC的练习赛。ACM赛制,难度也比较近似。我们今天选择的是其中的一道Medium难度的题,由于ACM赛制参赛人数相对较少,全场只有157人通过。但实际难度并不大,大约和一般比赛的C题接近。

链接:https://codeforces.com/contest/1425/problem/E

废话不多说,我们来看题意。

题意

有一个人在玩一个离子激活的游戏,题目的背景是模拟的化学当中的离子能量跃迁。在化学当中,离子吸收能量可以从低能态跃迁到高能态,并且放出一定的能量。

现在有N粒离子排成一排(下标1-N),每一个离子都有自己的能量参数。也就是吸收多少能量可以跃迁,并且跃迁之后会放出多少能量。还有一个特殊的性质就是这些离子之间带有连带关系,默认的连带关系是第i个离子与第i+1个离子连带。当i离子跃迁的时候,即使i+1的离子没有吸收能量也会发生跃迁。其中第N个离子无法建立连带关系

现在这个人获得了K次改变离子之间连带关系的能力,它必须要严格改变K个离子的连带对象。除此之外我们可以自由选择任意个离子进行激活,请问我们最多能够获得多少能量收益?能量收益是指获得的能量减去消耗的能量。

其中,对于每个离子吸收和释放的能量是小于的正整数。

样例

输入一共有3行,第一行是两个整数N和K。

第二行是N个整数,即这N个离子跃迁时释放的能量。第三行同样是N个整数,是这N个离子跃迁需要的能量。

在样例当中我们将5离子的纽带改变成1离子,激活离子5,这样我们一共可以让1-5离子都进行跃迁。所带来的的能量收益就是5 + 6 + 7 + 8 + 10 - 1 = 35

题解

拿到手没什么思路,我们可以先从简单的情况开始思考。

K=0

先来观察一下K=0时的情况,当K=0时,我们不能修改任何连带的情况。当我们选择了i离子进行激活之后,由于连带效应,序号大于i的离子全部都会被激活,并且我们只需要花费的能量。

这种情况非常简单,对于i离子来说,它的收益是。这里的是一个常量,而是一个序列和。我们只需要遍历一下,找到使得收益最大的i即可。

我们分析完了K=0最简单的情况之后,发现了一件事,就是除了我们改变连带的离子之外,其他的离子都是连续的。我们激活一个可以激活一串,这有点像是什么呢,像是链表,我们改变离子的连带关系,其实就是修改链表当中某一个节点的next指针。

比如我们看这张图,当离子a的连带对象从a+1修改成b之后,其实意味着我们将a节点的next指向了b。这样当我们遍历的时候,a的下一个位置就是b。

a的next位置是任意的,可以在a的前面也可以在a的后面,但是不能是a本身。想明白了这点之后,其实所谓的修改K个离子的连带关系,就是在链表当中修改K条边。然后我们可以选择若干个起始位置来遍历链表,使得题意规定的收益最大。

另外我们发现不论这K条边连接如何,除了这K条边之外的内容都还是顺序连接的。我们可以使用前缀和算法来快速求某一段区间的和。

前缀和算法

前缀和算法非常简单,它适用于在数组本身不发生变动的情况下,对于不同的a和b,快速求解的问题。

其实方法非常简单,甚至都算不上一个数据结构。我们只需要一个数组就可以完成,我们新申请一个数组,叫做presum。我们希望,也就是说presum[i]等于A数组前i个数之和。这样我们可以得到

并且我们要维护presum也非常简单,其中presum[1] = A[1],对于i > 1的所有i,都有presum[i]=presum[i-1] + A[i]。通过这么一个简单的递推式,我们就可以非常方便地求出所有的presum,计算所有的前缀和了。

前缀和非常方便,在很多题目当中都有使用,但是有一个小小的条件就是维护区间和的数组内的元素不能发生变化。否则的话就没办法使用了。

K >= 2

讲完了前缀和之后我们继续来分析问题,我们接着看下K >= 2时的情况。我们可以发现当K > 2时,我们只要激活一个小于N的i,就可以获得全部离子

我们来看一个例子,这是K=2时的情况:

我们激活的是某一个i,然后在a位置跳转到了1,又在i-1跳转到了a+1。虽然我们跳转了两次,但是所有的离子都没有浪费。

我们利用递推的思想可以发现,只要K >= 2,就可以保证将所有的离子都纳入囊中。所以我们很容易发现,我们只需要选择代价最小的i就行了

我们记录一下这种情况,就把他叫做情况1,情况1是:

很多人分析K >= 2的时候不小心就过去了,这里很容易忽略。情况1成立是有前提的,前提就是我们选择的激活的离子不能是最后一个,因为最后一个离子没有连接。很有可能前面N-1个离子的代价都大于收益,只有第N离子的收益是正的。我们需要特殊判断这种情况,也就是激活第N个离子。由于它没有连带对象,所以我们只能激活它一个离子。这就是情况2,它的结果是:

K = 1

为什么把K=1的情况放到最后呢?因为它最复杂,情况很多,非常非常容易遗漏。

我们前面曾经提到过,对于i位置而言,它的连带对象可以在它前面也可以在它后面。我们针对这两种情况需要单独分析,首先是它的连带对象在它前面。那么最优的情况一定是它和1号离子连带。这样的话,我们只要激活[2, i]区间里任何一个离子,都可以获得前i个离子的总收益。但是这里又有一个问题,我们要不要再激活离子i+1呢?如果激活的话,我们就获得所有离子的收益,也可以不激活。

所以我们又得到了两种情况,分别是情况3和情况4。我们也列一下,对于情况3,我们可以获得前i个离子的所有收益,付出的代价是前i个离子当中最小的那个,这里的i显然越大越好,最大是N-1。写出来就是:。情况4是在情况3的基础上再激活一个i+1离子,情况4,我们再分析一下会发现我们可以通过选择i,让D[i+1]也尽量小,小到成为全局倒数第二小

我们看完了连带的节点在前面的情况再来看看连带的情况在后面会怎么样,还是看这张图:

我们可以看到a点的连带在其后的b点,这时候我们也有两种情况,第一种情况是只激活1,这样的话,我们可以拿到除了[a+1, b-1]区间内的值。第二种情况是激活1之后再激活a+1,这样我们也可以获得全部的离子,但是需要花费A[1]和A[a+1]。

这只是表面的分析,当我们深入分析之后会发现,由于离子释放的能量一定是正数。我们当然希望它跳过的区间越小越好,因为跳过得多了都是损失。最极端的情况应该是b=a+2,也就是说我们只跳过a+1这一个元素。跳过a+1这个元素有两种情况,第一种是真跳,也就是说抛弃了这个取值,第二种情况是假跳,我们虽然跳过了,但是还是会人为将它激活。

我们整理一下得到情况5和情况6,情况5情况6是我们虽然跳过了a+1,但是还是会将它激活,所以结果应该是

我们分析到这里,已经有6种情况了,是不是觉得已经结束了?错了,其实还有一种关键的情况被我们忽略了。就是1号节点连带N节点,然后从2号节点开始寻找收益最大的节点进行激活。这才是最后一种情况,也就是情况7,写出来就是:

是的,你没有看错,这道题一共有7种情况,少了随便哪一种都没有办法AC。题目虽然简单,但是我们在做题的时候想要一下子把这7中情况都想明白理清楚实在是不容易,不过这样的题目才更加值得我们去做,因为真的很锻炼思维,对于提升我们思维的缜密程度非常有帮助。

最后,我们附上代码。

import sys
n, k = list(map(int, input().split(' ')))


activate = list(map(int, input().split(' ')))
deactivate = list(map(int, input().split(' ')))

presum = [0 for _ in range(n+2)]

for i in range(1, n+1):
    presum[i] = presum[i-1] + activate[i-1]

ret = 0

if k == 0:
    for i in range(1, n+1):
        # 情况0,不需要考虑连带
        ret = max(ret, presum[n] - presum[i-1] - deactivate[i-1])
elif k == 1:
    for i in range(1, n):
        # 情况3 n-1连1
        ret = max(ret, presum[n-1] - deactivate[i-1])
    
    # 情况3再加上第N个离子
    if deactivate[n-1] < activate[n-1]:
        ret += (activate[n-1] - deactivate[n-1])

    deac = sorted(deactivate[1:n-1])
    ac = sorted(activate[1:n-1])

    # 情况5,激活1,跳过1个节点
    ret = max(ret, presum[n] - deactivate[0] - ac[0])
    # 情况6 激活1,跳过1个节点后,再激活
    ret = max(ret, presum[n] - deactivate[0] - deac[0])
    # 情况4 
    ret = max(ret, presum[n] - deac[0] - deac[1])

    # 情况7 1连n,激活i
    for i in range(2, n+1):
        ret = max(ret, presum[n] - presum[i-1] - deactivate[i-1])
else:
    # 情况1
    for i in range(1, n):
        ret = max(ret, presum[n] - deactivate[i-1])   

    # 情况2
    ret = max(ret, activate[n-1] - deactivate[n-1])

print(ret)

这道题有点烧脑,光凭我说可能很难完全理解清楚,建议大家多拿纸笔出来画一画,会有助于思考。

今天的文章就到这里,衷心祝愿大家每天都有所收获。如果还喜欢今天的内容的话,请来一个三连支持吧~(点赞、关注、转发

原文链接,求个关注

本文使用 mdnice 排版

posted @ 2020-11-11 09:51  Coder梁  阅读(305)  评论(0编辑  收藏  举报