1. 程式人生 > >詳解狀態壓縮動態規劃演算法

詳解狀態壓縮動態規劃演算法

本文始發於個人公眾號:TechFlow,原創不易,求個關注


今天是演算法與資料結構專題的第16篇,也是動態規劃系列的第5篇。

今天文章的內容是動態規劃當中非常常見的一個分支——狀態壓縮動態規劃,很多人對於狀態壓縮畏懼如虎,但其實並沒有那麼難,希望我今天的文章能帶你們學到這個經典的應用。

二進位制表示狀態

在講解多重揹包問題的時候,我們曾經講過二進位制表示法來解決多重揹包。利用二進位制的性質,將多個物品拆分成少數個物品,轉化成了簡單的零一揹包來解決。今天的狀態壓縮同樣離不開二進位制,不過我個人感覺今天的二進位制應用更加容易理解一些。

二進位制的很多應用離不開集合這個概念,我們都知道在計算機當中,所有資料都是以二進位制的形式儲存的。一般一個int整形是4個位元組,也就是32位bit,我們通過這32位bit上0和1的組合可以表示多大21億個不同的數。如果我們把這32位bit看成是一個集合,那麼每一個數都應該對應集合的一種狀態,並且每個數的狀態都是不同的。

比如上圖當中,我們列舉了5個二進位制位,我們把其中兩個設定成了1,其餘的設定成了0。我們通過計算,可以得到6這個數字,那麼6也就代表了(00110)這個狀態。數字和狀態是一一對應的,因為每個整數轉化成二進位制都是唯一的。

也就是說一個整數可以轉化成二進位制數,它可以代表某個集合的一個狀態,這兩者一一對應。這一點非常重要,是後面一切推導的基礎。

狀態轉移

整數的二進位制表示可以代表一個二元集合的狀態,既然是狀態就可以轉移。在此基礎上,我們可以得出另一個非常重要的結論——我們可以用整數的加減表示狀態之間的轉移。

我們還用剛才的例子來舉例,上面的圖當中我們列舉了5個二進位制位,假設我們用這5個二進位制位表示5個小球,這些小球的編號分別是0到4。這樣一來,剛才的6可以認為表示拿取了1號和2號兩個小球的狀態。

如果這個時候我們又拿取了3號小球,那麼集合的狀態會發生變化,我們用一張圖來表示:

上圖當中粉絲的筆表示決策,比如我們拿取了3號球就是一個決策,在這個決策的影響下,集合的狀態發生了轉移。轉移之後的集合代表的數是14,它是由之前的集合6加上轉移帶來的變化,也就是 得到的。 剛好就代表拿取3號球這個決策,這樣我們就把整個過程串起來了。

總結一下,我們用二進位制的0和1表示一個二元集合的狀態。可以簡單認為某個物品存在或者不存在的狀態。由於二進位制的0和1可以轉化成一個int整數,也就是說我們用整數代表了一個集合的狀態。這樣一來,我們可以用整數的加減計算來代表集合狀態的變化。

這也就是狀態壓縮的精髓,所謂的壓縮,其實就是將一個集合壓縮成了一個整數的意思,因為整數可以作為陣列的下標,這樣操作會方便我們的編碼。

旅行商問題

明白了狀態壓縮的含義之後,我們來看一道經典的例題,也就是大名鼎鼎的旅行商問題。

旅行商問題的背景很有意思,說是有一個商人想要旅行各地並進行貿易。各地之間有若干條單向的通道相連,商人從一個地方出發,想要用最短的路程把所有地區環遊一遍,請問環遊需要的最短路程是多少?在這題當中,我們假設商人從0位置出發,最後依然回到位置0。

我們來看下面這張圖來直觀地感受一下:

假設我們的商人從0位置出發,想要環遊一週之後再次回到0,那麼它所需要經歷的最短距離是多少呢?

這個圖還是比較簡單的,如果在極端情況下也就是所有點之間都有連線的時候,對於每一個點來說,它可以選擇的下一個位置一共有n-1種。那麼一共可以選擇的路線總共有n!種,這是一個非常大的值,顯然是我們不能接受的。這也是為什麼我們說旅行商問題是一個NP-Hard問題。

NP問題

既然說到了NP問題,簡單和大家聊聊NP問題的定義。

很多演算法的初學者對於這些概念非常迷糊,也的確,這些概念聽起來都差不多,的確很容易搞暈。我們先從最簡單的開始介紹,首先是P問題。

P問題可以認為是已經解決的問題,這個解決的定義是可以做多項式的時間複雜度內解決。所謂的多項式,也就是 ,這裡的k是一個常數。與多項式相反的函式有很多,比如指數函式、階乘等等。

NP問題並不是P問題的反義,這裡的N不能理解成No,就好像noSQL不是非SQL的意思一樣。NP問題指的是可以在多項式內驗證解的問題。

比如給定一個排序的序列讓我們判斷它是不是有序的,這很簡單,我們只需要遍歷一下就好了。再比如大整數的因式分解,我們來做因式分解會很難,但是讓我們判斷一個因式分解的解法是不是正確則要簡單得多,我們直接把它們乘起來和原式比較就可以了。

顯然所有P問題都是NP問題,既然我們可以多項式內找到解,那麼必然我們也可以在多項式內驗證解是否正確。但是反過來是否成立呢,是否多項式時間內可以驗證解的問題,也可以通過某種演算法可以在多項式時間內被解開呢?究竟是我們暫時還沒有想到演算法,還是解法一開始就不存在呢?

上面的這個問題就是著名的NP=P是否成立的問題,這個問題目前仍然是一個謎,有些人相信成立,有些人不相信,這也被認為是二十一世紀的最大難題之一。

為了證明這個問題,科學家們又想出了一個辦法,就是給問題做規約。舉個例子,比如解方程,我們解一元一次方程非常簡單,而解二元一次方程則要困難一些。如果我們想出瞭解二元一次方程的辦法,那麼必然也可以用來解一元一次方程,因為我們只需要令另一個未知數等於0就是一元一次方程了。

同理,我們也可以把NP問題做轉化,將它的難度增大,增大到極限成為一個終極問題。由於這個終極問題是所有NP問題轉化得到的,只要我們想出演算法來解決了終極問題,那麼,所有的NP問題全部都迎刃而解。就比如如果我們想出瞭解N元方程的演算法,那麼這一類解方程的問題就都搞定了。這種轉化之後得到的問題稱為NP完全問題,也叫做NPC問題。

下面我們來看一個經典的NPC問題,即邏輯電路問題。

下圖是一個邏輯電路,假設我們知道它的輸出是True,我們也知道了電路的結構,那麼請問我們能否確定一定可以找到一個輸入的組合,使得最後的輸出是True嗎?

它顯然是一個NP問題,因為我們可以直接把解法代入電路去計算一下,就可以驗證這個解是否正確,但是想要得到答案卻很難。經過嚴謹的證明,所有NP問題都可以經過轉化得到它,也就是說如果我們找到一種解法可以在多項式內解決這個問題,那麼我們就解決了所有的NP問題。

最後,還有一個NP-Hard問題,NP-Hard問題是說所有NP問題可以經過轉化得到它,但是它本身並不是NP問題,也就是說我們無法在多項式時間內判斷它的解是否正確。

比如剛才提到的旅行商問題就是一個NP-Hard問題,因為即使我們給定了一個解,我們也沒有辦法快速判斷給定的解是否正確,必須要遍歷完所有的情況才可以。我們驗證的複雜度就已經超出了多項式的範疇,所以它不屬於NP問題,比NP問題更加困難,所以是一個NP-Hard問題。

狀態壓縮解法

說完了NP問題,我們回到演算法本身。

既然我們要用動態規劃的思路來解決這個問題,就不能脫離狀態和決策。前文說了我們利用二進位制可以用一個整數來表示一個集合的狀態,我們很容易會把這個狀態當成是動態規劃當中的狀態,但其實這是不對的。

單純集合之間的轉移沒有限制條件,比如之前的例子當中我們已經拿了1號球和2號球,後面只要是剩下的球都可以拿,但是旅行商問題不一樣,假設我們去過了0和1兩個地方,我們當前在位置1,我們是無法用2和5兩地之間的連線來更新這個狀態的,因為我們當前只能從1號位置出發。也就是說我們能採取的決策是有限制的。

所以我們不能只單純地拿集合的狀態來當做狀態,為了保證地點之間的移動順序正確,我們還需要加上一維,也就是當前所處的位置。所以真正的狀態是我們之前遍歷過的位置的狀態,加上當前所處的地點,這兩者的結合。

