0% found this document useful (0 votes)
10 views111 pages

Introduction To Dynamic Programming 1

Uploaded by

ray950104
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
10 views111 pages

Introduction To Dynamic Programming 1

Uploaded by

ray950104
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

動態規劃專題課程

Introduction to Dynamic Programming

2023 社群⽉演算法專題課程
2023 CISCON Algorithm Course
陳俊安 Colten
⾃我介紹 - 陳俊安 Colten
● 國⽴成功⼤學資訊⼯程系 ⼤⼆ (特選甲組)
● 熱愛競技程式設計、演算法、資料結構
● SYSTEX 精誠資訊 SEI 暑期實習⽣
● APCS 實作滿級分
● 台南⼥中資訊研究社 37 & 38th C++ 進階班講師
● 嘉義⼥中 111 學年度數理資優班 獨⽴研究資訊組 授課⽼師
● 2023 資訊之芽培訓計畫 南區算法班 講師
● 2022 ICPC Asia Taoyuan Regional Programming Contest Bronze Medal
動態規劃 Dynamic Programming

● 是⼀個把陣列名字叫做 dp 的技巧
○ 沒錯
● 是動態?還是規劃?
○ 他既⾮動態也⾮規劃
● 動態規劃是⼀個透過⼩的⼦問題解決⼤問題的技巧
○ 很像分治 (Divide and Conquer) ⼤事化⼩,⼩事化無
● 那為什麼叫動態規劃?
動態規劃 Dynamic Programming

● 我特別去找了⼀下資料,結果發明動態規劃的⼈的⾃傳有寫名⼦由來
● 發明動態規劃的⼈是 Bellman
● 他在他的⾃傳 《Eye of the Hurricane: An Autobiography》有提到
● 有興趣的可以參考 這個連結
● 為了給⼤家⼀點期待感我現在不想公布答案 : P
動態規劃 Dynamic Programming

● 動態規劃是什麼?
○ 在這邊我⽤⼀句話解釋動態規劃是什麼
■ ⻑江後浪推前浪,⼀替新⼈換舊⼈
○ 這句話呼應了動態規劃最核⼼的想法
○ ⽤以前的資訊來幫助我們得到當前最新的資訊
f(n) = f(n-1) + f(n-2)

● 可以把動態規劃想像成是⼀個公式
● 只要我們把公式需要的資訊先取得了,就可以慢慢往後推得後⾯的資訊
● f(0) = 0 , f(1) = 1
○ 有了以上這兩個資訊之後我們就可以依序算出 f(2), f(3) … f(n)
○ 求出 f(n) 的時間複雜度為 O(n)
f(n) = f(n-1) + f(n-2)

● 這樣⼦從底層往上慢慢推出後⾯資訊的⽅式稱為 Buttom-Up
● 與 Buttom-Up 相反的⽅式稱為 Top-Down
● 我們⼀樣來看這⼀個例⼦
f(n) = f(n-1) + f(n-2)

● 我們嘗試使⽤遞迴求得 f(n) 是多少


● 求得 f(n) 需要 f(n-1) 與 f(n-2) 的資訊
● 遞迴的終⽌條件則為 f(0) 與 f(1)
f(n) = f(n-1) + f(n-2)

● 這樣⼦的時間複雜度是 O(n) ㄇ?
● 我們畫出遞迴樹來看看
f(n) = f(n-1) + f(n-2)

● 你會發現有地⽅我們重複計算了
● 先前已經求過 f(2) 與 f(3) 了
f(n) = f(n-1) + f(n-2)

● 因此如果我們把之前算過的存起來
● 如果某次突然要使⽤到之前算過的資訊就可以直接拿出來⽤了
● 這就是動態規劃 Top-Down 的精神
時間複雜度?

● 因為這樣⼦我們可以保證這 n 個東⻄我們只會算過 1 次
● 時間複雜度⼜變回乾淨的 O(n) 了
Top-Down v.s Bottom-Up

● Top-Down 的缺點
○ 時間複雜度常數⼤
■ pass by value & pass by reference
■ 遞迴太深會導致 stack overflow
● Top-Down 的優點
○ 轉移式很直覺,只要記得把算過的存起來就好
Top-Down v.s Bottom-Up

● Bottom-Up 的缺點
○ 轉移式⽐較不直覺
● Bottom-Up 的優點
○ 寫起來很乾淨,時間複雜度常數⼩
● Bottom-Up 是動態規劃最常⾒的使⽤⽅法
● Top-Down ⼀個不⼩⼼遞迴太深就出⼤事了
排列組合動態規劃:CSES Problem Set Dice Combinations

