树上DP与状压DP

树上动态规划和状态压缩动态规划是竞赛编程中的高级技巧。树上DP利用树的递归结构进行状态转移,状压DP通过二进制位表示状态集合,都能解决复杂的组合优化问题。

树上DP基础

树上DP的核心思想是在树的后序遍历过程中,利用子树的信息计算当前节点的状态。

树的直径 - 经典树上DP

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class TreeDiameter {
private:
    vector<vector<pair<int, int>>> adj;
    int maxDiameter = 0;
    
public:
    TreeDiameter(int n) : adj(n) {}
    
    void addEdge(int u, int v, int w) {
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }
    
    // 返回以node为根的子树中,从node出发的最长路径
    int dfs(int node, int parent) {
        int maxPath1 = 0, maxPath2 = 0;
        
        for (auto& edge : adj[node]) {
            int child = edge.first;
            int weight = edge.second;
            
            if (child == parent) continue;
            
            int childPath = dfs(child, node) + weight;
            
            // 维护最长和次长路径
            if (childPath > maxPath1) {
                maxPath2 = maxPath1;
                maxPath1 = childPath;
            } else if (childPath > maxPath2) {
                maxPath2 = childPath;
            }
        }
        
        // 更新直径(经过当前节点的最长路径)
        maxDiameter = max(maxDiameter, maxPath1 + maxPath2);
        
        return maxPath1;
    }
    
    int getDiameter() {
        maxDiameter = 0;
        dfs(0, -1);
        return maxDiameter;
    }
};

// 树的重心
class TreeCentroid {
private:
    vector<vector<int>> adj;
    vector<int> subtreeSize;
    int n, centroid, minMaxSubtree;
    
public:
    TreeCentroid(int n) : n(n), adj(n), subtreeSize(n) {}
    
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    
    int dfs(int node, int parent) {
        subtreeSize[node] = 1;
        int maxSubtree = 0;
        
        for (int child : adj[node]) {
            if (child == parent) continue;
            
            int childSubtreeSize = dfs(child, node);
            subtreeSize[node] += childSubtreeSize;
            maxSubtree = max(maxSubtree, childSubtreeSize);
        }
        
        // 考虑删除当前节点后,上方的子树大小
        maxSubtree = max(maxSubtree, n - subtreeSize[node]);
        
        if (maxSubtree < minMaxSubtree) {
            minMaxSubtree = maxSubtree;
            centroid = node;
        }
        
        return subtreeSize[node];
    }
    
    int findCentroid() {
        minMaxSubtree = n;
        dfs(0, -1);
        return centroid;
    }
};

int main() {
    // 测试树的直径
    TreeDiameter td(6);
    td.addEdge(0, 1, 2);
    td.addEdge(0, 2, 3);
    td.addEdge(1, 3, 1);
    td.addEdge(1, 4, 4);
    td.addEdge(2, 5, 2);
    
    cout << "树的直径: " << td.getDiameter() << endl;
    
    // 测试树的重心
    TreeCentroid tc(6);
    tc.addEdge(0, 1);
    tc.addEdge(0, 2);
    tc.addEdge(1, 3);
    tc.addEdge(1, 4);
    tc.addEdge(2, 5);
    
    cout << "树的重心: " << tc.findCentroid() << endl;
    
    return 0;
}

树的直径通过树上DP在每个节点维护最长和次长路径。树的重心是删除后剩余连通分量最大值最小的节点。

换根DP技巧

换根DP先以任意节点为根计算答案,然后通过状态转移计算其他节点为根的答案。

换根DP - 计算每个节点到其他所有节点的距离和

#include <iostream>
#include <vector>
using namespace std;

