Hard

题目描述

给你一个整数 n 和一棵有 n 个节点的无向树,节点编号从 0 到 n - 1。树用一个长度为 n - 1 的二维数组 edges 表示,其中 edges[i] = [ui, vi] 表示节点 ui 和 vi 之间有一条无向边。

同时给你一个长度为 n 的整数数组 group,其中 group[i] 表示分配给节点 i 的组标签。

  • 如果 group[u] == group[v],则认为两个节点 u 和 v 属于同一组。
  • 节点 u 和 v 之间的交互成本定义为连接它们的树中唯一路径上的边数。

返回所有满足 u != v 且 group[u] == group[v] 的无序对 (u, v) 的交互成本之和。

示例 1:

输入:n = 3, edges = [[0,1],[1,2]], group = [1,1,1]
输出:4
解释:所有节点都属于组 1。节点对之间的交互成本为:
- 节点 (0, 1):1
- 节点 (1, 2):1  
- 节点 (0, 2):2
因此,总交互成本为 1 + 1 + 2 = 4。

示例 2:

输入:n = 3, edges = [[0,1],[1,2]], group = [3,2,3]
输出:2
解释:节点 0 和 2 属于组 3,它们之间的交互成本为 2。

约束条件:

  • 1 <= n <= 10^5
  • edges.length == n - 1
  • 0 <= ui, vi <= n - 1
  • group.length == n
  • 1 <= group[i] <= 20
  • 输入保证 edges 表示一棵有效的树

解题思路

这是一道关于树上路径统计的题目。我们需要计算所有相同组别节点对之间的距离总和。

核心思路:

对于树上的每条边,我们可以分析它对答案的贡献。当我们删除一条边时,树被分成两个连通分量。对于同一组别的节点,如果它们分别在两个连通分量中,那么它们之间的路径必须经过这条边,因此这条边对答案的贡献就是两个分量中该组别节点数的乘积。

具体算法:

  1. 使用后序DFS遍历树,对每个子树统计各组别的节点数量
  2. 对于每条边 (u, v),假设 v 是 u 的子节点,计算删除这条边后:
    • v 子树中每个组别的节点数:subtree_count[group]
    • 剩余部分中每个组别的节点数:total_count[group] - subtree_count[group]
  3. 该边对组别 g 的贡献为:subtree_count[g] × (total_count[g] - subtree_count[g])
  4. 将所有边、所有组别的贡献累加即为答案

这种方法的时间复杂度为 O(n),空间复杂度为 O(n),是最优解法。

代码实现

class Solution {
public:
    long long interactionCosts(int n, vector<vector<int>>& edges, vector<int>& group) {
        vector<vector<int>> graph(n);
        for (auto& edge : edges) {
            graph[edge[0]].push_back(edge[1]);
            graph[edge[1]].push_back(edge[0]);
        }
        
        // 统计每个组的总节点数
        unordered_map<int, int> totalCount;
        for (int g : group) {
            totalCount[g]++;
        }
        
        long long result = 0;
        
        function<unordered_map<int, int>(int, int)> dfs = [&](int node, int parent) -> unordered_map<int, int> {
            unordered_map<int, int> subtreeCount;
            subtreeCount[group[node]] = 1;
            
            for (int child : graph[node]) {
                if (child != parent) {
                    auto childCount = dfs(child, node);
                    
                    // 计算边 (node, child) 的贡献
                    for (auto& [g, count] : childCount) {
                        long long contribution = (long long)count * (totalCount[g] - count);
                        result += contribution;
                        
                        // 合并子树统计
                        subtreeCount[g] += count;
                    }
                }
            }
            
            return subtreeCount;
        };
        
        dfs(0, -1);
        return result;
    }
};
class Solution:
    def interactionCosts(self, n: int, edges: List[List[int]], group: List[int]) -> int:
        from collections import defaultdict, Counter
        
        # 构建邻接表
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)
        
        # 统计每个组的总节点数
        total_count = Counter(group)
        result = 0
        
        def dfs(node, parent):
            nonlocal result
            # 当前子树中各组的节点数
            subtree_count = defaultdict(int)
            subtree_count[group[node]] = 1
            
            for child in graph[node]:
                if child != parent:
                    child_count = dfs(child, node)
                    
                    # 计算边 (node, child) 的贡献
                    for g, count in child_count.items():
                        contribution = count * (total_count[g] - count)
                        result += contribution
                        
                        # 合并子树统计
                        subtree_count[g] += count
            
            return subtree_count
        
        dfs(0, -1)
        return result
public class Solution {
    public long InteractionCosts(int n, int[][] edges, int[] group) {
        var graph = new List<int>[n];
        for (int i = 0; i < n; i++) {
            graph[i] = new List<int>();
        }
        
        foreach (var edge in edges) {
            graph[edge[0]].Add(edge[1]);
            graph[edge[1]].Add(edge[0]);
        }
        
        // 统计每个组的总节点数
        var totalCount = new Dictionary<int, int>();
        foreach (int g in group) {
            totalCount[g] = totalCount.GetValueOrDefault(g, 0) + 1;
        }
        
        long result = 0;
        
        Dictionary<int, int> Dfs(int node, int parent) {
            var subtreeCount = new Dictionary<int, int>();
            subtreeCount[group[node]] = 1;
            
            foreach (int child in graph[node]) {
                if (child != parent) {
                    var childCount = Dfs(child, node);
                    
                    // 计算边 (node, child) 的贡献
                    foreach (var kvp in childCount) {
                        int g = kvp.Key;
                        int count = kvp.Value;
                        long contribution = (long)count * (totalCount[g] - count);
                        result += contribution;
                        
                        // 合并子树统计
                        subtreeCount[g] = subtreeCount.GetValueOrDefault(g, 0) + count;
                    }
                }
            }
            
            return subtreeCount;
        }
        
        Dfs(0, -1);
        return result;
    }
}
var interactionCosts = function(n, edges, group) {
    // 构建邻接表
    const graph = Array(n).fill(null).map(() => []);
    for (const [u, v] of edges) {
        graph[u].push(v);
        graph[v].push(u);
    }
    
    // 统计每个组的总节点数
    const totalCount = new Map();
    for (const g of group) {
        totalCount.set(g, (totalCount.get(g) || 0) + 1);
    }
    
    let result = 0;
    
    function dfs(node, parent) {
        const subtreeCount = new Map();
        subtreeCount.set(group[node], 1);
        
        for (const child of graph[node]) {
            if (child !== parent) {
                const childCount = dfs(child, node);
                
                // 计算边 (node, child) 的贡献
                for (const [g, count] of childCount) {
                    const contribution = count * (totalCount.get(g) - count);
                    result += contribution;
                    
                    // 合并子树统计
                    subtreeCount.set(g, (subtreeCount.get(g) || 0) + count);
                }
            }
        }
        
        return subtreeCount;
    }
    
    dfs(0, -1);
    return result;
};

复杂度分析

复杂度类型分析
时间复杂度O(n) - 每个节点访问一次,每条边处理一次
空间复杂度O(n) - 递归栈深度最大为 O(n),邻接表存储需要 O(n) 空间