P4719 【模板】“动态 DP“&动态树分治 题解

这篇博客介绍了如何使用动态DP和动态树分治的模板来解决竞赛编程中的问题。首先,提出了一个不考虑修改情况的DP状态转移方程,并通过树链剖分优化复杂度。然后,引入了线段树维护重儿子信息,实现每次修改时对链上信息的影响。最后,给出了完整的C++代码实现,用于解决动态修改和查询的问题。整个过程涉及到了矩阵快速幂和线段树等数据结构与算法。

P4719 【模板】"动态 DP"&动态树分治

P4719 【模板】“动态 DP”&动态树分治

先不考虑修改,可以想到 D p \tt Dp Dp

f ( i , 0 / 1 ) f(i, 0/1) f(i,0/1) 表示当前的点是否选择,那么转移可以得到:
f ( u , 0 ) = ∑ v ∈ s o n u max ⁡ ( f ( v , 0 ) , f ( v , 1 ) ) f ( u , 1 ) = ∑ v ∈ s o n u f ( v , 0 ) \begin{aligned} f(u, 0) &= \sum_{v \in son_u} \max(f(v, 0), f(v, 1))\\ f(u, 1) &= \sum_{v \in son_u} f(v, 0) \end{aligned} f(u,0)f(u,1)=vsonumax(f(v,0),f(v,1))=vsonuf(v,0)
发现每一次进行修改的时候影响的是一条链上的信息,我们不妨将其树链剖分一下,设 g ( u , 0 / 1 ) g(u, 0/1) g(u,0/1) 表示亲儿子对应的信息。
g ( u , 0 ) = ∑ v ∈ s o n l i g h t u max ⁡ ( f ( v , 0 ) , f ( v , 1 ) ) g ( u , 1 ) = ∑ v ∈ s o n l i g h t u f ( v , 0 ) \begin{aligned} g(u, 0) =& \sum_{v \in sonlight_u} \max(f(v, 0), f(v, 1)) \\ g(u, 1) =& \sum_{v \in sonlight_u} f(v, 0) \end{aligned} g(u,0)=g(u,1)=vsonlightumax(f(v,0),f(v,1))vsonlightuf(v,0)
我们可以把转移变成矩阵:
[ f ( v , 0 ) f ( v , 1 ) ] × [ g ( u , 0 ) g ( u , 1 ) g ( u , 0 ) − ∞ ] = [ f ( u , 0 ) f ( u , 1 ) ] \left[ \begin{matrix} f(v, 0) & f(v, 1) \end{matrix} \right] \times \left[ \begin{matrix} g(u, 0) & g(u, 1) \\ g(u, 0) & - \infty \end{matrix} \right]= \left[ \begin{matrix} f(u, 0) & f(u, 1) \end{matrix} \right] [f(v,0)f(v,1)]×[g(u,0)g(u,0)g(u,1)]=[f(u,0)f(u,1)]
我们使用线段树进行维护重儿子,每次跳链。复杂度是 O ( n log ⁡ 2 n ) O(n \log ^2 n) O(nlog2n)

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

//#define Fread
//#define Getmod

#ifdef Fread
char buf[1 << 21], *iS, *iT;
#define gc() (iS == iT ? (iT = (iS = buf) + fread (buf, 1, 1 << 21, stdin), (iS == iT ? EOF : *iS ++)) : *iS ++)
#define getchar gc
#endif // Fread

template <typename T>
void r1(T &x) {
	x = 0;
	char c(getchar());
	int f(1);
	for(; c < '0' || c > '9'; c = getchar()) if(c == '-') f = -1;
	for(; '0' <= c && c <= '9';c = getchar()) x = (x * 10) + (c ^ 48);
	x *= f;
}

template <typename T,typename... Args> inline void r1(T& t, Args&... args) {
    r1(t);  r1(args...);
}