class RerootingDP {
private:
    vector<vector<int>> adj;
    vector<long long> subtreeSum;    // 子树内距离和
    vector<int> subtreeSize;         // 子树大小
    vector<long long> ans;           // 每个节点的答案
    int n;
    
public:
    RerootingDP(int n) : n(n), adj(n), subtreeSum(n), subtreeSize(n), ans(n) {}
    
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    
    // 第一次DFS:计算以node为根时的子树信息
    void dfs1(int node, int parent) {
        subtreeSize[node] = 1;
        subtreeSum[node] = 0;
        
        for (int child : adj[node]) {
            if (child == parent) continue;
            
            dfs1(child, node);
            subtreeSize[node] += subtreeSize[child];
            subtreeSum[node] += subtreeSum[child] + subtreeSize[child];
        }
    }
    
    // 第二次DFS:换根计算每个节点的答案
    void dfs2(int node, int parent) {
        for (int child : adj[node]) {
            if (child == parent) continue;
            
            // 计算child为根时的答案
            // 从node转移到child:
            // 1. child的子树贡献不变
            // 2. 其余部分(包括node)的贡献需要重新计算
            
            long long childSubtreeSum = subtreeSum[child];
            int childSubtreeSize = subtreeSize[child];
            
            // 从node中移除child的贡献
            subtreeSum[node] -= (childSubtreeSum + childSubtreeSize);
            subtreeSize[node] -= childSubtreeSize;
            
            // 将node的贡献加到child
            subtreeSum[child] += (subtreeSum[node] + subtreeSize[node]);
            subtreeSize[child] += subtreeSize[node];
            
            ans[child] = subtreeSum[child];
            
            dfs2(child, node);
            
            // 恢复原始值
            subtreeSum[child] = childSubtreeSum;
            subtreeSize[child] = childSubtreeSize;
            subtreeSum[node] += (childSubtreeSum + childSubtreeSize);
            subtreeSize[node] += childSubtreeSize;
        }
    }
    
    vector<long long> solve() {
        dfs1(0, -1);
        ans[0] = subtreeSum[0];
        dfs2(0, -1);
        return ans;
    }
};

int main() {
    RerootingDP rd(6);
    rd.addEdge(0, 1);
    rd.addEdge(0, 2);
    rd.addEdge(1, 3);
    rd.addEdge(1, 4);
    rd.addEdge(2, 5);
    
    vector<long long> result = rd.solve();
    
    cout << "每个节点到其他节点的距离和:" << endl;
    for (int i = 0; i < result.size(); i++) {
        cout << "节点 " << i << ": " << result[i] << endl;
    }
    
    return 0;
}

换根DP通过两次DFS避免为每个根重新计算。它通过考虑移动根时贡献如何变化来高效转移状态。

状压DP基础

状态压缩动态规划使用二进制位表示状态集合,适用于指数状态空间但参数范围较小的问题。

旅行商问题 - 状压DP经典应用

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;

class TSP {
private:
    vector<vector<int>> dist;
    vector<vector<int>> dp;
    int n;
    
public:
    TSP(vector<vector<int>>& distances) : dist(distances) {
        n = dist.size();
        dp.assign(1 << n, vector<int>(n, INT_MAX));
    }
    
    int solve() {
        // 基础情况:从城市0开始
        dp[1][0] = 0; // mask = 1 (只访问了城市0), 当前城市 = 0
        
        for (int mask = 1; mask < (1 << n); mask++) {
            for (int u = 0; u < n; u++) {
                if (!(mask & (1 << u)) || dp[mask][u] == INT_MAX) continue;
                
                for (int v = 0; v < n; v++) {
                    if (mask & (1 << v)) continue; // 城市v已经访问过
                    
                    int newMask = mask | (1 << v);
                    dp[newMask][v] = min(dp[newMask][v], dp[mask][u] + dist[u][v]);
                }
            }
        }
        
        // 找到回到起始城市的最小代价
        int result = INT_MAX;
        int finalMask = (1 << n) - 1; // 所有城市都访问过
        
        for (int u = 1; u < n; u++) {
            if (dp[finalMask][u] != INT_MAX) {
                result = min(result, dp[finalMask][u] + dist[u][0]);
            }
        }
        
        return result == INT_MAX ? -1 : result;
    }
    
