TG可能會用到的動態規劃-簡易自學
最新更新
完整校訂版見此
戳我閱讀
以下為未核對不完整版本。
因版權原因,完整精校版不向所有公眾開放。
請從您找到本博客的地址查找附帶密碼(比如簡書分享了本網址,請您從簡書分享頁底部查詢密碼),感謝您的配合。
這裏Pleiades_Antares小可愛qiumi
這幾天突然回憶起來以前上的一個課qiumi
然後把當時上課做的筆記簡單整理了一下發上來(主要是為了給自己復習orz)
於是就有了這個自學教程(基本上看著這個博客是完全可以學會1551)
希望能夠幫助到正在拼命復習NOIP的你辣
沒有學過動態規劃的戳這個超級簡單易懂的動態規劃教程!!包教包會!
TG考試可能會用到的動態規劃
by Pleiades_Antares
NOIP的例題大概會講
NOIP2014 飛揚的小鳥
NOIP2017 寶藏
(還有更多的但是
NOIP對動態規劃的考察主要在狀態和轉移方程的設計方面,往往設計出優秀的狀態和轉移即可在題目中取得不錯的分數。
一、樹形DP
樹形DP用於解決樹上問題。
狀態常常用f(u,S)來表示已經對子樹u這個子問題進行求解,狀態為S的值。
轉移往往會有子樹合並。
如上圖所示,可以考慮通過這個我手繪的圖片來輔助理解。
其代碼常常如下所示:
1 dfs(u) 2 initial f(u)3 foreach v in son[u] 4 dfs(v) 5 update f(u) with f(v)
解釋下:
樹的直徑:
兩個畫了圈的點的距離即為樹的直徑。
f(u)表示以u為根的子樹,到u的最長鏈的長度
來一道例題熱身:
有一顆n個節點n-1條邊的無向樹,樹上節點用1,2,.....n編號。
初始時每個節點都是白色。如果選中了節點u,則對於樹中的每條邊(u,v),v都會被染成黑色。註意u自身不會被染黑。
現在總共要選擇恰好k個點,問有多少種方法使得所有節點被染黑。
數據範圍:1<=n<=100000,1<=k<=min(n,100)
先考慮n<=1000時要怎麽做?
令dp(u,k,color,choice)表示對於以u為根的子樹,裏面選了k個點,u的顏色為黑/白,u有沒有被選中。u子樹外點的選擇只能影響到u子樹中u點的顏色,
u子樹只有u點的選擇情況能影響到u子樹外的點的顏色。所以這樣的狀態時足夠的。
時間復雜度:O(nk2)
1 dp[u][k][color][choice] 2 t[k][color][choice] 3 4 void dfs(int u) { 5 dp[u][1][0][1] = 1; 6 dp[u][0][0][0] = 1; 7 for (int v: son[u]) { 8 dfs(v); 9 for (int uk = 0; uk <= k; ++uk) { 10 for (int vk = 0; vk <= k; ++vk) { 11 for (int ucolor = 0; ucolor < 2; ++ucolor) { 12 for (int vcolor = 0; vcolor < 2; ++vcolor) { 13 for (int uchoice = 0; uchoice < 2; ++uchoice) { 14 for (int vchoice = 0; vchoice < 2; ++vchoice) { 15 t[uk + vk][u_new_color][uchoice] += dp[u][uk][ucolor][uchoice] * dp[v][vk][vcolor][vchoice] 16 } 17 } 18 } 19 } 20 } 21 } 22 } 23 }
//嘗試在用u的孩子v的dp數組f(v)更新f(u)的時候,只枚舉f(u)
實際上,時間復雜度是O(nk)而非O(nk2)
這就是——01背包實際上。
關於01背包的模板可以參考這篇文章,網上也有很多其他大神有關於01背包的理解,這裏就不再贅述。
把std源代碼發出來:
1 #include <bits/stdc++.h> 2 3 using namespace std; 4 5 const int maxn = 1e5 + 10, maxk = 110, mod = 1e9 + 7; 6 7 void judge() { 8 freopen("action.in", "r", stdin); 9 freopen("action.out", "w", stdout); 10 return; 11 } 12 13 int n, k, siz[maxn], q[maxn], par[maxn], fnt, rar, ans; 14 int dp[maxn][maxk][2][2], pd[maxk][2][2]; 15 vector<int> g[maxn]; 16 17 inline void Add(int &x, int y) { 18 x = x + y < mod ? x + y : x + y - mod; 19 return; 20 } 21 22 int main() { 23 judge(); 24 scanf("%d%d", &n, &k); 25 for (int i = 1; i < n; ++i) { 26 int u, v; 27 scanf("%d%d", &u, &v); 28 g[u].push_back(v); 29 g[v].push_back(u); 30 } 31 q[rar++] = 1; 32 while(fnt != rar) { 33 int u = q[fnt++]; 34 siz[u] = 1; 35 dp[u][0][0][0] = dp[u][1][1][0] = 1; 36 for (int i = 0; i < g[u].size(); ++i) { 37 int &v = g[u][i]; 38 if(v != par[u]) { 39 par[q[rar++] = v] = u; 40 } 41 } 42 } 43 for (int ti = rar - 1; ti; --ti) { 44 int &u = q[ti], &p = par[u], Endu = min(siz[u], k), Endp = min(siz[p], k); 45 for (int ip = 0; ip <= Endp; ++ip) { 46 for (int xp = 0; xp < 2; ++xp) { 47 for (int yp = 0; yp < 2; ++yp) { 48 pd[ip][xp][yp] = dp[p][ip][xp][yp]; 49 dp[p][ip][xp][yp] = 0; 50 } 51 } 52 } 53 for (int ip = 0; ip <= Endp; ++ip) { 54 for (int xp = 0; xp < 2; ++xp) { 55 for (int yp = 0; yp < 2; ++yp) { 56 static int s; 57 if(s = pd[ip][xp][yp]) { 58 int End = min(Endu, k - ip); 59 for (int iu = 0; iu <= End; ++iu) { 60 for (int xu = 0; xu < 2; ++xu) { 61 for (int yu = xp ^ 1; yu < 2; ++yu) { 62 Add(dp[p][iu + ip][xp][yp | xu], (long long) s * dp[u][iu][xu][yu] % mod); 63 } 64 } 65 } 66 } 67 } 68 } 69 } 70 siz[p] += siz[u]; 71 } 72 for (int xu = 0; xu < 2; ++xu) { 73 Add(ans, dp[1][k][xu][1]); 74 } 75 printf("%d\n", (ans + mod) % mod); 76 return 0; 77 }
二、數位DP
如果題目給定了一個很大的數字,問你有多少符合一系列與
例題:
定義 S(n) 為將 n在 10 進制下的所有數位從小到大排序後得到的數。例如:S(1)=1, S(50394)=3459, S(323) =233
給定 X 求取模的結果。
數據範圍:1<=X<=10700.
考慮如何計算答案,可以通過分別計算每一位對總和的貢獻來求。定義 cnt(i, x)為 S(1),S(2),...,S(X)中第i位為x 的數量。
直接求 cnt(i, x)仍然較為困難,考慮差分。令dlt(i, x) 為第 i 位上有多少個數 ≥ x。對於一個 S(y) 中第 i 位 ≥ x 的數 y,可以發現其必須滿足有至少 i 個數位 ≥ x,這樣就可以進行dp了。
另 dp(i, j, x, cmp) 表示填了前 i位,有至少 j個數字≥ x,與 N 的大小關系位cmp。
轉移時直接枚舉第i + 1 位數字是多少即可。
三、狀壓DP
解決狀態較為復雜的問題,時間復雜度常常是指數級別。
(qwq我真的列不出來只能放截圖了)
1 S = 1 << n; 2 for (int s = 0; s < S; ++s) { 3 for (int t = s; ; t = (t - 1) & s) { 4 ... 5 if(t == 0) break; 6 } 7 }
來一道例題
再來一道例題:
NOIP2014
飛揚的小鳥
思路詳解??
NOIP2017 寶藏
先看一下題目:
這道題有很多種做法(霧) 放一個正確的代碼(DP)參與考古挖掘的小明得到了一份藏寶圖,藏寶圖上標出了 nn 個深埋在地下的寶藏屋,也給出了這 nn個寶藏屋之間可供開發的 mm 條道路和它們的長度。
小明決心親自前往挖掘所有寶藏屋中的寶藏。但是,每個寶藏屋距離地面都很遠,也就是說,從地面打通一條到某個寶藏屋的道路是很困難的,而開發寶藏屋之間的道路則相對容易很多。
小明的決心感動了考古挖掘的贊助商,贊助商決定免費贊助他打通一條從地面到某個寶藏屋的通道,通往哪個寶藏屋則由小明來決定。
在此基礎上,小明還需要考慮如何開鑿寶藏屋之間的道路。已經開鑿出的道路可以任意通行不消耗代價。每開鑿出一條新道路,小明就會與考古隊一起挖掘出由該條道路所能到達的寶藏屋的寶藏。另外,小明不想開發無用道路,即兩個已經被挖掘過的寶藏屋之間的道路無需再開發。
新開發一條道路的代價是:
這條道路的長度 xx 從贊助商幫你打通的寶藏屋到這條道路起點的寶藏屋所經過的寶藏屋的數量(包括贊助商幫你打通的寶藏屋和這條道路起點的寶藏屋)。
請你編寫程序為小明選定由贊助商打通的寶藏屋和之後開鑿的道路,使得工程總代價最小,並輸出這個最小值。
1 #include <cstdio> 2 #include <cstdlib> 3 #include <cstring> 4 #include <cmath> 5 #include <iostream> 6 #include <algorithm> 7 #include <set> 8 #include <queue> 9 #include <map> 10 #include <vector> 11 #include <utility> 12 #include <string> 13 #include <functional> 14 15 #define rep(i, n) for(int i = 0; i < (n); ++i) 16 #define forn(i, l, r) for(int i = (l); i <= (r); ++i) 17 #define per(i, n) for(int i = (n) - 1; i >= 0; --i) 18 #define nrof(i, r, l) for(int i = (r); i >= (l); --i) 19 #define SZ(x) ((int)(x).size()) 20 #define ALL(x) (x).begin(), (x).end() 21 #define mp make_pair 22 #define pb push_back 23 #define X first 24 #define Y second 25 26 using namespace std; 27 28 typedef long long LL; 29 typedef pair<int, int> pii; 30 31 const int maxn = 12, maxs = 1 << 12, oo = 1e9 + 7; 32 33 int n, m, g[maxn][maxn], h[maxn][maxs]; 34 int dp[maxn][maxs], f[maxs][maxs]; 35 36 void judge() { 37 freopen("treasure.in", "r", stdin); 38 freopen("treasure.out", "w", stdout); 39 return; 40 } 41 42 inline bool chkmin(int &x, const int &y) { 43 return x > y ? x = y, 1 : 0; 44 } 45 46 int main() { 47 // judge(); 48 scanf("%d%d", &n, &m); 49 if(n == 1) { 50 puts("0"); 51 return 0; 52 } 53 rep(i, n) { 54 rep(j, n) { 55 g[i][j] = oo; 56 } 57 g[i][i] = 0; 58 } 59 while(m--) { 60 int u, v, w; 61 scanf("%d%d%d", &u, &v, &w); 62 --u; 63 --v; 64 if(w < g[u][v]) { 65 g[u][v] = g[v][u] = w; 66 } 67 } 68 m = 1 << n; 69 rep(i, n) { 70 rep(s, m) { 71 if((s >> i) & 1) { 72 continue; 73 } 74 h[i][s] = oo; 75 rep(j, n) { 76 if((s >> j) & 1) { 77 chkmin(h[i][s], g[i][j]); 78 } 79 } 80 } 81 } 82 rep(s, m) { 83 int T = m - 1 ^ s; 84 for (int t = T; t; t = (t - 1) & T) { 85 rep(i, n) { 86 if((s >> i) & 1) { 87 if(h[i][t] == oo) { 88 f[s][t] = oo; 89 break; 90 } 91 f[s][t] += h[i][t]; 92 } 93 } 94 } 95 } 96 rep(i, n) { 97 rep(s, m) { 98 dp[i][s] = oo; 99 } 100 } 101 rep(i, n) { 102 dp[0][1 << i] = 0; 103 } 104 int tmp; 105 rep(i, n - 1) { 106 rep(s, m) { 107 if((tmp = dp[i][s]) < oo) { 108 // printf("dp[%d][%d] = %d\n", i, s, tmp); 109 int T = m - 1 ^ s; 110 for (int t = T; t; t = (t - 1) & T) { 111 if(f[t][s] != oo) { 112 chkmin(dp[i + 1][s | t], tmp + f[t][s] * (i + 1)); 113 } 114 } 115 } 116 } 117 } 118 --m; 119 int ans = oo; 120 rep(i, n) { 121 chkmin(ans, dp[i][m]); 122 } 123 printf("%d\n", ans); 124 return 0; 125 }
有 30000個島嶼從左到右排列,有 n個寶石,在 p1?, p2?, ..., pn?上。 你初始時在 0 號島上,第一次跳到 d 號島上,第i (i > 2)次你向右跳躍的距離為第i − 1次跳躍距離 l 或者 l-1 或者l + 1。問最多能拿多少寶石。
數據範圍:1 ≤ n, d ≤ 30000。
再看一道題:
TG可能會用到的動態規劃-簡易自學