1. 程式人生 > 其它 >記一次嘗試性的CCB積分頁面登入(三)

記一次嘗試性的CCB積分頁面登入(三)

二分答案

結合洛谷P2678 跳石頭

“二分答案”。顧名思義,它用二分的方法列舉答案,並且列舉時判斷這個答案是否可行。但是,二分並不是在所有情況下都是可用的,使用二分需要滿足兩個條件。一個是有界,一個是單調。

二分答案應該是在一個單調閉區間上進行的。也就是說,二分答案最後得到的答案應該是一個確定值,而不是像搜尋那樣會出現多解。二分一般用來解決最優解問題。剛才我們說單調性,那麼這個單調性應該體現在哪裡呢?

可以這樣想,在一個區間上,有很多數,這些數可能是我們這些問題的解,換句話說,這裡有很多不合法的解,也有很多合法的解。我們只考慮合法解,並稱之為可行解。考慮所有可行解,我們肯定是要從這些可行解中找到一個最好的作為我們的答案, 這個答案我們稱之為最優解。

最優解一定可行,但可行解不一定最優。我們假設整個序列具有單調性,且一個數x為可行解,那麼一般的,所有的x'(x'<x)都是可行解。並且,如果有一個數y是非法解,那麼一般的,所有的y'(y'>y)都是非法解。

那麼什麼時候適用二分答案呢?注意到題面:使得選手們在比賽過程中的最短跳躍距離儘可能長。如果題目規定了有“最大值最小”或者“最小值最大”的東西,那麼這個東西應該就滿足二分答案的有界性(顯然)和單調性(能看出來)。

那就好辦了。我們二分跳躍距離,然後把這個跳躍距離“認為”是最短的跳躍距離,然後去以這個距離為標準移石頭。使用一個judge判斷這個解是不是可行解。如果這個解是可行解,那麼有可能會有比這更優的解,那麼我們就去它的右邊二分。為什麼去右邊?答案是,這個區間是遞增的 ,而我們求的是最短跳躍距離的最大值,顯然再右邊的值肯定比左邊大,那麼我們就有可能找到比這更優的解,直到找不到,那麼最後找到的解就有理由認為是區間內最優解。反過來,如果二分到的這個解是一個非法解,我們就不可能再去右邊找了。因為性質,右邊的解一定全都是非法解。那麼我們就應該去左邊找解。整個過程看起來很像遞迴,實際上,這個過程可以遞迴寫, 也可以寫成非遞迴形式,我個人比較喜歡使用非遞迴形式。

下一個問題,這個judge怎麼實現呢?judge函式每個題有每個題的寫法,但大體上的思想應該都是一樣的——想辦法檢測這個解是不是合法。拿這個題來說,我們去判斷如果以這個距離為最短跳躍距離需要移走多少塊石頭,先不必考慮限制移走多少塊,等全部拿完再把拿走的數量和限制進行比對,如果超出限制,那麼這就是一個非法解,反之就是一個合法解,很好理解吧。

可以去模擬這個跳石頭的過程。開始你在i(i=0)位置,我在跳下一步的時候去判斷我這個當前跳躍的距離,如果這個跳躍距離比二分出來的mid小,**那這就是一個不合法的石頭,應該移走。**為什麼?我們二分的是最短跳躍距離,已經是最短了,如果跳躍距離比最短更短豈不是顯然不合法,是這樣的吧。移走之後要怎麼做?先把計數器加上1,再考慮向前跳啊。去看移走之後的下一塊石頭,再次判斷跳過去的距離,如果這次的跳躍距離比最短的長,那麼這樣跳是完全可以的,我們就跳過去,繼續判斷,如果跳過去的距離不合法就再拿走,這樣不斷進行這個操作,直到i = n+1,為啥是n+1?河中間有n塊石頭,顯然終點在n+1處。(這裡千萬要注意不要把n認為是終點,實際上從n還要跳一步才能到終點)。

模擬完這個過程,我們檢視計數器的值,這個值代表的含義是我們以mid作為答案需要移走的石頭數量,然後判斷這個數量 是不是超了就行。如果超了就返回false,不超就返回true。

注意:以上內容非原創,但是不記得以前在哪看到的

 

二分答案過程中的上下界問題

 

我寫二分答案時,上下界沒有寫好導致Wrong Answer。又看到討論區有人的上下界寫錯了,於是TLE。所以決定仔細分析一下二分的上下界問題。

眾所周知,二分答案時,需要有上界、下界、中點。分別命名為left,right,mid。

此題中,要求最大值,因此有重要結論:若mid為解,則最優解一定屬於[mid, right]。若mid不為解,則最優解一定屬於[left, mid)。

第一種理解
閉區間[left, right]為最優解存在的區間。

令mid = (left + right) / 2。

若mid為解(無論是否最優),則使left = mid. 此時不使left = mid + 1是因為mid可能是最優解,而最優解必須屬於上述閉區間.

若mid不為解,則使right = mid - 1.

重複上述過程,直到閉區間只有一個值時跳出迴圈,即left == right。

但是,當left == right - 1時,mid = (left + right)/2 = left,則此次迴圈最後會使left = mid,程式陷入死迴圈。

出現死迴圈的問題,直接原因是整除的誤差(3div2結果為1),而根本原因是區間範圍卡的過死。只有當left嚴格等於right時,我們才能宣佈找出了最優解,又加之整除誤差,因此在區間範圍很小時,程式難以繼續二分。

因此,我們需要把區間範圍卡的“寬鬆”一點。

第二種理解
區間[left, right)為最優解存在的區間。

令mid = (left + right) / 2

若mid為解,則使left = mid.

若mid不為解,則使right = mid.

重複以上過程,直到right == left + 1跳出迴圈,left即為最優解。(可以確定left為最優解,但並不是最後一次二分的mid就是left)

這種區間的定義中,right為開區間,相比於閉區間較為“寬鬆”,有效避免了死迴圈的問題,程式碼可以AC。

但是,這兩種理解方式區別很小,容易混淆,不建議使用,因此有了第三種更清晰的理解方法。

第三種理解
定義變數ans,儲存當前優解。定義閉區間[left, right],代表程式當前正在此閉區間內尋找答案(尋找潛在的比ans更優的解)(與前兩種方法不同的是,最優解不一定要屬於該閉區間)。

令mid = (left + right)/2

若mid為解,則ans = max(ans, mid), left = mid + 1.此時我們更新了最優解,同時在最優解的右側尋找潛在的更有解。

若mid不為解,則right = mid - 1.mid不是解,因此我們在mid左邊尋找更優解。

重複上述過程,直到left > right時跳出迴圈,ans即為最優解。

注意,當left == right時,也必須要在此區間內進行判斷,因為當前還不能確定該區間內是否存在更優解。

程式碼:

//二分答案 
while(left <= right)
{
    int mid = (left + right) / 2;
    if(judge(mid))
    {
        left = mid + 1;
        ans = max(ans, mid);
    }
    else
        right = mid - 1;
}
printf("%d", ans);

注意:以上內容非原創,但是不記得以前在哪看到的