動態規劃算法詳解

http://blog.jobbole.com/67588/html

 

【編注】:動態規劃(Dynamic programming)是一種在數學、計算機科學和經濟學中使用的,經過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。 動態規劃經常適用於有重疊子問題和最優子結構性質的問題,動態規劃方法所耗時間每每遠少於樸素解法。python

動態規劃背後的基本思想很是簡單。大體上,若要解一個給定問題,咱們須要解其不一樣部分(即子問題),再合併子問題的解以得出原問題的解。 一般許多子問題很是類似,爲此動態規劃法試圖僅僅解決每一個子問題一次,從而減小計算量: 一旦某個給定子問題的解已經算出,則將其記憶化存儲,以便下次須要同一個子問題解之時直接查表。 這種作法在重複子問題的數目關於輸入的規模呈指數增加時特別有用。—— 維基百科算法

動態規劃是一種用來解決定義了一個狀態空間的問題的算法策略。這些問題可分解爲新的子問題,子問題有本身的參數。爲了解決它們,咱們必須搜索這個狀態空間而且在每一步做決策時進行求值。得益於這類問題會有大量相同的狀態的這個事實,這種技術不會在解決重疊的子問題上浪費時間。編程

正如咱們看到的,它也會致使大量地使用遞歸,這一般會頗有趣。數組

爲了說明這種算法策略,我會用一個很好玩的問題來做爲例子,這個問題是我最近參加的 一個編程競賽中的 Tuenti Challenge #4 中的第 14 個挑戰問題。緩存

Train Empire

咱們面對的是一個叫 Train Empire 的棋盤遊戲(Board Game)。在這個問題中,你必須爲火車規劃出一條最高效的路線來運輸在每一個火車站的貨車。規則很簡單:app

  • 每一個車站都有一個在等待着的將要運送到其餘的車站的貨車。
  • 每一個貨車被送到了目的地會獎勵玩家一些分數。貨車能夠放在任意車站。
  • 火車只在一條單一的路線上運行,每次能裝一個貨車,由於燃料有限只能移動必定的距離。

咱們能夠把咱們的問題原先的圖美化一下。爲了在燃料限制下贏得最大的分數,咱們須要知道貨車在哪裏裝載,以及在哪裏卸載。函數

咱們在圖片中能夠看到,咱們有兩條火車路線:紅色和藍色。車站位於某些座標點上,因此咱們很容易就能算出它們之間的距離。每個車站有一個以它的終點命名的貨車,以及當咱們成功送達它能夠獲得的分數獎勵。優化

如今,假定咱們的貨車能跑3公里遠。紅色路線上的火車能夠把 A 車站的火車送到它的 終點 E (5點分數),藍色路線上的火車能夠運送貨車 C(10點分數),而後運送貨車 B(5點分數)。 能夠取得最高分20分。ui

狀態表示

咱們把火車的位置,以及火車所走的距離和每一個車站的貨車表格叫作一個問題狀態。 改變這些值咱們獲得的還是相同的問題,可是參數變了。咱們能夠看到每次咱們移動 一列火車,咱們的問題就演變到一個不一樣的子問題。爲了算出最佳的移動方案,咱們 必須遍歷這些狀態而後基於這些狀態做出決策。讓咱們開始把。

咱們將從定義火車路線開始。由於這些路線不是直線,因此圖是最好的表示方法。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

import math

from decimal import Decimal

from collections import namedtuple, defaultdict

 

class TrainRoute:

 

    def __init__(self, start, connections):

        self.start = start

 

        self.E = defaultdict(set)

        self.stations = set()

        for u, v in connections:

            self.E[u].add(v)

            self.E[v].add(u)

            self.stations.add(u)

            self.stations.add(v)

 

    def next_stations(self, u):

        if u not in self.E:

            return

        yield from self.E[u]

 

    def fuel(self, u, v):

        x = abs(u.pos[0] - v.pos[0])

        y = abs(u.pos[1] - v.pos[1])

        return Decimal(math.sqrt(x * x + y * y))

TrainRoute 類實現了一個很是基本的有向圖,它把頂點做爲車站存在一個集合中,把車站間 的鏈接存在一個字典中。請注意咱們把 (u, v) 和 (v, u) 兩條邊都加上了,由於火車能夠 向前向後移動。

在 next_stations 方法中有一個有趣東西,在這裏我使用了一個很酷的 Python 3 的特性 yield from。這容許一個生成器 能夠委派到另一個生成器或者迭代器中。由於每個車站都映射到一個車站的集合,咱們只 須要迭代它就能夠了。

讓咱們來看一下 main class:

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

TrainWagon = namedtuple('TrainWagon', ('dest', 'value'))

TrainStation = namedtuple('TrainStation', ('name', 'pos', 'wagons'))

 