● 骰⼦有 1 ~ 6 點,現在可以骰無限顆骰⼦,依序骰每⼀個骰⼦
● 求最後共有幾種骰法會使所有骰⼦的點數和為 n
● Example:
○ n = 3 , answer = 4
○ 1+1+1
○ 2+1
○ 1+2
○ 3
排列組合動態規劃:CSES Problem Set Dice Combinations

● 動態規劃的第⼀個步驟都是 定義轉移式
● 有點類似定義⼀個 Function 的概念
● 像是這題我們會定義 dp[i] = 骰出點數為 i 的組合數
排列組合動態規劃:CSES Problem Set Dice Combinations

● 定義完轉移式之後接下來就可以開始把公式推出來了
● 對於點數 i 來說,要使骰出的點數總和為 i ,會有 6 種可能
○ 點數 i - 6 時再骰出 1 個 6 點
○ 點數 i - 5 時再骰出 1 個 5 點
○ 點數 i - 4 時再骰出 1 個 4 點
○ and so on…
● 因此轉移式為 dp[i] = dp[i-1] + dp[i-2] + … + dp[i-6]
○ 加法原理
排列組合動態規劃:CSES Problem Set Dice Combinations

● 如此⼀來,很簡單的就可以⽤迴圈解決了,時間複雜度 O(n)
● 題⽬有說答案可能很⼤,只要輸出 mod 10^9 + 7 的結果就好
排列組合動態規劃:CSES Problem Set Coin Combinations I

● 有 n 種硬幣,每種硬幣的⾯額分別是 ci
● 接下來每⼀次你可以選擇其中⼀種硬幣 (可以重複拿⼀樣的)
● 求最後湊出總⾦額 x 的選法有幾種
排列組合動態規劃:CSES Problem Set Coin Combinations I

● 定義轉移式:dp[i] = 湊出總和為 i 的湊法有幾種


● 對於總和 i 來說,你有可能是透過:
○ i - c1 再拿 1 個 c1 硬幣得來的
○ i - c2 再拿 1 個 c2 硬幣得來的
○ i - c3 再拿 1 個 c3 硬幣得來的
○ and so on…
● 因此你會發現轉移式跟骰⼦那⼀題⼀樣
● 差別只在於骰⼦固定 1 ~ 6,硬幣是 c1 ~ cn
排列組合動態規劃:CSES Problem Set Coin Combinations I

● 對於每⼀個總和 i 我們都需要去枚舉 c1 ~ cn
● 因此整體時間複雜度為 O(nx)
● 我⾃⼰變數 x 是取名叫做 m
● 因為我⽐較叛逆⼀點
○ m 剛好在 n 旁邊
○ 打字會⽐較快
選擇困難的動態規劃:Atcoder DP Contest Frog 1

● 現在有⼀隻⻘蛙在第⼀個⽯頭,n 個⽯頭,每⼀個⽯頭的⾼度數字 hi
● 這⼀隻⻘蛙每⼀次只能跳 1 格或 2 格
● 如果原本在⾼度 a 的⽯頭,跳到⾼度 b 的⽯頭需要花費 | a - b |
● 求⻘蛙最後跳到第 n 個⽯頭所需要的最少花費
選擇困難的動態規劃:Atcoder DP Contest Frog 1

● 對於⻘蛙第 i 個⽯頭來說只有兩種可能
○ 從第 i - 1 個⽯頭跳過來
○ 從第 i - 2 個⽯頭跳過來
● 所以如果我們能算出跳到 i - 1 與 i - 2 的最佳答案,就可以求得跳到 i
的最佳答案
選擇困難的動態規劃:Atcoder DP Contest Frog 1

● 定義 dp[i] = 跳到 i 所需要的最⼩花費
● dp[1] = 0 , dp[2] = | h1 - h2 |
● 如果第 i - 2 個⽯頭的⾼度是 a , 第 i - 1 個⽯頭的⾼度是 b
● 從第 i - 2 個⽯頭跳過來所需花費為 dp[i-2] + | a - hi |
● 從第 i - 1 個⽯頭跳過來所需花費為 dp[i-1] + | b - hi |
選擇困難的動態規劃:Atcoder DP Contest Frog 1

● 要選出最好的⽅案,因此整個轉移式合併在⼀起就會變成:
● dp[i] = min( dp[i-2] + | a - hi | , dp[i-1] + | b - hi | )
● 整體時間複雜度 O(n)
選擇困難的動態規劃:Atcoder DP Contest Vacation

● Colten 放暑假只會做 3 件事情


○ 寫程式
○ ⽔餃
○ 睡覺
● 如果在第 i 天做第 1 件事情 Colten 會得到 ai 的快樂度
● 如果在第 i 天做第 2 件事情 Colten 會得到 bi 的快樂度
● 如果在第 i 天做第 3 件事情 Colten 會得到 ci 的快樂度
選擇困難的動態規劃:Atcoder DP Contest Vacation

