$ Dynamic \ Programming $ $$ Tecy $$
线性 $dp$ 引入 问题陈述 有$N$块石头,编号为$1, 2, \ldots, N$。每块$i$($1 \leq i \leq N$),石头$i$的高度为$h_i$。
有一只青蛙,它最初在$1$号石头上。它会重复下面的动作若干次以到达石块$N$:
如果青蛙目前在石块$i$上,则跳到石块$i + 1$或石块$i + 2$上。这里需要付出$|h_i - h_j|$的代价,其中$j$是要降落的石块。
求青蛙到达石块$N$之前可能产生的最小总成本。
$f_{i}$ 表示到达 $i$ 所需花费
$ f_{i} = min(f_{i - 1} + |h_{i} - h_{i - 1}|, f_{i - 2} + |h_{i} - h_{i - 2}|)$
复杂度 $O(N)$
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int n;cin >> n; vector<int > val (n) ;for (auto & x : val){ cin >> x; } vector<int > f (n) ;f[1 ] = abs (val[1 ] - val[0 ]); for (int i = 2 ; i < n; i++){ f[i] = min (f[i - 1 ] + abs (val[i] - val[i - 1 ]), f[i - 2 ] + abs (val[i] - val[i - 2 ])); } cout << f.back ();
问题陈述 有$N$块石头,编号为$1, 2, \ldots, N$。每块$i$($1 \leq i \leq N$),石头$i$的高度为$h_i$。
有一只青蛙,它最初在石块 $1$ 上。它会重复下面的动作若干次以到达石块$N$:
如果青蛙目前在石块$i$上,请跳到以下其中一个位置:石块$i + 1, i + 2, \ldots, i + K$。这里会产生$|h_i - h_j|$的代价,其中$j$是要降落的石头。
求青蛙到达石块$N$之前可能产生的最小总成本。
$f_{i}$ 表示到达 $i$ 所需花费
$ f_{i} = min(f_{i - j} + |h_{i} - h_{i - j}|) \quad if \quad j \le k $
复杂度 $O(K \times N)$
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const int inf = 1e9 ;int n, k;cin >> n >> k; vector<int > val (n) ;for (auto & x : val){ cin >> x; } vector<int > f (n, inf) ;f[0 ] = 0 ; for (int i = 1 ; i < n; i++){ for (int j = 1 ; j <= k; j++){ if (i - j >= 0 ){ f[i] = min (f[i], f[i - j] + abs (val[i] - val[i - j])); } } } cout << f.back ();
经典 $dp$ 问题陈述 给你字符串 $s$ 和 $t$。请找出一个最长的字符串,它同时是 $s$ 和 $t$ 的子串。
$f_{i, j}$ 表示考虑 $s$ 前 $i$ 个, $t$ 前 $j$ 个最长子串 $$ f_{i, j} = \begin{cases} f_{i - 1, j - 1} + 1 \quad \quad \quad \ \ \ if \quad s[i] = t[j] \ max(f_{i, j - 1}, f_{i - 1, j}) \quad if \quad s[i] \neq t[j] \end{cases} $$ 求出长度后,反向可推出最长公共子串(不唯一) $$ if \quad s[i] == t[j] \rightarrow s[i] $$
复杂度 $O(|s| \times |t|)$
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 string s, t; cin >> s >> t; s = " " + s; t = " " + t; int n = s.size ();int m = t.size ();vector<vector<int >> f (n, vector <int >(m)); for (int i = 1 ; i < n; i++){ for (int j = 1 ; j < m; j++){ if (s[i] == t[j]){ f[i][j] = f[i - 1 ][j - 1 ] + 1 ; }else { f[i][j] = max (f[i - 1 ][j], f[i][j - 1 ]); } } } int sp = n - 1 ;int tp = m - 1 ;string ans = "" ; while (sp >= 1 && tp >= 1 ){ if (s[sp] == t[tp]){ ans += s[sp]; sp--; tp--; }else { if (f[sp][tp] == f[sp - 1 ][tp]){ sp--; }else { tp--; } } } reverse (ans.begin (), ans.end ());cout << ans;
不能用 $f_{i, j} = f_{i - 1, j - 1} + 1 \quad \rightarrow \quad s[i]$
因为当 $f_{i, j} = f_{i - 1, j}$ 且 $f_{i - 1, j} = f_{i - 2, j} + 1$ 且 $f_{i, j} = f_{i - 1, j - 1} + 1$ 时会错
问题陈述。 方法一:
数列 $ a_{i} $ ,求最长不下降子序列。 $$ f_{i} = max(f_{j} + 1) \quad if \quad a_{i} \ge a_{j} \quad and \quad i \ge j $$ 复杂度 $O(N^{2})$
方法一 main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const long long inf = 1e18 ;int n;cin >> n; vector<long long > val (n + 1 , -inf) ;for (int i = 1 ; i <= n; i++){ cin >> val[i]; } vector<int > f (n + 1 ) ;for (int i = 1 ; i <= n; i++){ for (int j = 0 ; j < i; j++){ if (val[i] >= val[j]){ f[i] = max (f[i], f[j] + 1 ); } } } cout << *max_element (f.begin (), f.end ());
方法二:
考虑进来一个元素 $a_{i}$ :
$d \ is \ empty$ 或元素大于等于 $d.back()$ ,直接将该元素插入到 $d$ 序列的末尾。
元素小于 $d.back()$ ,找到 第一个 大于等于它的元素,用 $a_{i}$ 替换它。
复杂度 $O(N log N)$
方法二 main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int n;cin >> n; vector<long long > val (n) ;for (auto & x : val){ cin >> x; } vector<long long > d; for (auto & x : val){ if (d.empty () || d.back () <= x){ d.push_back (x); }else { int p = lower_bound (d.begin (), d.end (), x) - d.begin (); d[p] = x; } } cout << d.size ();Q
求两个串的最长公共上升子序列
结合以上就可解决本题。
$f_{i, j}$ 表示 $s[1 - i], t[1 - j]$ 以 $t[j]$ 结尾 $LCIS$ 的长度 $$ f_{i, j} = f_{i - 1, j} \quad if \quad s[i] \neq t[j] $$
$$ f_{i, j} = max(f_{i - 1, k} + 1) \quad if \quad s[i] = t[j] \quad and \quad s[i] > t[k] \quad and \quad j > k $$ 复杂度 $O(N^{3})$
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 int n;cin >> n; vector<int > s (n + 1 , -1 ) ;for (int i = 1 ; i <= n; i++){ cin >> s[i]; } int m;cin >> m; vector<int > t (m + 1 , -1 ) ;for (int i = 1 ; i <= m; i++){ cin >> t[i]; } int ans = 0 ;vector<vector<int >> f (n + 1 , vector <int >(m + 1 )); vector<vector<int >> p (n + 1 , vector <int >(m + 1 )); for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (s[i] == t[j]){ for (int k = 0 ; k < j; k++){ if (s[i] > t[k]){ if (f[i - 1 ][k] + 1 > f[i][j]){ p[i][j] = k; } f[i][j] = max (f[i][j], f[i - 1 ][k] + 1 ); } } }else { p[i][j] = p[i - 1 ][j]; f[i][j] = f[i - 1 ][j]; } } } int tp = m;for (int i = 1 ; i <= m; i++){ if (ans < f[n][i]){ ans = f[n][i]; tp = i; } } vector<int > res; while (ans > 0 ){ res.push_back (t[tp]); tp = p[n][tp]; ans--; } reverse (res.begin (), res.end ());cout << res.size () << "\n" ; for (auto & x : res){ cout << x << " " ; }
优化 该题还可优化为 $O(N^{2})$
可以看到枚举 $k$ 时 $s[i] = t[j]$ 这个条件不用检查,只用检查 $s[i] \ge t[k]$,枚举时决策取最大即可
$prefix_max_{i, j}$ 表示 $f[1 - i][1 - j]$ 的最大值 $$ f_{i, j} = f_{i - 1, j} \quad if \quad s[i] \neq t[j] $$
$$ f_{i, j} = prefix_max_{i - 1, j - 1} + 1 \quad if \quad s[i] = t[j] $$
$$ prefix_max_{i - 1, j} = max(prefix_max_{i - 1, j - 1}, f_{i - 1, j}) \quad if \quad s[i] > t[j] $$
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 int n;cin >> n; vector<int > s (n + 1 , -1 ) ;for (int i = 1 ; i <= n; i++){ cin >> s[i]; } int m;cin >> m; vector<int > t (m + 1 , -1 ) ;for (int i = 1 ; i <= m; i++){ cin >> t[i]; } int ans = 0 ;vector<vector<int >> f (n + 1 , vector <int >(m + 1 )); vector<vector<int >> p (n + 1 , vector <int >(m + 1 )); for (int i = 1 ; i <= n; i++){ int to = 0 ; int prefix_max = 0 ; for (int j = 1 ; j <= m; j++){ if (s[i] == t[j]){ if (prefix_max + 1 > f[i][j]){ p[i][j] = to; } f[i][j] = max (f[i][j], prefix_max + 1 ); }else { p[i][j] = p[i - 1 ][j]; f[i][j] = f[i - 1 ][j]; } if (s[i] > t[j]){ if (prefix_max < f[i - 1 ][j]){ to = j; } prefix_max = max (prefix_max, f[i - 1 ][j]); } } } int tp = m;for (int i = 1 ; i <= m; i++){ if (ans < f[n][i]){ ans = f[n][i]; tp = i; } } vector<int > res; while (ans > 0 ){ res.push_back (t[tp]); tp = p[n][tp]; ans--; } reverse (res.begin (), res.end ());cout << res.size () << "\n" ; for (auto & x : res){ cout << x << " " ; }
背包 题意概要:有 $n$ 个物品和一个容量为 $v$ 的背包,每个物品有重量 $cost_{i}$ 和价值 $value_{i}$ 两种属性,要求选若干物品放入背包使背包中物品的总价值最大 且背包中物品的总重量不超过背包的容量。(每个物品最多选一次)
$f_{i, j}$ 表示考虑前 $i$ 个物品,当前容量为 $j$ 是最优解 $$ f_{i, j} = max(f_{i - 1, j - cost_{i}} + value_{i}) \quad if \quad j \ge cost_{i} $$ $$ f_{i, j} = f_{i - 1, j} \quad if \quad j < cost_{i} $$
为什么要先枚举 $i$ ,后枚举 $j$ ?
物品被选多次。
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int n, m;cin >> n >> m; vector<int > cost (n + 1 ) , value (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> cost[i] >> value[i]; } vector<vector<int >> f (n + 1 , vector <int >(m + 1 )); for (int i = 1 ; i <= n; i++){ for (int j = 0 ; j <= m; j++){ if (j >= cost[i]){ f[i][j] = max (f[i - 1 ][j], f[i - 1 ][j - cost[i]] + value[i]); }else { f[i][j] = f[i - 1 ][j]; } } } cout << f[n][m];
一维优化空间 $f_{j} = max(f_{j - cost_{i}})$
复杂度 $O(n \times v)$
要倒序枚举 $j$ !!!
$f_{i,j}$ 会被 $f_{i, j - cost_{i}}$ 影响,相当于物品被选多次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int n, m;cin >> n >> m; vector<int > cost (n + 1 ) , value (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> cost[i] >> value[i]; } vector<int > f (m + 1 ) ;for (int i = 1 ; i <= n; i++){ for (int j = m; j >= cost[i]; j--){ f[j] = max (f[j], f[j - cost[i]] + value[i]); } } cout << f[m];
题意概要:有 $n$ 个物品和一个容量为 $v$ 的背包,每个物品有重量 $cost_{i}$ 和价值 $value_{i}$ 两种属性,要求选若干物品放入背包使背包中物品的总价值最大 且背包中物品的总重量不超过背包的容量。(每个物品可选无限次)
先考虑朴素解: $$ f_{i, j} = max(f_{i - 1, j - k \times cost_{i}} + k \times value_{i}) $$ 复杂度可能达到 $O(n^{2} \times v)$
简单优化
可以发现,对于 $f_{i, j}$ ,只要通过 $f_{i, j - cost_{i}}$ 转移就可以了。因为状态转移方程为: $$ f_{i, j - cost_{i}} = max(f_{i - 1, j - (k + 1) \times cost_{i}} + (k + 1) \times value_{i}) $$ 对比上面转移方程只多了一个,我们通过局部最优子结构的性质重复使用了之前的枚举过程,优化了枚举的复杂度。 $$ f_{i, j} = max(f_{i - 1, j}, f_{i, j - cost_{i}} + value_{i}) $$
复杂度 $O(n \times v)$
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 int m, n;cin >> m >> n; vector<int > cost (n + 1 ) , value (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> cost[i] >> value[i]; } vector<vector<int >> f (n + 1 , vector <int >(m + 1 )); for (int i = 1 ; i <= n; i++){ for (int j = 0 ; j <= m; j++){ if (j >= cost[i]){ f[i][j] = max (f[i - 1 ][j], f[i][j - cost[i]] + value[i]); }else { f[i][j] = f[i - 1 ][j]; } } } cout << f[n][m];
一维优化空间 $f_{j} = max(f_{j - cost_{i}})$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int m, n;cin >> m >> n; vector<long long > cost (n + 1 ) , value (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> cost[i] >> value[i]; } vector<long long > f (m + 1 ) ;for (int i = 1 ; i <= n; i++){ for (int j = cost[i]; j <= m; j++){ f[j] = max (f[j], f[j - cost[i]] + value[i]); } } cout << f[m];
发现与0-1背包方程一样, 这时候要正序枚举 $j$
题意概要:有 $n$ 个物品和一个容量为 $v$ 的背包,每个物品有重量 $cost_{i}$ 和价值 $value_{i}$ 两种属性,要求选若干物品放入背包使背包中物品的总价值最大 且背包中物品的总重量不超过背包的容量。(每个物品可选 $k_{i}$ ) $$ f_{i, j} = max(f_{i - 1, j - k \times cost_{i}} + k \times value_{i}) \quad if \quad k \le k_{i} $$
二进制优化 将 $k_{i}$ 拆成 $1, 2, 4, …, 2^{p}, x$ ( $x$ 为剩下补足,范围 $0 \le x < 2^{p + 1}$ ),这样每个物品最多产生 $log_{2}(k_{i})$ 个物品
然后当成0-1背包即可
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int n, m;cin >> n >> m; vector<long long > v (n + 1 ) , c (n + 1 ) , k (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> v[i] >> c[i] >> k[i]; } vector<long long > cost, value; for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j < k[i]; k[i] -= j, j *= 2 ){ cost.push_back (c[i] * j); value.push_back (v[i] * j); } cost.push_back (c[i] * k[i]); value.push_back (v[i] * k[i]); } n = cost.size (); vector<long long > f (m + 1 ) ;for (int i = 0 ; i < n; i++){ for (int j = m; j >= cost[i]; j--){ f[j] = max (f[j], f[j - cost[i]] + value[i]); } } cout << f[m];
复杂度 $O(v \times \sum\limits_{i = 1}^{n} log_{2}(k_{i}))$
单调队列优化 $$ f_{i, j} = max(f_{i - 1, j - k \times cost_{i}} + k \times value_{i}) $$
设 $g_{i, x, y} = f_{i, x \times cost_{i} + y}$ $$ g_{i, x, y} = max(g_{i - 1, x - k, y} + k \times value_{i}) \quad if \quad k \le k_{i} $$ 设 $G_{i, x, y} = g_{i, x, y} - x \times cost_{i}$ $$ g_{i, x, y} = max(G_{i - 1, x - k, y}) + x \times cost_{i} $$ 可以看出 $g_{i, x, y}$ 由窗口 $[i - k, i - 1]$ 转移,滑动窗口问题求 $max$ 可用单调队列求取
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 int n, m;cin >> n >> m; vector<long long > value (n + 1 ) , cost (n + 1 ) , k (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> value[i] >> cost[i] >> k[i]; } vector<long long > f (m + 1 ) ;for (int i = 1 ; i <= n; i++){ auto g = f; for (int y = 0 ; y < cost[i]; y++){ deque<long long > q; for (int x = y; x <= m; x += cost[i]){ while (!q.empty () && q.front () < x - k[i] * cost[i]){ q.pop_front (); } if (!q.empty ()){ f[x] = max (f[x], g[q.front ()] + (x - q.front ()) / cost[i] * value[i]); } while (!q.empty () && g[q.back ()] - (q.back () - y) / cost[i] * value[i] <= g[x] - (x - y) / cost[i] * value[i]){ q.pop_back (); } q.push_back (x); } } } cout << f[m];
复杂度 $O(n \times v)$
一般形式 $$ f_{l, r} = max(f_{l, k} + f_{k + 1, r} + cost) $$
$n$ 堆石子,$a_1, a_2,…,a_n$ 每次合并相邻两堆,代价是两堆石子之和,求变为一堆的最小代价
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const int inf = 1e9 ;int n;cin >> n; vector<int > val (n + 1 ) , sum (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> val[i]; sum[i] = sum[i - 1 ] + val[i]; } vector<vector<int >> f (n + 1 , vector <int >(n + 1 , inf)); for (int i = 1 ; i <= n; i++){ f[i][i] = 0 ; } for (int len = 1 ; len <= n; len++){ for (int l = 1 ; l + len <= n; l++){ int r = l + len; for (int k = l; k < r; k++){ f[l][r] = min (f[l][r], f[l][k] + f[k + 1 ][r] + sum[r] - sum[l - 1 ]); } } } cout << f[1 ][n];
$$ f_{u} = (f_v + …) $$ 即在父节点时考虑子节点
某大学有 $n$ 个职员,编号为 $1\ldots n$。
他们之间有从属关系,他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 $r_i$,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。求最大的快乐指数。
$f_u$ 表示包含 $u$ 的结果
$g_u$ 表示不包含 $u$ 的结果
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int n;cin >> n; vector<int > val (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> val[i]; } int root = 0 ;vector<vector<int >> relation (n + 1 ); for (int i = 1 ; i < n; i++){ int v, u; cin >> v >> u; relation[u].push_back (v); root ^= i ^ v; } root ^= n; vector<int > f (n + 1 ) ;vector<int > g (n + 1 ) ;auto dp = [&](auto self, int u) -> void { f[u] = val[u]; for (int & v : relation[u]){ self (self, v); f[u] += g[v]; g[u] += max (f[v], g[v]); } }; dp (dp, root);cout << max (f[root], g[root]);
数位是指把一个数字按照个、十、百、千等等一位一位地拆开,关注它每一位上的数字。如果拆的是十进制数,那么每一位数字都是 0~9,其他进制可类比十进制。
数位 DP:用来解决一类特定问题,这种问题比较好辨认,一般具有这几个特征:
要求统计满足一定条件的数的数量(即,最终目的为计数);
这些条件经过转化后可以使用「数位」的思想去理解和判断;
输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;
上界很大(比如 $10^{18}$ ),暴力枚举验证会超时。
数位 $dp$ 经常与记忆化搜索一同出现
求区间计数一般转化为前缀 即求 $[l, r]$ 变为 $[0, r] - [0, l - 1]$
不含前导零且相邻两个数字之差至少为 $2$ 的正整数被称为 $windy$ 数。$windy$ 想知道,在 $l$ 和 $r$ 之间,包括 $l$ 和 $r$ ,总共有多少个 $windy$ 数?
$f_{i, j}$ 表示当前是第 $i$ 位,该位为 $j$ ,$windy$ 数的个数 $$ f_{i, j} = \sum\limits_{k = 0}^{9} f_{i - 1, k} \quad if \quad abs(j - k) \ge 2 $$
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 const int N = 10 ;const int M = 12 ;long long f[M][N] = { 0 };for (int i = 0 ; i < N; i++){ f[0 ][i] = 1 ; } for (int i = 1 ; i < M; i++){ for (int j = 0 ; j < N; j++){ for (int k = 0 ; k < N; k++){ if (abs (j - k) >= 2 ){ f[i][j] += f[i - 1 ][k]; } } } } auto get = [&](long long x) -> long long { if (x == 0 ){ return 0 ; } vector<int > digit; while (x != 0 ){ digit.push_back (x % 10 ); x /= 10 ; } int last = -2 ; long long ans = 0 ; for (int i = digit.size () - 1 ; i >= 0 ; i--){ for (int j = (i == digit.size () - 1 ); j < digit[i]; j++){ if (abs (j - last) >= 2 ){ ans += f[i][j]; } } if (abs (last - digit[i]) < 2 ){ break ; } last = digit[i]; if (i == 0 ){ ans++; } } for (int i = 0 ; i < digit.size () - 1 ; i++){ for (int j = 1 ; j <= 9 ; j++){ ans += f[i][j]; } } return ans; }; long long l, r;cin >> l >> r; cout << get (r) - get (l - 1 );
复杂度 $O(M \times N^{2})$
用二进制位来表示状态
需要用到位运算知识
在 $N \times N$ 的棋盘里面放 $K$ 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 $8$ 个格子。
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 int n, k;cin >> n >> k; auto get = [&](int x) -> int { int ans = 0 ; for (int i = 0 ; i < n; i++){ if ((x >> i) & 1 ){ ans++; } } return ans; }; vector<int > bits, cnts; for (int i = 0 ; i < (1 << n); i++){ if (((i >> 1 ) & i) == 0 ){ bits.push_back (i); cnts.push_back (get (i)); } } auto check = [&](int j, int p) -> bool { if (j & p){ return false ; } if ((j >> 1 ) & p){ return false ; } if (j & (p >> 1 )){ return false ; } return true ; }; vector<vector<vector<long long >>> f (n + 2 , vector<vector<long long >>(1 << n, vector <long long >(k + 1 ))); f[0 ][0 ][0 ] = 1 ; for (int i = 1 ; i <= n + 1 ; i++){ for (int j = 0 ; j < bits.size (); j++){ for (int p = 0 ; p < bits.size (); p++){ for (int c = 0 ; c <= k; c++){ if (check (bits[j], bits[p]) && c + cnts[j] <= k){ f[i][bits[j]][c + cnts[j]] += f[i - 1 ][bits[p]][c]; } } } } } cout << f[n + 1 ][0 ][k];
$dp$ 优化 数据结构优化 当 $dp$ 需要前一个 $or$ 后一个, 大于(等于) $or$ 小于(等于)它数列的状态时,可以考虑单调栈
给你一个数组 $[p_1, p_2, \dots, p_n]$ ,其中所有元素都是不同的。
你可以对它进行几个(可能是零)操作。在一个操作中,你可以选择 $p$ 的一个连续的子段 ,并从该子段中删除所有 元素,除 该子段中最小的元素。例如,如果是 $p = [3, 1, 4, 7, 5, 2, 6]$ ,你选择了从 $3$ -rd元素到 $6$ -th元素的子段,那么得到的数组就是 $[3, 1, 2, 6]$ 。
如果数组 $a$ 可以通过上述几种(也许是零种)操作从 $p$ 中得到,那么这个数组 $a$ 就叫做可达数组。计算可达数组的个数,并打印出它的模数 $998244353$ 。
$f_i$ 表示考虑前 $i$ 个数,不以第 $i$ 项结尾的个数
$g_i$ 表示考虑前 $i$ 个数,并以第 $i$ 项结尾的个数
考虑 $f_{i}$ 的转移, 因为不以第 $i$ 项结尾,前面必有比他小的数来删除它 $$ f_{i} = \sum\limits_{j = 0 \vee p_i > p_j }^{i - 1} g_{j} $$ 考虑前一个小于它的数, 记为 $p_{l[i]}$
对于 $p_j < p_{l[i]} < p_i$ 的 $g_j$ , $p_{l[i]}$ 被 $p_{j}$ 删除,此时即 $f_{l[i]}$, 根据公式 $f_{l[i]} = \sum\limits_{j = 0 \vee p_{l[i]} > p_j }^{l[i] - 1} g_{j}$,而区间 $[l[i], i - 1]$ 没有小于 $p_i$ 的数,贡献为 $0$
对于 $p_i > p_j > p_{l[i]}$ 的 $g_j$ , $p_{l[i]}$ 未被 $p_{j}$ 删除,此时即 $g_{l[i]}$,会以 $p_{l[i]}$ 结尾 $$ \sum\limits_{j = 0 \vee p_i > p_j }^{i - 1} g_{j} = f_{l[i]} + g_{l[i]} $$
考虑 $g_i$ 的转移,以第 $i$ 项结尾,那它可以删除大于它的项,保留第一个小于它的项 $$ g_{i} = \sum\limits_{j = l[i]}^{i - 1} g_{j} + f_{l[i]} $$ 该式 $\sum\limits_{j = l[i]}^{i - 1} g_{j}$ 可用前缀和计算
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const int mod = 998244353 ;int n;cin >> n; vector<int > val (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> val[i]; } vector<int > l (n + 1 ) ;vector<int > stk = { 0 }; for (int i = 1 ; i <= n; i++){ while (!stk.empty () && val[i] < val[stk.back ()]){ stk.pop_back (); } l[i] = stk.back (); stk.push_back (i); } vector<long long > f (n + 1 ) , g (n + 1 ) , sum (n + 1 ) ;g[0 ] = 1 ; sum[0 ] = 1 ; for (int i = 1 ; i <= n; i++){ if (l[i]){ f[i] = (f[l[i]] + g[l[i]]) % mod; g[i] = (f[l[i]] + sum[i - 1 ] - sum[l[i] - 1 ]) % mod; }else { g[i] = (f[l[i]] + sum[i - 1 ]) % mod; } sum[i] = (sum[i - 1 ] + g[i]) % mod; } cout << ((f[n] + g[n]) % mod + mod) % mod << "\n" ;
有 $n$ 座烽火台,每个烽火台发出信号都有代价。在连续 $m$ 个烽火台中至少要有一个发出信号。计算最少的代价。
$f_{i}$ 表示考虑前 $i$ 个,并且在 $i$ 设立的最小代价
根据题意 $$ f_{i} = min(f_{j} + cost_{i}) \quad if \quad i - j \le m $$ 即一个大小为 $m$ 的滑动窗口, 求最值,用队列维护
设队头为最优值
当窗口离开它时,队头出列
当新值优于队尾时,队尾出列
加入新值
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const int inf = 1e9 ;int n, m;cin >> n >> m; vector<int > cost (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> cost[i]; } int ans = inf;deque<int > q = { 0 }; vector<int > f (n + 1 , inf) ;f[0 ] = 0 ; for (int i = 1 ; i <= n; i++){ while (!q.empty () && i - q.front () > m){ q.pop_front (); } f[i] = min (f[i], f[q.front ()] + cost[i]); while (!q.empty () && f[i] <= f[q.back ()]){ q.pop_back (); } q.push_back (i); if (n - i < m){ ans = min (ans, f[i]); } } cout << ans;
线段树/树状数组优化 $dp$ 需要区间状态的某些性质时,可以考虑线段树/树状数组优化
斜率优化 一般形式 $$ f_{i} = min(f_{j} + a_{i} \times b_{i}) \quad if \quad j < i $$
四边形不等式
对于任何 $a \le b \le c \le d$ 均有 $f(a, c) + f(b, d) \le f(a, d) + f(b, c)$
则称 $f$ 满足四边形不等式
还是石子合并那题
用 $p_{l, r}$ 记录分割点,优化决策
main code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const int inf = 1e9 ;int n;cin >> n; vector<int > val (n + 1 ) , sum (n + 1 ) ;for (int i = 1 ; i <= n; i++){ cin >> val[i]; sum[i] = sum[i - 1 ] + val[i]; } vector<vector<int >> p (n + 2 , vector <int >(n + 2 )); vector<vector<int >> f (n + 2 , vector <int >(n + 2 , inf)); for (int i = 1 ; i <= n; i++){ f[i][i] = 0 ; p[i][i] = i; } for (int delta = 1 ; delta < n; delta++){ for (int l = 1 ; l + delta <= n; l++){ int r = l + delta; for (int k = p[l][r - 1 ]; k <= p[l + 1 ][r]; k++){ if (f[l][r] > f[l][k] + f[k + 1 ][r] + sum[r] - sum[l - 1 ]){ f[l][r] = f[l][k] + f[k + 1 ][r] + sum[r] - sum[l - 1 ]; p[l][r] = k; } } } } cout << f[1 ][n];
复杂度 $O(n^{2})$