//#define int long long
const int maxn = 1e5 + 5;
const int maxm = maxn << 1;
const int inf = 1e9;
struct Matrix {
    int a[2][2];
    Matrix(void) { memset(a, 0, sizeof(a)); }
    int * operator [] (const int &x) { return a[x]; }
    void init() {
        memset(a, 0, sizeof(a));
        a[0][1] = a[1][0] = -inf;
    }
    void Min() {
        for(int i = 0; i < 2; ++ i) a[i][0] = a[i][1] = -inf;
    }
    Matrix operator * (const Matrix &z) const {
        Matrix res; res.Min();
        for(int i = 0; i < 2; ++ i) {
            for(int j = 0; j < 2; ++ j)
            for(int k = 0; k < 2; ++ k)
            res.a[i][j] = max(res.a[i][j], a[i][k] + z.a[k][j]);
        }
        return res;
    }
};

vector<int> vc[maxn];
void add(int u,int v) {
    vc[u].emplace_back(v);
}

int dfntot(0);
int dfn[maxn], fdfn[maxn], top[maxn], fa[maxn], son[maxn], siz[maxn];
int bot[maxn];
void dfs(int p,int pre) {
    siz[p] = 1;
    for(int v : vc[p]) if(v != pre) {
        dfs(v, p);
        siz[p] += siz[v];
        if(siz[v] > siz[son[p]]) son[p] = v;
    }
}

void dfs1(int p,int pre,int topf) {
    dfn[p] = ++ dfntot;
    fdfn[dfntot] = p;
    top[p] = topf;
    fa[p] = pre;
    if(son[p]) dfs1(son[p], p, topf), bot[p] = bot[son[p]];
    else bot[p] = p;
    for(int v : vc[p]) if(v != pre && v != son[p]) {
        dfs1(v, p, v);
    }
}
int f[maxn][2], g[maxn][2], a[maxn];
void dfs2(int p,int pre) {
    f[p][1] = a[p];
    if(son[p]) dfs2(son[p], p), f[p][0] = max(f[son[p]][0], f[son[p]][1]), f[p][1] += f[son[p]][0];
    for(int v : vc[p]) if(v != pre && v != son[p]) {
        dfs2(v, p);
        g[p][0] += max(f[v][0], f[v][1]);
        g[p][1] += f[v][0];
    }
    f[p][0] += g[p][0];
    f[p][1] += g[p][1];
}

Matrix t[maxn << 2];
struct Seg {
    #define ls (p << 1)
    #define rs (p << 1 | 1)
    #define mid ((l + r) >> 1)

    void pushup(int p) {
        t[p] = t[rs] * t[ls];
    }

    void build(int p,int l,int r) {
        if(l == r) {
            int id = fdfn[l];
            t[p].a[0][0] = g[id][0];
            t[p].a[1][0] = g[id][0];
            t[p].a[0][1] = g[id][1] + a[id];
            t[p].a[1][1] = - inf;
            return ;
        }
        build(ls, l, mid), build(rs, mid + 1, r);
        pushup(p);
    }

    void change(int p,int l,int r,int pos) {
        if(l == r) {
            int id = fdfn[pos];
            t[p].a[0][0] = g[id][0];
            t[p].a[1][0] = g[id][0];
            t[p].a[0][1] = g[id][1] + a[id];
            t[p].a[1][1] = - inf;
            return ;
        }
        if(pos <= mid) change(ls, l, mid, pos);
        else change(rs, mid + 1, r, pos);
        pushup(p);
    }

    Matrix Ask(int p,int l,int r,int ll,int rr) {
        if(ll <= l && r <= rr) return t[p];
        Matrix res; res.init();
        if(mid < rr) res = res * Ask(rs, mid + 1, r, ll, rr);
        if(ll <= mid) res = res * Ask(ls, l, mid, ll, rr);
        return res;
    }

    #undef ls
    #undef rs
    #undef mid
}T;

int n, m;