● Colten 不會連續 2 天做同⼀件事情


● 求如果有 n 天,Colten 在最佳規劃下,快樂度最⼤可以是多少?
選擇困難的動態規劃:Atcoder DP Contest Vacation

● 對於每⼀天會有 3 種選擇
● 定義 dp[i][k] = 如果第 i 天做第 k 件事情能得到的最⼤快樂度
● dp[1][1] = a1 , dp[1][2] = b1 , dp[1][3] = c1
選擇困難的動態規劃:Atcoder DP Contest Vacation

● 對於 dp[i][1] 來說
○ 前⼀天 ( 第 i - 1 天 ) 不能也做第 1 件事情
● 對於 dp[i][2] 來說
○ 前⼀天 ( 第 i - 1 天 ) 不能也做第 2 件事情
● 對於 dp[i][3] 來說
○ 前⼀天 ( 第 i - 1 天 ) 不能也做第 3 件事情
選擇困難的動態規劃:Atcoder DP Contest Vacation

● 如果第 i 天要做第 1 件事情


○ 第 i - 1 天只能做第 2、3 件事情
○ 可以列出轉移式
■ dp[i][1] = max( dp[i-1][2] , dp[i-1][3] ) + ai
● 如果第 i 天要做第 2 件事情
○ 第 i - 1 天只能做第 1、3 件事情
○ 可以列出轉移式
■ dp[i][2] = max( dp[i-1][1] , dp[i-1][3] ) + bi
選擇困難的動態規劃:Atcoder DP Contest Vacation

● 如果第 i 天要做第 3 件事情


○ 第 i - 1 天只能做第 1、2 件事情
○ 可以列出轉移式
■ dp[i][3] = max( dp[i-1][1] , dp[i-1][2] ) + ci
選擇困難的動態規劃:Atcoder DP Contest Vacation

● 因此只要把每⼀天的所有選擇的最佳答案計算出來,就可以⼀直往後推
出最佳的答案
● 最後答案為 max( dp[n][1] , dp[n][2] , dp[n][3] )
● 時間複雜度:O(n)
0 / 1 背包問題

● 動態規劃當中最具代表性的問題之⼀
● ⽬前屬於 NP-Hard (還沒有找到多項式時間內的解法)
● 題⽬會給 n 個物品,容量 m 的背包
● 第 i 個物品會佔據背包 wi 的容量,價值為 vi
● 你的⽬標是最後讓背包裡的所有物品價值越⾼越好
0 / 1 背包問題

● 定義 dp[i][k] 表⽰考慮前 i 個物品的情況下,背包容量為 k 時所能得


到的最⼤價值
● 對於 dp[i][k] 來說,只有兩種選擇
○ 拿第 i 個物品
○ 不拿第 i 個物品
0 / 1 背包問題

● 如果我們要拿第 i 個物品,且當前背包只有 k 的容量


● 那在我們考慮前 i - 1 個物品時,背包容量只能有 k - wi
○ 因為拿第 i 個物品會佔據掉 wi 的容量
○ 如果在考慮前 i - 1 個物品時就⽤掉了超出 k - wi 的容量,第 i 個
物品是裝不下容量只有 k 的背包的
● 因此如果我們要拿第 i 個物品,轉移式為:
○ dp[i][k] = dp[i-1][k-wi] + vi
0 / 1 背包問題

● 如果我們不拿第 i 個物品,且當前背包只有 k 的容量


● 轉移式很簡單:
○ dp[i][k] = dp[i-1][k]
● 把這兩種可能的轉移式合併在⼀起就會變成
● dp[i][k] = max( dp[i-1][k] , dp[i-1][k-wi] + vi )
● ⽽我們最後要求的答案是 dp[n][m] ( n 個物品、背包容量 m )
● 因此我們就必須依序求出 dp[1][0~m], dp[2][0~m], … dp[n][0~m]
0 / 1 背包問題

● 求出 dp[i][0~m] 之前必須先把 dp[i-1][0~m] 求得


● ⽽我們已知 dp[0][0~m] 的結果都是 0
● 因此我們可以從底部開始推答案,推出 dp[n][m] 的結果
● 這就是動態規劃最重要的核⼼精神
● 整體時間複雜度:O(nm)
0 / 1 背包問題
無限背包問題

● 跟 0 / 1 背包問題要求的東⻄⼀樣
● 只是每⼀個物品的數量是無限的
無限背包問題

● 由於物品可以重複拿,我們的狀態就不⽤特別註明當前是考慮前幾種物
品,因此我們重新定義轉移式:
○ dp[i] = 背包容量為 i 時,所可以得到的最⼤價值
無限背包問題

● 對於容量 i 來說有 m 種可能


○ 在最⼤容量 i - w1 時再拿⼀個第 1 種物品
○ 在最⼤容量 i - w2 時再拿⼀個第 2 種物品
○ 在最⼤容量 i - w3 時再拿⼀個第 3 種物品
○ and so on…
● 你有發現ㄇ?是不是跟前⾯骰⼦還有硬幣那⼀題⼀樣了!
● 只差在最後要求的東⻄不⼀樣⽽已
無限背包問題

● 對於容量 i 來說拿第 k 個物品的話:


○ dp[i] = max( dp[i] , dp[i-w_k] + v_k )
● 整體時間複雜度:O(nm)
背包問題的瓶頸

● 時間複雜度我們可能無法改變,⽬前找不到什麼好⽅法
● 那我們來看看空間複雜度
背包問題的空間複雜度

● 需要開 n * m 的 dp 表格去紀錄 (除了無限背包有 O(m) 的作法),因


此空間複雜度為 O(nm)
● DP 是⼀個⽤ 空間 換取 時間 的技巧
● 也就是說 n * m 如果太⼤,我們是完成不了 0/1 背包問題的
● 但其實 0/1 背包問題也有空間複雜度 O(m) 的作法
● 所以接下來我們來講 DP 當中的第⼀個優化技巧 滾動陣列
DP 優化:滾動陣列

● 如果⼀般的動態規劃是:
○ ⻑江後浪推前浪,⼀替新⼈換舊⼈
● 那麼被 滾動陣列 優化後的動態規劃就是:
○ ⻑江後浪推前浪,前浪死在沙灘上
DP 優化:滾動陣列

● 不是把陣列拿起來滾
● 滾動陣列的核⼼精神是:
○ 把沒有⽤到的陣列拿來繼續重複使⽤
● 其實就是有點資源回收的概念
DP 優化:滾動陣列

● 我們來看看 0/1 背包問題:


○ dp[i][k] = max( dp[i-1][k] , dp[i-1][k-wi] + vi )
● 有發現什麼事情ㄇ?
● 如果我們現在正在算 dp[5][0~m]
○ 那麼 dp[1][0~m], dp[2][0~m], dp[3][0~m] 以後都⽤不到了
○ 因為每⼀次轉移都只要知道前⼀次的結果
○ 這個時候滾動陣列這個技巧就可以派上⽤場了
DP 優化:滾動陣列

● 對於背包問題來說只需要記錄前 1 次的結果
● 我們就可以開兩組⻑度為 m 的陣列就好
○ 其中⼀組紀錄上⼀次的結果
○ 其中⼀組紀錄這⼀次轉移的結果
● 這樣的話空間複雜度就從 O(nm) 被我們優化成 O(m)
DP 優化:滾動陣列

● 我⾃⼰習慣把陣列開成 dp[2][m]
● 然後假設 i 是奇數時就表⽰ i - 1 是偶數,因此可以寫成
○ dp[i mod 2][k] = max( dp[ ( i - 1 ) mod 2 ][k] ,
dp[ ( i - 1 ) mod 2 ][k-wi] + vi )
0/1 背包甚⾄可以不使⽤⼆維陣列

● 因為每⼀次都是拿前 1 次的結果,⽽且結果是可以⼀直使⽤的不⽤清
空,所以其實 0/1 背包問題可以只開⼀維陣列解決
● 但是有個超級⼤的陷阱
0/1 背包甚⾄可以不使⽤⼆維陣列

● 如果我們寫成這樣會發⽣什麼事情?
0/1 背包甚⾄可以不使⽤⼆維陣列

● 如果我們寫成這樣會發⽣什麼事情?
● 假設 dp[10] 我們拿了第 1 個物品,這個物品的容量是 10
● 那如果我在轉移 dp[20] 的時候:dp[20] = dp[20-10] + v_1
● 我們重複拿了第 1 個物品 2 次!這是不合法的!
0/1 背包甚⾄可以不使⽤⼆維陣列

● 但是如果我們如果把第⼆個迴圈倒著回來?
● 我們每⼀次拿的資訊都是前⼀次的資訊,不會重複拿
● 這樣就完成⼀維陣列版本的 0/1 背包了
那這個是什麼背包?

● 我們剛剛說這樣⼦因為會重複拿同⼀個物品,不符合 0/1 背包
● 那什麼樣的背包問題可以重複拿同⼀個物品?
那這個是什麼背包?

● 我們剛剛說這樣⼦因為會重複拿同⼀個物品,不符合 0/1 背包
● 那什麼樣的背包問題可以重複拿同⼀個物品?
● 無限背包!因此無限背包也可以這樣寫
背包問題的變化?

● 背包問題的變形其實也很多
● 通常⽐賽遇到背包問題的題⽬都會被包裝的很精美
背包問題 - 改:平分問題

● 這其實也很常⾒了
● 有 n 個物品,每個物品的精美程度為 ai
● 現在必須把這 n 個物品分給某兩個⼈
● 你的⽬標是要讓最後這兩個⼈拿到的物品總精美程度的差越⼩越好
○ 講簡單⼀點:就是要盡可能讓兩個⼈的精美程度⼀樣
● 想想看這該怎麼做ㄅ
背包問題 - 改:平分問題

● 在最完美的情況下,兩個⼈的總精美程度都會是 ( a1 + … + an ) / 2
● 把題⽬轉成,給定 n 個物品,你的背包容量是 ( a1 + … + an ) / 2
● 每個物品的精美程度是 ai,佔據的空間也是 ai
● 求出這個背包最多能裝多少精美程度的物品
● 從這邊就可以發現,題⽬就變回⼀個很單純的 0/1 背包問題了
● 最後答案就會是 ( a1 + … + an ) / 2 - dp[ ( a1 + … + an ) / 2 ]
LIS: CSES Problem Set Increasing Subsequence

● 給定⼀個⻑度為 n 的序列 a
● 找出 a 序列的最⻑遞增⼦序列
● 相信 O(n^2) 的做法你們應該都會了,但可以做到 O(n * log n)
LIS: CSES Problem Set Increasing Subsequence

● 我們先複習 O(n^2) 的作法


● 定義 dp[i] 表⽰以 i 這個位置為結尾的 LIS ⻑度
● 我們從左到右求出 dp[i]
LIS: CSES Problem Set Increasing Subsequence

● 假設我們在求 dp[5]
● 那麼我們就去看看前⾯的 a[1] , a[2] , a[3] , a[4]
○ 如果 a[1] < a[5] , 那就 dp[5] = max( dp[5] , dp[1] + 1 )
○ 如果 a[2] < a[5] , 那就 dp[5] = max( dp[5] , dp[2] + 1 )
○ 如果 a[3] < a[5] , 那就 dp[5] = max( dp[5] , dp[3] + 1 )
○ 如果 a[4] < a[5] , 那就 dp[5] = max( dp[5] , dp[4] + 1 )
● 因為前⾯的 LIS 我們都已經求出來了,就想像成多接⼀個 a[5] 進去就
可以了
LIS: CSES Problem Set Increasing Subsequence

● 最後全部求完後記得答案不是 dp[n],不要衝動
● 我們轉移式:dp[i] = 以 i 這個位置為結尾的 LIS ⻑度
● 因此答案為:max( dp[1] , … , dp[n] )
● 時間複雜度:O(n^2)
LIS: CSES Problem Set Increasing Subsequence

● 但這題的 n 範圍太⼤,O(n^2) 會 TLE,怎麼辦?


● 這個時候我們需要⼀個額外的演算法來輔助我們:
○ ⼆分搜尋 Binary Search
○ 貪婪演算法 Greedy Algorithm
LIS: CSES Problem Set Increasing Subsequence

● 我們額外開⼀個陣列 b
● 定義如果有⼀個⻑度為 i + 1 的 LIS,那麼把 b[i] 放在此 LIS 的最後
⼀個位置會是最⼩的數字
○ 如果我們能讓最後⼀個數字越⼩,表⽰我們成功的機會越⼤ (貪⼼)
○ 如果 b[2] = 3 , 那麼某個⻑度為 2 的遞增⼦序列可能是 ?, ?, 3
● ⼀開始 b 序列為 { }
LIS: CSES Problem Set Increasing Subsequence

● 我們從 a[0] ~ a[n-1],每次看看 a[i] 可以被插⼊在 b 的什麼位置


○ 很像插⼊排序
● 假設 b = { 10,20,30 },這個時候 a[i] = 13
● 我們就要將 b 改為 { 10,13,30 }
○ 因為在⻑度為 2 時,LIS 的最後⼀個數字選 13 是最⼩的
■ { 10 , 20 } and { 10 , 13 }
○ 只要讓選的數字越⼩越好,我們後⾯要構造⻑度更⻑的 LIS 就會更
容易構造
LIS: CSES Problem Set Increasing Subsequence

● 那如果 b = { 10,13,30 },這個時候 a[i] = 40


● 就表⽰有⼀個當前最⼤的數字可以接在最後⾯
● 因此 b = { 10,13,30,40 }
LIS: CSES Problem Set Increasing Subsequence