狀態確定了,決策就很簡單了,凡是當前地點能去的之前沒有去過的位置,都可以構成決策。

我們之前說過,在動態規劃問題當中,複雜度等於狀態數乘上決策數,狀態數是 ,決策數就是n,所以總體的複雜度是 。雖然這個數字看起來仍然大得誇張,但是仍然要比n!小很多。

我們舉個例子來看下,如果n=10,n!=3628800, ,兩者相差了三十多倍。隨著n的增大,兩者的差距還會更大。

最後,我們來實現以下演算法:

import math

if __name__ == "__main__":
    inf = 1 << 31
    # 鄰接矩陣儲存邊權重
    d = [[inf for _ in range(10)] for _ in range(10)]
    # 測試資料
    edges = [[0, 1, 3], [1, 2, 5], [2, 3, 5], [3, 4, 3], [4, 0, 7], [4, 1, 6], [0, 3, 4], [2, 0, 4]]
    for u, v, l in edges:
        d[u][v] = l

    # 初始化成近似無窮大的值
    dp = [[inf for _ in range(5)] for _ in range((1 << 5))]
    dp[0][0] = 0

    # 遍歷狀態
    for s in range(1, (1 << 5)):
        for u in range(5):
            # 遍歷決策
            for v in range(5):
                # 必須要求這個點沒有去過
                if (s >> v) & 1 == 0:
                    continue
                dp[s][v] = min(dp[s][v], dp[s - (1 << v)][u] + d[u][v])

    print(dp[(1 << 5) - 1][0])

在acm競賽的程式碼風格當中,我們通常用u表示邊的起點,v表示邊的終點。所以上面的三重迴圈第一種是遍歷了所有的狀態,後面兩重迴圈是枚舉了起點和終點,也就是所有的邊。我們遍歷的是當前這個狀態之前的最後一次移動的邊,也就是說當前的點是v,之前的點是u,所以之前的狀態是s - ,決策帶來的開銷是d[u][v],也就是從u到v的距離。

如果讀過之前文章的小夥伴,會發現這是一個逆推的動態規劃。我們列舉當前的狀態和當前狀態的所有來源,從而找到當前狀態的最優解。如果對這個概念不熟悉的同學,可以檢視一下之前動態規劃下的其他文章。

這段程式碼當中有兩個細節,第一個細節是我們沒有做u的合法判斷,有可能我們u是不合法的,比如我們的集合當中只有2和3兩個點,但是我們卻枚舉了從4到5的策略。這樣是沒問題的,因為我們開始的時候把所有的狀態都設定成了無窮大,只有合法的狀態才不是無窮,由於我們希望最後得到的結果越小越好,不合法的狀態是不會被用來更新的。

第二個細節稍微隱蔽一些,就是我們在初始化的時候設定了dp[0][0] = 0。這表示我們是從空集開始的,而不是從0點開始的。因為0點已經遍歷過的狀態對應的數字是1,當然我們也可以設定成0已經訪問過了,從0點開始,這樣的話由於每個點不能重複訪問,所以最後我們是無法回到0點的,要得到正確結果我們還需要加上回到0點需要的消耗。

分析一下會發現第一點是第二點的基礎,如果我們在列舉策略的時候都判斷一下u點是否也合法,那麼這個演算法就沒有辦法執行,因為對於空集而言,所有點都是未訪問過的,也都是非法狀態,我們就找不到一個訪問過的u作為決策的起點。

如果你看不懂上面的做法也沒有關係,我再附上一種稍稍簡單一些的方法:

    # 我們從0點已經遍歷開始
    dp[1][0] = 0

    for s in range(2, (1 << 5)):
        for u in range(5):
            # 嚴格限制u必須已經遍歷過
            if (s >> u) & 1 == 0:
                continue
            for v in range(5):
                if (s >> v) & 1 == 0:
                    continue
                dp[s][v] = min(dp[s][v], dp[s - (1 << v)][u] + d[u][v])

    ans = inf
    # 最後加上回到0點的距離
    for i in range(5):
        ans = min(ans, dp[(1 << 5) - 1][i] + d[i][0])
        
    print(ans)

在這一種做法當中,我們從狀態1開始,也就是說我們把0號位置看成當前所在的點,並且已經遍歷過了,所以標記成了1。這樣的問題是我們沒有辦法再回到0了,因為一個點只能走一次,所以最後的時候需要再尋找回到0點的最優路徑。

