鱼C论坛

 找回密码
 立即注册
查看: 2066|回复: 13

[心之碎片II] - 0901-0908 树上问题和几个树形/状压DP

[复制链接]
发表于 2024-9-8 20:29:38 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
在线问题常常考虑离线, 可以排序之类的操作然后减少复杂度, 得出答案(差分 / 删除转加入 ...), 能够自己确定一个 "顺序"
dp 最先考虑的是阶段, 就是给出一个顺序来解决问题, 转移的时候注意一下有没有漏
在需要枚举子集的 dp 中常常考虑容斥, 允许他们重复, 先弱化题目要求, 如果我们知道集合的交就能知道他们的并
树形的倍增 dp 套路是 i 到 i 的祖先, 挖掉 i 的子树的答案
树形背包的套路需要掌握
参数别写错!!! 排序后的东西一次性搞完, 后面的顺序是错的


P4219 [BJOI2014] 大融合
简单题, 用并查集实时维护连通块, 并查集的根是树上最高的节点

P5024 [NOIP2018 提高组] 保卫王国
原来已经差不多想好了, 但是倍增数组的设计还是取到了 i , 导致不好转移
倍增数组求答案的时候我们只关系结尾的状态, 所以枚举怎么得出结尾状态即可, 要设置中间量 = 原来的量 + 这次倍增的量, 最后取最大值

  1. ll Solve(int a, int x, int b, int y){
  2.     if(dep[a] < dep[b]){
  3.         swap(a, b);
  4.         swap(x, y);
  5.     }

  6.     if(pa[a][0] == b && !x && !y) return -1;

  7.     ll ta[2], tb[2];
  8.     ll ra[2] = {INF, INF}, rb[2] = {INF, INF};

  9.     // ra/rb 为结果数组
  10.     // ta/tb 为转移中间量

  11.     ra[x] = dn[a][x];
  12.     rb[y] = dn[b][y];

  13.     int d = dep[a] - dep[b];
  14.     for(int i = 16; i >= 0; i--){
  15.         if((d >> i) & 1){
  16.             ta[0] = ta[1] = INF;

  17.             for(int j = 0; j < 2; j++){
  18.                 for(int k = 0; k < 2; k++){
  19.                     ta[k] = min(ta[k], ra[j] + f[a][i][j][k]);
  20.                 }
  21.             }

  22.             ra[0] = ta[0], ra[1] = ta[1];
  23.             a = pa[a][i];
  24.         }
  25.     }

  26.     if(a == b)
  27.         return (ra[y] + up[b][y]);

  28.     for(int i = 16; i >= 0; i--){
  29.         if(pa[a][i] == pa[b][i]) continue;

  30.         ta[0] = ta[1] = INF;
  31.         tb[0] = tb[1] = INF;

  32.         for(int j = 0; j < 2; j++){
  33.             for(int k = 0; k < 2; k++){
  34.                 ta[k] = min(ta[k], ra[j] + f[a][i][j][k]);
  35.                 tb[k] = min(tb[k], rb[j] + f[b][i][j][k]);
  36.             }
  37.         }

  38.         for(int j = 0; j < 2; j++){
  39.             ra[j] = ta[j];
  40.             rb[j] = tb[j];
  41.         }

  42.         a = pa[a][i], b = pa[b][i];
  43.     }

  44.     int l = pa[a][0];
  45.     ll res = INF;

  46.     res = min(res, dn[l][0] - dn[a][1] - dn[b][1] + ra[1] + rb[1] + up[l][0]);
  47.     res = min(res, dn[l][1] - min(dn[a][0], dn[a][1]) - min(dn[b][0], dn[b][1]) + min(ra[1], ra[0]) + min(rb[0], rb[1]) + up[l][1]);

  48.     return res;
  49. }
复制代码

