6.12 ACM-ICPC动态规划算法 动态 DP

6.12 ACM-ICPC动态规划算法 动态 DP

动态DP问题在现代算法竞赛中是一个非常有趣且具有挑战性的领域,尤其是在解决涉及复杂数据结构和操作的问题时。本篇博客旨在深入探讨猫锟在WC2018所介绍的“动态DP”黑科技,这是一种常用于解决树上带有点权(或边权)修改操作的动态规划问题。

前置知识

在深入动态DP之前,我们需要了解一些基础的数据结构和算法技术,特别是矩阵运算和树链剖分。矩阵提供了一种强大的数学工具来处理结构化数据,而树链剖分则允许我们在树结构上进行高效的修改和查询操作。

动态DP的定义和应用

动态DP主要用于解决树结构上的动态问题,其中节点权值可以随时间修改,而我们需要在每次修改后快速计算某个特定的动态规划状态。这类问题的一个典型例子是计算树的最大权独立集大小,尤其是在多次修改后。

例题分析:洛谷 P4719【模板】动态 DP

考虑一个具体的例题,一棵有n个节点的树,每个节点有一个权值。进行m次操作,每次操作修改一个节点的权值。要求每次操作后输出树的最大权独立集的权值大小。

广义矩阵乘法

在动态DP中,我们将使用一种特殊的矩阵乘法,称为广义矩阵乘法。定义如下: 𝐶𝑖,𝑗=max⁡𝑘=1𝑛(𝐴𝑖,𝑘+𝐵𝑘,𝑗)Ci,j​=maxk=1n​(Ai,k​+Bk,j​) 这里的操作将普通的矩阵乘法中的乘法替换为加法,加法替换为max操作。

由于广义矩阵乘法满足结合律,我们可以利用矩阵快速幂来加速计算过程。

不带修改操作的动态规划

定义 f_{i,0}f_{i,1}

  • f_{i,0} 表示不选择节点i的最大答案
  • f_{i,1} 表示选择节点i的最大答案

根据这些定义,我们可以建立以下DP方程:

  • f_{i,0} = \sum_{son} \max(f_{son,0}, f_{son,1})
  • f_{i,1} = w_i + \sum_{son} f_{son,0}

答案是 max(f_{root,0}, f_{root,1})

带修改操作的动态规划

为了处理修改操作,首先对树进行树链剖分。通过这种方法,我们可以将树分解为多条链,并在每条链上建立数据结构(如线段树)来支持快速修改和查询。

对于每个节点,我们定义:

  • g_{i,0}g_{i,1} 分别表示不选择和选择节点i的情况下,只考虑其轻子树的最大答案。

对于修改操作,我们只需要更新受影响的节点和它们的父链上的矩阵即可。通过树链剖分和线段树,我们可以有效地处理这些修改,并快速重新计算答案。

代码实现

#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int maxn = 500010;
const int INF = 0x3f3f3f3f;

int Begin[maxn], Next[maxn], To[maxn], e, n, m;
int sz[maxn], son[maxn], top[maxn], fa[maxn], dis[maxn], p[maxn], id[maxn],
    End[maxn];
// p[i]表示i树剖后的编号,id[p[i]] = i
int cnt, tot, a[maxn], f[maxn][2];

struct matrix {
  int g[2][2];

  matrix() { memset(g, 0, sizeof(g)); }

  matrix operator*(const matrix &b) const  // 重载矩阵乘
  {
    matrix c;
    for (int i = 0; i <= 1; i++)
      for (int j = 0; j <= 1; j++)
        for (int k = 0; k <= 1; k++)
          c.g[i][j] = max(c.g[i][j], g[i][k] + b.g[k][j]);
    return c;
  }
} Tree[maxn], g[maxn];  // Tree[]是建出来的线段树,g[]是维护的每个点的矩阵

void PushUp(int root) { Tree[root] = Tree[root << 1] * Tree[root << 1 | 1]; }

void Build(int root, int l, int r) {
  if (l == r) {
    Tree[root] = g[id[l]];
    return;
  }
  int Mid = l + r >> 1;
  Build(root << 1, l, Mid);
  Build(root << 1 | 1, Mid + 1, r);
  PushUp(root);
}

