[HOT 100] 1617. 统计子树中城市之间最大距离

1. 题目链接


1617. 统计子树中城市之间最大距离 - 力扣(LeetCode)

2. 题目描述


给你 n 个城市,编号为从 1n 。同时给你一个大小为 n-1 的数组 edges ,其中 edges[i] = [ui, vi] 表示城市 uivi 之间有一条双向边。题目保证任意城市之间只有唯一的一条路径。换句话说,所有城市形成了一棵 树 。

一棵 子树 是城市的一个子集,且子集中任意城市之间可以通过子集中的其他城市和边到达。两个子树被认为不一样的条件是至少有一个城市在其中一棵子树中存在,但在另一棵子树中不存在。

对于 d1n-1 ,请你找到城市间 最大距离 恰好为 d 的所有子树数目。

请你返回一个大小为 n-1 的数组,其中第 d 个元素(下标从 1 开始)是城市间 最大距离 恰好等于 d 的子树数目。

请注意,两个城市间距离定义为它们之间需要经过的边的数目。


3. 题目示例


示例 1 :

输入:n = 4, edges = [[1,2],[2,3],[2,4]]
输出:[3,4,0]
解释:
子树 {1,2}, {2,3} 和 {2,4} 最大距离都是 1 。
子树 {1,2,3}, {1,2,4}, {2,3,4} 和 {1,2,3,4} 最大距离都为 2 。
不存在城市间最大距离为 3 的子树。

示例 2 :

输入:n = 2, edges = [[1,2]]
输出:[1]

4. 解题思路

  1. 问题理解
    • 给定一棵树,统计所有连通子图
    • 对每个可能的直径d,计算有多少子图的直径等于d
    • 直径定义为子图中最长路径的边数
  2. 核心算法
    • 使用回溯法枚举所有可能的节点子集(2^n种可能)
    • 对每个子集检查是否构成连通子图
    • 计算连通子图的直径并统计结果
  3. 直径计算
    • 采用树形DP方法
    • 通过DFS计算每个节点的最长路径
    • 在递归过程中维护全局直径
  4. 连通性检查
    • 比较访问标记数组和选择数组
    • 两者一致说明子图连通

5. 题解代码


class Solution {
    private List<Integer>[] g; // 邻接表存储树结构
    private boolean[] inSet;    // 记录哪些节点在当前子集中
    private boolean[] vis;      // DFS访问标记
    private int[] ans;          // 结果数组,ans[d]表示直径为d+1的子图数量
    private int n, diameter;   // n是节点总数,diameter记录当前子图的直径

    public int[] countSubgraphsForEachDiameter(int n, int[][] edges) {
        this.n = n;
        // 初始化邻接表
        g = new ArrayList[n];
        Arrays.setAll(g, e -> new ArrayList<>());
        // 构建树结构(节点编号转为0-based)
        for (var e : edges) {
            int x = e[0] - 1, y = e[1] - 1;
            g[x].add(y);
            g[y].add(x);
        }

        ans = new int[n - 1]; // 直径范围是1到n-1
        inSet = new boolean[n];
        f(0); // 开始递归枚举所有子集
        return ans;
    }

    // 递归枚举所有可能的节点子集
    private void f(int i) {
        // 递归终止条件:处理完所有节点
        if (i == n) {
            // 检查当前子集是否构成连通子图
            for (int v = 0; v < n; ++v)
                if (inSet[v]) {
                    vis = new boolean[n];
                    diameter = 0;
                    dfs(v); // 从第一个选中的节点开始DFS
                    break;
                }
            // 如果子图连通且直径有效,更新结果
            if (diameter > 0 && Arrays.equals(vis, inSet))
                ++ans[diameter - 1];
            return;
        }

        // 不选当前节点i
        f(i + 1);

        // 选当前节点i
        inSet[i] = true;
        f(i + 1);
        inSet[i] = false; // 回溯
    }

    // 计算子图的直径(同时标记访问过的节点)
    private int dfs(int x) {
        vis[x] = true;
        int maxLen = 0; // 记录从x出发的最长路径长度
        for (int y : g[x])
            if (!vis[y] && inSet[y]) { // 只考虑选中的且未访问的邻居
                int ml = dfs(y) + 1;  // 递归计算子节点路径长度
                diameter = Math.max(diameter, maxLen + ml); // 更新直径
                maxLen = Math.max(maxLen, ml); // 更新当前最大长度
            }
        return maxLen;
    }
}


6. 复杂度分析


时间复杂度:O(n * 2^n)

  • 枚举所有子集:O(2^n)
  • 对每个子集进行DFS:O(n)
  • 总复杂度为两者的乘积

空间复杂度:O(n)

  • 邻接表存储空间:O(n)
  • 递归调用栈深度:O(n)
  • 各种标记数组:O(n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值