【笔记】动态规划 2023.12.16

笔记 2023.12.16:动态规划

今天题目很多,可能有些题不口胡了。

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\) 倍,我同样不知道它是怎么做到的,但是你要使得最终答案最小。这样就可以算:

\[f(l, r, fl, fr)=\sum_kf(l, k - 1, *, *)+f(k+1, r, *, *)+a_k\times\circ \]

其中 \(*, \circ\) 我待定系数,仔细考察一下系数啊,这里 \(k\) 会翻 \(fl+fr\) 倍计入答案,因为它被打到左边和右边都算。

\[f(l, r, fl, fr)=\sum_kf(l, k - 1, fl, fl + fr)+f(k+1, r, fl + fr, fr)+a_k\times(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

\[f_u=a_u+x+\sum_{v\in son(u)}\max(f_v, 0) \]

\(\max(\circ, 0)\) 太草,考虑一个森林,初始时全是独立点,随着 \(x\) 增长,\(f_u\) 增长,当 \(f_u>0\) 时加入 \(u\to fa_u\) 的边。然后写成:

\[f_u=a_u+x+\sum_{v\in son(u)} f_v=\sum_{v\in subtree(u)}a_v+x\sum_{v\in subtree(u)}1 \]

也就是 \(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\)

\[f_i=\min_{1\leq j<i}\{f_j+(h_i-h_j)^2+w_{i-1}-w_j\} \]

就纯纯的斜率优化,复习一下:

\[f_j+h_j^2-w_j=2h_ih_j+f_i-h_i^2-w_{i-1} \]

\[y = kx + b\implies b = y - kx \]

但是斜率和横坐标全部不递增,主要是这个 \(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_u(x)=\min_{i\in\{0, 1\}}\{[col_u\neq i]+\sum_{v\in son(u)}g_v(x - i)\} \]

首先求一下 \(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;
  }
};

posted @ 2023-12-16 13:53  caijianhong  阅读(65)  评论(0编辑  收藏  举报