void Solve(int u) {
    while(top[u] != 1) {
        Matrix res = T.Ask(1, 1, n, dfn[top[u]], dfn[bot[u]]);
        g[fa[top[u]]][0] -= max(res.a[0][0], res.a[0][1]);
        g[fa[top[u]]][1] -= res.a[0][0];
        T.change(1, 1, n, dfn[u]);
        res = T.Ask(1, 1, n, dfn[top[u]], dfn[bot[u]]);
        g[fa[top[u]]][0] += max(res.a[0][0], res.a[0][1]);
        g[fa[top[u]]][1] += res.a[0][0];
//        printf("%d\n", top[u]);
        u = fa[top[u]];
//        printf("u = %d\n", u);
    }
    T.change(1, 1, n, dfn[u]);
}

signed main() {
//    freopen("S.in", "r", stdin);
//    freopen("S.out", "w", stdout);
    int i, j;
    r1(n, m);
    for(i = 1; i <= n; ++ i) r1(a[i]);
    for(i = 1; i < n; ++ i) {
        int u, v; r1(u, v), add(u, v), add(v, u);
    }
    dfs(1, 0);
    dfs1(1, 0, 1);
    dfs2(1, 0);
//    for(i = 1; i <= n; ++ i) printf("%d : %d\n", i, dfn[i]);
//    for(i = 1; i <= n; ++ i) printf("%d : %d\n", i, fdfn[i]);
    T.build(1, 1, n);
    for(i = 1; i <= m; ++ i) {
        int x, y;
        r1(x, y), a[x] = y;
        Solve(x);
        Matrix res = T.Ask(1, 1, n, dfn[1], dfn[bot[1]]);
        printf("%d\n", max(res.a[0][0], res.a[0][1]));
    }
	return 0;
}
动态规划(Dynamic Programming,简称 DP)是算法中非常重要的一类题型,但也是很多初学者最难理解的部分之一。如果你在做 DP 题目时**没思路、看不懂题解、不知道怎么下手**,这是非常正常的。下面我会从**学习方法、解题思路、常见题型结构**等方面系统地帮你梳理如何克服 DP 难题。 --- ## ✅ 一、为什么动态规划难?常见误区 1. **没有建立&ldquo;状态转移&rdquo;思维**:DP 的核心是状态定义和状态转移方程,很多同学直接看题解,却不知道为什么状态这样定义。 2. **没有理解 DP 的本质**:DP&ldquo;记忆化递归 + 最优子结构&rdquo;,不是单纯写个 for 循环就能解决的。 3. **跳过基础题型,直接挑战难题**:比如没掌握&ldquo;背包问题&rdquo;就去做&ldquo;最长回文子序列&rdquo;等综合题,自然看不懂。 4. **没有总结套路**:每类 DP 题都有其&ldquo;套路&rdquo;,比如线性 DP、区间 DP、背包 DP、树形 DP 等。 --- ## ✅ 二、如何从零开始学动态规划? ### 1. 先掌握基本概念 - **状态定义(dp[i]、dp[i][j] 等)** - **状态转移方程(递推关系)** - **初始条件(base case)** - **最终结果(取 dp[n]、dp[n][m] 或者 max/min)** ### 2. 推荐学习路径(从易到难) | 阶段 | 推荐题目 | 目标 | |------|----------|------| | 入门 | [斐波那契数列](https://leetcode.cn/problems/fibonacci-number/)、[爬楼梯](https://leetcode.cn/problems/climbing-stairs/) | 理解递归、记忆化、DP | | 基础 | [最大子序和](https://leetcode.cn/problems/maximum-subarray/)、[不同路径](https://leetcode.cn/problems/unique-paths/) | 掌握线性 DP 和二维 DP | | 背包 | [0-1 背包问题](https://www.acwing.com/problem/content/2/)、[目标和](https://leetcode.cn/problems/target-sum/) | 掌握背包模型 | | 提高 | [最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/)、[最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) | 掌握区间 DP、子序列 DP | | 进阶 | [编辑距离](https://leetcode.cn/problems/edit-distance/)、[正则表达式匹配](https://leetcode.cn/problems/regular-expression-matching/) | 掌握复杂状态定义 | --- ## ✅ 三、动态规划通用解题步骤(套路) ### 步骤 1:定义状态 - 明确 `dp[i]` 或 `dp[i][j]` 表示什么含义。 - 例如:`dp[i]` 表示前 i 个元素的最优解。 ### 步骤 2:写出状态转移方程 - 找到递推关系式。 - 例如:`dp[i] = max(dp[i], dp[i - 1] + nums[i])` ### 步骤 3:初始化 - 给出初始值,比如 `dp[0] = nums[0]` ### 步骤 4:确定遍历顺序 - 是从前往后?还是从后往前?还是斜着遍历? ### 步骤 5:返回结果 - 是 `dp[n]`、`dp[n][m]`、还是 `max(dp)`? --- ## ✅ 四、以经典题目为例:最长递增子序列(LIS) ### 题目描述 给定一个未排序的整数数组 `nums`,返回最长递增子序列的长度。 ### 示例 ```python 输入: nums = [10,9,2,5,3,7,101,18] 输出: 4 解释: 最长的递增子序列是 [2,3,7,101],所以长度为 4。 ``` ### 解法代码(Python) ```python def length_of_lis(nums): if not nums: return 0 n = len(nums) dp = [1] * n # 初始化:每个数至少是长度为1的子序列 for i in range(1, n): for j in range(i): if nums[i] &gt; nums[j]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) ``` ### 分析 - **状态定义**:`dp[i]` 表示以 `nums[i]` 结尾的最长递增子序列长度。 - **状态转移**:如果 `nums[i] &gt; nums[j]`,说明可以接在 `nums[j]` 后面形成更长的子序列。 - **初始化**:每个数至少是长度为1的子序列。 - **结果**:所有 `dp[i]` 中的最大值。 --- ## ✅ 五、动态规划学习建议 1. **先从简单题开始**,理解状态定义和转移方式。 2. **手动画 DP 表格**,观察状态如何变化。 3. **多总结题型套路**,比如: - 线性 DP:`dp[i] = ...` - 二维 DP:`dp[i][j] = ...` - 背包 DP:容量限制 + 选择物品 - 区间 DP分治思想,枚举中间点 4. **多刷题 + 多复盘**,推荐 LeetCode 分类刷题: - LeetCode 动态规划分类:https://leetcode.cn/tag/dynamic-programming/ --- ## ✅ 六、常见 DP 类型与模板总结 | 类型 | 模板状态定义 | 举例 | |------|---------------|------| | 线性 DP | `dp[i]` 表示前 i 个元素的最优解 | 最大子序和 | | 二维 DP | `dp[i][j]` 表示前 i 个和 j 个的组合 | 不同路径、编辑距离 | | 背包 DP | `dp[i][j]` 表示前 i 个物品在容量 j 下的最大值 | 0-1 背包、完全背包 | | 区间 DP | `dp[i][j]` 表示区间 [i, j] 的最优解 | 最长回文子串 | | 子序列 DP | `dp[i][j]` 表示两个序列的匹配 | 最长公共子序列 | --- ## ✅ 七、如何快速看懂别人写的 DP 题解? 1. **先看状态定义**:题解中的 `dp[i]` 表示什么? 2. **看状态转移方程**:怎么从前面的状态推导当前状态? 3. **看初始化条件**:初始值怎么设置? 4. **看遍历顺序**:是正序?逆序?嵌套循环? 5. **看最终结果**:返回的是 `dp[n]` 还是 `max(dp)`? --- ## ✅ 总结:动态规划学习路线图 ``` 入门 &rarr; 理解递归与记忆化 &rarr; 理解状态定义 &rarr; 掌握线性 DP &rarr; 掌握二维 DP &rarr; 学习背包问题 &rarr; 掌握区间 DP &rarr; 综合训练 ``` --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值