完整校订版见此
以下为未核对不完整版本。
因版权原因,完整精校版不向所有公众开放。
请从您找到本博客的地址查找附带密码(比如简书分享了本网址,请您从简书分享页底部查询密码),感谢您的配合。
这里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 ", (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 宝藏
先看一下题目:
参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 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 ", 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 ", 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。
再看一道题: