【笔记】动态规划 2023.12.16
笔记 2023.12.16:动态规划
- 笔记 2023.12.16:动态规划
- LOJ6089 小 Y 的背包计数问题
- LOJ3776 Uplifting Excursion
- P5336 [THUSC2016] 成绩单
- AGC035D Add and Remove
- *AGC039E Pairing Points
- CF1517F Reunion
- AGC034E Complete Compress
- P7897 [Ynoi2006] spxmcq
- CF1326G Spiderweb Trees
- *CF1456E XOR-ranges
- *CF1158F Density of subarrays
- *CF1466H Finding satisfactory solutions
- [CEOI2017] Building Bridges
- gym104821D Red Black Tree
今天题目很多,可能有些题不口胡了。
LOJ6089 小 Y 的背包计数问题
前 \(\sqrt n\) 个物品直接做单调队列优化是 \(O(n\sqrt n)\)。
大于 \(\sqrt n\) 的是完全背包。考虑到完全背包 \(v\) 的 OGF 为 \(\dfrac{1}{1-x^{v}}\)。这不行。
你考虑到对于一个物品序列,一个物品可以看作是先加入 \(\sqrt n\),然后不断加一。对于一个序列,就是加入 \(\sqrt n\) 和 \(+1\) 不断进行,不需要交错,所以 \(f_{i,j}\) 表示放进去 \(i\leq \sqrt n\) 个物品,\(j\) 是总体积,那么 \(f_{i,j}=f_{i-1, j-\sqrt n}+f_{i, j-i}\)。\(O(n\sqrt n)\)。
两边的背包可以轻易合并到 \(n\)。
总结:可以改变状态定义,尤其是根号分治的时候把小的一维扔进去,这里就是物品数量 \(\leq \sqrt n\)。
LOJ3776 Uplifting Excursion
一开始暴力加入所有负数,然后从小到大加入正数,直到 \(>L\);如果整个都选了,删除绝对值最大的负数,如果还是 \(\leq L\) 直接无解跑路。现在重量 \(W\in[L, L+m)\)。考虑进行调整。
- 调整就是删除一些数字,加入一些数字,观察到你可以通过调整调整的顺序使得 \(W\in[L-m, L+m]\)。非常简单,\(W<0\) 就做加的,\(W>0\) 就做减的,因为它们的绝对值 \(\leq m\)。
- 如果 \(W\) 两次到达同一个地方,说明中间跨过这些数字都是无用的,可以删掉,于是认为 \(W\) 只会在 \([L-m, L+m]\) 之间横跳且不会重复。\(\implies\) 物品数量 \(\leq 2m+1\)。
- 所以我们对加入和删除的物品,直接 dp 到容积 \([-2m^2, 2m^2]\) 即可。\(O(m^3)\)。
P5336 [THUSC2016] 成绩单
\(f_{l, r}\) 表示删完区间 \([l, r]\) 的答案,\(g_{l, r, mn, mx}\) 用于辅助求 \(f\),转移分为 \(r+1\) 是归进最外面一层还是在里面删除。
AGC035D Add and Remove
\(a_1, a_n\) 是删不掉的。考虑最后一次删了 \(a_x\),那么就是说答案是 \(a_1+a_n+2a_x\) 注意这个 \(2\) 很关键。我们定义神秘的状态 \(f(l, r, fl, fr)\) 表示 \(l-1\) 最终会以神秘姿态翻个 \(fl\) 倍计入答案,我不知道它是怎么算的,但是我要求是 \([l, r]\) 删完以后你要把贡献打在 \(l-1\) 上;\(r+1\) 会翻 \(fr\) 倍,我同样不知道它是怎么做到的,但是你要使得最终答案最小。这样就可以算:
其中 \(*, \circ\) 我待定系数,仔细考察一下系数啊,这里 \(k\) 会翻 \(fl+fr\) 倍计入答案,因为它被打到左边和右边都算。
声称是 \(O(2^n)\)。
*AGC039E Pairing Points
CF1517F Reunion
枚举答案 \(< r\),意思是黑点距离 \(r\) 以内必有一个白点,考虑树形 DP,点 \(u\) 所在子树内最浅的白点和最深的黑点的深度都记下,然后可以合并。考虑到子树内如果黑点已经覆盖完了就不用记黑点,否则这个白点没有覆盖掉最深的黑点,覆盖最深黑点的白点在外面,此时它已经劣了,向上走没用。所以只需要记一个。\(O(n^3)\)。
AGC034E Complete Compress
枚举最终全部到达根 \(rt\)。树形 DP,考虑子树 \(u\) 。记 \(g_u\) 表示 \(\sum_{v\in subtree(u), col(v)}dist(u, v)\)。每一次操作,一定是 \(g_u\) 减二,不变的操作对答案无影响甚至劣。\(f_u\) 表示子树内最少能剩下多少操作。在 \(u\) 上,我们选取两个来自不同子树的点进行操作,观察 \(g_v\),因为 \(dis\) 会整体加所以记一个 \(siz\) 表示子树内有颜色的点的个数,如果不存在 \(v\) 使得 \(f_v+siz_v>g_u-(g_v+siz_v)\) 就是没有一个子树是”控制全场“,那么可以每次选两个最大剩余的子树消掉(可以稍微讨论一下选 \(f\) 还是 \(g\),取决于其它的),直接消完。否则 \(u\) ”控制全场“,他单挑,变成 \(f_u=f_v+siz_v-g_u+g_v+siz_v\),就是 \(u\) 只有一个,\(u\) 的操作次数尽量小,其它尽量大,消的多一点。
换根 DP 可以优化。
P7897 [Ynoi2006] spxmcq
\(\max(\circ, 0)\) 太草,考虑一个森林,初始时全是独立点,随着 \(x\) 增长,\(f_u\) 增长,当 \(f_u>0\) 时加入 \(u\to fa_u\) 的边。然后写成:
也就是 \(f_u=sum_u+xsiz_u\)。随着 \(x\) 的增长,当 \(sum_u+xsiz_u>0\implies x>-\frac{sum_u}{siz_u}\) 时它就加边,加边的时候用并查集打通他到当前连通块的根的路径,然后做链加,在根上修改这个阈值,用任意数据结构维护新的阈值。
CF1326G Spiderweb Trees
题目是选连通块。看错好久了。
任意钦定根,考虑子树内 DP,设 \(dp_u\) 表示只考虑 \(u\) 子树内的方案数。考虑覆盖 \(u\) 的连通块,只有 \(u\) 或者是 \(u\) 连出的一条边,可以直接暴力算。考虑怎么刻画一个蛛网树,考虑最终的叶子是 \(B_1, B_2, B_3, \cdots\),要求这些叶子是凸包。不妨定义,点对 \((i, j)\) 是好的,首先不能有边连接它们,其次它们树上路径的点在 \(\vec{A_iA_j}\) 同侧(\(A_i\) 是点 \(i\)),不妨钦定为左边,然后对这个点对定义 \(g\) 表示所有 \(u\) 的 \(dp\) 的乘积,其中 \(u\) 不在路径上,\(u\) 的树上父亲在路径上左侧。然后就强力 DP 了吧。绕一圈,注意叶子 \(\iff\) 凸包边的限制,其实是可以做的。看着比较吓人。
*CF1456E XOR-ranges
数位 DP 要同时卡上下界,其实是很不喜欢这样搞的;如果没有上下界限制,直接全部相同即可,启发我们把相同的绑在一起,
*CF1158F Density of subarrays
*CF1466H Finding satisfactory solutions
[CEOI2017] Building Bridges
对 \(w\) 做一次前缀和,于是直接 \(f_1=0\),
就纯纯的斜率优化,复习一下:
但是斜率和横坐标全部不递增,主要是这个 \(h\) 坏事了。。。
cdq 分治的话,就左半边向右半边贡献云云。然后就是说凸包三点不共线,然后我觉得这部分需要参考代码防止自己忘了怎么写:
#include <bits/stdc++.h>
using namespace std;
#ifdef LOCAL
#define debug(...) fprintf(stderr, ##__VA_ARGS__)
#else
#define debug(...) void(0)
#endif
typedef long long LL;
struct dot {
LL x, y;
bool operator<(const dot& b) const { return x == b.x ? y < b.y : x < b.x; }
dot operator-(const dot& b) const { return {x - b.x, y - b.y}; }
friend LL cross(const dot& a, const dot& b) { return a.x * b.y - a.y * b.x; }
};
int n;
LL h[100010], w[100010], f[100010];
void cdq(int l, int r) {
if (l == r) return ;
int mid = (l + r) >> 1;
cdq(l, mid);
vector<dot> vec;
for (int j = l; j <= mid; j++) {
vec.push_back({2 * h[j], f[j] + h[j] * h[j] - w[j]});
}
sort(vec.begin(), vec.end());
vector<dot> stk;
for (auto&& p : vec) {
while (stk.size() >= 2 && cross(*next(stk.rbegin()) - stk.back(), p - stk.back()) > 0)
stk.pop_back();
stk.push_back(p);
}
vector<int> q;
for (int i = mid + 1; i <= r; i++) q.push_back(i);
sort(q.begin(), q.end(), [&](int i, int j) { return h[i] < h[j]; });
auto it = stk.begin();
for (int i : q) {
while (next(it) != stk.end() && cross({1, h[i]}, *next(it) - *it) < 0) ++it;
f[i] = min(f[i], it -> y - h[i] * it -> x + h[i] * h[i] + w[i - 1]);
}
cdq(mid + 1, r);
}
int main() {
#ifndef LOCAL
cin.tie(nullptr)->sync_with_stdio(false);
#endif
cin >> n;
for (int i = 1; i <= n; i++) cin >> h[i];
for (int i = 1; i <= n; i++) cin >> w[i], w[i] += w[i - 1];
memset(f, 0x3f, sizeof f);
f[1] = 0;
cdq(1, n);
cout << f[n] << endl;
return 0;
}
或者其实李超线段树也很好写。
gym104821D Red Black Tree
红黑树要求每个点到任意后代叶子节点的路径上,黑色点的数量都相同。称该性质为“红黑树性质”。
现在给定一棵树,每个点是红色或黑色,对于所有 \(k ∈ [1, n]\),求为了让以节点 \(k\) 为根的子树满足红黑树性质,至少要修改几个点的颜色。\(n ≤ 10^5\)。
发现自己不是很会维护凸包。烟火表演那题很妙,所以我尝试自己推一下这个严格弱化版。
\(g_u(x)\) 表示以 \(u\) 为根的子树中,使得所有叶子到 \(u\) 路径上的黑点个数都为 \(x\) 的最小代价。
首先求一下 \(G(x)=\sum_{v\in sonu}g_v(x)\) 这玩意,注意这玩意是对应点值相加。猜测它是凸的。凸了以后,要么是 \(\min(G(x), G(x-1)+1)\),要么是 \(\min(G(x)+1, G(x-1))\)。边界状态是 \((0, 1)\) 或者 \((1, 0)\)。
然后最终就是要在 \(k=0\) 的线段左边或者右边插入 \(1\) 或 \(-1\)。貌似直接暴力,好像也可以暴力维护斜率会好一点。就是黑色插入 \(-1\),红色插入 \(1\)。
拜读了官方题解之后发现维护这个东西是有技巧的。维护凸包的差分数组,一个 vector
存正数差分(绝对值从小到大),另一个 vector
存负数差分(绝对值从小到大),另外一个变量记录 \(0\) 的个数。然后可以轻易插入 \(\pm1\),合并是两个凸包大小的 \(\min\) 的复杂度(你要精细实现一下,如果父节点还没有凸包直接把凸包 std::move
过去,否则才暴力合并重构)。最后求答案是负数 vector
的和,也好维护。
这里注意一下,凸包的大小是子树内叶子最小深度。
复杂度因为合并复杂度是取 \(\min\),所以是线性的,被满二叉树卡算了一下也是线性。相当于一种变异的长链剖分。
struct Jason {
vector<int> pos, neg;
int zer;
int nsum;
Jason() : pos(), neg(), zer(0), nsum(0) {}
Jason(vector<int> vec) {
for (auto it = vec.begin(); it != vec.end() && *it < 0; ++it) neg.push_back(*it);
for (auto it = vec.rbegin(); it != vec.rend() && *it > 0; ++it) pos.push_back(*it);
nsum = accumulate(neg.begin(), neg.end(), 0);
zer = vec.size() - neg.size() - pos.size();
}
int operator[](int i) const {
if (i < (int)neg.size()) return neg[i];
else i -= neg.size();
if (i < zer) return 0;
else i -= zer;
if (i < (int)pos.size()) return pos[(int)pos.size() - i - 1];
else return assert(0), -1;
}
int size() const { return pos.size() + zer + neg.size(); }
friend Jason operator+(const Jason& a, const Jason& b) {
vector<int> dif;
for (int i = 0; i < min(a.size(), b.size()); i++) {
dif.push_back(a[i] + b[i]);
}
return dif;
}
};
本文来自博客园,作者:caijianhong,转载请注明原文链接:https://www.cnblogs.com/caijianhong/p/17904776.html