● 怎麼求出最後答案的⻑度?
● 我們回想看看我們 b 陣列的定義:
○ 如果有⼀個⻑度為 i 的 LIS,那麼把 b[i] 放在此 LIS 的最後⼀個位
置會是最⼩的數字
● 那如果我們 b 陣列的⻑度是 k
○ 有⼀個⻑度 k 的 LIS,把 b[k-1] 放在這⼀個 LIS 會是最佳選擇
○ 換句話說存在這樣的⻑度為 k 的 LIS
○ 因此我們最後整個 a 序列的 LIS 為 k
LIS: CSES Problem Set Increasing Subsequence

● 在尋找插⼊點的這⼀個過程我們可以使⽤ ⼆分搜 來優化


● 整體的時間複雜度就會是 O(NlogN)
● 這⼀個求 LIS 的演算法稱為:Robinson-Schensted-Knuth Algorithm
有限背包問題

● 0/1 背包問題的改版
● 每⼀個物品可拆分
● 如果有⼀個物品的重量是 10 , 價格 100
● 可以把這個物品拆成兩個物品 A , B
● A 重量 2, 價格 20
● B 重量 8, 價格 80
有限背包問題

● 有限背包問題我們可以轉成 0/1 背包問題來解


● 但是需要⽤到⼆進位的概念
有限背包問題

● 如果有⼀個物品的重量是 97
● 那麼表⽰這個物品可能會被拆出 1, 2, 3, … , 96 這些重量
● 0/1 背包問題是 選 與 不選 之間做選擇
● 我們只要把物品拆分的聰明⼀點,被拆分的結果只要能組合出 1 ~ 97
● 這樣我們就可以直接當 0/1 背包問題解
有限背包問題

● 舉個例⼦來說:
○ 現在有⼀個物品重量 12
○ 我們如果拆成 1, 2, 4, 5
○ 這樣的話我們不管怎麼組合,都可以湊出 1 ~ 12 這些情況
○ ⼤家可以試試看 ><
有限背包問題

● 那我們應該如何聰明的拆?
● 12 的⼆進位是 1100
● 表⽰ 1 ~ 12 的⼆進位最多只會⽤到四個位置
● 那我們要先確保 2^2 , 2^1 , 2^0 存在
● 這樣如果數字的⼆進位⻑度只有 3,我們⼀定可以透過他們三個完成
有限背包問題

● 12 = 1100(2)
● 但為什麼我們不需要 2^3 = 8 ?
● 1 + 2 + 4 + 8 > 12
● 因此我們不可能拆成這樣
● 1 + 2 + 4 + ( 12 - ( 1 + 2 + 4 ) ) > 12 必須修改成這樣
● 但是這樣 8 ~ 11 這些數字我們構造的出來ㄇ?
有限背包問題

● 我們現在已經確定可以構造出 0 ~ 7 還有 12
● 7 + 5 = 12
● 6 + 5 = 11
● 5 + 5 = 10
● 4+5=9
● 3+5=8
● 因此 1, 2, 4, 5 這四個數字是可以湊出 0 ~ 12 任何⼀個數字的
有限背包問題

● 因此我們只要從 2^0, 2^1, 2^2 這樣⼀直給下去


● 直到給個東⻄的總和已經極限了之後再把剩下的補⿑
● 12 = 2^0 + 2^1 + 2^2 + 5 (補⿑)
● 把每個物品都做這件事情之後做⼀次 0/1 背包問題就可以了
有限背包問題

● 時間複雜度:?
● 空間複雜度:?
● ⼤家理解之後算算看吧!
概念類似:CSES Problem Set Missing Coin Sum

● 給定 n 個硬幣,每個硬幣都有專屬的⾯額
● 請求出最⼩的⾯額,這⼀個⾯額是不能透過這 n 個硬幣湊出來的
概念類似:CSES Problem Set Missing Coin Sum

● 將硬幣由⼩排到⼤
● 設⼀個變數 x,表⽰⽬前硬幣能湊出的最⼤⾯額為 x - 1
○ 換句話說,接下來的硬幣⾯額必須要是 1 ~ x
○ 否則如果硬幣⾯額是 x + 1,所有的硬幣就湊不出 x 了
● 從第⼀個硬幣試到最後⼀個硬幣
● 途中如果硬幣的⾯額 > x,就可以輸出答案了
● 時間複雜度:O(n * log n)
概念類似:CSES Problem Set Missing Coin Sum
有限背包問題:CSES Problem Set Book Shop II

● 練習看看有限背包問題吧!
⼀些無關的實戰:CF 765 pC. Road Optimization

● 難度 1700
● 有 n 個速度牌,每個速度牌上有數字
● 從起點開始,如果經過速度牌且該速度牌的數字是 x,接下來每單位花
費的時間就會是 x
⼀些無關的實戰:CF 765 pC. Road Optimization