    vector<int> getPath() {
        vector<int> path;
        int mask = (1 << n) - 1;
        int current = 0;
        
        // 找到给出最小代价的结束城市
        for (int u = 1; u < n; u++) {
            if (dp[mask][u] + dist[u][0] < dp[mask][current] + dist[current][0]) {
                current = u;
            }
        }
        
        // 重构路径
        while (mask != 1) {
            path.push_back(current);
            int prevMask = mask ^ (1 << current);
            
            for (int u = 0; u < n; u++) {
                if ((prevMask & (1 << u)) && 
                    dp[prevMask][u] + dist[u][current] == dp[mask][current]) {
                    mask = prevMask;
                    current = u;
                    break;
                }
            }
        }
        
        path.push_back(0);
        reverse(path.begin(), path.end());
        return path;
    }
};

// 使用状压DP的任务分配问题
class AssignmentProblem {
private:
    vector<vector<int>> cost;
    vector<int> dp;
    int n;
    
public:
    AssignmentProblem(vector<vector<int>>& costs) : cost(costs) {
        n = cost.size();
        dp.assign(1 << n, INT_MAX);
    }
    
    int solve() {
        dp[0] = 0; // 没有任务被分配
        
        for (int mask = 0; mask < (1 << n); mask++) {
            if (dp[mask] == INT_MAX) continue;
            
            int person = __builtin_popcount(mask); // 已分配任务的数量
            if (person >= n) continue;
            
            for (int task = 0; task < n; task++) {
                if (mask & (1 << task)) continue; // 任务已被分配
                
                int newMask = mask | (1 << task);
                dp[newMask] = min(dp[newMask], dp[mask] + cost[person][task]);
            }
        }
        
        return dp[(1 << n) - 1];
    }
};

int main() {
    // 测试TSP
    vector<vector<int>> distances = {
        {0, 10, 15, 20},
        {10, 0, 35, 25},
        {15, 35, 0, 30},
        {20, 25, 30, 0}
    };
    
    TSP tsp(distances);
    int minCost = tsp.solve();
    vector<int> path = tsp.getPath();
    
    cout << "TSP最小代价: " << minCost << endl;
    cout << "最优路径: ";
    for (int i = 0; i < path.size(); i++) {
        cout << path[i];
        if (i < path.size() - 1) cout << " -> ";
    }
    cout << " -> 0" << endl;
    
    // 测试任务分配问题
    vector<vector<int>> costs = {
        {9, 2, 7, 8},
        {6, 4, 3, 7},
        {5, 8, 1, 8},
        {7, 6, 9, 4}
    };
    
    AssignmentProblem ap(costs);
    cout << "\n任务分配最小代价: " << ap.solve() << endl;
    
    return 0;
}

状压DP使用二进制位编码访问状态。TSP使用dp[mask][u]表示访问mask中城市并以u结尾的最小代价。任务分配问题将任务最优分配给人员。

高级树上DP应用

高级树上DP处理复杂约束和多状态维度,常与其他技巧结合使用。

树着色问题

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

class TreeColoring {
private:
    vector<vector<int>> adj;
    vector<vector<long long>> dp;
    int n, k;
    const int MOD = 1000000007;
    
public:
    TreeColoring(int n, int k) : n(n), k(k), adj(n), dp(n, vector<long long>(k, 0)) {}
    
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    
    // dp[u][c] = 以u为根的子树中u着色为c的方案数
    void dfs(int u, int parent) {
        // 基础情况:叶子节点可以着任意颜色
        for (int c = 0; c < k; c++) {
            dp[u][c] = 1;
        }
        
        for (int v : adj[u]) {
            if (v == parent) continue;
            
            dfs(v, u);
            
            // 合并子节点v的结果
            vector<long long> newDp(k, 0);
            
            for (int uc = 0; uc < k; uc++) {
                for (int vc = 0; vc < k; vc++) {
                    if (uc != vc) { // 相邻节点必须颜色不同
                        newDp[uc] = (newDp[uc] + dp[u][uc] * dp[v][vc]) % MOD;
                    }
                }
            }
            
            dp[u] = newDp;
        }
    }
    
