本文始發於個人公眾號:TechFlow
之前的文章當中我們詳細闡述了二分法,尤其是討論了我們在編寫代碼時候的邊界問題。傳送門:
今天這一篇文章,我們繼續來講算法,我們不講二分法了。來講講二分法的進階版——三分法。
是的,你們沒有看錯,這不是我任性起的名字,而是實實在在的有這個算法。不過如果去搜索引擎里搜,大概率會搜到攝影的三分構圖法,而很難搜索三分查找的算法。
主要原因是它和二分法比較起來要小眾得多,幾乎所有的算法書籍當中都沒有三分法的介紹。它更多的是存在於ACM-ICPC這類算法競賽當中,不過小眾沒關系,只要有用就好。三分法的原理也很簡單,和二分法幾乎一模一樣,只不過我們分隔區間的時候,不是將區間一分為二,而是一分為三。之后,我們同樣通過縮小區間的方法來確定要查找的值所在。
看到這里,我相信你們應該都能理解算法原理,但是肯定會有一個問題要問:既然分成兩份就能解決問題,我們為什么要分成三份呢?
在回答這個問題之前,我們先來看另一個問題。在數學上,二分法究竟解決了一個什么問題?
還記得二分法使用的前提么?數組必須是有序的,所以二分法其實解決的是單調函數的求解的問題。只要數組是有序的,根據函數的定義就可以看做是一個將數組下標映射到數組取值的函數。顯然,這是一個單調函數。我們通過二分法查找其中的一個元素v,本質其實是查找 f(x) = v
這個函數的解。
所以,二分法使用的場景是單調函數,也就是一次函數。那如果我要搜索二次函數的最小值,用二分法可行嗎?
顯然不可行,因為我們在取完mid之后, 並不知道答案可能出現在左右哪個區間。
這個時候就需要三分法出場了。
三分法會將區間分成三份,這個我們都已經知道了。分成三份,自然需要兩個端點。這兩個端點各有一個值,我們分別叫做m1和m2。我們要求的是函數的最小值,所以我們要想極值逼近。
但是我們有兩個中間點,該怎么逼近呢?
我們直接根據函數圖像來分析,根據上圖我們可以看出來,m1和m2的函數值和它們距離極值點的遠近是有關系的。離極值點越近,函數值越小(也有可能越大,視函數而定)。在上圖當中,
,所以m2離極值點更近。我們要縮小區間范圍,逼近極值點,所以我們應該讓l=m1
這里有一點小問題,我們怎么確定極值點在m2和m1中間呢?萬一在m2的右側該怎么辦呢?
我們畫出圖像來看,這種情況其實並沒有區別,我們只會拋棄區間[l, m1]
,並不會影響極值點。
會不會極值點在m1左側呢?這是不可能的,因為如果極值點在m1左側,那么m2距離極值點一定比m1遠,這種情況下m2處的函數值是不可能小於m1的。
也就是說,三分法的精髓在於,每次通過比較兩個值的大小,縮小三分之一的區間。直到最后區間的范圍小於我們設定的閾值為止。算法並不難理解,但是當我們真正碰到二次函數的極值問題的時候,如果沒有事先接觸過三分法,很難一下想到算法。
三分法本身並不難,我們理解了算法之后,寫出偽代碼來就很容易了:
def trichotomy():
l, r = start, end
epsilon = 1e-6
# 我們自定義的終止條件是區間長度小於1d-6
while l + epsilon < r:
margin = (r - l) / 3.0
m1 = l + margin
m2 = m1 + margin
if f(m1) <= f(m2):
r = m2
else:
l = m1
return l
最后, 我們看一道三分法相關的算法題,來實際演練一下三分法吧。
這是一道POJ3737,鏈接如下:http://poj.org/problem?id=3737
我來簡單介紹一下題意。題意很簡單,當下我們擁有一塊布,我們知道布的大小,要將布做成一個圓錐形。要使得圓錐形的體積最大,假設布在加工的過程當中沒有損耗,請問這個圓錐的體積、底面半徑和高分別是多少?
我這里先不放答案,大家不妨自己想一下,實在做不出來,在公眾號內回復三分法答案,獲取題解。
如果覺得文章有所幫助,請掃碼給個關注吧: