# 動態規劃 --- ## 核心概念: ## 將問題分成子問題重複利用 - 定義dp狀態(無切入點可增加維度) - 列出轉移式(依需求調整定義) - 初始化邊界條件 ---- ## [IOI1994 -- 数字三角形](https://www.luogu.com.cn/problem/P1216) 給定一個 $1\leq n\leq1000$ 層的數字金字塔,第 $i$ 層共有 $i$ 個數字,從金字塔頂部開始,每次可往左下或右下走,直到最下層,求路徑數字和最大值 ---- - 設 $dp[i][j]$ 為從座標 $(i,j)$ 往下走的最大值 - $dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+ar[i][j]$ - $dp[n][j]=ar[n][j]$ - $ans=dp[1][1]$ ---- 程式碼: ```cpp! #include <bits/stdc++.h> void db() { cerr << "\n"; } template <class T, class... U> void db(T a, U... b) { cerr << a << " ", db(b...); } inline char gc() { const static int SZ = 1 << 16; static char buf[SZ], *p1, *p2; if (p1 == p2 && (p2 = buf + fread(p1 = buf, 1, SZ, stdin), p1 == p2)) return -1; return *p1++; } void rd() {} template <typename T, typename... U> void rd(T &x, U &...y) { x = 0; bool f = 0; char c = gc(); while (!isdigit(c)) f ^= !(c ^ 45), c = gc(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = gc(); f && (x = -x), rd(y...); } template <typename T> void prt(T x) { if (x < 0) putchar('-'), x = -x; if (x > 9) prt(x / 10); putchar((x % 10) ^ 48); } const int N = 1005; int dp[N][N], ar[N][N]; signed main() { int n; rd(n); for (int i = 1; i <= n; i++) for (int j = 1; j <= i; j++) rd(ar[i][j]); for (int i = n; i > 0; i--) for (int j = 1; j <= i; j++) dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + ar[i][j]; prt(dp[1][1]), putchar('\n'); } ``` ---- ## [CSES -- Dice Combinations](https://cses.fi/problemset/task/1633) 給定一個整數 $1\leq n\leq 10^6$ ,可以骰六面骰子任意次數,求有多少種方法使得點數總和是 $n$ ,順序交換算不同次 ---- - 設$dp[i]$為總和$i$點的方法數 - $dp[i]=\sum_{k=1}^{6}{dp[i-k]}$ - $dp[0]=1$ - $i-k\geq 0$ ---- 程式碼: ```cpp! #include <bits/stdc++.h> using namespace std; const int M=1e9+7; signed main(){ int n; scanf("%d",&n); vector<int> dp(n+1,0); dp[0]=1; for (int i=1;i<=n;i++){ for (int j=1;j<=6&&j<=i;j++){ (dp[i]+=dp[i-j])%=M; } } printf("%d\n",dp[n]); } ``` ---- ## [CSES --Maximum Subarray Sum](https://cses.fi/problemset/task/1643) 給出一個數列$a$,$-10^9\leq a_i\leq 10^9$,求數列的最長非空連續和 ---- - 設dp[i]為考慮i以前,選了i的最長連續和 - $dp[i]=max(a_i,a_i+dp[i-1])$ - $dp[0]=0$ - 答案是 $max \{dp[i]\ |\ i>0\}$ ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define ml(a, b) ((1ll * (a) * (b)) % M) #define tml(a, b) (a) = ((1ll * (a) * (b)) % M) #define ad(a, b) ((0ll + (a) + (b)) % M) #define tad(a, b) (a) = ((0ll + (a) + (b)) % M) #define mi(a, b) ((0ll + M + (a) - (b)) % M) #define tmi(a, b) (a) = ((0ll + M + (a) - (b)) % M) #define tmin(a, b) (a) = min((a), (b)) #define tmax(a, b) (a) = max((a), (b)) #define iter(a) (a).begin(), (a).end() #define riter(a) (a).rbegin(), (a).rend() #define init(a, b) memset((a), (b), sizeof(a)) #define cpy(a, b) memcpy((a), (b), sizeof(a)) #define uni(a) a.resize(unique(iter(a)) - a.begin()) #define pb emplace_back #define mpr make_pair #define ls(i) ((i) << 1) #define rs(i) ((i) << 1 | 1) #define INF 0x3f3f3f3f #define NIF 0xc0c0c0c0 #define eps 1e-9 #define F first #define S second #define AC cin.tie(0)->sync_with_stdio(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; typedef complex<double> cd; // const int M = 998244353; // random_device rm; // mt19937 rg(rm()); // default_random_engine rg(rm()); // uniform_int_distribution<int> rd(INT_MIN, INT_MAX); // uniform_real_distribution<double> rd(0, M_PI); void db() { cerr << "\n"; } template <class T, class... U> void db(T a, U... b) { cerr << a << " ", db(b...); } template <typename T> void rd(T &x) { x = 0; bool f = 0; char c = getchar(); while (!isdigit(c)) f ^= !(c ^ 45), c = getchar(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); f && (x = -x); } template <typename T> void prt(T x) { if (x < 0) putchar('-'), x = -x; if (x > 9) prt(x / 10); putchar((x % 10) ^ 48); } signed main() { llt n, cr = 0, mx = 0, x, b = -INF; rd(n); while (n--) { rd(x); tmax(b, x); cr += x; tmax(cr, 0ll); tmax(mx, cr); } if (b < 0) prt(b); else prt(mx); } ``` ---- ## 推薦提單: [CSES -- Dynamic Programming](https://cses.fi/problemset/) 基礎技巧,相對簡單 [Atcoder -- dp contest](https://atcoder.jp/contests/dp) 前面基礎技巧,後面有基礎優化 [Luogu -- DP](https://www.luogu.com.cn/training/1435) 涵蓋範圍超大,刷完就是dp神了 --- # 經典問題 ---- ## [UVA -- 10405](https://zerojudge.tw/ShowProblem?problemid=c001) 給出兩個字串$s,t$,求兩個字串的最長共同子序列長度,$1\leq|s|,|t|,\leq 2000$ 子序列:透過刪除任意數量元素(含0或全部),剩下來的序列(按原順序) ---- - dp[i][j]為s長度為i前綴與t長度j前綴的LCS - $s[i]=t[j]\Rightarrow dp[i][j]=dp[i-1][j-1]+1$ - $dp[i][j]=max(dp[i-1][j],dp[i][j-1])$ - $dp[i][0] = dp[0][j] = 0$ - 答案:$dp[|s|][|t|]$ ---- ## 滾動陣列 - 觀察到dp[i][?]僅會用到dp[i-1][?] - 迭代到dp[i][?]時dp[i-1][?]以前沒用 - 可以用兩個陣列交替 - dp[i][j]只會用到$dp[k][l],k\leq i\ \&\ l\leq j$ - 僅用到比自己小,先更新大再更新小 - 只需要一條陣列 ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define eps 1e-9 #define F first #define S second #define AC ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; string s1, s2; int ln1, ln2; int dp[2000]; int main() { AC; while (getline(cin, s1) && getline(cin, s2)) { ln1 = s1.length(); ln2 = s2.length(); memset(dp, 0, sizeof(dp)); for (int i = 0; i < ln1; i++) { for (int j = ln2; j > 0; j--) if (s1[i] == s2[j - 1]) dp[j] = dp[j - 1] + 1; for (int j = 1; j <= ln2; j++) dp[j] = max(dp[j - 1], dp[j]); } printf("%d\n", dp[ln2]); } } ``` ---- ## [Atecoder -- LCS](https://atcoder.jp/contests/dp/tasks/dp_f) 給出兩個字串$s,t$,求一組兩個字串的最長共同子序列,$1\leq|s|,|t|,\leq 2000$ ---- - 需要進行回朔 - 紀錄轉移點來源fr[i][j] - 就會形成一張圖,邊指向的轉移點 - 使用滾動陣列就很麻煩了 - [卡記憶體得回朔題](https://codeforces.com/problemset/problem/101/E) ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define eps 1e-9 #define F first #define S second #define AC ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; signed main() { AC; int ln1, ln2; string s1, s2; cin >> s1 >> s2; ln1 = s1.length(), ln2 = s2.length(); vector<int> dp(ln1 + 1, 0); vector<string> ds(ln1 + 1, ""); for (int i = 0; i < ln2; i++) { for (int j = ln1; j > 0; j--) { if (s1[j - 1] == s2[i]) { dp[j] = dp[j - 1] + 1; ds[j] = ds[j - 1]; ds[j].push_back(s2[i]); } } for (int j = 1; j <= ln1; j++) { if (dp[j] < dp[j - 1]) { dp[j] = dp[j - 1]; ds[j] = ds[j - 1]; } } } cout << ds[ln1] << '\n'; } ``` ---- ## [CSES --Increasing Subsequence](https://cses.fi/problemset/task/1145) 給出一個序列,求最長嚴格遞增子序列長度 ---- - 定義dp[i]為考慮i以前,選$a_i$的答案 - $dp[i]=max(1,max\{dp[j]\ |\ j<i\ \land\ a_j<a_i\})$ - $dp[0]=0,ans=max\{dp[i]\}$ - $O(n^2)$不會過,需要優化 ---- 解法一:二分搜壓轉移 - dp[i]定義長度為i的最小結尾 - 每加入一個數$a_i$,二分搜$max\{j\ |\ dp[j]<a_i\}$ - 更新$dp[j]=a_i$ - dp[0]=-INF - 加入$a_i$需要O(logn),總共O(nlogn)會過 ---- 程式碼: ```cpp! #include <bits/dtfc++.h> #define lowbit(x) ((x) & -(x)) #define INF 0x3f3f3f3f #define eps 1e-9 #define F first #define S second #define AC ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; int ar[200005]; signed main() { int n, x; memset(ar, INF, sizeof(ar)); scanf("%d", &n); while (n-- && scanf("%d", &x)) *lower_bound(ar, ar + 200005, x) = x; for (x = 0; ar[x] != INF; x++) ; printf("%d\n", x); } ``` ---- 解法二:資料結構優化 - 定義dp[i]為考慮i以前,選$a_i$的答案 - 線段樹或bit存a_i小於一定值的最大dp值 - 轉移時取dp[i]=線段樹$a_i$前綴+1 - 更新線段樹以dp[i]更新線段樹$a_i$位置 - 值域太大須離散化 --- # 背包問題 ---- ## [Atcoder -- D - Knapsack 1](https://atcoder.jp/contests/dp/tasks/dp_d) 有$N$種物品,每個物品有重量$w_i$與價值$v_i$,對於每個物品可以選或是不選,求選的物品重量不超過$W$的情況,價值總和最大是多少? $1\leq N \leq 100,1\leq w_i\leq W\leq 10^5,1\leq v_i\leq10^9$ - 又被稱為01背包問題,代表選或是不選 ---- - 定義dp[i][j]為前 i 個物品總重量$\leq j$ 的最大價值 - $dp[i][j] = max(dp[i-1][j],dp[i-1][j-w_i]+v_i)$ - $dp[0][j]=0$ - 總複雜度O(NW),可以壓成一條陣列 ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define eps 1e-9 #define F first #define S second #define AC ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; signed main() { int n, lmt, w, v; scanf("%d%d", &n, &lmt); vector<llt> dp(lmt + 1, 0); while (n-- && scanf("%d%d", &w, &v)) { for (int i = lmt; i >= w; i--) dp[i] = max(dp[i], dp[i - w] + v); } printf("%lld\n", dp[lmt]); } ``` ---- ## [Atcoder -- D - Knapsack 2](https://atcoder.jp/contests/dp/tasks/dp_e) 有$N$種物品,每個物品有重量$w_i$與價值$v_i$,對於每個物品可以選或是不選,求選的物品重量不超過$W$的情況,價值總和最大是多少? $1\leq N \leq 100,1\leq w_i\leq W\leq 10^9,1\leq v_i\leq 10^3$ - 重量很大,價值很小 ---- - 定義dp[i][j]為前 i 個物品總價值$\geq j$ 的最小重量 - $dp[i][j] = min(dp[i-1][j],dp[i-1][j-v_i]+w_i)$ - $dp[0][0]=0,dp[0][j>0]=INF$ - 總複雜度$O(N\sum{v_i})$,可以壓成一條陣列 - 答案取$max\{i\ |\ dp[N][i]\leq W\}$ ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define eps 1e-9 #define N 100005 #define F first #define S second #define AC ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; signed main() { int n, lmt, w, v; scanf("%d%d", &n, &lmt); vector<int> dp(N, lmt + 1); dp[0] = 0; while (n-- && scanf("%d%d", &w, &v)) { for (int i = N - 1; i >= v; i--) dp[i] = min(dp[i], dp[i - v] + w); } v = N - 1; while (dp[v] > lmt) v--; printf("%d\n", v); } ``` ---- ## [luogu -- 疯狂的采药](https://www.luogu.com.cn/problem/P1616) 有$N$種物品,每個物品有重量$w_i$與價值$v_i$,對於每個物品可以選任意數量,求選的物品重量不超過$W$的情況,價值總和最大是多少? $1\leq N \leq 100,1\leq w_i\leq W\leq 10^5,1\leq v_i\leq10^9$ - 無限背包問題,每種物品有無限個 ---- - 定義dp[i][j]為前 i 個物品總重量$\leq j$ 的最大價值 - $dp[i][j] = max(dp[i-1][j],dp[i][j-w_i]+v_i)$ - $dp[0][j]=0$ - 總複雜度O(NW),可以壓成一條陣列 - 因為重複轉移,所以迴圈順序要改 ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define eps 1e-9 #define F first #define S second #define AC ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; int main() { int n, m, w, v; cin >> n >> m; vector<llt> dp(n + 1, 0); while (m-- && cin >> w >> v) { for (int i = w; i <= n; i++) dp[i] = max(dp[i], dp[i - w] + v); } cout << dp[n] << '\n'; } ``` ---- ## [luogu -- 宝物筛选](https://www.luogu.com.cn/problem/P1776) 有$N$種物品,每個物品有重量$w_i$價值$v_i$與數量$c_i$,對於第i個物品可以選任意$\leq c_i$ 數量,求選的物品重量不超過$W$的情況,價值總和最大是多少? $1\leq N \leq 100,1\leq w_i\leq W\leq 10^4,1\leq v_i\leq10^9,c_i\leq 10^5$ - 有限背包問題,每種物品有數量限制,不一定是1 ---- 解法一:二的冪次 - 物品一定數量打包一起,看成一個 - t個物品打包,$w'=w_i\times t,v'=v_i\times t$ - 將物品數量拆成二的冪次 - $c_i=2^{\log_{2}c_i}-1+k$ - 後面一個物品,前面$\log_{2}c_i$個物品 - 拆好物品用01背包問題解 - 複雜度 $O(W\sum\log_2 c_i )$ ---- 程式碼: ```cpp! #include <bits/stdc++.h> using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; signed main() { int n, m, c; llt v, w; scanf("%d%d", &n, &m); vector<llt> dp(m + 1, 0); while (n-- && scanf("%lld%lld%d", &v, &w, &c)) { for (llt cr = 1; cr <= c; cr <<= 1) { c -= cr; llt cv = v * cr, cw = w * cr; for (int i = m; i >= cw; i--) dp[i] = max(dp[i], dp[i - cw] + cv); } if (c) { llt cv = v * c, cw = w * c; for (int i = m; i >= cw; i--) dp[i] = max(dp[i], dp[i - cw] + cv); } } printf("%lld\n", dp[m]); } ``` ---- 解法二:單調隊列優化 ![](https://i.imgur.com/RrsWSmx.png) ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define pb emplace_back #define mpr make_pair #define ls(i) ((i) << 1) #define rs(i) ((i) << 1 | 1) #define INF 0x3f3f3f3f #define NIF 0xc0c0c0c0 #define eps 1e-9 #define F first #define S second #define AC cin.tie(0)->sync_with_stdio(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; typedef complex<double> cd; signed main() { int n, m, c; llt v, w; scanf("%d%d", &n, &m); vector<llt> dp(m + 1, 0), tp(m + 1, 0); while (n-- && scanf("%lld%lld%d", &v, &w, &c)) { deque<llt> q[w]; for (int i = 0; i <= m; i++) { const int cr = i % w; if (!q[cr].empty() && (i - q[cr].front()) / w > c) q[cr].pop_front(); //數量超過pop while (!q[cr].empty() && dp[i] >= dp[q[cr].back()] + (i - q[cr].back()) / w * v) q[cr].pop_back(); //當前更優 q[cr].push_back(i); tp[i] = dp[q[cr].front()] + (i - q[cr].front()) / w * v; } tp.swap(dp); } printf("%lld\n", dp[m]); } ``` ---- [CSES -- Coin Combinations II](https://cses.fi/problemset/task/1636) 有$n$種硬幣,幣值為$w_i$,求有多少種方法湊出$W$塊錢,不同順序算一同種 $1\leq n\leq 100,1\leq w_i\leq W\leq10^5$ - 類似背包問題,幣值看成重量,求達成特定重量方式 ---- - 定義dp[i][j]為使用前i種硬幣湊j的方法數 - $dp[i][j]=dp[i-1][j]+dp[i][j-w_i]$ - dp[0][0]=1 - 類似無限背包,max改成加法 ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define INF 0x3f3f3f3f using namespace std; const int M=1e9+7; signed main(){ int n,x,m; scanf("%d%d",&m,&n); vector<int> dp(n+1,0); dp[0]=1; while (m--&&scanf("%d",&x)){ for (int i=x;i<=n;i++) (dp[i]+=dp[i-x])%=M; } printf("%d\n",dp[n]); } ``` ---- [CSES -- Coin Combinations I](https://cses.fi/problemset/task/1635) 有$n$種硬幣,幣值為$w_i$,求有多少種方法湊出$W$塊錢,不同順序算不同種 $1\leq n\leq 100,1\leq w_i\leq W\leq10^5$ - 怎沒算不同排列,排列組合有用? ---- - dp[i]定義湊出i塊的方法 - $dp[i]=\sum_{k\in w_i}{dp[i-k]}$ - dp[0]=1 - 錢變成轉移方式而不是物品 - 有時候轉移方式會根據輸入不同 ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define INF 0x3f3f3f3f using namespace std; const int M=1e9+7; signed main(){ int n,x,m; scanf("%d%d",&m,&n); vector<int> dp(n+1,0),ar(m); dp[0]=1; for (int i=0;i<m;i++) scanf("%d",&ar[i]); for (int i=1;i<=n;i++){ for (int j:ar){ if (i>=j) (dp[i]+=dp[i-j])%=M; } } printf("%d\n",dp[n]); } ``` --- # 區間dp ---- ## [Atcoder -- Slimes](https://atcoder.jp/contests/dp/tasks/dp_n) 有一個長度為 $n$ 的序列,要選兩個相鄰的數$a_i,a_{i+1}$將其合併,花費是$a_i+a_{i+1}$,原本的位置留下$a_i+a_{i+1}$,請問最少要用多少花費才能將序列合併到長度為1? $1\leq n\leq 400$ ---- - dp[l][r]為合併區間[l,r]最小花費 - $dp[l][r]=\sum_{k=l}^{r}{a_k}+min\{dp[l][m]+dp[m+1][r]\ |\ m\in[l,r)\}$ - $dp[k][k]=0,ans=dp[1][n]$ - 轉移順序,長度小到長度大 ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define eps 1e-9 #define F first #define S second #define AC ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; signed main() { int n; scanf("%d", &n); vector<int> ar(n); vector<vector<llt>> dp(n, vector<llt>(n, LLONG_MAX)); for (int i = 0; i < n; i++) scanf("%d", &ar[i]), dp[i][i] = 0; for (int k = 1; k < n; k++) { llt sum = 0; for (int i = 0; i < k; i++) sum += ar[i]; for (int i = k; i < n; i++) { sum += ar[i]; for (int j = i - k; j < i; j++) dp[i - k][i] = min(dp[i - k][j] + dp[j + 1][i], dp[i - k][i]); dp[i - k][i] += sum, sum -= ar[i - k]; } } printf("%lld\n", dp[0][n - 1]); } ``` ---- ## [NOI2007 -- 矩阵取数游戏](https://www.luogu.com.cn/problem/P1005) 給定一個 $n$ 行 $m$ 列的矩陣,每次從每一行頭或尾取出一個數字,一次共 $n$ 個 $m$ 次取完,設第 $i$ 次取的元素和為 $s_i$ ,可獲得 $s_i\times 2^i$ 分,求最大得分 $1\leq n,m\leq 80$ ---- - 每一行是獨立的,考慮一行做 $n$ 次 - 設 $dp[l][r]$ 為序列 $a_l...a_r$ 的答案 - $dp[l][r]=max(dp[l+1][r]+a_l\times 2^k,$ $dp[l][r-1]+a_r\times 2^k),k=m-r+l$ - $dp[i][i]=a_i\times 2^m$ - 記得要用int128或是自己寫大數運算 ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define ml(a, b) ((1ll * (a) * (b)) % M) #define tml(a, b) (a) = ((1ll * (a) * (b)) % M) #define ad(a, b) ((0ll + (a) + (b)) % M) #define tad(a, b) (a) = ((0ll + (a) + (b)) % M) #define mi(a, b) ((0ll + M + (a) - (b)) % M) #define tmi(a, b) (a) = ((0ll + M + (a) - (b)) % M) #define tmin(a, b) (a) = min((a), (b)) #define tmax(a, b) (a) = max((a), (b)) #define iter(a) (a).begin(), (a).end() #define riter(a) (a).rbegin(), (a).rend() #define init(a, b) memset((a), (b), sizeof(a)) #define cpy(a, b) memcpy((a), (b), sizeof(a)) #define uni(a) a.resize(unique(iter(a)) - a.begin()) #define pb emplace_back #define mpr make_pair #define ls(i) ((i) << 1) #define rs(i) ((i) << 1 | 1) #define INF 0x3f3f3f3f #define NIF 0xc0c0c0c0 #define eps 1e-9 #define F first #define S second #define AC cin.tie(0)->sync_with_stdio(0) using namespace std; typedef long long llt; typedef __int128_t lll; typedef pair<int, int> pii; void db() { cerr << "\n"; } template <class T, class... U> void db(T a, U... b) { cerr << a << " ", db(b...); } template <typename T> void rd(T &x) { x = 0; bool f = 0; char c = getchar(); while (!isdigit(c)) f ^= !(c ^ 45), c = getchar(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = getchar(); f && (x = -x); } template <typename T> void prt(T x) { if (x < 0) putchar('-'), x = -x; if (x > 9) prt(x / 10); putchar((x % 10) ^ 48); } const int N = 90; lll dp[N][N], pl[N]; int ar[N]; signed main() { pl[0] = 1; for (int i = 1; i < N; i++) pl[i] = pl[i - 1] << 1; int n, m; rd(n), rd(m); lll ans = 0; while (n--) { for (int i = 0; i < m; i++) rd(ar[i]), dp[i][i] = pl[m] * ar[i]; for (int l = 1; l < m; l++) for (int r = l; r < m; r++) dp[r - l][r] = max(dp[r - l + 1][r] + ar[r - l] * pl[m - l], dp[r - l][r - 1] + ar[r] * pl[m - l]); ans += dp[0][m - 1]; } prt(ans), putchar('\n'); } ``` ---- ## 環狀dp 陣列是環狀,頭尾相接,所以會有一些變化 1. 分case - 通常是區間dp會這樣處理 - 分成環狀不影響和有跨越頭尾的 - 尾端接開頭可以看成切區間 2. 陣列多複製一份 - 更常會這樣處理,尾端接第二份開始 - 可以正常dp,但要小心不要自己轉移自己 - 可以控制轉移距離<陣列長度 ---- ## [leetcode -- Maximum Sum Circular Subarray](https://leetcode.com/problems/maximum-sum-circular-subarray/) 給定一個序列,求環狀最大連續和 ---- - 採用方法一,分成一般區間和跨越頭尾 - 一般區間用原本的greedy或dp解 - 特殊區間用總和扣區間連續最小和 - 兩個結果取max就是答案 ---- 程式碼: ```cpp! class Solution { public: int maxSubarraySumCircular(vector<int>& ar) { int sum = 0, mn = INT_MAX, mx = INT_MIN, sx = 0, sn = 0; for (auto i : ar) { sn=min(sn+i,i); sx=max(sx+i,i); mx=max(mx,sx); mn=min(mn,sn); sum+=i; } return sum-mn==0?mx:max(sum-mn,mx); } }; ``` ---- ## [NOI1995 -- 石子合并](https://www.luogu.com.cn/problem/P1880) 有 $n$ 堆石頭排成圓形,第 $i$ 堆石頭有 $a_i$ 顆,每次要將兩堆石頭合併,得分是合併後該堆石頭數量,求合併成一堆的最大和最小得分 ---- - 採用方法二複製一份陣列,dp式與之前一樣 - 答案取$max\{dp[l][l+n-1]\ |\ l\in[1,n]\}$ - 有一個間隔不會被合併,可以斷環為鍊 - 可以把狀態改成左界位置與長度 ---- 程式碼: ```cpp! #include <bits/stdc++.h> using namespace std; typedef long long llt; void db() { cerr << "\n"; } template <class T, class... U> void db(T a, U... b) { cerr << a << " ", db(b...); } inline char gc() { const static int SZ = 1 << 16; static char buf[SZ], *p1, *p2; if (p1 == p2 && (p2 = buf + fread(p1 = buf, 1, SZ, stdin), p1 == p2)) return -1; return *p1++; } void rd() {} template <typename T, typename... U> void rd(T &x, U &...y) { x = 0; bool f = 0; char c = gc(); while (!isdigit(c)) f ^= !(c ^ 45), c = gc(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = gc(); f && (x = -x), rd(y...); } template <typename T> void prt(T x) { if (x < 0) putchar('-'), x = -x; if (x > 9) prt(x / 10); putchar((x % 10) ^ 48); } const int N = 205; int ar[N], mx[N][N], mn[N][N]; signed main() { int n; rd(n); for (int i = 0; i < n; i++) rd(ar[i]), ar[i + n] = ar[i]; int n2 = n << 1; for (int r = 0; r < n2; r++) { int sum = ar[r]; for (int t = 1; t < n; t++) { int l = r - t; if (l < 0) break; mx[l][r] = ~INF, mn[l][r] = INF; sum += ar[l]; for (int k = l; k < r; k++) tmax(mx[l][r], mx[l][k] + mx[k + 1][r]), tmin(mn[l][r], mn[l][k] + mn[k + 1][r]); mx[l][r] += sum, mn[l][r] += sum; } } int anx = ~INF, ann = INF; for (int i = 0; i < n; i++) tmax(anx, mx[i][i + n - 1]), tmin(ann, mn[i][i + n - 1]); prt(ann), putchar('\n'); prt(anx), putchar('\n'); } ``` --- # 樹型dp ---- ## 暖身題 有一棵 $n$ 個節點的樹,每個點上有點權 $v_i$ ,接下來有 $q$ 筆詢問,每筆詢問有兩種類型 - 將節點 i 的權重改為 k - 詢問與節點 i 相鄰節點的點權和 $1\leq n,q\leq 10^5$ ---- - 每次修改更新鄰居? - 每次詢問加總鄰居? - 一個點可以連O(n)條邊 - 總複雜度 O(nq) $\Rightarrow$ TLE ---- - 每次修改時更新父節點 - 每次詢問時小孩都已經算好的 - 再加上父節點的權值就是答案 - 複雜度 O(n+q) #### 結論:善用父節點只有一個,均攤掉複雜度 ---- ## [树的深度及子树大小](https://vjudge.net/problem/51Nod-2627) 給出一棵 $n$ 個節點的樹,節點1是根節點,求每個的深度和子樹大小 $1\leq n\leq 10^5$ ---- - 深度為父節點深度+1 - 子樹大小 $sz[u]=1+\sum_{(u,v)\in E}{sz[v]}$ - 在dfs子節點時,先更新子節點深度 - 拜訪完子節點更新父節點大小 ---- ## [Luogu -- 沒有上司的舞會](https://www.luogu.com.cn/problem/P1352) 給出一棵 $n$ 個節點的有根樹,節點有點權 $v_i$ ,我們想選一些節點,但是若選了節點 $u$ ,其父節點就不能選,求能選出的最大點權和 $1\leq n\leq 10^5$ ---- - 定義 dp[0][u]為不選 u 其子樹點權最大和 - 定義 dp[1][u]為選 u 後其子樹點權最大和 - $dp[1][u] = v_i+\sum_{(u,v)\in E}{dp[0][v]}$ - $dp[0][u] = \sum_{(u,v)\in E}{max(dp[0][v],dp[1][v])}$ ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define ml(a, b) ((1ll * (a) * (b)) % M) #define tml(a, b) (a) = ((1ll * (a) * (b)) % M) #define ad(a, b) ((0ll + (a) + (b)) % M) #define tad(a, b) (a) = ((0ll + (a) + (b)) % M) #define mi(a, b) ((0ll + M + (a) - (b)) % M) #define tmi(a, b) (a) = ((0ll + M + (a) - (b)) % M) #define tmin(a, b) (a) = min((a), (b)) #define tmax(a, b) (a) = max((a), (b)) #define iter(a) (a).begin(), (a).end() #define riter(a) (a).rbegin(), (a).rend() #define init(a, b) memset((a), (b), sizeof(a)) #define cpy(a, b) memcpy((a), (b), sizeof(a)) #define uni(a) a.resize(unique(iter(a)) - a.begin()) #define pb emplace_back #define mpr make_pair #define ls(i) ((i) << 1) #define rs(i) ((i) << 1 | 1) #define INF 0x3f3f3f3f #define NIF 0xc0c0c0c0 #define eps 1e-9 #define F first #define S second #define AC cin.tie(0)->sync_with_stdio(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; typedef complex<double> cd; const int N = 6005; vector<int> eg[N]; int d1[N], d2[N], ar[N]; bool vd[N]; void dfs(int u) { d1[u] = ar[u], d2[u] = 0; for (const int &v : eg[u]) { dfs(v); d1[u] += d2[v]; d2[u] += max(d2[v], d1[v]); } } signed main() { int n, x, y, ans; scanf("%d", &n); for (int i = 1; i <= n; i++) scanf("%d", &ar[i]); for (int i = 1; i < n; i++) { scanf("%d%d", &x, &y); eg[y].pb(x), vd[x] = 1; } for (int i = 1; i <= n; i++) { if (vd[i]) continue; dfs(i), ans = max(d1[i], d2[i]); } printf("%d\n", ans); } ``` ---- ## [Luogu -- 选课](https://www.luogu.com.cn/problem/P2014) 給出一棵 $n$ 個節點樹,根節點是0,每個節點有點權 $v_i$ ,我們想選 $m$ 個節點,使得點權和最大,但是若選的一個節點,其父節點也必須選,也就是所有祖先都要選,求最大的點權和是多少? - 樹背包問題,北市賽有出過 ---- - 定義dp[i][j]為節點i子樹選了j個點的最大和 - 子樹要選代表節點i也要選 - dp[i][j]會依序被子節點更新 - 被更新後代表選了一部分子樹,定義稍稍改變 - $dp[i][j]=max(dp[i][j],dp[v][k]+dp[i][j-k]),(i,v)\in E\land j>k$ - 有 $n^2$個子節點要更新,每次更新 $n$,複雜度 $O(n^3)$ - 觀察到子樹大小有限,會轉移很多沒用的點 - 再dp子樹大小,只轉移有用的點,可以證明複雜度是 $O(n^2)$ ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define ml(a, b) ((1ll * (a) * (b)) % M) #define tml(a, b) (a) = ((1ll * (a) * (b)) % M) #define ad(a, b) ((0ll + (a) + (b)) % M) #define tad(a, b) (a) = ((0ll + (a) + (b)) % M) #define mi(a, b) ((0ll + M + (a) - (b)) % M) #define tmi(a, b) (a) = ((0ll + M + (a) - (b)) % M) #define tmin(a, b) (a) = min((a), (b)) #define tmax(a, b) (a) = max((a), (b)) #define iter(a) (a).begin(), (a).end() #define riter(a) (a).rbegin(), (a).rend() #define init(a, b) memset((a), (b), sizeof(a)) #define cpy(a, b) memcpy((a), (b), sizeof(a)) #define uni(a) a.resize(unique(iter(a)) - a.begin()) #define pb emplace_back #define mpr make_pair #define ls(i) ((i) << 1) #define rs(i) ((i) << 1 | 1) #define INF 0x3f3f3f3f #define NIF 0xc0c0c0c0 #define eps 1e-9 #define F first #define S second #define AC cin.tie(0)->sync_with_stdio(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; typedef complex<double> cd; const int N = 305; vector<int> eg[N]; int dp[N][N], fa[N], ar[N], sz[N]; void dfs(int u) { dp[u][1] = ar[u], sz[u] = 1; for (const int &v : eg[u]) { dfs(v), sz[u] += sz[v]; for (int i = sz[u]; i > 0; i--) for (int j = min(sz[v], i - 1); j > 0; j--) tmax(dp[u][i], dp[u][i - j] + dp[v][j]); } } signed main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d%d", &fa[i], &ar[i]); eg[fa[i]].pb(i); } dfs(0); printf("%d\n", dp[0][m + 1]); } ``` ---- ## 換根dp - 我們發現根節點固定以後性質很好 - dp根節點很容易,但是根不固定 - 將根題換成相鄰的點,dp值只有更換的兩點變化 - 根據dfs序更換根節點,算出需要的答案 ---- ## [Luogu -- STA-Station](https://www.luogu.com.cn/problem/P3478) 給定一棵 $n$ 個節點的樹,求以哪個節點為根,所有節點的深度總和最大,若有多個答案,輸出任意一個即可 - 裸題,直接告訴你要選不同根 ---- - 固定一個根,dp可以很好 O(n) 算出深度總和 - 定一個根,定義dp[u]為 u 節點子樹的深度和 - $dp[u] = \sum_{(u,v)\in E}{dp[v]+sz[v]}$ - 我們可以 O(1) 將根節點從 u 換成小孩 v - $dp[u]=dp[u]-dp[v]-sz[v],sz[u]=sz[u]-sz[v]$ - $dp[v]=dp[v]+dp[u]+sz[u],sz[v]=sz[v]+sz[u]$ - 記得走完一個子節點要還原 ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define ml(a, b) ((1ll * (a) * (b)) % M) #define tml(a, b) (a) = ((1ll * (a) * (b)) % M) #define ad(a, b) ((0ll + (a) + (b)) % M) #define tad(a, b) (a) = ((0ll + (a) + (b)) % M) #define mi(a, b) ((0ll + M + (a) - (b)) % M) #define tmi(a, b) (a) = ((0ll + M + (a) - (b)) % M) #define tmin(a, b) (a) = min((a), (b)) #define tmax(a, b) (a) = max((a), (b)) #define iter(a) (a).begin(), (a).end() #define riter(a) (a).rbegin(), (a).rend() #define init(a, b) memset((a), (b), sizeof(a)) #define cpy(a, b) memcpy((a), (b), sizeof(a)) #define uni(a) a.resize(unique(iter(a)) - a.begin()) #define pb emplace_back #define mpr make_pair #define ls(i) ((i) << 1) #define rs(i) ((i) << 1 | 1) #define INF 0x3f3f3f3f #define NIF 0xc0c0c0c0 #define eps 1e-9 #define F first #define S second #define AC cin.tie(0)->sync_with_stdio(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; typedef complex<double> cd; const int N = 1e6 + 5; vector<int> eg[N]; llt dp[N], sz[N], ans; int p; void dfs(int u, int fa) { dp[u] = 0, sz[u] = 1; for (const int &v : eg[u]) { if (v == fa) continue; dfs(v, u); sz[u] += sz[v], dp[u] += dp[v] + sz[v]; } } void slv(int u, int fa) { if (dp[u] > ans) ans = dp[u], p = u; for (const int &v : eg[u]) { if (v == fa) continue; dp[u] -= dp[v] + sz[v], sz[u] -= sz[v]; dp[v] += dp[u] + sz[u], sz[v] += sz[u]; slv(v, u); dp[v] -= dp[u] + sz[u], sz[v] -= sz[u]; dp[u] += dp[v] + sz[v], sz[u] += sz[v]; } } signed main() { int n, x, y; scanf("%d", &n); for (int i = 1; i < n; i++) { scanf("%d%d", &x, &y); eg[x].pb(y); eg[y].pb(x); } dfs(1, -1), slv(1, -1); printf("%d\n", p); } ``` --- # 前綴優化 - 單純計算前後綴和或極值 - 快速計算區間和,可以乘倍數($ps[i]=\sum_{k=1}^i{a_k\times k}$) - 配合差分運用,令$a_i=\sum_{k=1}^i{f_k}$ ---- ## [TIOJ -- 最大不連續和問題](https://tioj.ck.tp.edu.tw/problems/2048) 給定一個序列,求其最大不連續和,不連續指不完全連續,不一定要完全分開 - 基礎運用 ---- - 計算前綴後綴最大和(可不連續) - 枚舉空格,一個空格答案是前綴+後綴 - 所有答案取max ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define eps 1e-9 #define F first #define S second #define AC ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; signed main() { int n; llt ans = LLONG_MIN; scanf("%d", &n); vector<llt> vr(n, 0); for (int i = 0; i < n; i++) scanf("%lld", &vr[i]); vector<llt> fr(vr.begin(), vr.end()); vector<llt> br(vr.begin(), vr.end()); for (int i = 1; i < n; i++) fr[i] = max(fr[i], fr[i - 1] + max(0ll, fr[i])); for (int i = n - 2; i >= 0; i--) br[i] = max(br[i], br[i + 1] + max(0ll, br[i])); for (int i = 1; i < n - 1; i++) ans = max(ans, fr[i - 1] + br[i + 1]); printf("%lld\n", ans); } ``` ---- ## [BJOI2018 -- 求和](https://www.luogu.com.cn/problem/P4427) 給定一棵跟節點為 $1$ 的樹,接下來有 $q$ 比詢問,每次求 $u$ 到 $v$ 路徑上節點深度的 $k$ 次方和,深度定義是到跟節點路徑上的邊數 $1\leq n,q\leq3\times10^5$ $1\leq k\leq50$ ---- - 用樹上前綴和來解 - dp[i][x] 為根節點到 $x$ 深度的 $k$ 次方和 - 將路徑拆分成到根節點的路徑 - $ans=dp[k][u]+dp[k][v]-$ $(dp[k][lca(u,v)]\times 2+dep(lca(u,v))^k)$ - 差分的問題同理 ---- 程式碼: ```cpp! // #pragma GCC optimize("Ofast,no-stack-protector") // #pragma GCC optimize("O3,unroll-loops") // #pragma GCC target("avx,avx2,bmi,bmi2,lzcnt,popcnt") #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define ml(a, b) ((1ll * (a) * (b)) % M) #define tml(a, b) (a) = ((1ll * (a) * (b)) % M) #define ad(a, b) ((0ll + (a) + (b)) % M) #define tad(a, b) (a) = ((0ll + (a) + (b)) % M) #define mi(a, b) ((0ll + M + (a) - (b)) % M) #define tmi(a, b) (a) = ((0ll + M + (a) - (b)) % M) #define tmin(a, b) (a) = min((a), (b)) #define tmax(a, b) (a) = max((a), (b)) #define iter(a) (a).begin(), (a).end() #define riter(a) (a).rbegin(), (a).rend() #define inin(a, b) memset((a), (b), sizeof(a)) #define cpy(a, b) memcpy((a), (b), sizeof(a)) #define uni(a) a.resize(unique(iter(a)) - a.begin()) #define size(x) (int)x.size() #define pb emplace_back #define mpr make_pair #define ls(i) ((i) << 1) #define rs(i) ((i) << 1 | 1) #define INF 0x3f3f3f3f #define NIF 0xc0c0c0c0 #define eps 1e-9 #define F first #define S second #define AC cin.tie(0)->sync_with_stdio(0) using namespace std; typedef long long llt; typedef unsigned long long ull; typedef __int128_t lll; typedef pair<int, int> pii; const int M = 998244353; void db() { cerr << "\n"; } template <class T, class... U> void db(T a, U... b) { cerr << a << " ", db(b...); } inline char gc() { const static int SZ = 1 << 16; static char buf[SZ], *p1, *p2; if (p1 == p2 && (p2 = buf + fread(p1 = buf, 1, SZ, stdin), p1 == p2)) return -1; return *p1++; } void rd() {} template <typename T, typename... U> void rd(T &x, U &...y) { x = 0; bool f = 0; char c = gc(); while (!isdigit(c)) f ^= !(c ^ 45), c = gc(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = gc(); f && (x = -x), rd(y...); } template <typename T> void prt(T x) { if (x < 0) putchar('-'), x = -x; if (x > 9) prt(x / 10); putchar((x % 10) ^ 48); } const int N = 3e5 + 5; int dp[50][N], in[N], ou[N], ct, ac[20][N]; vector<int> eg[N]; void dfs(int u, int fa, int d) { in[u] = ++ct; for (int v : eg[u]) { if (v == fa) continue; dp[0][v] = d; for (int i = 1; i < 50; i++) dp[i][v] = ml(dp[i - 1][v], d); for (int i = 0; i < 50; i++) tad(dp[i][v], dp[i][u]); ac[0][v] = u; for (int i = 1; i < 20; i++) ac[i][v] = ac[i - 1][ac[i - 1][v]]; dfs(v, u, d + 1); } ou[u] = ct; } inline bool isa(int a, int p) { return in[a] <= in[p] && ou[p] <= ou[a]; } inline int lca(int u, int v) { if (isa(u, v)) return u; for (int i = 19; i >= 0; i--) if (ac[i][u] && !isa(ac[i][u], v)) u = ac[i][u]; return ac[0][u]; } signed main() { int n, q, x, y, k; rd(n); for (int i = 1; i < n; i++) { rd(x, y); eg[x].pb(y); eg[y].pb(x); } dfs(1, -1, 1); rd(q); while (q--) { rd(x, y, k); int p = lca(x, y); prt(mi(ad(dp[k - 1][x], dp[k - 1][y]), ad(dp[k - 1][p], dp[k - 1][ac[0][p]]))); putchar('\n'); } } ``` --- # 位元dp - 也可以叫狀壓dp,用來壓縮狀態 - 把狀態用01表示(有或無) - 數字二進位即可以代表狀態 - 狀態轉換成數字,就可以用陣列存資訊了 ---- 假設當前狀態為t,可以進行以下操作 - 判斷第k位是不是1 $\Rightarrow((t>>k)\&1)$ - 與狀態s取聯集 $\Rightarrow(s\ |\ t)$ - 與狀態s取聯集 $\Rightarrow(s\ \&\ t)$ - 與狀態s取差對稱集 $\Rightarrow(s\ \oplus\ t)$ ---- ## [ZJ -- 旅行銷售員問題](https://zerojudge.tw/ShowProblem?problemid=c239) 給定一個 $n\leq16$ 點的圖(以鄰接矩陣),求從任意一點開始,經過所有其他點後,回到原本的點,最長路徑除以最短路徑是多少? - 最大最小本質一樣,差在取max取min,做兩次就可以 ---- - 狀態定義,二進位是1,代表該點走過 - 設dp[i][s]為當前 i 點走過 s 最短路徑 - $dp[i][s] = min\{dp[j][s\oplus 2^j]+ar[j][i]\ |\ j\&s\}$ - $dp[j][s\ |\ 2^j] = max(dp[j][s\ |\ 2^j],\ dp[i][s]+ar[i][j])$ - 從誰開始都一樣,用1,答案是$dp[1][2^n-1]$ ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define ml(a, b) ((1ll * (a) * (b)) % M) #define tml(a, b) (a) = ((1ll * (a) * (b)) % M) #define ad(a, b) ((0ll + (a) + (b)) % M) #define tad(a, b) (a) = ((0ll + (a) + (b)) % M) #define mi(a, b) ((0ll + M + (a) - (b)) % M) #define tmi(a, b) (a) = ((0ll + M + (a) - (b)) % M) #define tmin(a, b) (a) = min((a), (b)) #define tmax(a, b) (a) = max((a), (b)) #define iter(a) (a).begin(), (a).end() #define riter(a) (a).rbegin(), (a).rend() #define inin(a, b) memset((a), (b), sizeof(a)) #define cpy(a, b) memcpy((a), (b), sizeof(a)) #define uni(a) a.resize(unique(iter(a)) - a.begin()) #define size(x) (int)x.size() #define pb emplace_back #define mpr make_pair #define ls(i) ((i) << 1) #define rs(i) ((i) << 1 | 1) #define INF 0x3f3f3f3f #define NIF 0xc0c0c0c0 #define eps 1e-9 #define F first #define S second #define AC cin.tie(0)->sync_with_stdio(0) using namespace std; typedef long long llt; typedef unsigned long long ull; typedef __int128_t lll; typedef pair<int, int> pii; const int N = 15; int mx[N][1 << N], mn[N][1 << N], gd[N][N]; signed main() { int n; while (~scanf("%d", &n)) { for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) scanf("%d", &gd[i][j]), gd[j][i] = gd[i][j]; inin(mn, INF), inin(mx, ~INF), mx[0][1] = mn[0][1] = 0; for (int mk = 0; mk < 1 << n; mk++) { for (int i = 0; i < n; i++) { if (~(mk >> i) & 1) continue; for (int j = 0; j < n; j++) if (~(mk >> j) & 1) tmax(mx[j][mk ^ (1 << j)], mx[i][mk] + gd[i][j]), tmin(mn[j][mk ^ (1 << j)], mn[i][mk] + gd[i][j]); } } int anx = ~INF, ann = INF, msk = (1 << n) - 1; for (int i = 0; i < n; i++) tmax(anx, gd[0][i] + mx[i][msk]), tmin(ann, gd[0][i] + mn[i][msk]); int g = __gcd(anx, ann); anx /= g, ann /= g; if (ann == 1) printf("%d\n", anx); else printf("%d/%d\n", anx, ann); } } ``` ---- ## [UVA -- Mega Man's Mission](https://onlinejudge.org/index.php?option=onlinejudge&Itemid=8&page=show_problem&problem=2895) 有 $n \leq16$ 隻怪物,一開始玩家有特定武器,可以擊殺某些怪物,擊殺怪物會掉一個武器,可以擊殺令一些怪物,每個武器可以無限制重複使用,請問有多少種順序殺死所有怪物? ---- - 武器狀態第i是1代表可以擊殺怪物i - 通關狀態第i位是1代表怪物i已死 - 對於一個通關狀態,武器狀態是固定 - 通關獲得武器與原始武器聯集 - 可用武器代表可以轉移出去的點 - dp[i]代表i狀態的方法數 - $dp[i\oplus 2^j]=dp[i\oplus 2^j]+dp[i],j\in i$ 狀態的武器 ---- 程式碼: ```cpp! #include <cstdio> #include <algorithm> using namespace std; int wp[65536], n; long long s[65536] = {1}; bool vd[65536] = {1}; int gw() { int sum = 0; long long x; scanf("%lld", &x); for (int i = 0; i < n; i++) { sum <<= 1; sum += x & 1; x /= 10; } return sum; } int main() { int t, x, lm; scanf("%d", &t); for (int ti = 1; ti <= t; ti++) { scanf("%d", &n); lm = 1 << n; fill(wp, wp + lm, gw()); fill(s + 1, s + lm, 0); fill(vd + 1, vd + lm, 0); for (int i = 0; i < n; i++) { x = gw(); for (int j = 1; j < lm; j++) if ((j >> i) & 1) wp[j] |= x; } for (int i = 0; i < lm; i++) for (int j = 1; j + i < lm; j <<= 1) if (!(i & j) && wp[i] & j) s[i + j] += s[i]; printf("Case %d: %lld\n", ti, s[lm - 1]); } } ``` ---- ## [POI2004 -- PRZ](https://www.luogu.com.cn/problem/P5911) 有 $n$ 個人要過橋,每個人有重量 $w_i$ 與過橋時間 $t_i$ ,橋樑有限重 $W$ ,你可以將人分組,只要同組重量和不超過限重,過橋時間是組中最久的,求所有人都過橋最短需要多少時間 $1\leq n\leq16$ ---- - 狀態定義:二進位的1代表已經過橋的人 - $dp[S]$ 為過橋人集合為 $S$ 的最短時間 - $dp[S]=min\{dp[S\oplus S']\ |\ S'\in S\ \&\ w(S)\leq W\}$ - 必須用子集枚舉,複雜度 $O(3^N)$ - $S$ 的子集已知 $S'$,下一個為$(S'-1)\&S$ - 模擬退火亂做(x ---- 程式碼: ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define ml(a, b) ((1ll * (a) * (b)) % M) #define tml(a, b) (a) = ((1ll * (a) * (b)) % M) #define ad(a, b) ((0ll + (a) + (b)) % M) #define tad(a, b) (a) = ((0ll + (a) + (b)) % M) #define mi(a, b) ((0ll + M + (a) - (b)) % M) #define tmi(a, b) (a) = ((0ll + M + (a) - (b)) % M) #define tmin(a, b) (a) = min((a), (b)) #define tmax(a, b) (a) = max((a), (b)) #define iter(a) (a).begin(), (a).end() #define riter(a) (a).rbegin(), (a).rend() #define inin(a, b) memset((a), (b), sizeof(a)) void db() { cerr << "\n"; } template <class T, class... U> void db(T a, U... b) { cerr << a << " ", db(b...); } inline char gc() { const static int SZ = 1 << 16; static char buf[SZ], *p1, *p2; if (p1 == p2 && (p2 = buf + fread(p1 = buf, 1, SZ, stdin), p1 == p2)) return -1; return *p1++; } void rd() {} template <typename T, typename... U> void rd(T &x, U &...y) { x = 0; bool f = 0; char c = gc(); while (!isdigit(c)) f ^= !(c ^ 45), c = gc(); while (isdigit(c)) x = (x << 1) + (x << 3) + (c ^ 48), c = gc(); f && (x = -x), rd(y...); } template <typename T> void prt(T x) { if (x < 0) putchar('-'), x = -x; if (x > 9) prt(x / 10); putchar((x % 10) ^ 48); } const int N = 16; int dp[1 << N], sum[1 << N], wr[N], tr[N]; signed main() { int W, n; rd(W, n); for (int i = 0; i < n; i++) rd(tr[i], wr[i]); for (int mk = 0; mk < 1 << n; mk++) { int tp = 0; for (int i = 0; i < n; i++) if ((mk >> i) & 1) tmax(sum[mk], tr[i]), tp += wr[i]; if (tp > W) sum[mk] = INF; } inin(dp, INF), dp[0] = 0; for (int mk = 1; mk < 1 << n; mk++) for (int s = mk; s; s = (s - 1) & mk) tmin(dp[mk], dp[mk ^ s] + sum[s]); prt(dp[(1 << n) - 1]), putchar('\n'); } ``` --- # 資料結構優化 ---- ## [Atcoder -- Flowers](https://atcoder.jp/contests/dp/tasks/dp_q) 有 N 朵花排成一列,第 i 朵的高度是 $h_i$、美麗度是 $a_i$,你要選一些花,使得它們的高度 由左至右嚴格遞增,且美麗度總和最大。 $N ≤ 10^6$、$1 ≤ h_i ≤ 10^6$ ---- - dp[i]考慮前i個,最後一個選i的答案 - $dp[i]=a_i+max\{dp[j]\ |\ j<i\ \land\ h_j<h_i\}$ - 考慮到 $i$ 時 $j$ 本來就比i小 - 線段樹值域k維護$k=h_j$ 結尾的最大dp值 - 用線段樹轉移,算好dp值更新線段樹 ---- 程式碼:zkw線段樹看起來很怪 ```cpp! #include <bits/stdc++.h> #define lowbit(x) ((x) & -(x)) #define eps 1e-9 #define F first #define S second #define AC ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; typedef long long llt; typedef pair<int, int> pii; typedef pair<double, double> pdd; typedef pair<llt, llt> pll; signed main() { int n, l, r; llt tp, x; scanf("%d", &n); vector<int> hr(n); for (int i = 0; i < n; i++) scanf("%d", &hr[i]); vector<llt> sgt(n << 1, 0); for (int i = 0; i < n; i++) { scanf("%lld", &x), tp = 0; for (l = n, r = hr[i] + n - 1; l <= r; l >>= 1, r >>= 1) { if (l & 1) tp = max(tp, sgt[l++]); if (~r & 1) tp = max(tp, sgt[r--]); } x += tp; for (l = hr[i] + n - 1; l > 0; l >>= 1) sgt[l] = max(sgt[l], x); } printf("%lld\n", sgt[1]); } ``` ---- ## 其他應用 - 線段樹可以區間修改,可以logn轉移區間 - 轉移出去可能連續,轉移進來也可能連續 - 根據題目需求決定轉移方向
{"title":"動態規劃","description":"定義dp狀態","contributors":"[{\"id\":\"33757841-f501-406f-a770-4a908423f414\",\"add\":45438,\"del\":4453}]"}
    410 views