1. 程式人生 > >codeforces 1425E,一萬種情況的簡單題

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 排版

![](https://img2020.cnblogs.com/blog/1906483/202011/1906483-20201111095043745-5073787