(1 << n) - 1的值是從0到n-1個二進位制位都是1的值,表示這n個位置全部已經遍歷過了。然後我們遍歷所有回到0點的出發點,找到距離最近的那條。相比於上面的做法,這種做法更容易理解一些,但是程式碼多寫幾行,但是更容易理解一些。我建議如果直接理解第一段程式碼有困難的話,可以先搞懂第二段,然後再想明白為什麼第一段程式碼也成立。

總結

不知道有多少人成功看到了這裡,動態規劃的確不簡單,第一次學的話會覺得很困難難以理解是正常的。但是它是屬於那種入門之前覺得特別難,但是一旦想明白了之後就特別簡單的問題。而且大家從程式碼量上也看得出來,我用了幾千字描述的演算法,寫出來居然只有十幾行。

動態規劃演算法一直都是如此,程式碼不長,但每一行都是精髓。從這點上來說,它的價效比還真的是蠻高的。

好了,今天的文章就是這些,如果覺得有所收穫,請順手點個關注或者轉發吧,你們的舉手之勞對我來說很重要。

相關推薦

狀態壓縮動態規劃演算法

本文始發於個人公眾號:TechFlow,原創不易,求個關注 今天是演算法與資料結構專題的第16篇,也是動態規劃系列的第5篇。 今天文章的內容是動態規劃當中非常常見的一個分支——狀態壓縮動態規劃,很多人對於狀態壓縮畏懼如虎,但其實並沒有那麼難,希望我今天的文章能帶你們學到這個經典的應用。 二進位制表示狀態 在

Vijos 1921 嚴厲的班長 【狀態壓縮動態規劃

num con comment 數列 emoji 愛慕 狀態 -1 names 嚴厲的班長 描述 木姑娘在班級裏面是班長。雖然是副班長,卻有著比正班長更高的威信,並深受小朋友們的愛戴。 每天眼保健操時間,木姑娘都要監督所有小朋友認真做眼保健操

信息學奧賽一本通 5.4 狀態壓縮動態規劃

har gif string cout lowbit -a 需要 names std #loj 10170. 「一本通 5.4 例 1」騎士 看數據範圍n<=10,所以不是搜索就是狀壓dp,又因為搜索會超時所以用dp dp[i][k][j]表示現已經放到第i行,前面

【jzoj4908】【NOIP2016提高組】【憤怒的小鳥】【狀態壓縮動態規劃

題目大意 解題思路 由於點的個數很少可以狀態壓縮,我們可以找到第一個沒有覆蓋的點,再隨便列舉一個點(記得打break),兩個點就可以確定一條拋物線,再掃一遍就可以知道有哪些點被覆蓋,

Poj-3570 Fund Management(狀態壓縮動態規劃)

Frank is a portfolio manager of a closed-end fund for Advanced Commercial Markets (ACM). Fund collects money (cash) from individual investors for a certai

動態規劃演算法

問題描述: 有兩個字串,求最長公共子串的長度,例如 "ANKNGIEK" "AKGBIOK" 上面這兩個字串,公共子序列為AKGIK,長度為5。最長公共子序列就是說有一個字串在兩個字串中都出現過,這裡只考慮從頭到尾的順序,也就是說AKGIK,這個字串中的字母,在第一個字串中出現

動態規劃演算法及經典例題

動態規劃 什麼是動態規劃? 動態規劃的大致思路是把一個複雜的問題轉化成一個分階段逐步遞推的過程,從簡單的初始狀態一步一步遞推,最終得到複雜問題的最優解。 基本思想與策略編輯: 由於動態規劃解決的問題多數有重疊子問題這個特點,為減少重複計算,對每一個子問題只解一次,將其不同階段的不同狀態儲存在一個二維陣列中。

利用動態規劃演算法01揹包問題->二維陣列傳參->cpp記憶體管理->堆和棧的區別->常見的記憶體錯誤及其對策->指標和陣列的區別->32位系統是4G

1、利用動態規劃演算法解01揹包問題 https://www.cnblogs.com/Christal-R/p/Dynamic_programming.html 兩層for迴圈,依次考察當前石塊是否能放入揹包。如果能,則考察放入該石塊是否會得到當前揹包尺寸的最優解。 // 01 knap

