Hard

题目描述

Alice 有一个标记为 0 到 n - 1 的 n 个节点的无向树。该树表示为长度为 n - 1 的二维整数数组 edges,其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间有一条边。

Alice 希望 Bob 找到树的根。她允许 Bob 对她的树进行几次猜测。在一次猜测中,他执行以下操作:

  • 选择两个不同的整数 u 和 v,使得树中存在边 [u, v]。
  • 他告诉 Alice,u 是树中 v 的父节点。

Bob 的猜测由二维整数数组 guesses 表示,其中 guesses[j] = [uj, vj] 表示 Bob 猜测 uj 是 vj 的父节点。

Alice 比较懒,不会回复 Bob 的每个猜测,而只是说他的猜测中至少有 k 个是正确的。

给定二维整数数组 edges、guesses 和整数 k,返回可以作为 Alice 的树的根的可能节点数。如果没有这样的树,则返回 0。

示例 1:

输入:edges = [[0,1],[1,2],[1,3],[4,2]], guesses = [[1,3],[0,1],[1,0],[2,4]], k = 3
输出:3
解释:
Root = 0, 正确猜测 = [1,3], [0,1], [2,4]
Root = 1, 正确猜测 = [1,3], [1,0], [2,4]
Root = 2, 正确猜测 = [1,3], [1,0], [2,4]
Root = 3, 正确猜测 = [1,0], [2,4]
Root = 4, 正确猜测 = [1,3], [1,0]
将 0、1 或 2 作为根节点都会导致 3 个正确猜测。

示例 2:

输入:edges = [[0,1],[1,2],[2,3],[3,4]], guesses = [[1,0],[3,4],[2,1],[3,2]], k = 1
输出:5
解释:
Root = 0, 正确猜测 = [3,4]
Root = 1, 正确猜测 = [1,0], [3,4]
Root = 2, 正确猜测 = [1,0], [2,1], [3,4]
Root = 3, 正确猜测 = [1,0], [2,1], [3,2], [3,4]
Root = 4, 正确猜测 = [1,0], [2,1], [3,2]
将任何节点作为根都至少有 1 个正确猜测。

约束条件:

  • edges.length == n - 1
  • 2 <= n <= 10^5
  • 1 <= guesses.length <= 10^5
  • 0 <= ai, bi, uj, vj <= n - 1
  • ai != bi
  • uj != vj
  • edges 表示一个有效的树
  • guesses[j] 是树的一条边
  • guesses 是唯一的
  • 0 <= k <= guesses.length

解题思路

这是一个典型的换根DP问题。我们需要计算每个节点作为根时正确猜测的数量,然后统计满足条件的根节点数。

核心思路:

  1. 建图和预处理:构建无向图,并将猜测存储在哈希集合中便于快速查询。

  2. 初始根计算:选择任意节点(如节点0)作为初始根,通过DFS计算以它为根时的正确猜测数量。

  3. 换根DP:利用换根的特性,当根从节点u转移到相邻节点v时:

    • 如果原来猜测[u,v]是正确的,换根后变为错误,正确数-1
    • 如果原来猜测[v,u]是错误的,换根后变为正确,正确数+1
  4. 状态转移:对于每个节点,递归地计算其作为根时的正确猜测数,并统计满足条件的节点数。

算法步骤:

  • 首先以节点0为根进行DFS,计算初始正确猜测数
  • 然后从节点0开始进行换根DFS,利用状态转移公式更新每个节点的正确猜测数
  • 统计正确猜测数≥k的节点个数

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

代码实现