    long long solve() {
        dfs(0, -1);
        long long result = 0;
        for (int c = 0; c < k; c++) {
            result = (result + dp[0][c]) % MOD;
        }
        return result;
    }
};

// 树上最大权独立集
class MaxWeightIndependentSet {
private:
    vector<vector<int>> adj;
    vector<int> weight;
    vector<vector<long long>> dp;
    int n;
    
public:
    MaxWeightIndependentSet(vector<int>& weights) : weight(weights) {
        n = weight.size();
        adj.resize(n);
        dp.assign(n, vector<long long>(2, 0));
    }
    
    void addEdge(int u, int v) {
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    
    // dp[u][0] = 不选择u时的最大权重
    // dp[u][1] = 选择u时的最大权重
    void dfs(int u, int parent) {
        dp[u][0] = 0;
        dp[u][1] = weight[u];
        
        for (int v : adj[u]) {
            if (v == parent) continue;
            
            dfs(v, u);
            
            // 如果不选择u,v可以选择或不选择
            dp[u][0] += max(dp[v][0], dp[v][1]);
            
            // 如果选择u,v不能被选择
            dp[u][1] += dp[v][0];
        }
    }
    
    long long solve() {
        dfs(0, -1);
        return max(dp[0][0], dp[0][1]);
    }
    
    vector<int> getSelectedNodes() {
        vector<int> selected;
        function<void(int, int, bool)> reconstruct = [&](int u, int parent, bool canSelect) {
            if (canSelect && dp[u][1] >= dp[u][0]) {
                selected.push_back(u);
                for (int v : adj[u]) {
                    if (v != parent) {
                        reconstruct(v, u, false);
                    }
                }
            } else {
                for (int v : adj[u]) {
                    if (v != parent) {
                        reconstruct(v, u, true);
                    }
                }
            }
        };
        
        reconstruct(0, -1, true);
        return selected;
    }
};

int main() {
    // 测试树着色
    TreeColoring tc(5, 3);
    tc.addEdge(0, 1);
    tc.addEdge(0, 2);
    tc.addEdge(1, 3);
    tc.addEdge(1, 4);
    
    cout << "用3种颜色给树着色的方案数: " << tc.solve() << endl;
    
    // 测试最大权独立集
    vector<int> weights = {10, 5, 2, 7, 8};
    MaxWeightIndependentSet mwis(weights);
    mwis.addEdge(0, 1);
    mwis.addEdge(0, 2);
    mwis.addEdge(1, 3);
    mwis.addEdge(1, 4);
    
    cout << "最大权独立集: " << mwis.solve() << endl;
    
    vector<int> selected = mwis.getSelectedNodes();
    cout << "选择的节点: ";
    for (int node : selected) {
        cout << node << " ";
    }
    cout << endl;
    
    return 0;
}

高级树上DP同时处理多个约束。树着色确保相邻节点颜色不同。最大权独立集选择不相邻节点使总权重最大。

竞赛编程技巧

树上DP核心概念:

  • 利用树的递归结构进行自底向上的状态计算
  • 基于子树属性和节点特征定义状态
  • 在后序遍历过程中合并子状态计算父状态
  • 对于需要所有节点作为潜在根的问题考虑换根

状压DP应用:

  • 指数状态空间但参数约束较小的问题 (n ≤ 20)
  • 子集枚举和选择问题
  • 使用二进制表示进行状态压缩
  • 使用位运算进行高效转移

实现技巧:

  • 树上DP:在DFS过程中正确处理父子关系
  • 状压DP:使用__builtin_popcount()进行位计数
  • 内存优化:可能时使用滚动数组
  • 状态重构:跟踪转移选择以恢复解

常见模式:

  • 树的直径、重心和距离问题
  • TSP、任务分配和子集选择问题
  • 树着色和独立集问题
  • 具有树或子集约束的计数问题