感觉还是好多东西记不得了,gg。
感觉还是好多东西记不得了,gg。
这一道题其实挺好的,我们来分析一下。
首先,我们知道在一个方格纸上,从左上角到右下角的路径的数量是\(C_{H+W-2}^{H-1}\),可以理解为横向路径的选法总数。之后,我们来进行容斥。在本题中,我们需要容斥掉那些经过了至少一次黑点的路径数量。因为本题中黑点个数小于\(5000\),所以我们直接来跑一个\(O(n^2)\)的 DP。设\(F[i]\)为经过了此点的路径数,然后我们把右下角的点设为第\(n+1\)个点,最后答案存在\(F[n+1]\)处。我们可以很快得到一个递推式:
\[ F[i] = C_{x_i + y_i – 2}^{x_i – 1} – \sum_{j = 1}^{i-1} (F[j] * C_{x_i-x_j+y_i-y_j}^{x_i-x_j}),且x_i \geq x_j, y_i \geq y_j. \]
解释:对于到点\(i\)的路径,首先不考虑之前的黑点则有\( C_{x_i + y_i – 2}^{x_i – 1} \)种路;而要容斥掉至少经过过一次的黑点的路径,可以直接对于之前的每一个点进行计算,计算之前的点\(j\)到现在的点\(i\)有多少种路径。
// CF559C.cpp #include <bits/stdc++.h> #define ll long long #define pr pair<ll, ll> using namespace std; const int MOD = 1e9 + 7, MAX_N = 2020, LEVEL_RG = 200000; ll level[200010], inv[200010], n, h, w, F[MAX_N]; pr pts[MAX_N]; ll quick_power(ll bas, ll tim, ll mod) { ll ans = 1; while (tim) { if (tim & 1) ans = ans * bas % mod; bas = bas * bas % mod; tim >>= 1; } return ans; } ll C(ll n, ll m) { return level[n] * inv[n - m] % MOD * inv[m] % MOD; } int main() { scanf("%d%d%d", &h, &w, &n); for (int i = 1; i <= n; i++) scanf("%d%d", &pts[i].first, &pts[i].second); sort(pts + 1, pts + 1 + n); pts[n + 1].first = h, pts[n + 1].second = w; level[0] = inv[0] = 1; for (int i = 1; i <= LEVEL_RG; i++) level[i] = level[i - 1] * i % MOD; ll inv_bas = quick_power(level[LEVEL_RG], MOD - 2, MOD); for (int i = LEVEL_RG; i >= 1; i--) inv[i] = inv_bas, inv_bas = inv_bas * i % MOD; for (int i = 1; i <= n + 1; i++) { F[i] = C(pts[i].first + pts[i].second - 2, pts[i].first - 1) % MOD; for (int j = 1; j < i; j++) if (pts[j].first <= pts[i].first && pts[j].second <= pts[i].second) F[i] = (F[i] - F[j] * C(pts[i].first + pts[i].second - pts[j].first - pts[j].second, pts[i].first - pts[j].first)) % MOD; } printf("%lld", (F[n + 1] + MOD) % MOD); return 0; }
这道题比较有趣,一开始我想出了\(O(n^3)\)的复杂度,打上去 TLE 之后思考前缀做差的方法,但是我太 Naive,不太相信(?)模意义下的减法,所以这个思路在我看了题解之后才搞定。
这道题的 DP 方程不难想到,为:
\[ dp[u][i] = \sum_{t \in dst} \sum_{j \in color(t)} dp[t][j], i \neq j \]
其实我们可以把第二维消掉,考虑总和\(tot[]\)数组,得:
\[ dp[u][i] = \sum_{t \in dst} tot[t] – dp[t][i] \]
然后就出来了。
// P3914.cpp #include <bits/stdc++.h> using namespace std; const int MAX_N = 5050, MOD = 1e9 + 7; int head[MAX_N], current, n, m, tmpx, tmpy, dp[MAX_N][MAX_N], tot[MAX_N]; struct edge { int to, nxt; } edges[MAX_N << 1]; void addpath(int src, int dst) { edges[current].to = dst, edges[current].nxt = head[src]; head[src] = current++; } void dfs(int u, int fa) { for (int i = head[u]; i != -1; i = edges[i].nxt) if (edges[i].to != fa) dfs(edges[i].to, u); for (int i = 1; i <= m; i++) { if (!dp[u][i]) continue; for (int e = head[u]; e != -1; e = edges[e].nxt) { if (edges[e].to == fa) continue; dp[u][i] = (1LL * dp[u][i] * 1LL * (tot[edges[e].to] - dp[edges[e].to][i])) % MOD; } while (dp[u][i] < 0) dp[u][i] += MOD; tot[u] = (1LL * tot[u] + 1LL * dp[u][i]) % MOD; } } int main() { memset(head, -1, sizeof(head)); scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &tmpy); for (int j = 1; j <= tmpy; j++) scanf("%d", &tmpx), dp[i][tmpx] = 1; } for (int i = 1; i < n; i++) scanf("%d%d", &tmpx, &tmpy), addpath(tmpx, tmpy), addpath(tmpy, tmpx); dfs(1, 0); printf("%d", tot[1] % MOD); return 0; }
有的时候我们在日常写题时,会碰见这样的毒瘤 DP 题目,他们一般有以下特性:
这个时候我们可以使用单调队列来优化,时间复杂度可以进一步降至\(O(n^2)\)或者是\(O(nm)\)。如果有不会单调队列的同学呢可以去学习以下,顺便可以看看这篇文章:CH1201:最大子序和题解
我们来看一道例题:Fence – POJ这道题是一道比较简单的经典 DP。设计状态\(dp[i][j]\)表示前\(i\)个人搞定了前\(j\)个单位的墙。写出方程式:
\[dp[i][j] = min_{S_i – l_i \leq k < S_i}\{dp[i-1][k]+profit_i*(j-k)\}\]
这样的话,整个算法的复杂度是\(O(n^3)\)的。我们能不能将其降为\(O(n^2)\)呢?我们可以把整个方程变个形,之后方程是这样的:
\[dp[i][j] = min_{S_i – l_i \leq k < S_i}\{dp[i-1][k]-profit_i*k\}+profit_i*j\]
我们发现,这样的话整个方程在\(i\)的循环之下,算是只跟变量\(k\)扯上关系了。我们会发现,只要有\(k_1<k_2\)且\(dp[i-1][k_1]-profit_i*k_1<dp[i-1][k_2]-profit_i*k_2\),那么就可以直接排除掉答案\(k_1\)。那么就可以使用单调队列来做到这一点。
下面是核心的 DP 代码:
int calc(int i, int k) { return dp[i - 1][k] - wks[i].p * k; } // in ( int main() ) for (int i = 1; i <= k; i++) { deque<int> q; for (int j = max(0, wks[i].s - wks[i].l); j <= wks[i].s - 1; j++) { while (!q.empty() && calc(i, q.back()) <= calc(i, j)) q.pop_back(); q.push_back(j); } for (int j = 1; j <= n; j++) { dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); if (j >= wks[i].s) { while (!q.empty() && q.front() < j - wks[i].l) q.pop_front(); if (!q.empty()) dp[i][j] = max(dp[i][j], calc(i, q.front()) + wks[i].p * j); } } }
整个单调队列优化会为后面的斜率优化做基础(因为我学这个单调队列优化就是有一道题要求使用斜率优化),所以请务必搞懂这个操作。
这是一道比较优秀的方案数 DP 题。乍一看以为是状压 DP,然后发现数据范围过大所以只能重新思考(也就是看题解)。我们可以写出状态格式:
\[dp[ln][opit][tpit],其中 ln 为行号,opit 代表第 ln 行内一个棋子的列数,tpit 代表第 ln 行内两个棋子的列数。\]
然后我们就可以根据不同情况来进行转移了。我们来看下面几组代码,其中每组都会有对应方程的意义。我们记\(dp[ln+1]\)为当前行号。首先,我们先要判定转移源的方案数不为\(0\),之后再进行转移。
// 不放置棋子的情况; // Situation without placing; dp[ln + 1][opit][tpit] = (dp[ln + 1][opit][tpit] + dp[ln][opit][tpit]) % MOD;
// 在不放置棋子的位置上放置一个棋子; // Situation of placing one on empty lines; if (M - opit - tpit >= 1) dp[ln + 1][opit + 1][tpit] = (dp[ln + 1][opit + 1][tpit] + dp[ln][opit][tpit] * (M - opit - tpit)) % MOD;
// 在放置了一个棋子的位置上放置一个棋子; // Situation of placing one on lines with only piece; if (opit >= 1) dp[ln + 1][opit - 1][tpit + 1] = (dp[ln + 1][opit - 1][tpit + 1] + dp[ln][opit][tpit] * opit) % MOD;
// 在两个没有放置棋子的位置上放置两个棋子,其中 C2(num) = C^num_2; // Situation of placing two pieces on the two empty slots; if (M - opit - tpit >= 2) dp[ln + 1][opit + 2][tpit] = (dp[ln + 1][opit + 2][tpit] + dp[ln][opit][tpit] * C2(M - opit - tpit)) % MOD;
// 在一个没有棋子的位置和一个有一个棋子的位置放置共两个棋子; // Situation of placing one on the empty and another on the only slot; if (M - opit - tpit >= 1 && opit >= 1) dp[ln + 1][opit][tpit + 1] = (dp[ln + 1][opit][tpit + 1] + dp[ln][opit][tpit] * (M - opit - tpit) * opit) % MOD;
// 在两个一个棋子的位置放置共两个棋子; // Situation of placing two on two only slots; if (opit >= 2) dp[ln + 1][opit - 2][tpit + 2] = (dp[ln + 1][opit - 2][tpit + 2] + dp[ln][opit][tpit] * C2(opit)) % MOD;
完整源码:
// P2051.cpp #include <iostream> #include <cstdio> #include <cstring> #define ll long long using namespace std; const int MX_N = 102, MOD = 9999973; ll dp[MX_N][MX_N][MX_N], N, M; ll C2(ll num) { return (num * (num - 1)) / 2; } int main() { scanf("%lld%lld", &N, &M); dp[0][0][0] = 1; for (int ln = 0; ln < N; ln++) for (int opit = 0; opit <= M; opit++) for (int tpit = 0; tpit + opit <= M; tpit++) if (dp[ln][opit][tpit]) { // 不放置棋子的情况; // Situation without placing; dp[ln + 1][opit][tpit] = (dp[ln + 1][opit][tpit] + dp[ln][opit][tpit]) % MOD; // 在不放置棋子的位置上放置一个棋子; // Situation of placing one on empty lines; if (M - opit - tpit >= 1) dp[ln + 1][opit + 1][tpit] = (dp[ln + 1][opit + 1][tpit] + dp[ln][opit][tpit] * (M - opit - tpit)) % MOD; // 在放置了一个棋子的位置上放置一个棋子; // Situation of placing one on lines with only piece; if (opit >= 1) dp[ln + 1][opit - 1][tpit + 1] = (dp[ln + 1][opit - 1][tpit + 1] + dp[ln][opit][tpit] * opit) % MOD; // 在两个没有放置棋子的位置上放置两个棋子,其中 C2(num) = C^num_2; // Situation of placing two pieces on the two empty slots; if (M - opit - tpit >= 2) dp[ln + 1][opit + 2][tpit] = (dp[ln + 1][opit + 2][tpit] + dp[ln][opit][tpit] * C2(M - opit - tpit)) % MOD; // 在一个没有棋子的位置和一个有一个棋子的位置放置共两个棋子; // Situation of placing one on the empty and another on the only slot; if (M - opit - tpit >= 1 && opit >= 1) dp[ln + 1][opit][tpit + 1] = (dp[ln + 1][opit][tpit + 1] + dp[ln][opit][tpit] * (M - opit - tpit) * opit) % MOD; // 在两个一个棋子的位置放置共两个棋子; // Situation of placing two on two only slots; if (opit >= 2) dp[ln + 1][opit - 2][tpit + 2] = (dp[ln + 1][opit - 2][tpit + 2] + dp[ln][opit][tpit] * C2(opit)) % MOD; } ll ans = 0; for (int i = 0; i <= M; i++) for (int j = 0; i + j <= M; j++) ans = (ans + dp[N][i][j]) % MOD; printf("%lld\n", ans); return 0; }
这道题目是我思考了很久的一道题目,可能是因为我太弱了吧。我们先来分析题目。
这一问是求一套系统能最大拦截的导弹数量。抽象化后,我们可以得出,其实这一问就是让我们求这个序列中最长的不上升序列。我们先来分析样例输入。
389 207 155 300 299 170 158 65
显然,对于这个序列,他的最长不上升序列是 \(389,300,299,170,158,65\)。那么,我们如何用最佳的算法求出此子序列呢?
我们先设定一个指向变量 cursor = 0
,这个变量记录当前算法在序列S中的位置。我们再设定一个 pos = 0
的变量,记录我们子序列H的尾部位置。
如果S[cursor]>H[pos]
,我们可以直接把 S[cursor]
置于 H
的尾部,即 H[++pos] = S[cursor]
。而如果不是这样,我们则可以在 H[0:pos]
,即 0
到 pos
这段区间内,总可以找到第一个小于 S[cursor]
的的量(事实上,我们可以使用二分搜索来查找这个量,因为序列 H
是拍好了顺序的),我们就让 H[bias] = S[cursor]
。反复如此。
为什么要把第一个在序列 H
中小于 S[cursor]
的赋为 S[cursor]
?因为当我们把这个值赋入后,我们后续需要插入数据的时候,便会更容易地加入数字(因为在相同位置下,值越大,后续加入的数字的限制也会缩小),也就会使这个子序列最长。
而如果我们要追求 \(\Theta(n \log n)\) 的做法时,便需要二分查找。接下来我来介绍两个函数。
lower_bound(start, end, key, [cmp])
该函数调用后会返回一个有序序列中第一个大于等于(如果有 cmp
函数传入,那就是另外一回事)key
的元素地址。
upper_bound(start, end, key, [cmp])
该函数调用后会返回一个有序序列中第一个大于(如果有 cmp
函数传入,那就是另外一回事)key
的元素地址。
奇怪的是,这两个函数跟字面上的完全不同,upper_bound
和 lower_bound
在没有 cmp
参数传入的情况下,默认的序列排序情况是一致的。搭配之后,便可以获得 \(\Theta(n \log n)\)的神奇速度。
在一个数字序列中,最大不上升子序列的个数为最大上升子序列的长度。亦而反之。
对于我这种蒟蒻而言,Dilworth 定理能帮助到我的只是这句话。我曾经翻阅过很多博客,也翻阅过《组合数学》,我并没有找到易于理解的语句。而在我翻阅到一篇绝世神作之后,我便把我的理解归于这一句话。对于 OIer 而言,Dilworth 最有力的部分便是这句话了。
第二问的疑问是求出导弹防御系统的套数,根据 Dilworth 定理,套数应该等于最大不下降子序列的长度。所以,按照第一问的贪心思路,我们很容易类比出本题求最大不上升子序列的贪心策略。所以,在这里,我便不再赘述。