class Solution {
public:
    int rootCount(vector<vector<int>>& edges, vector<vector<int>>& guesses, int k) {
        int n = edges.size() + 1;
        vector<vector<int>> graph(n);
        set<pair<int, int>> guessSet;
        
        for (auto& edge : edges) {
            graph[edge[0]].push_back(edge[1]);
            graph[edge[1]].push_back(edge[0]);
        }
        
        for (auto& guess : guesses) {
            guessSet.insert({guess[0], guess[1]});
        }
        
        int correctGuesses = 0;
        function<void(int, int)> dfs = [&](int node, int parent) {
            for (int child : graph[node]) {
                if (child != parent) {
                    if (guessSet.count({node, child})) {
                        correctGuesses++;
                    }
                    dfs(child, node);
                }
            }
        };
        
        dfs(0, -1);
        
        int result = 0;
        function<void(int, int, int)> reroot = [&](int node, int parent, int correct) {
            if (correct >= k) result++;
            
            for (int child : graph[node]) {
                if (child != parent) {
                    int newCorrect = correct;
                    if (guessSet.count({node, child})) newCorrect--;
                    if (guessSet.count({child, node})) newCorrect++;
                    reroot(child, node, newCorrect);
                }
            }
        };
        
        reroot(0, -1, correctGuesses);
        return result;
    }
};
class Solution:
    def rootCount(self, edges: List[List[int]], guesses: List[List[int]], k: int) -> int:
        n = len(edges) + 1
        graph = [[] for _ in range(n)]
        guess_set = set()
        
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)
        
        for u, v in guesses:
            guess_set.add((u, v))
        
        correct_guesses = 0
        
        def dfs(node, parent):
            nonlocal correct_guesses
            for child in graph[node]:
                if child != parent:
                    if (node, child) in guess_set:
                        correct_guesses += 1
                    dfs(child, node)
        
        dfs(0, -1)
        
        result = 0
        
        def reroot(node, parent, correct):
            nonlocal result
            if correct >= k:
                result += 1
            
            for child in graph[node]:
                if child != parent:
                    new_correct = correct
                    if (node, child) in guess_set:
                        new_correct -= 1
                    if (child, node) in guess_set:
                        new_correct += 1
                    reroot(child, node, new_correct)
        
        reroot(0, -1, correct_guesses)
        return result
public class Solution {
    public int RootCount(int[][] edges, int[][] guesses, int k) {
        int n = edges.Length + 1;
        List<int>[] graph = new List<int>[n];
        HashSet<(int, int)> guessSet = new HashSet<(int, int)>();
        
        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]);
        }
        
        foreach (var guess in guesses) {
            guessSet.Add((guess[0], guess[1]));
        }
        
        int correctGuesses = 0;
        
        void Dfs(int node, int parent) {
            foreach (int child in graph[node]) {
                if (child != parent) {
                    if (guessSet.Contains((node, child))) {
                        correctGuesses++;
                    }
                    Dfs(child, node);
                }
            }
        }
        
        Dfs(0, -1);
        
        int result = 0;
        
        void Reroot(int node, int parent, int correct) {
            if (correct >= k) result++;
            
            foreach (int child in graph[node]) {
                if (child != parent) {
                    int newCorrect = correct;
                    if (guessSet.Contains((node, child))) newCorrect--;
                    if (guessSet.Contains((child, node))) newCorrect++;
                    Reroot(child, node, newCorrect);
                }
            }
        }
        
        Reroot(0, -1, correctGuesses);
        return result;
    }
}
var rootCount = function(edges, guesses, k) {
    const n = edges.length + 1;
    const graph = Array.from({length: n}, () => []);
    const guessSet = new Set();
    
    for (const [u, v] of edges) {
        graph[u].push(v);
        graph[v].push(u);
    }
    
    for (const [u, v] of guesses) {
        guessSet.add(`${u},${v}`);
    }
    
    let correctGuesses = 0;
    
    function dfs(node, parent) {
        for (const child of graph[node]) {
            if (child !== parent) {
                if (guessSet.has(`${node},${child}`)) {
                    correctGuesses++;
                }
                dfs(child, node);
            }
        }
    }
    
    dfs(0, -1);
    
    let result = 0;
    
    function reroot(node, parent, correct) {
        if (correct >= k) result++;
        
        for (const child of graph[node]) {
            if (child !== parent) {
                let newCorrect = correct;
                if (guessSet.has(`${node},${child}`)) newCorrect--;
                if (guessSet.has(`${child},${node}`)) newCorrect++;
                reroot(child, node, newCorrect);
            }
        }
    }
    
    reroot(0, -1, correctGuesses);
    return result;
};

复杂度分析

复杂度类型说明
时间复杂度O(n + m)其中 n 是节点数,m 是猜测数。需要遍历所有边和猜测各一次
空间复杂度O(n + m)图的邻接表存储需要 O(n),猜测集合存储需要 O(m),递归栈深度 O(n)

相关题目