這篇文章帶來四個經典演算法問題的基礎暴力解法。四個著名演算法分別是Pancake sorting, De Bruijn sequence, Convex hull algorithms, Knapsack problem。 這四個問題都非常經典,也衍生出了很多複雜的解法。然而在面試的時候如果碰到原問題或相似的問題,我認為只要寫出最基本的暴力解法就可以獲得一個起碼是weak hire的評價。四個演算法的定義都可以在連結裡面找到。

Pancake sorting的本質是排序。與其他排序的操作不同,我們在這裡面不能隨意的swap陣列內的元素,而只能reverse/flip前若干個元素組成的prefix subarray(A[0。。i])。我們的目標是透過不斷的reverse操作,來將陣列排序。當然,reverse的操作越少越好。

暴力的演算法是,對於在陣列中第i大的元素(從小數第i個),我們希望它出現在陣列的第i個位置。這裡為了方便說明,陣列計數從1開始。我們找到這個目標元素,發現它出現在陣列中的位置j,reverse A[1。。j],這時候第i大的元素出現在了陣列開頭。然後reverse A[1。。i],這時候我們就把第i大的元素放在第i位上。值得注意的是,我們對要操作的元素,應該從最大的開始。因為在處理某元素的時候,可能會改變該元素之前已經排好的順序。因此如果從小到大,之前排好的序又被破壞,但是從大到小的順序不會。我們從最大的元素開始往前處理,每次找到剩餘陣列中最大的,然後透過兩次reverse將它放到屬於它的位置。看一下geeksforgeeks的程式碼。

int

pancakeSort

int

*

arr

int

n

{

// Start from the complete array and one by one reduce current size by 1

for

int

curr_size

=

n

curr_size

>

1

——

curr_size

{

// Find index of the maximum element in arr[0。。curr_size-1]

int

mi

=

findMax

arr

curr_size

);

// Move the maximum element to end of current array

// if it‘s not already at the end

if

mi

!=

curr_size

-

1

{

// To move at the end, first move maximum number to beginning

flip

arr

mi

);

// Now move the maximum number to end by reversing current array

flip

arr

curr_size

-

1

);

}

}

}

可以看到,除了最小的元素,其他每個元素都需要兩次reverse操作才能去到對應的位置。所以最多需要2(n-1)次reverse操作。每次操作需要O(n)時間找到當前最大的元素,以及O(n)時間flip。所以該排序演算法的總時間複雜度是O(n^2)。 一個簡單的小最佳化是對於大小為2的陣列,最多隻需要reverse一次就可以讓它有序。因此最壞的情況可以縮減到2(n-2)+1次reverse操作。

Pancake sorting一個變形是burnt pancake problem,有興趣的可以看一下。基本的解法就是在處理第i個pancake的時候,在兩次flip之間插入如下操作:判斷它burnt的那一面是否朝上,如果不是則單獨將它額外flip一次。

推薦擴充套件閱讀:比爾蓋茨的論文

De Brujin Sequence的一個簡單問題是,找到長度最小的string,裡面包含了所有長度為4的substring(alphabet為數字0到9,需要包含的substring從0000到9999)(google面經題)。我們如果把最後的結果考慮成一個環形,那麼結果實際上長度為10^4。暴力DFS,用一個長度為10^4的陣列來做visited list。每一層嘗試把0到9中的每一個數字加到當前這一層的位置。藉助visited list判斷,如果此時獲得的新結果的最後四位數字組成的四位數沒有出現過,那麼就進入下一層DFS的遞迴。值得注意的是,當遞迴到最後一層的時候,我們要判斷每種wrap around的數字的情況(由結果的最後幾位和前幾位concatenate而成,相當於每一個經過環形介面的可能)。這裡我直接放stellari的解法了。(我測試過了。)

class

DeBruijn10

{

bool

DFS

int

ind

{

if

ind

==

upper

{

// No more number left

for

int

i

=

1

i

<=

n

-

1

++

i

{

// Still need to validate the numbers that “wrap around”

string

st

=

seq

substr

upper

-

n

+

i

n

-

i

+

seq

substr

0

i

);

if

avail

stoi

st

)]

==

false

return

false

}

return

true

}

int

prefix

=

stoi

seq

substr

ind

-

n

-

1

),

n

-

1

))

*

10

for

int

i

=

0

i

<=

9

++

i

{

int

cur

=

prefix

+

i

// Try each possible choice at the current location

if

avail

cur

])