動態規劃演算法-----找零錢問題(求最優

動態規劃演算法通常用於求解具有某種最優性質的問題。動態規劃演算法與分治法類似,其基本思想都是將待求解問題分解成若干個子問題,先求解子問題,然後從這些子問題的解得到原問題的解。與分治法不同的是,適合於用動態規劃求解的問題,經分解得到的子問題往往不是互相獨立的。如果

GZIP壓縮原理分析(19)——第五章 Deflate演算法(五10) 演算法分析(04) 格式說明(03) 靜態哈夫曼編碼

靜態哈夫曼編碼(Compression with fixed Huffman codes),這部分內容只要看格式就好,出現在這裡的碼錶只是為了說明,細節此時可能不懂,但是後面會鋪開來講,不用擔心。

動態規劃演算法最長公共子序列LCS問題

原文地址:http://blog.csdn.net/rrrfff/article/details/7523437 動態規劃演算法解LCS問題 作者 July 二零一零年十二月三十一日 本文參考:

動態規劃演算法--最長公共子序列問題

如今最好,沒有來日方長! 一、簡述動態規劃演算法 1.動態規劃演算法簡介 (1)背景 動態規劃(英語:Dynamic programming,簡稱:DP)是一種演算法設計技術,是運籌學的一個分支,是求解決策過程(Decision pr

【轉載】Spring AOP 、 JDK動態代理、CGLib動態代理

rto 工廠 第一個 lec 僅支持 sel clas sleep gpo 原文地址:https://www.cnblogs.com/kukudelaomao/p/5897893.html AOP是Aspect Oriented Programing的簡稱,面向切面

Linux 命令-存檔壓縮

創建 file 目錄 命令 lock 詳解 進行 linux 命令詳解 1. linux存檔壓縮1.創建歸檔文件tar cf myfile.tar files/ c :createf:file2.查看歸檔的文件tar -tf myfile.tar3.對文件進行歸檔壓縮(

[SCOI2007]壓縮(動態規劃,區間dp,字串雜湊)

[SCOI2007]壓縮 狀態:設\(dp[i][j]\)表示前i個字元,最後一個\(M\)放置在\(j\)位置之後的最短字串長度. 轉移有三類,用刷表法來實現. 第一種是直接往壓縮串後面填字元,這樣就是: \[dp[i+1][j]=min(dp[i+1][j],dp[i][j]+1)\] 另外一種

(二十二)訪問者模式(偽動態雙分派)

 作者:zuoxiaolong8810(左瀟龍),轉載請註明出處,特別說明:本博文來自博主原部落格,為保證新部落格中博文的完整性,特複製到此留存,如需轉載請註明新部落格地址即可。               本次LZ

動態規劃演算法的理解

一、對動態規劃的理解   動態規劃思想與分治法類似,都是將問題分解為多個子問題,通過求解子問題來得到最終答案,而動態規劃的優勢在於,動態規劃防止了子問題的重複計算,每個問題只計算一次,自底向上地求出原問題的解。 二、程式設計題1、2的遞迴方程  第一題 int max(int *p,

西南交通大學計算機專業考研真題答案6:2012年演算法設計題

一、考研真題 1、下面是求兩個集合A和B的並集(AUB)的演算法,集合A和集合B分別用單鏈表La和Lb的帶頭結點的單鏈表表示(連結串列中的資料按升序排序),其並集用單鏈表Lc表示(帶頭結點,其資料也按升序排列),請填空完善演算法。(每空2分)。 2、對給定的帶頭結點的單鏈表L,結點值得型

西南交通大學計算機專業考研真題答案8:2010年演算法設計題

一、考研真題 3、設計一演算法,實現在資料元素有序的順序儲存結構的線性表中插入一個值為x的操作。如果無儲存空間則插入失敗,函式的返回值為插入成功與否的標誌。(8分) 4、設有兩個整數集合A和B,分別用遞增有序連結串列表示,設計一演算法實現兩個集合的聯合運算,運算結果也有遞增有序連結串列表

深度學習 --- BP演算法(誤差反向傳播演算法

本節開始深度學習的第一個演算法BP演算法,本打算第一個演算法為單層感知器,但是感覺太簡單了,不懂得找本書看看就會了,這裡簡要的介紹一下單層感知器: 圖中可以看到,單層感知器很簡單,其實本質上他就是線性分類器,和機器學習中的多元線性迴歸的表示式差不多,因此它具有多元線性迴歸的優點和缺點。