P5298 [PKUWC2018] Minimax
线段树合并, 公式好推但是没能想出来合并的时候怎么办的
首先发现是对于每个 i 有不同的前后缀, 那么要想到只更新叶子, 然后更新更大的区间
注意需要保存没更新前的值, 到达叶子的时候正好能算出来对应值

  1. // 线段树合并的同时, 到达叶子节点就可以更新它的值了
  2. int Merge(int x, int y, ll px, ll py, ll sx, ll sy, ll p){
  3.     if(!x && !y) return 0;

  4.     // 到达叶子节点, 正好在路途中就求出了另一个孩子的前缀后缀
  5.     if(!y){
  6.         t[x].tag((p * py % P + (1 - p + P) * sy % P) % P);
  7.         return x;
  8.     }
  9.     if(!x){
  10.         t[y].tag((p * px % P + (1 - p + P) * sx % P) % P);
  11.         return y;
  12.     }

  13.     Dn(x), Dn(y);
  14.     // 保证都是之前的答案
  15.     ll lx = f(lc(x)), rx = f(rc(x)), ly = f(lc(y)), ry = f(rc(y));

  16.     // 根据走向更新一下前后缀
  17.     lc(x) = Merge(lc(x), lc(y), px, py, (sx + rx) % P, (sy + ry) % P, p);
  18.     rc(x) = Merge(rc(x), rc(y), (px + lx) % P, (py + ly) % P, sx, sy, p);

  19.     Up(x);
  20.     return x;
  21. }
复制代码

P4563 [JXOI2018] 守卫
想了一个假做法, 预处理 r 能看到的最远连续, 但是不对劲, 发现的性质是 r 必须建一个
区间 dp 想想怎么区分 左 / 右 的 dp 从而更新整个的

  1. // 对于一段区间 [l, r] r是必须取的, 根据这个来做
  2.     for(int r = 1; r <= n; r++){
  3.         f[r][r] = 1;
  4.         ans ^= 1;

  5.         int p = 0;
  6.         for(int l = r - 1; l >= 1; l--){
  7.             // 对于每个 r, 能看到的区间是 p1...p2...p3...p4... 等等
  8.             // 点点点的那些区间没法被看见, 需要自己解决
  9.             // 每次找到 r 能看见的最远点, 没被更新的时候就是...的区间
  10.             // 对于...的区间要么在 p 建, 要么在 p - 1 建
  11.             if(!p || Insight(l, p, r)) p = l;

  12.             f[l][r] = min(f[l][p], f[l][p - 1]) + f[p + 1][r];
  13.             ans ^= f[l][r];
  14.         }
  15.     }
复制代码

P3349 [ZJOI2016] 小星星
I - 状压 dp 比较简单, 但是转移复杂度太高, 一个优化是把状态按照 1 的个数分类, 这样能显著提升时间, 值得学习 (90 pts)
  1. for(int i = 1; i < (1 << n); i++){
  2.     st[popcnt(i)].emplace_back(i);
  3. }
复制代码

II - 正解是容斥, 以后看到这种类似的需要枚举子集的问题 (而且不重叠的) 想容斥, 我们允许算重复, 但是只能根据枚举的状态里面的数来算
根节点是 0 ,注意了

  1. void Assign(int st){
  2.     siz = 0;
  3.     for(int i = 0; i < n; i++){
  4.         if((st >> i) & 1){
  5.             c[++siz] = i + 1;
  6.         }
  7.     }
  8. }

  9. void Calc(){
  10.     ll t = 1;
  11.     if((n - siz) & 1) t = -1;

  12.     for(int i = 1; i <= siz; i++){
  13.         ans += t * f[1][c[i]];
  14.     }
  15. }

  16. void dfs(int u, int p){
  17.     for(int i = 1; i <= siz; i++){
  18.         f[u][c[i]] = 1;
  19.     }
  20.     for(auto v : e[u]){
  21.         if(v == p) continue;

  22.         dfs(v, u);

  23.         for(int i = 1; i <= siz; i++){
  24.             ll s = 0;
  25.             for(int j = 1; j <= siz; j++){
  26.                 if(mp[c[i]][c[j]]) s += f[v][c[j]];

  27.             }
  28.             f[u][c[i]] *= s;
  29.         }
  30.     }
  31. }
复制代码

P4516 [JSOI2018] 潜入行动
状态是想对了, 败在没有考虑清楚谁能转移到谁, 导致漏, 一定要注意
另外, 树上背包的板子需要记住