{

// If this number hasn’t appeared so far

avail

cur

=

false

// then try it。

seq

ind

=

i

+

‘0’

if

DFS

ind

+

1

))

return

true

avail

cur

=

true

}

}

return

false

}

public

int

n

int

upper

vector

<

bool

>

avail

string

seq

DeBruijn10

int

_n

n

_n

),

upper

pow

10

n

)),

avail

upper

true

),

seq

upper

‘0’

{

avail

0

=

false

DFS

n

);

}

};

對於n=4的情況,這種寫法還是很快的。當然還有很多更好的解法。問題的本質是尋找歐拉回路,有很多經典演算法例如Fleury‘s和Hierholzer’s。對於為何是找歐拉回路,可以看擴充套件閱讀:

http://

xwk。iteye。com/blog/2129

621

Convex Hull是說在二維平面內,給定一些點,找到最外層的一圈點,他們的連線將把其他剩餘的點都包含進去。

首先對於三個不共線的點A,B,C, 我們是否可以判斷出ABC這樣的走法是逆時針的呢?我們先假定A在最左邊,我們可以透過比較AB和AC之間的斜率得到。僅當AB的斜率小於AC的時候,ABC的走法是逆時針的。那如果A不在最左邊呢?其實ABC和BCA和CAB的本質是一樣的,所以我們只要找到三個點中最左邊的點,然後做相應的轉化就可以了。

回到找Convex Hull的問題上來。我們可以先找到最左邊的點作為起點(反證法易得,此點必然屬於Convex Hull的點集,最右邊,最上面,最下面的點均可)。然後我們依次尋找下一個端點直到回到最初的起點。對於已經確定的端點P,下一個端點是:對於一個站在P點上,看著其他的點的人來說,最靠右的點。換言之,如果下一個端點是Q,那麼對於任意的其他點R,PQR都是逆時針的。Q點可以透過O(n)次逆時針測試獲得。遍歷所有的點(除了P自己),對於一個可能的Q candidate,如果PQR的逆時針測試失敗的話,那麼這個R就成為了新的Q candidate。因為如果當前的PQR不是逆時針,那麼R一定對於P來說在這個Q candidate的右邊,因此R有資格成為了新的Q candidate。總的時間複雜度是O(n^2)。具體的程式碼可以看擴充套件閱讀裡的Jeff‘s Notes。

擴充套件閱讀:Jeff’s Notes。(裡面還有nlgn的解法)。

最後說說揹包問題裡的01揹包和完全揹包(取材於

dd大牛的《揹包九講》

)。

有N件物品和一個容量為V的揹包。第i件物品的所佔用的體積是c[i],價值是w[i]。將哪些物品裝入揹包滿足物品所佔體積的總和不超過V,且價值總和最大。

01揹包是每個物品最多隻能取一個。狀態轉移方程是f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。

f[i][v]代表的是,對於前i件物品,以及容量為v的子問題的答案。對於這個子問題,我們有兩種選擇,要麼不放第i件物品,也就是f[i-1][v],要麼放第i件物品。放的話,剩餘的體積只有v-c[i],所以其他的價值最多隻能是f[i-1][v-c[i]]。最後我們加上第i件物品的價值,和不放的情況比較,取最大值。這種解法的時間複雜度為O(VN),空間複雜度也是O(VN)。因為f[i][v]只依賴於f[i-1][v],因此可以只維護兩個大小為V的陣列,空間複雜度可以輕易最佳化到O(V)。這裡甚至可以最佳化到只維護一個數組(依賴關係畫圖可知,若i為縱座標,v為橫座標,每個格子依賴左上方的格子,因此要從右邊逆序走)。

完全揹包是說每個物品最多可取無窮多個。首先我們想到,若兩件物品i、j滿足c[i]<=c[j]且w[i]>=w[j],則將物品j去掉,不用考慮。然後對於每個物品i 來說,我們找到單放這個物品最多可以放x個(求解 argmax(x, x*c[i] <= V))。對於物品i,縱然它有無限多個,揹包裡最多放x個,因此我們可以轉化成擁有x個物品i,但每個物品只能取一次的情況。因此就把完全揹包問題化解成了01揹包問題。當然還有更優的拆分方法,比如:“把第i種物品拆成費用為c[i]*2^k、價值為w[i]*2^k的若干件物品,其中k滿足c[i]*2^k

擴充套件閱讀:dd大牛的《揹包九講》

最後,到底該放什麼題圖才能吸引目光又不被舉報呢。。。放張神仙姐姐養養眼吧。