● 現在你可選擇 0 ~ k 個速度牌並把他們拆掉
● 請你設計⼀個程式算出最少的花費時間
⼀些無關的實戰:CF 765 pC. Road Optimization

● 定義 dp[i][j] 表⽰在拆掉 j 個速度牌的情況下,⾛到第 i + 1 個速度牌


所需要花費的最少時間
⼀些無關的實戰:CF 765 pC. Road Optimization

● 第 0 個速度牌不能拆
● 第 n 個速度牌是指終點,實際上沒有速度牌
⼀些無關的實戰:CF 765 pC. Road Optimization

● 對於每⼀個速度牌有兩種可能
○ 把這個速度牌拆掉
○ 不要拆
⼀些無關的實戰:CF 765 pC. Road Optimization

● 對於到達第 i 個速度牌來說:
○ 他的前⼀個速度牌是誰? (要枚舉)
○ 如果他的前⼀個速度牌是第 i - 3 個速度牌
○ 表⽰在這之間,有 2 個速度牌被拆了
⼀些無關的實戰:CF 765 pC. Road Optimization

● 到達第 i 個速度牌 ( 沒拆 i ),如果總共拆了 j 個速度牌 ( j 也要枚舉 )


○ 枚舉,如果是從第 p 個速度牌過來的
○ dp[i][j] = dp[k][ j - ( i - p - 1) ] + ( pos[i] - pos[p] ) * speed[p]
○ 如果總共拆 5 個且 p - i = 2 ,表⽰從起點到 p 拆了 3 個速度牌
⼀些無關的實戰:CF 765 pC. Road Optimization

● 最後答案就會是 min( dp[n][0] … dp[n][k] )


● 0 <= k <= n - 1
● 時間複雜度:O(n^3)
賽局動態規劃:CSES Problem Set Removal Game

● 給定⼀個⻑度 n 的序列,玩家有 2 個⼈
● 每⼀個⼈可以在每⼀個回合選擇序列的 頭 或 尾,並把該數字加到⾃⼰
的分數上,選擇後該位置的數字會消失
● 如果沒有數字可以拿了則遊戲結束
● 求出先⼿能得到的最⼤分數
○ 在兩位玩家都使⽤最佳策略的情形
賽局動態規劃:CSES Problem Set Removal Game

● 最佳策略的題⽬有⼀個重要的⼿筋,⼩技巧
● ⼀定要從 "最後" 做回 "最前"
● 我們必須要先能預測最後的結果,才能依靠這樣⼦的結果推得前⾯的最
佳策略,因此跟賽局有關的題⽬我們都必須反著做回來
賽局動態規劃:CSES Problem Set Removal Game

● 還有⼀個要注意的點,如果總分是 x
● 先⼿玩家在最佳策略下能得到 y 分
● 那麼後⼿玩家會得到 x - y 分
賽局動態規劃:CSES Problem Set Removal Game

● 定義 dp[L][R] 表⽰當序列只有 [ L , R ] 時,先⼿玩家能獲得的最佳分



● 在這樣的情況下先⼿玩家只能拿第 L 個數字或第 R 個數字
賽局動態規劃:CSES Problem Set Removal Game

● 假設先⼿玩家選第 L 個數字
● 表⽰原本的先⼿玩家變成了後⼿,另⼀個玩家⼀定會使⽤最佳策略讓對
⼿的分數越低,因此可以推出轉移式:
○ dp[L][R] = ( a[L+1] + … + a[R] ) - dp[L+1][R] + a[L]
賽局動態規劃:CSES Problem Set Removal Game

● 假設先⼿玩家選第 R 個數字
● 表⽰原本的先⼿玩家變成了後⼿,另⼀個玩家⼀定會使⽤最佳策略讓對
⼿的分數越低,因此可以推出轉移式:
○ dp[L][R] = ( a[L] + … + a[R-1] ) - dp[L][R-1] + a[R]
賽局動態規劃:CSES Problem Set Removal Game

● 這兩個式⼦取 max 就會是 dp[L][R] 的結果


● 但是在計算的過程要⼩⼼計算的順序
● dp[L][R] = ( a[L+1] + … + a[R] ) - dp[L+1][R] + a[L]
● dp[L][R] = ( a[L] + … + a[R-1] ) - dp[L][R-1] + a[R]
● 假設⽬前在算 dp[2][3]
○ dp[3][3] 跟 dp[2][2] 必須已經算完了
○ 第⼀個維度必須由⼤到⼩枚舉,第⼆個則是由⼩到⼤
賽局動態規劃:CSES Problem Set Removal Game

參考程式碼:[Link]
賽局動態規劃:ABC pD. Game in Momotetsu World