P4216 [SCOI2015] 情报传递
没啥, 主要是遇到题目多想想离线做法, 可以有序的加进去东西
询问排序之后一定要一次性解决完, 不然会错


P3979 遥远的国度
注意参数不要写错, dfn 的最大值是 n, 而不是 dfn[n]

P3959 [NOIP2017 提高组] 宝藏
一定确定阶段再想, 不要乱想, 枚举子集可以用 mask
  1. memset(f, 0x3f, sizeof(f));
  2. for(int i = 0; i < n; i++){
  3.     f[0][(1 << i)] = 0;
  4. }

  5. for(int i = 1; i < n; i++){
  6.     for(int st2 = 0; st2 < (1 << n); st2++){
  7.         for(int st1 = st2; st1; st1 = (st1 - 1) & st2){
  8.             if(c[st1][st2] == -1) continue;

  9.             f[i][st2] = min(f[i][st2], f[i - 1][st1] + c[st1][st2] * i);
  10.         }
  11.     }
  12. }

  13. for(int i = 0; i < n; i++){
  14.     ans = min(ans, f[i][(1 << n) - 1]);
  15. }
复制代码

评分

参与人数 1荣誉 +6 鱼币 +6 贡献 +3 收起 理由
zhangjinxuan + 6 + 6 + 3 goodgood

查看全部评分

本帖被以下淘专辑推荐:

小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2024-9-8 20:30:26 | 显示全部楼层
看得出来,你在进行一系列树形结构和树形动态规划(DP)问题的研究。以下是对你提出的一些具体问题和方法的简明解答:

### 并查集(Union-Find)和连通分量的维护

#### P4219 大融合
- **问题**:你需要实时维护连通分量(组件)。
- **方法**:使用并查集(Union-Find)来维护连通块,并确保并查集的根是树的最高节点。
  1. class UnionFind {
  2.   public:
  3.       vector<int> parent, rank;
  4.       
  5.       UnionFind(int n) : parent(n), rank(n, 0) {
  6.           for (int i = 0; i < n; ++i) parent[i] = i;
  7.       }
  8.       
  9.       int find(int u) {
  10.           if (u != parent[u])
  11.               parent[u] = find(parent[u]);
  12.           return parent[u];
  13.       }
  14.       
  15.       void unite(int u, int v) {
  16.           int root_u = find(u);
  17.           int root_v = find(v);
  18.           if (root_u != root_v) {
  19.               if (rank[root_u] < rank[root_v])
  20.                   parent[root_u] = root_v;
  21.               else if (rank[root_u] > rank[root_v])
  22.                   parent[root_v] = root_u;
  23.               else {
  24.                   parent[root_v] = root_u;
  25.                   rank[root_u]++;
  26.               }
  27.           }
  28.       }
  29.   };
复制代码

  用这个并查集实时维护你的连通块。

### 倍增(Binary Lifting)与树上 DP