class TrainEmpire:

 

    def __init__(self, fuel, stations, routes):

        self.fuel = fuel

        self.stations = self._build_stations(stations)

        self.routes = self._build_routes(routes)

 

    def _build_stations(self, station_lines):

        # ...

 

    def _build_routes(self, route_lines):

        # ...

 

    def maximum_route_score(self, route):

 

        def score(state):

            return sum(w.value for (w, s) in state.wgs if w.dest == s.name)

 

        def wagon_choices(state, t):

            # ...

 

        def delivered(state):

            # ...

 

        def next_states(state):

            # ...

 

        def backtrack(state):

            # ...

 

        # ...

 

    def maximum_score(self):

        return sum(self.maximum_route_score(r) for r in self.routes)

我省略了一些代碼,可是咱們能夠看到一些有趣的東西。兩個 命名元組 將會幫助保持咱們的數據整齊而簡單。main class 有咱們的火車可以運行的最長的距離,燃料, 和路線以及車站這些參數。maximum_score 方法計算每條路線的分數的總和,將成爲解決問題的 接口,因此咱們有:

  • 一個 main class 持有路線和車站之間的鏈接
  • 一個車站元組,存有名字,位置和當前存在的貨車列表
  • 一個帶有一個值和目的車站的貨車

動態規劃

我已經嘗試解釋了動態規劃如何高效地搜索狀態空間的關鍵,以及基於已有的狀態進行最優的決策。 咱們有一個定義了火車的位置,火車剩餘的燃料,以及每一個貨車的位置的狀態空間——因此咱們已經能夠表示初始狀態。

咱們如今必須考慮在每一個車站的每一種決策。咱們應該裝載一個貨車而後把它送到目的地嗎? 若是咱們在下一個車站發現了一個更有價值的貨車怎麼辦?咱們應該把它送回去或者仍是往前 移動?或者仍是不帶着貨車移動?

很顯然,這些問題的答案是那個可使咱們得到更多的分數的那個。爲了獲得答案,咱們必須求出 全部可能的情形下的前一個狀態和後一個狀態的值。固然咱們用求分函數 score 來求每一個狀態的值。

Python

 

1

2

3

4

5

6

7

8

9

10

def maximum_score(self):

    return sum(self.maximum_route_score(r) for r in self.routes)

 

State = namedtuple('State', ('s', 'f', 'wgs'))

 

wgs = set()

for s in route.stations:

    for w in s.wagons:

        wgs.add((w, s))

initial = State(route.start, self.fuel, tuple(wgs))

從每一個狀態出發都有幾個選擇:要麼帶着貨車移動到下一個車站,要麼不帶貨車移動。停留不動不會進入一個新的 狀態,由於什麼東西都沒改變。若是當前的車站有多個貨車,移動它們中的一個都將會進入一個不一樣的狀態。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

def wagon_choices(state, t):

    yield state.wgs  # not moving wagons is an option too

 

    wgs = set(state.wgs)

    other_wagons = {(w, s) for (w, s) in wgs if s != state.s}

    state_wagons = wgs - other_wagons

    for (w, s) in state_wagons:

        parked = state_wagons - {(w, s)}

        twgs = other_wagons | parked | {(w, t)}

        yield tuple(twgs)

 

def delivered(state):

    return all(w.dest == s.name for (w, s) in state.wgs)

 

def next_states(state):

    if delivered(state):

        return

    for s in route.next_stations(state.s):

        f = state.f - route.fuel(state.s, s)

        if f < 0:

            continue

        for wgs in wagon_choices(state, s):

            yield State(s, f, wgs)

next_states 是一個以一個狀態爲參數而後返回全部這個狀態能到達的狀態的生成器。 注意它是如何在全部的貨車都移動到了目的地後中止的,或者它只進入到那些燃料仍然足夠的狀態。wagon_choices 函數可能看起來有點複雜,其實它僅僅返回那些能夠從當前車站到下一個車站的貨車集合。

這樣咱們就有了實現動態規劃算法須要的全部東西。咱們從初始狀態開始搜索咱們的決策,而後選擇 一個最有策略。看!初始狀態將會演變到一個不一樣的狀態,這個狀態也會演變到一個不一樣的狀態! 咱們正在設計的是一個遞歸算法:

  • 獲取狀態
  • 計算咱們的決策
  • 作出最優決策

顯然每一個下一個狀態都將作這一系列的一樣的事情。咱們的遞歸函數將會在燃料用盡或者全部的貨車都被運送都目的地了時中止。

Python

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

max_score = {}

 

def backtrack(state):

    if state.f <= 0:

        return state

    choices = []

    for s in next_states(state):

        if s not in max_score:

            max_score[s] = backtrack(s)

        choices.append(max_score[s])

    if not choices:

        return state

    return max(choices, key=lambda s: score(s))

 

