2023.7.12 动态规划专题一
T1 [USACO20OPEN] Exercise G
首先我们要承认的一点是 这题我没看懂题干(悲
但是结合样例的话 我们大概可以猜测假如把 \(n\) 拆成 \(a_1 + a_2 + a_3 + ...\) 的形式 那么循环节就是这些数的最小公倍数
并且呢因为显然 \(n\) 是可以拆成 \((n - 1) + 1\) \((n - 2) + 1 + 1\) 等形式 这种的话对应的答案显然就是 \(n - 1\) \(n - 2\) 的答案 所以对于 \(n\) 而言 实际上它的答案是 \(1 \sim n\) 这些数的答案之和
那么新的问题就来了 就是这个计数有可能重 比如说我把 \(30\) 拆分成 \(12 + 18\) 那此时的答案和把 \(13\) 拆分成 \(4 + 9\) 的答案等一车东西 重了
那么为了不重 我们钦定只把这个数拆成类似于 \(p_1^{c_1} + p_2^{c_2} + p_3^{c_3} + ...\) 的形式 这样显然 \(gcd\) 就等于 \(\prod\limits_{i=1}^x p_i^{c_i}\) 就不会重
那么现在问题就转化为把一个数 \(n\) 拆分成 \(p_1^{c_1} + p_2^{c_2} + p_3^{c_3} + ...\) 的形式的方案数 我们先把要用的质数筛出来 然后用完全背包转移即可
答案即为 \(\sum\limits_{i =1}^n f_i\)
T2 [USACO20FEB] Delegation G
首先枚举 \(k\) 肯定是逃不掉的 但是我们可以优化一下 因为显然当 \(k\) 不为 \(n - 1\) 的因子时直接丢掉就好了
那么我们思考对于一个指定长度 \(k\) 如何判断它合不合法
我们考虑如果这个点上有这样一条链的话 要么它是儿子-它-儿子 要么是儿子-它-父亲(只有一条)
所以我们对每个点进行 \(dfs\) 并尝试将儿子进行配对
具体地 假如我 \(dfs\) 完了这个儿子的信息 寻找还没被匹配的正好和它加起来等于 \(k\) 的链 把它俩一起丢掉
否则把它也丢进待匹配的集合中
那么最后判待匹配的儿子个数就行了 \(0\) 显然合法 \(1\) 就说明它是过这个点往父亲连的一条链
否则就不合法了
具体可以拿 \(multiset\) 维护 这里贴一下 因为很多函数都是第一次见
int dfs(int x, int fa, int len) {
multiset<int> s;
for (int i = head[x]; i; i = nxt[i]) {
int y = to[i];
if (y == fa) continue;
int ans = dfs(y, x, len);
if (ans == -1) return -1; //如果搜到了不合法情况
++ans; //加上儿子和自己连的那条边
ans %= len;
if (ans == 0) continue; //如果正好用完
if (s.count(len - ans)) s.erase(s.find(len - ans)); //寻找能配对的链的出现次数(count) 并删除(find返回len - ans的迭代器)
else s.insert(ans); //否则把它丢进去
}
if (s.size() == 0) return 0; //全配对上了
if (s.size() == 1) return *s.begin(); //注意begin返回的是迭代器
else return -1; //否则不合法
}
T3 Tiles for Bathroom
Konata:对于二维的东西 我们思考一下一维怎么处理
我们考虑这样一件事 假如我知道 \(\left[l, r\right]\) 这段区间内不同元素的个数恰好为 \(p\)
那么实际上 \(l \sim l + 1\) \(l \sim l + 2\) 直到 \(l \sim r\) 这些的区间都满足不超过 \(q\)
所以我们找到恰好为 \(q\) 的最长区间即可 这个东西显然用双指针就能干掉 但是双指针这个东西...扩展到二维会非常的爆炸
然后我们发现这题的 \(q\) 很小 思考有没有一种和 \(q\) 有关的做法
那么我们可以枚举右端点 然后维护离它最近的前 \(q\) 种颜色的位置和颜色 这样每次右端点++的时候就可以比较方便地转移了
进而把这个做法扩展到二维 我们枚举右上角 然后以层为关键字排序后取前 \(q + 1\) 种颜色
然后往右上角移动并进行转移 这里用一下luogu题解的图


如图 你把黄色线段的颜色丢进去更新即可
T4 Karen and Supermarket
(说句闲话 做的时候就感觉这个妹子真好看 后来搜了一下那场比赛 发现每道题都有这个妹子 给大家贴两张


upd:原来这个妹子叫九条可怜
好了现在说正经的
首先这题优惠券的依赖结构长得就像一棵树 所以就建成一棵树考虑在树上 \(DP\)
并且我们发现 对于此题 把商品数量放到状态里 把花费换成 \(f\) 表示的东西相对好做
然后就会发现这个的转移非常像树上背包
并且我们注意到优惠券可以使用也可以不使用 所以我们还需要设一个 \(0/1\) 来表示 不使用/使用 这个节点的优惠券
我们设 \(f_{i, j, 0/1}\) 表示第 \(i\) 个点 买 \(j\) 件物品 不使用/使用 优惠券的最小花费
那么我们就有
-
\(f_{i, j, 1} = min(f_{i, j - k, 1} + f_{son_i, k, 1})\)(儿子买 \(k\) 个 并且儿子用优惠券)
-
\(f_{i, j, 1} = min(f_{i, j - k, 1} + f_{son_i, k, 0})\)(儿子买 \(k\) 个 并且儿子不用优惠券)
-
\(f_{i, j, 1} = min(f_{i, j - k, 0} + f_{son_i, k, 0})\) (儿子买 \(k\) 个 并且儿子不可以用优惠券)
初值就是
- \(f_{i, 1, 1} = c_i - d_i\)
- \(f_{i, 1, 0} = c_i\)
- \(f_{i, 0, 0} = 0\)
统计答案把 \(0/1\) 都算上即可
注意一下 \(O(n^2)\) 的树上背包写法:
for (int i = head[x]; i; i = nxt[i]) {
int y = to[i];
if (y == fa) continue;
dfs(y, x);
for (int j = siz[x]; j >= 0; --j) { //注意写法 不然复杂度会退化
for (int k = 0; k <= siz[y]; ++k) {
f[x][j + k][1] = min(f[x][j + k][1], f[x][j][1] + f[y][k][1]);
f[x][j + k][1] = min(f[x][j + k][1], f[x][j][1] + f[y][k][0]);
f[x][j + k][0] = min(f[x][j + k][0], f[x][j][0] + f[y][k][0]);
}
}
siz[x] += siz[y];
}
T5 Tree Elimination
为啥我会想不开把这玩意写了
很新的一个做法 实际上序列数就是消除标记的方案数 那么对于一条边 我们可以选择消两边任何一个点嘛
那么我们考虑对于一个点 它如果被消掉了 那就要么被父亲边消了 要么被儿子边消了
考虑转移 假设 \(x\) 是被父亲边消掉的 那么就要求所有编号比父亲边小的儿子边对应的儿子都要被父亲边消掉(不然它自己就没了) 编号比父亲大的就无所谓了
所以我们不妨把“被儿子边消掉”细分一下 变成“在父亲边之前被儿子边消掉”和“在父亲边之后被儿子边消掉”
设 \(f_{i, 0/1/2/3/4}\) 表示 \(i\) 被父亲边之前的儿子边消掉/被父亲边消掉/被父亲边之后的儿子消掉/没被消掉
那么就有

它被这个儿子删掉 就需要这个儿子被父亲边之后的边删/没被删 之前的儿子都不能把父亲删掉 所以要么被父亲删要么在父亲边之前就被删 后面的不能被父亲边删掉(因为父亲已经没了)
这个转移注意如果这个儿子边在父亲边之前 就转移 \(0\) 否则转移 \(2\)

这个就是最开始说的那个

哪个儿子都不能把把它删掉 所以要么被父亲删要么在父亲边之前就被删
具体转移方法就见仁见智了 我那个写法是先 \(dfs\) 然后维护前缀/后缀积再枚举一遍儿子进行转移
T6 Miss Punyverse
首先第一步把点权减一下 判块的权值和 \(>0\)
然后我们设 \(f_{i, j}\) 表示以 \(i\) 为根的子树分成了 \(j\) 块 合法的块数的最大值
然后我们考虑 因为带根节点的那个连通块有可能要向上合并 所以我们要让此块的大小越大越好 所以我们还要开个 \(g\) 数组来记录这个最大值
然后就是个树上背包的转移

然后因为带根节点那块(即你 \(g\) 数组记录的那玩意)有可能权值是负的 所以 \(f\) 数组里记录的合法连通块是不包含根节点那个连通块的 统计答案的时候直接判 \(g\) 是否 \(>0\) 然后 \(+1\) 即可
那初值就是 \(f_{i, 1} = 0\) \(g_{i, 1} = val_i\)
T7 [NOIP2017 提高组] 宝藏
数据范围一眼状压 想到类似哈密顿路的 \(DP\) 转移 结果假了
然后发现因为这题转移的代价和在这棵树里的深度有关 所以我们考虑把深度加进状态里
设 \(f_{i, j}\) 表示深度为 \(i\) 当前连通状态为 \(j\) 的最小花费
那我们就有 \(f_{i, j} = min(f_{i - 1, k} + trans_{k, j} * (i - 1))\)
其中 \(k\) 为 \(j\) 的子集 \(trans_{k, j}\) 表示这次转移的花费
所以我们要对每个状态枚举子集 并确定其转移的最小花费
具体可以参考一下luogu题解代码:
for (int i = 0; i < m; ++i) {
for (int j = i; j != 0; j = (j - 1) & i) { //枚举子集小技巧
bool OK = true; //能转移到
int temp = i ^ j; //这一层要转移的点
for (int k = n - 1; k >= 0; --k) {
if (temp >= (1 << k)) { //k是这层要转移的点
int tmin = 0x3f3f3f3f;
for (int o = 1; o <= n; ++o) {
if (((1 << o - 1) & j) == (1 << o - 1)) tmin = min(tmin, dis[o][k + 1]); //如果o在j里
}
if (tmin == 0x3f3f3f3f) { //转移不到
OK = false;
break;
}
trans[j][i] += tmin;
temp -= (1 << k);
}
}
if (OK == false) trans[j][i] = 0x3f3f3f3f;
}
}
T8 修缮长城 Fixing the Great Wall
首先第一步肯定是排序
然后我们发现一件事 就是已经修复的点一定是一段连续的区间(因为修复点不需要时间 假如说你把那个左端点修了肯定这一段路上的顺路也修了)
所以我们可以设 \(f_{l, r}\) 表示已经修复 \(\left[l, r\right]\) 这段区间的最小费用
然后发现这题还需要增加一维时间 但是我们又不能把它加到状态里 不然就炸了
那我们考虑这样做:
把“未来肯定会发生的费用”也加进这个 \(f\) 数组里
具体来说就是 因为所有点最后都是要修的嘛 那么假如说你花了 \(t\) 的时间新修了一个点
那所有没修的点的修缮费用都会加上 \(t * val_i\) 我们把这个费用也加到 \(f\) 数组里
然后考虑转移 假如我们当前的修好区间是 \(\left[l, r\right]\) 那么我们可以修 \(l - 1\) 那个点也可以修 \(r + 1\) 那个点
那我们就要算出到达这个点所需的时间 然后再加上这个点的修缮费用还有上面说的那个“未来可定会发生的费用”
所以我们还需要知道目前在哪
进而我们发现 假如 \(\left[l, r\right]\) 修完了 那么显然我要么在 \(l\) 那要么在 \(r\) 那
所以我们再开一维 \(0/1\) 表示当前在区间的左端点/右端点
那往左走 我们就有:
-
\(f_{l - 1, r, 0} = f_{l, r, 0} + c_{l - 1} + (x_l - x_{l - 1}) / v * (\sum\limits_{i = 1} ^ {l - 1} d_i + \sum\limits_{i = r + 1} ^ n d_i)\)
-
\(f_{l - 1, r, 0} = f_{l, r, 1} + c_{l - 1} + (x_r - x_{l - 1}) / v * (\sum\limits_{i = 1} ^ {l - 1} d_i + \sum\limits_{i = r + 1} ^ n d_i)\)
这俩取 \(min\)
往右走就同理了
T9 [省选联考 2021 A/B 卷] 滚榜
首先数据范围很状压 那首先我们就有一维状态是当前已经公布分数的集合
然后因为我们要让下一个反超当前的 所以还要记当前的编号
又因为要求所有 \(b_i\) 之和为 \(k\) 所以还要记目前已经用了多少 \(b\)
然后因为单调不降 还要记给当前这个分配了多少 \(b\)
然后就时间空间双炸了
我们考虑压掉一维 发现有可能对于不同的 \(b_i\) 分配方案 队伍的出现顺序可能是一样的
那我们就考虑一个最优的 \(b_i\) 分配方案 即尽量少的给它分配 最后剩下的给最后一次分配即可
那么显然就要分配 \(max(a_{i - 1} - a_i, 0)\)
然后借鉴上一道题转移的思路 因为要求 \(b\) 单调不减 所以假如说你给 \(i\) 分配了 \(b_i\) 那么后面的 \(i + 1 \sim n\) 这些点都需要分配 \(b_i\)
所以我们直接把这些差值提前直接加到那个记录 \(b_i\) 的维即可
T10 [NOI Online #3 提高组] 优秀子序列
首先我们考虑把每个数都拆分为一个二进制的集合 那么题意实际就是选择若干个不相交的集合 求它们的并
那么我们设 \(f_i\) 表示当前这些二进制集合的并为 \(i\) 的方案数
然后我们枚举加进去的点 判不相交然后转移即可
欧拉函数预处理出来就行

浙公网安备 33010602011771号