● 有兩個⼈在玩遊戲,遊戲在⼀個 n * m 的網格進⾏
● 兩個⼈的位置⼀樣,並且會同時⼀起移動
● 兩個⼈輪流決定如何移動,必須從 (1,1) ⾛到 (n,m)
● 只能往下⾛或往右⾛
● 採到 + 會加⼀分、反之採到 - 會扣⼀分
● 如果兩個⼈都採取最佳策略,請判斷誰會贏,或是回報平⼿
賽局動態規劃:ABC pD. Game in Momotetsu World

● ⽤跟剛剛⼀樣的概念,我們必須從最後往回推
● 定義 dp[i][j] 表⽰從 ( i , j ) ⾛到 ( n , m ) 第⼀個玩家能得到的分數
● 判斷⾛到 ( i , j ) 時,⽬前是誰的回合
● 當前位置如果是 ( i , j ),下⼀步可以到 ( i + 1 , j ) 或 ( i , j + 1 )
○ 因此跟 dp[i][j] 有關係的會是 dp[i+1][j] 跟 dp[i][j+1]
○ 如果當前是後⼿的玩家必須將兩者取 min,反之取 max
○ 先⼿玩家會盡量讓⾃⼰越好,後⼿要讓先⼿越差
賽局動態規劃:ABC pD. Game in Momotetsu World

● 剩下程式碼實作的部分⼤家練習看看吧!
● 時間複雜度:O(nm)
● 空間複雜度:O(nm)
積⽊型排組 DP:CSES Problem Set Counting Towers
積⽊型排組 DP:CSES Problem Set Counting Towers

● 狀況有點複雜,所以把某些情況分開會⽐較好處理
● 我們將整個積⽊分成左右兩半
● 定義 dp[i][0] 表⽰第 i 層的積⽊左右兩邊是屬於不同塊積⽊
● 定義 dp[i][1] 表⽰第 i 層的積⽊左右兩邊是屬於同⼀塊積⽊
積⽊型排組 DP:CSES Problem Set Counting Towers

● dp[1][0] = 1 , dp[1][1] = 1
● 可以推得:
○ dp[i][0] = dp[i-1][0] * 4 + dp[i-1][1]
○ dp[i][1] = dp[i-1][0] + dp[i-1] * 2
● 最後的答案就是 dp[n][0] + dp[n][1]
● 記得取 mod
動態規劃 - 狀態壓縮優化

● ⼀種把狀態透過另⼀種表⽰形式表現,讓 DP 可以更⽅便轉移的技巧
● ⼤部分題⽬都是將⼆進位狀態表⽰成⼗進位做轉移
○ 有些⼈會叫做位元 DP
● 很經典的例⼦:旅⾏推銷員問題
狀態壓縮 DP:CSES Problem Set Elevator Rides

● 有 n 個⼈要搭電梯,每⼀個⼈的重量為 ai
● 電梯每次可以載重量 k 單位
● 求把所有⼈載完最少需要幾次
狀態壓縮 DP:CSES Problem Set Elevator Rides

● n <= 20
● 但感覺⼜不能枚舉什麼東⻄?
● 這時候⼤⽅向就必須往位元 DP 這邊想了
狀態壓縮 DP:CSES Problem Set Elevator Rides

● 我們考慮⽤⼆進位的⽅式解決這題
● 0 表⽰不考慮這個⼈,1 表⽰考慮這個⼈
○ 101111 我們從右邊往左邊編號
○ 表⽰⽬前只考慮第 1、2、3、4、6 這幾個⼈
狀態壓縮 DP:CSES Problem Set Elevator Rides

● 對於 1011 的狀況來說,有三種可能
○ 原本電梯的狀況是:
■ 0011
■ 1001
■ 1010
● 因此我們如果知道在這三個情況的最佳答案,就可以推得 1011 這個
狀況的最佳答案
狀態壓縮 DP:CSES Problem Set Elevator Rides

● 如果從⼩數字做到⼤數字你會發現⼆進位剛好有⼀個特⾊
○ 對於 1011 來說:
■ 0011 算過了
■ 1001 算過了
■ 1010 算過了
狀態壓縮 DP:CSES Problem Set Elevator Rides

● 因此我們可以透過 bottom-up 的概念推出答案


● dp[i] 除了記錄次數之外,我們還必須額外紀錄⼀些東⻄:
○ first - 紀錄電梯使⽤次數
○ second - 最後⼀次的電梯重量最輕可以是幾公⽄
■ { 2 , 100 } 與 { 2 , 50 } 後者明顯⽐較好!
狀態壓縮 DP:CSES Problem Set Elevator Rides

● 要求的是 n 個⼈都搭電梯的答案
● 因此答案被存在 dp[(2^n)-1]

You might also like