max_score[initial] = backtrack(initial)

return score(max_score[initial])

完成動態規劃策略的最後一個陷阱:在代碼中,你能夠看到我使用了一個 max_score 字典, 它實際上緩存着算法經歷的每個狀態。這樣咱們就不會重複一遍又一遍地遍歷咱們的咱們早就已經 經歷過的狀態的決策。

當咱們搜索狀態空間的時候,一個車站可能會到達屢次,這其中的一些可能會致使相同的燃料,相同的貨車。 火車怎麼到達這裏的不要緊,只有在那個時候作的決策有影響。若是咱們咱們計算過那個狀態一次而且保存了 結果,咱們就不在須要再搜索一遍這個子空間了。

若是咱們沒有用這種記憶化技術,咱們會作大量徹底相同的搜索。 這一般會致使咱們的算法很難高效地解決咱們的問題。

總結

Train Empire 提供了一個絕佳的的例子,以展現動態規劃是如何在有重疊子問題的問題作出最優決策。 Python 強大的表達能力再一次讓咱們很簡單地就能把想法實現,而且寫出清晰且高效的算法。

完整的代碼在 contest repository

 

五種經常使用算法之三:動態規劃

動態規劃

基本思想:

動態規劃算法一般用於求解具備某種最優性質的問題。在這類問題中,可能會有許多可行解。每個解都對應於一個值,咱們但願找到具備最優值的解。動態規劃算法與分治法相似,其基本思想也是將待求解問題分解成若干個子問題,先求解子問題,而後從這些子問題的解獲得原問題的解。與分治法不一樣的是,適合於用動態規劃求解的問題,經分解獲得子問題每每不是互相獨立的。若用分治法來解這類問題,則分解獲得的子問題數目太多,有些子問題被重複計算了不少次。若是咱們可以保存已解決的子問題的答案,而在須要時再找出已求得的答案,這樣就能夠避免大量的重複計算,節省時間。咱們能夠用一個表來記錄全部已解的子問題的答案。無論該子問題之後是否被用到,只要它被計算過,就將其結果填入表中。這就是動態規劃法的基本思路。具體的動態規劃算法多種多樣,但它們具備相同的填表格式

 

與分治法最大的差異是:適合於用動態規劃法求解的問題,經分解後獲得的子問題每每不是互相獨立的(即下一個子階段的求解是創建在上一個子階段的解的基礎上,進行進一步的求解)

 

應用場景:

適用動態規劃的問題必須知足最優化原理、無後效性和重疊性。
1.最優化原理(最優子結構性質) 最優化原理可這樣闡述:一個最優化策略具備這樣的性質,不論過去狀態和決策如何,對前面的決策所造成的狀態而言,餘下的諸決策必須構成最優策略。簡而言之,一個最優化策略的子策略老是最優的。一個問題知足最優化原理又稱其具備最優子結構性質。

2.無後效性  將各階段按照必定的次序排列好以後,對於某個給定的階段狀態,它之前各階段的狀態沒法直接影響它將來的決策,而只能經過當前的這個狀態。換句話說,每一個狀態都是過去歷史的一個完整總結。這就是無後向性,又稱爲無後效性。

3.子問題的重疊性  動態規劃將原來具備指數級時間複雜度的搜索算法改進成了具備多項式時間複雜度的算法。其中的關鍵在於解決冗餘,這是動態規劃算法的根本目的。動態規劃實質上是一種以空間換時間的技術,它在實現的過程當中,不得不存儲產生過程當中的各類狀態,因此它的空間複雜度要大於其它的算法。

 

求全路徑最短路徑的Floyd算法就是漂亮地運用了動態規劃思想。

 

下面是我找到的一個關於 0-1揹包問題 的動態規劃思想PPT截圖:

問題描述:
給定n種物品和一揹包。物品i的重量是wi,其價值爲vi,揹包的容量爲C。問應如何選擇裝入揹包的物品,使得裝入揹包中物品的總價值最大?

對於一種物品,要麼裝入揹包,要麼不裝。因此對於一種物品的裝入狀態能夠取0和1.咱們設物品i的裝入狀態爲xi,xi∈ (0,1),此問題稱爲0-11揹包問題。

數據:物品個數n=5,物品重量w[n]={0,2,2,6,5,4},物品價值V[n]={0,6,3,5,4,6},
(第0位,置爲0,不參與計算,只是便於與後面的下標進行統一,無特別用處,也可不這麼處理。)總重量c=10。揹包的最大容量爲10,那麼在設置數組m大小時,能夠設行列值爲6和11,那麼,對於m(i,j)就表示可選物品爲i…n揹包容量爲j(總重量)時揹包中所放物品的最大價值。

相關文章
相關標籤/搜索