matrix Query(int root, int l, int r, int L, int R) {
  if (L <= l && r <= R) return Tree[root];
  int Mid = l + r >> 1;
  if (R <= Mid) return Query(root << 1, l, Mid, L, R);
  if (Mid < L) return Query(root << 1 | 1, Mid + 1, r, L, R);
  return Query(root << 1, l, Mid, L, R) *
         Query(root << 1 | 1, Mid + 1, r, L, R);
  // 注意查询操作的书写
}

void Modify(int root, int l, int r, int pos) {
  if (l == r) {
    Tree[root] = g[id[l]];
    return;
  }
  int Mid = l + r >> 1;
  if (pos <= Mid)
    Modify(root << 1, l, Mid, pos);
  else
    Modify(root << 1 | 1, Mid + 1, r, pos);
  PushUp(root);
}

void Update(int x, int val) {
  g[x].g[1][0] += val - a[x];
  a[x] = val;
  // 首先修改x的g矩阵
  while (x) {
    matrix last = Query(1, 1, n, p[top[x]], End[top[x]]);
    // 查询top[x]的原本g矩阵
    Modify(1, 1, n,
           p[x]);  // 进行修改(x点的g矩阵已经进行修改但线段树上的未进行修改)
    matrix now = Query(1, 1, n, p[top[x]], End[top[x]]);
    // 查询top[x]的新g矩阵
    x = fa[top[x]];
    g[x].g[0][0] +=
        max(now.g[0][0], now.g[1][0]) - max(last.g[0][0], last.g[1][0]);
    g[x].g[0][1] = g[x].g[0][0];
    g[x].g[1][0] += now.g[0][0] - last.g[0][0];
    // 根据变化量修改fa[top[x]]的g矩阵
  }
}

void add(int u, int v) {
  To[++e] = v;
  Next[e] = Begin[u];
  Begin[u] = e;
}

void DFS1(int u) {
  sz[u] = 1;
  int Max = 0;
  f[u][1] = a[u];
  for (int i = Begin[u]; i; i = Next[i]) {
    int v = To[i];
    if (v == fa[u]) continue;
    dis[v] = dis[u] + 1;
    fa[v] = u;
    DFS1(v);
    sz[u] += sz[v];
    if (sz[v] > Max) {
      Max = sz[v];
      son[u] = v;
    }
    f[u][1] += f[v][0];
    f[u][0] += max(f[v][0], f[v][1]);
    // DFS1过程中同时求出f[i][0/1]
  }
}

void DFS2(int u, int t) {
  top[u] = t;
  p[u] = ++cnt;
  id[cnt] = u;
  End[t] = cnt;
  g[u].g[1][0] = a[u];
  g[u].g[1][1] = -INF;
  if (!son[u]) return;
  DFS2(son[u], t);
  for (int i = Begin[u]; i; i = Next[i]) {
    int v = To[i];
    if (v == fa[u] || v == son[u]) continue;
    DFS2(v, v);
    g[u].g[0][0] += max(f[v][0], f[v][1]);
    g[u].g[1][0] += f[v][0];
    // g矩阵根据f[i][0/1]求出
  }
  g[u].g[0][1] = g[u].g[0][0];
}

int main() {
  scanf("%d%d", &n, &m);
  for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
  for (int i = 1; i <= n - 1; i++) {
    int u, v;
    scanf("%d%d", &u, &v);
    add(u, v);
    add(v, u);
  }
  dis[1] = 1;
  DFS1(1);
  DFS2(1, 1);
  Build(1, 1, n);
  for (int i = 1; i <= m; i++) {
    int x, val;
    scanf("%d%d", &x, &val);
    Update(x, val);
    matrix ans = Query(1, 1, n, 1, End[1]);  // 查询1所在重链的矩阵乘
    printf("%d\n", max(ans.g[0][0], ans.g[1][0]));
  }
  return 0;
}

通过这样的深入解析和示例,希望能帮助你更好地理解和运用动态DP来解决复杂的动态问题。

 

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值