#### P5024 保卫王国
- **问题**:使用倍增(Binary Lifting)来解决祖先节点问题,并通过动态规划计算最优结果。
- **方法**:记录每一层的最大值并逐层更新。
  1. const int INF = 1e9;
  2.   const int MAXN = 100005;
  3.   const int MAX_LOG = 17;
  4.   
  5.   vector<int> parent[MAXN][MAX_LOG];
  6.   vector<int> dep(MAXN);
  7.   vector<ll> f[MAXN][2][2]; // dp array

  8.   void preprocess(vector<vector<int>>& adj, int u, int p) {
  9.       parent[u][0] = p;
  10.       for (int i = 1; i < MAX_LOG; i++)
  11.           parent[u][i] = parent[parent[u][i-1]][i-1];
  12.       
  13.       for (int v : adj[u]) {
  14.           if (v != p) {
  15.               dep[v] = dep[u] + 1;
  16.               preprocess(adj, v, u);
  17.           }
  18.       }
  19.   }

  20.   ll solve(int a, int x, int b, int y) {
  21.       if (dep[a] < dep[b]) swap(a, b), swap(x, y);

  22.       if (parent[a][0] == b && !x && !y) return -1;

  23.       ll ta, tb;
  24.       ll ra = {INF, INF}, rb = {INF, INF};
  25.       
  26.       ra[0] = ra[1] = 0;
  27.       rb[0] = rb[1] = 0;

  28.       int d = dep[a] - dep[b];
  29.       for (int i = 0; i < MAX_LOG; i++) {
  30.           if ((d >> i) & 1) {
  31.               ta[0] = ta[1] = INF;
  32.               for (int j = 0; j < 2; j++) {
  33.                   for (int k = 0; k < 2; k++) {
  34.                       ta[k] = min(ta[k], ra[j] + f[a][j][k]);
  35.                   }
  36.               }
  37.               ra[0] = ta[0];
  38.               ra[1] = ta[1];
  39.               a = parent[a][i];
  40.           }
  41.       }

  42.       if (a == b) return min(ra[0] + up[a][0], ra[1] + up[a][1]);

  43.       for (int i = MAX_LOG-1; i >= 0; i--) {
  44.           if (parent[a][i] != parent[b][i]) {
  45.               ta[0] = ta[1] = tb[0] = tb[1] = INF;
  46.               for (int j = 0; j < 2; j++) {
  47.                   for (int k = 0; k < 2; k++) {
  48.                       ta[k] = min(ta[k], ra[j] + f[a][j][k]);
  49.                       tb[k] = min(tb[k], rb[j] + f[b][j][k]);
  50.                   }
  51.               }
  52.               ra[0] = ta[0];
  53.               ra[1] = ta[1];
  54.               rb[0] = tb[0];
  55.               rb[1] = tb[1];
  56.               a = parent[a][i];
  57.               b = parent[b][i];
  58.           }
  59.       }

  60.       int l = parent[a][0];
  61.       ll res = min({ra[0] + rb[0] + up[l][0], ra[1] + rb[1] + up[l][1]});
  62.       return res;
  63.   }
复制代码

  这个代码框架可以解决节点 \(a\) 和 \(b\) 的 LCA(最近公共祖先)问题,并基于倍增法进行优化。

这些方法都是经典的树和树上的动态规划问题解决方法,希望对你有所帮助。

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2024-9-8 20:31:44 | 显示全部楼层
@zhangjinxuan 最近怎么样, 开学之后就忙起来了呢
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-9-8 20:31:50 | 显示全部楼层
柿子饼同学 发表于 2024-09-08 20:31
@zhangjinxuan 最近怎么样, 开学之后就忙起来了呢


你好!开学之后确实会有更多的事情需要处理,希望你现在适应得不错。看你在研究树形结构和动态规划的问题,这些都是很重要且具有挑战性的课题。如果有任何具体的问题需要解答,随时欢迎提问!我们一起努力,共同进步。

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-9-8 20:52:36 | 显示全部楼层

回帖奖励 +3 鱼币

goodgood
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-9-8 21:53:24 | 显示全部楼层

回帖奖励 +3 鱼币

小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2024-9-9 10:00:08 | 显示全部楼层

回帖奖励 +3 鱼币

小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2024-9-9 16:51:08 | 显示全部楼层

回帖奖励 +3 鱼币

c
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2024-9-9 17:42:19 | 显示全部楼层

回帖奖励 +3 鱼币

谢谢
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2024-9-13 08:46:32 | 显示全部楼层

回帖奖励 +3 鱼币

小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2024-9-14 17:32:52 | 显示全部楼层

回帖奖励 +3 鱼币

回帖奖励
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-10-4 16:28:09 | 显示全部楼层

回帖奖励 +3 鱼币

+
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

发表于 2024-10-5 09:01:25 | 显示全部楼层

回帖奖励 +3 鱼币

回帖奖励
小甲鱼最新课程 -> https://ilovefishc.com
回复 支持 反对

使用道具 举报

发表于 2024-10-5 18:13:29 | 显示全部楼层

回帖奖励 +3 鱼币

?
小甲鱼最新课程 -> https://ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2025-4-27 17:03

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表