2020动态规划刷题
动态规划:
1、洛谷P5017摆渡车(noip2018PJT3)
先瞄一眼每个人到的时间的范围:\(0\leq t \leq4\times10^6\)。果然,这道题可以以时间为状态进行\(dp\)
我们以发车时间做为状态,\(f[i]\)表示在第\(i\)分钟发了一辆车
转移:\(f[i]=min\{f[i],f[j]+等待时间(j+1\to i)\}\)
\((i - 2\times m \leq j \leq i - m)\)
\(\because j是上次发车的时间,设k为上次发车后车回来的时间\)
\(\therefore 易知k=j+m\)
\(\because 当k小于i-m时,必定可以先发车在回来一次,答案一定更优\)
\(\therefore i-m \leq k \leq i~ 即 ~i-2\times m \leq j \leq i-m\)
注意:这里等待时间应该是\(j+1\)到\(i\),因为如果在\(j\)时到达车站,可以坐上那部车
因为只是动态规划就用了近\(O(mt)\)的时间,所以必须保证\(O(1)\)的等待时间查询,这项工作我们用前缀和来做,先求到这个时间点的人数总和,再求到这个时间点的到达时间总和,等待时间即为\((区间人数\times发车时间-区间时间总和)\)
但是\(O(mt)\)不足以通过这道题,我们要继续剪枝。
考虑此时发车时间为第\(i\)分钟,若在发车前的等待时间里(即\(i - m\)分钟到\(i\)分钟,前面证明了若发车等待时间超过\(m\)分钟那么答案一定可以更优)没有任何人来,那么\(f[i]\)直接等于\(f[i-m]\)就可以了。实现了剪枝。
2、洛谷P2679子串
令\(f[n][m][k]\)为\(A\)字符串到第\(n\)位,\(B\)字符串到第\(m\)位,总共有\(k\)个子串,有多少种情况。
可得\(dp\)方程:
\(f[n][m][k] = f[n-1][m][k]~~~(上一个不选)\)
\(~~~~~~~~~~~~~~~~~~~+f[n-len-1][m-len-1][k-1]~~~~~\)
\((1\leq n\leq min(n,m)-1且A中以n为结尾len为长的字符串与B中以m结尾len为长的字符串一致)即选一个len长的子串\)
可以写出这样的程序:
f[0][0][0] = 1;
for (int i = 1; i <= n; i++) {
f[i][0][0] = 1;
for (int j = 1; j <= m; j++) {
for (int s = 0; s <= k; s++) {
f[i][j][s] += f[i - 1][j][s] % mod;
for (int kk = 0; kk <= min(i, j) - 1; kk++) {
if (a[i - kk] == b[j - kk])
f[i][j][s] = (f[i][j][s] + f[i - kk - 1][j - kk - 1][s - 1]) % mod;
else break;
}
}
}
}
观察到一旦子串不匹配后面更大的子串也不匹配,所以,可以用前缀和求出。
f[0][0][0] = 1;
for (int i = 1; i <= n; i++) {
f[i][0][0] = 1;
for (int j = 1; j <= m; j++) {
for (int s = 0; s <= k; s++) {
f[i][j][s] += f[i - 1][j][s] % mod;
if (a[i] == b[j])
sum[i][j][s] = (sum[i][j][s] + sum[i - 1][j - 1][s] + f[i - 1][j - 1][s - 1]) % mod;
else sum[i][j][s] = 0;
f[i][j][s] = (f[i][j][s] + sum[i][j][s]) % mod;
}
}
}
因为\(n\)的值很大,会爆内存,观察到\(i\)只会从\(i-1\)推得,故滚动数组即可,在滚动过程中要记得清零\(sum\)数组。
3、洛谷P1941飞扬的小鸟
这道题的细节蛮多的,具体见代码,在这里只说下大致思想。
状态转移方程也是很好想的:设\(f[n][m]\)为到\(n\)列\(m\)行最少的点击次数(设左下坐标为\(f[0][0]\))。
一个是通过下降转移,一个是通过上升转移。
我们需要特判\(m\)为天花板的情况,因为即使在\(m_{max}-up[n-1]<m'\leq m_{max}\)时,我们也可以通过一次点击来使\(m'\)达到天花板。
这样就能复杂度为\(O(nm^2)\),不足以通过此题。
观察到\(f[n][m]=min(f[n][m],~f[n-1][m-up[n-1]]+1,~f[n-1][m-2\times up[n-1]]+2,~......)\),找到规律,于是可以事先通过\(f[n][m]=min(f[n][m],~f[n][m-up[n]]+1)\)来优化从下往上飞多段的情况(相当于多重背包),就不需要像之前那样循环\(k\)来比较了,直接\(f[n][m] = min(f[n][m],~f[n-1][m-up[n-1]]+1)\)即可。
将复杂度优化到了\(O(nm)\),可以通过此题。
4、洛谷P1280尼克的任务
这题显然是一个线性动规,那么肯定是第一时间想到设\(f[i]\)为\(1\)到\(i\)的最大空闲时间,但是,想了一下之后发现,第\(i\)时刻的最大空闲时间是和后面选择任务的持续时间的时刻有关系的,那么,正着找肯定是不行的,我们来试一下倒着搜,经尝试,发现是完全可行的,可以列出动态转移方程如下:
(本时刻无任务)\(f[i]=f[i+1]+1\);//继承上一个时刻的最大空闲时间后+1
(本时刻有任务)\(f[i]=max(f[i],f[i+a[sum])\)//a[sum]表示在这个时刻的任务的持续时间,找出选择哪一个本时刻任务使空闲时间最大化
5、洛谷P2701巨大的牛棚
题意:给定一个01棋盘,求其中全为1的最大正方形边长。
方程:\(f[i][j]=min(min(f[i][j-1],f[i-1][j]),f[i-1][j-1])+1;\)
注意这里是\(min\),因为对于一个边长,若与当前点相邻的点的最大边长小于它,显然这个边长是不可取的。
6、洛谷P1472奶牛家谱
有时候计算一下无用的状态反而是有用的。
我们不妨设\(dp[i][j]\)表示\(i\)个点小于等于\(j\)层的方案数,那么最终我们所需的答案就是\(dp[n][k]-dp[n][k-1]\)。
于是解法就显而易见了:枚举一个\(t\),表示分\(t\)个点给左子树,剩下\(i-t-1\)(除去树的根)分给右子树。
方程为:\(dp[i][j] = \sum_{t = 1}^{i-2}dp[t][k-1]\times dp[i-t-1][k-1]\),由于每个点的度为\(0\)或\(2\),所以\(t\)每次加\(2\)。
7、洛谷P1052过河
这是一道路径压缩动态规划,直接列出方程\(f[i] = min(f[j])+is\_stone[i]\) 满足\(i-t\leq j \leq i -s\)。
可是观察到总路程\(1\leq L\leq10^9\),无法作为数组下标,但石子数\(1\leq M\leq 100\)却非常小。所以可以得到路径压缩的思想:先离散化路径,再\(dp\)。
具体来说就是,当两点间的距离\(dis\)大于\(t\)时,一定可以由\(dis\%t\)跳过来,对于每块石头,都只要计算左右两边距离都为\(t\)的路径即可。所以只需要\(t+dis\%t\)种距离的状态就可以表示这两个石子之间的任意距离关系。这样就把题目路径压缩成了\(2\times t\times m\)最多不超过\(2000\),然后就可用\(dp\)了。又因为\(dp\)的终点是一个范围而非确切的一个点,最后还要在这个范围内取最小值
本题值得复习
8、洛谷P1879玉米田
观察到数据范围很小,所以考虑用状压\(dp\)。
我们可以用一个数组表示每行所有的可用草地的集合,到时候只需要判断目标集合是否是它的子集就知道可不可行了;再用一个数组判断每个状态是否满足没有连续的两块草地的条件,判断方法就是把这个二进制数左移一位与,然后右移一位与。如果这个状态是合法的,那么都应该返回 \(0\)。
然后就开始动规,在每行里找所有状态,如果这个状态是合法的,且不会在贫瘠的草地上,那么接下来开始找上一行的合法情况(上下两行之间没有相邻的草地),把上一行的情况数加到这个状态的答案里。
最后输出最后一行所有状态答案的和。
9、洛谷P1896互不侵犯
这道题既然数据范围小,所以是状压\(dp\)。
方程需假设 \(f[n][k][s]\) 为到第 \(n\) 行,当前用了 \(k\) 个国王(若不加这个状态则无法转移),当前行状态为 \(s\) 的方案数(\(1\) 为放国王)。方程:\(f[n][k][s] = \sum f[n - 1][k - popcount(s)][lasts]\) 其中 \(lasts\) 为上一行可能的状态,且该状态不与本行冲突。我们为了优化时间复杂度,先预处理出每个状态上一行所对应的所有可能状态。先特判第一行的情况,答案即为最后一行所有状态的和。
10、洛谷P1040加分二叉树
这道题是一个区间 \(dp\)。\(f[i][j]\) 表示以 \(i\) 到 \(j\) 节点作为一颗子树的最大加分(因为中序遍历为 \(1\) 到 \(n\),所以一个子树的结点编号是连续的),另设一个 \(rt[i][j]\) 记录该区间内的根结点,以便于输出先序遍历。
转移方程即是 \(f[i][j]=max(f[i][k-1]*f[k+1][j]+f[k][k])\)
这道题建模的思想还是比较妙的。
11、洛谷P3648/bzoj3675序列分割
这道题先要看出,答案和分割顺序无关。
如果我们有长度为 \(3\) 的序列 \(x,y,z\) 将其分为 \(3\) 部分,有如下两种分割方法:
- 先在 \(x\) 后面分割,答案为 \(x(y+z)+yz\) 即为 \(xy+yz+zx\)。
- 先在 \(y\) 后面分割,答案为 \((x+y)z+xy\) 即为 \(xy+yz+zx\)。
设 \(f[i][k]\) 为前 \(i\) 个元素,分为 \(k\) 块,且最后一刀切在第 \(i\) 个元素后,最大的收益。
则稍加思考,列出方程为:\(f[i][k] = max_{0 \leq j < i} \{f[j][k - 1] + sum[j]\times (sum[i] - sum[j]) \}\)
再将 \(max\) 改为 \(-min\{- \}\) 即可,发现此时符合斜率优化的要求。
复杂度 \(O(nk)\)。
12、HDU2196
求离每个节点最远的点的距离。
换根 \(dp\),先算出到子树中的最长距离(注意:要求出一个 \(best\) 一个 \(second\))。然后再统计向上路径中的最长距离,具体是当前节点到之前祖先的距离加上祖先到子树中的最长距离。
13、洛谷P2657windy数
数位 \(dp\) 入门题
我们用 \(f[i][j]\) 表示从高到低位搜到第 \(i\) 位,上一位数是 \(j\) 的情况下的方案总数。我们可以处理出 \(sum[A - 1]\) 和 \(sum[B]\),最终答案即为 \(sum[B] - sum[A - 1]\)。
这道题有两个难点,① 因为我们要求一个数以下所有符合条件的数的个数,所以我们搜索的数允许有前导零;
② 不能超过给定那个数最高位的限制(特殊地,\(solve(43278)\),如果我们顺着考虑第一位为 \(4\),第二位为 \(3\),也就是在上限上走,那么讨论的第三位就一定 \(\leq2\) (因为第三位为 \(2\),而我们讨论的数不能超过 \(43278\)))。
考虑如何转移:记录当前位数,上一位数,是否有前导零,是否有最高位限制。进行记忆化搜索即可。
注意:记忆化的时候只记忆不受前导零和最高位限制的答案。
14、洛谷P2327扫雷
这道题的难点在于动态规划方程,设 \(f[i][0/1][0/1]\) 表示前 \(i\) 行,这一行是否埋雷,下一行是否埋雷的方案数。
先将 \(f[0][0/1][0/1]\) 全部设为 \(1\)。然后分类讨论:
15、洛谷P2577午餐
容易想到贪心:吃饭慢的先打饭节约时间, 所以先将人按吃饭时间从大到小排序。
证明:因为所有人打饭时间是节省不了一点的,所有只能让吃的慢的先盛饭这样节约时间。
下面就是 \(dp\) 了,状态和转移都比较难想。
首先,应该想到 \(f[i][j][k]\) 表示前 \(i\) 个人,在 \(1\) 号窗口打饭总时间 \(j\),在 \(2\) 号窗口打饭总时间 \(k\),这样所用最少时间。当然,这样会爆空间,所以想到去掉一维。
原方程可以改为 \(f[i][j]\) 表示前 \(i\) 个人,在 \(1\) 号窗口打饭总时间 \(j\),所用最少的时间。
我们可以发现 \(j+k=\text{前i个人打饭总和}\),\(k = sum(i)-j\)。 所以可以省掉一维。
前面式子表示当前人去 \(1\) 号窗口打饭的情况,若这个人打饭加上吃饭的时间还不如前面人打饭加上吃饭的时间多,那就取后者,反之取前者转移。后面式子同理,表示当前人去 \(2\) 号窗口打饭,其余一致。
16、洛谷P1850 换教室
期望 \(dp\)
容易想到设 \(f[i][j][0/1]\) 为到了第 \(i\) 次课,使用了 \(j\) 次机会,当前在 \(0\) 还是 \(1\) 号教室,最小的期望值。
首先,根据性质,期望的和等于和的期望,我们分开讨论每两次课之间的体力值,然后加起来就是答案。
17、hdu3001
这道题是三进制状压的模板题
设 \(f[i][s]\) 为现在在 \(i\) 点,目前访问的节点情况是 \(s\)(每一位 \(0\) 表示没有访问过,\(1\) 表示访问过一次,\(2\) 表示访问过两次)
状态转移就是 \(f[j][s + \{j\}] = min\{f[i][s] + dis[i][j]\}\) (当且仅当 \(s\) 中 \(j\) 点访问次数小于 \(2\))。考虑初始化,使用 \(bit[i]\) 先将访问每个点一次的状态写出来(\(3^i\)),于是 \(f[i][bit[i]] = 0\) 即可。
三进制无法像二进制一样使用位运算来取得每一位的值,于是初始化 \(digit[s][i]\) 表示状态 \(s\) 中第 \(i\) 位的数,这样就可以像二进制一样判断了。
注意:\(dp\) 中先枚举 \(s\) 再枚举 \(i, j\)。
18、洛谷P4401矿工配餐
这道题妙在设 \(dp\) 方程上。设 \(f[i][a][b][c][d][0/1]\) 表示到了第 \(i\) 个餐车,\(a、b\) 为不包括当前这个餐车送往 \(1\) 号矿场的餐车前一个为 \(a\) 类车,再前一个为 \(b\) 类车(若类型为 \(3\) 则说明没有),\(c、d\) 为送往 \(2\) 号矿场的餐车,定义同理。最后一维则表示当前餐车送往 \(1\) 号矿场还是 \(2\) 号矿场。
可以列出方程:
还有一种做法是设 \(f[i][a][b][c][d]\) 表示包括当前餐车的结果,简便一些。
19、洛谷P5888
设 \(f[n][m]\) 为经过 \(m\) 次传到 \(n\) 号的方案数,容易列出状态转移方程 \(f[n][m] = \sum\limits_{i\text{可以传给n}}f[i][m - 1]\)。
可是这样发现 \(n\) 的大小太大,无法转移。于是考虑只求传到受到限制的点的方案数,因为不受限制的都可以被剩下 \(N - 1\) 个人传到即可以直接求出。具体地,离散化一下受到限制的人的号码(把 \(1\) 号也加入,因为方便直接输出 \(1\) 号的 \(dp\) 值作为答案),枚举第 \(i\) 次传球,求出第 \(i\) 次传球后不受限制的总方案数(上一次传球后受限制的总方案数 \(\times(n - 1)\)),则 \(f[n][m]\) 为不受限制传到 \(n\) 的方案数(上一次传球后受限制的总方案数 \(-f[n][m - 1]\) 即可以传到 \(n\) 的方案数 \(\times 1\))\(-\sum\limits_{i\text{不能传给n}}f[i][m - 1]\)。同时可以计算出本次传球后受限制的总方案数。
这题是一道很好的正难则反的动归题
20、洛谷P6478 游戏
首先,我们设 \(f[u][k]\) 表示在 \(u\) 的子树下,选择一共 \(k\) 对不平局的点,有多少种情况,很显然转移式子可以列为:
最后将当前子树的根节点 \(u\) 和下面不同类型的点进行匹配,譬如当 \(u\) 为 \(A\) 集合中的点,\(B[u]\) 表示 \(u\) 下有多少 \(B\) 集合的点,那么有 \(f[u][k + 1] = f[u][k] \times (B[u] - k)\)。
如此,我们能够 \(O(n^2)\) 求出数组值,设 \(F(k) = f[1][k] \times(\dfrac{n}{2}-k)!\),后面乘以一个剩余点数的阶乘,表示大于等于 \(k\) 对的所有情况。然而,我们能够发现这个 \(F(k)\) 其实是有重复的,我们有两种方式解决这个问题:
- 设 \(ans(k)\) 表示恰好有 \(k\) 対的情况数,那么,根据二项式反演,可得:
- 同样是上面的逻辑,发现每个 \(ans(i)\) 只与 \(ans(j)\) 其中 \(j\ge i\) 有关,显然我们可以倒序求出每个 \(ans(i)\),完全不需要二项式反演
21、poj3783 鹰蛋
这题先给出朴素算法,然后引出一个更优的套路算法。
算法一:设 \(f[n][m]\) 表示 \(n\) 层楼,\(m\) 颗蛋,至少需要多少次才能检测出临界值,那么容易有如下转移方程式:
在这个算法中,我们枚举 \(k\) 层楼作为临界值,然后向上或者向下继续寻找,时间复杂度 \(O(n^2m)\)
算法二:设 \(f[m][k]\) 表示现在有 \(m\) 颗蛋,扔 \(k\) 次,最多能够确定多少层楼的临界值,那么可以获得转移:
如果蛋碎了,那么剩下的蛋能确定的层数是第一项,如果没碎就是第二项。随后只需要在求答案时二分出这个 \(k\) 即可。
22、洛谷P6622 [省选联考 2020 A/B 卷] 信号传递
这道题可以先设 \(f[n][S]\) 表示前 \(n\) 个位置,使用了 \(S\) 这些信号站,答案最小为多少。
则容易有 \(f[n][S \bigcup \{x \}] = \max\{f[n - 1][S] + g[x][S] \}\),其中 \(g[x][S]\) 表示在前面位置用了 \(S\) 信号站的情况下,当前位置用 \(x\) 信号站对答案的贡献。观察到 \(g[x][S]\) 只可能从 \(S\) 中 \(1\) 个数少的更新到个数多的,于是可以压缩状态。具体地,我们这里可以使用一个经典结论:二进制从 \(0\) 数到 \(n\),所有比特位变化的总次数为 \(O(n)\)。我们直接去掉 \(g\) 的 \(S\) 那一维。然后按二进制数大小顺序枚举 \(S\),并按照 \(S+1\) 相对 \(S\) 的变化暴力修改 \(g\)。根据上述结论,我们对每个比特位的变化,修改 \(g\) 的代价是 \(O(m)\)。那么总修改代价就为 \(O(m2^m)\)。
23、[USACO11FEB]Best Parenthesis
本题和括号树的思路很像,考虑维护一个 \(f[n]\) 表示以 \(n\) 为右端点的子段的值是多少,使用栈来维护即可。
本题还有一个巧妙的做法,考虑分治计算,记 \(solve(l, r)\) 表示 \([l, r]\) 的答案,若 \(l, r\) 恰好匹配,则 \(solve(l + 1, r-1)\),否则分治下去。
24、洛谷P2592 [ZJOI2008]生日聚会
设 \(f[n][m][x][y]\) 表示有 \(n\) 个男孩,\(m\) 个女孩,后缀中男孩比女孩多 \(x\) 人,女孩比男孩多 \(y\) 人有多少种情况。
注意本题用当前更新下一状态比较容易
25、洛谷P3622 动物园
这是一道隐藏的非常深的状压 \(dp\) 题,我们注意到每个小朋友能观察到的区间长度只有 \(5\),于是可以考虑将动物保留与否压缩成二进制。我本来想是对于每个小朋友进行 \(dp\),但这样实现太麻烦了,不如先预处理出以每个位置开始的五个动物保留与否能使多少小朋友高兴,然后再按照位置 \(dp\),在 \(dp\) 过程中,我们会发现最后几个位置的状态会回到刚开始的状态,因此我们先枚举刚开始的状态再 \(dp\),时间复杂度 \(O(2^{10}n)\)
26、uva1252 twenty questions
这是一道很好的状压 \(dp\) 题,状态的设计十分巧妙。
假设当前询问的是 \(W\),设 \(f[S_1][S_2]\) 表示目前已经询问了 \(S_1\) 位置,确定了 \(S_2\) 在 \(W\) 中,还需要询问的次数。
边界情况是当只有一个物品满足具备 \(S_2\) 特性却不具备 \(S_1-S_2\),此时已经能够判断出来了,\(f[S_1][S_2] = 0\)。于是我们只需要预处理 \(cnt[S_1][S_2]\) 表示有多少物体满足条件即可。
27、uva1439 exclusive access
本题题意不是特别好懂。简要题意:给定一张无向图,要求将无向边定向,使得 \(DAG\) 的最长路最短。
我们可以转化一下模型,将 \(DAG\) 剥为一层一层的,这可以理解为图的色数问题,即将一张无向图黑白染色,要求相同颜色的点之间没有边,且颜色数最少。这个经典模型可以用状压 \(dp\) 解决,时间复杂度 \(O(3^n)\)。现在考虑如何输出方案,只要记录每个点的层数,然后只允许层数低的连向层数高的。