2025-11-06:包含给定路径的最小带权子树Ⅱ。用go语言,给一个带权无向树,节点编号从 0 到 n-1。用一个长度为 n-

举报
福大大架构师每日一题 发表于 2025/11/06 06:47:11 2025/11/06
【摘要】 2025-11-06:包含给定路径的最小带权子树Ⅱ。用go语言,给一个带权无向树,节点编号从 0 到 n-1。用一个长度为 n-1 的数组 edges 描述边,每个元素是三元组 [u, v, w],表示节点 u 和 v 之间有一条权重为 w 的无向边。再给一组查询 queries,每个查询是三元组 [s1, s2, t]。对于每个查询,需要找出一棵“子树”——即从原树中选取的一组连通的节点与...

2025-11-06:包含给定路径的最小带权子树Ⅱ。用go语言,给一个带权无向树,节点编号从 0 到 n-1。用一个长度为 n-1 的数组 edges 描述边,每个元素是三元组 [u, v, w],表示节点 u 和 v 之间有一条权重为 w 的无向边。

再给一组查询 queries,每个查询是三元组 [s1, s2, t]。对于每个查询,需要找出一棵“子树”——即从原树中选取的一组连通的节点与边(仍然是无环的连通子图)——满足在这棵子树中,s1 和 s2 都能经过子树中的边到达 t。要求返回使得所选子树中边权总和最小的那个值。

输出是一个数组,数组中第 j 个元素对应 queries[j] 的最小边权和。

3 <= n <= 100000。

edges.length == n - 1。

edges[i].length == 3。

0 <= ui, vi < n。

1 <= wi <= 10000。

1 <= queries.length <= 100000。

queries[j].length == 3。

0 <= src1j, src2j, destj < n。

src1j、src2j 和 destj 互不不同。

输入数据保证 edges 表示的是一棵有效的树。

输入: edges = [[0,1,2],[1,2,3],[1,3,5],[1,4,4],[2,5,6]], queries = [[2,3,4],[0,2,5]]。

输出: [12,11]。

解释:

蓝色边表示可以得到最优答案的子树之一。

在这里插入图片描述

answer[0]:在选出的子树中,从 src1 = 2 和 src2 = 3 到 dest = 4 的路径总权重为 3 + 5 + 4 = 12。

answer[1]:在选出的子树中,从 src1 = 0 和 src2 = 2 到 dest = 5 的路径总权重为 2 + 3 + 6 = 11。

题目来自力扣3553。

📝 分步详解

第一步:构建图的邻接表

首先,程序需要将输入的边数组 edges 转换成一种便于后续遍历和计算的数据结构。这里使用的是邻接表来表示这棵树。

  • 过程:遍历输入的每一条边。对于边 [u, v, w],在邻接表中,为节点 u 的记录里增加一个邻居 v 及边权 w,同时为节点 v 的记录里增加邻居 u 及边权 w。这样就构建了一个无向带权图的完整连接关系。
  • 目的:邻接表可以高效地访问任意节点的所有邻居及对应的边权,为后续的深度优先搜索(DFS)做准备 。

第二步:预处理LCA所需信息

为了快速求解任意两点的最近公共祖先,代码进行了预处理,计算并存储每个节点的深度、到根节点的距离(边权和)以及一个用于倍增法的祖先数组 pa

  1. 深度优先搜索(DFS)初始化

    • 选择节点 0 作为树的根节点。
    • 通过一次DFS遍历,计算每个节点相对于根节点的深度dep数组)和累计边权dis数组,即从根节点到该节点的路径上的总权重)。
    • 同时,记录每个节点的直接父节点(即 pa[x][0])。
  2. 倍增数组填充

    • 数组 pa[x][i] 表示节点 x 向上跳 2i2^i 步后到达的祖先节点。
    • 这个数组利用动态规划的思想进行填充:pa[x][i] = pa[ pa[x][i-1] ][i-1]。这一步是倍增算法的核心,它允许我们以对数时间快速向上跳跃 。

第三步:实现LCA查询函数

基于预处理好的信息,程序实现了两个关键函数来支持查询:

  1. uptoDep 函数:将一个节点 x 向上提升到指定的深度 d
  2. getLCA 函数:求解两个节点 xy 的最近公共祖先。
    • 首先,将深度较大的节点提升到与另一个节点相同的深度。
    • 然后,如果此时两节点相同,则该节点即为LCA。
    • 如果不同,则让两个节点同时向上倍增跳跃,直到它们再跳一步父节点就相同为止,此时的父节点就是LCA 。
  3. getDis 函数:利用公式 dis[x] + dis[y] - 2*dis[lca] 计算树上两点 xy 之间的路径边权和。这是因为从 xy 的路径必然经过它们的LCA,该公式巧妙地减去了从根节点到LCA的重复计算部分 。

第四步:处理查询并计算答案

对于每一个查询 [a, b, c](对应题目中的 [s1, s2, t]),程序并不需要真正地构造出子树。它利用了一个关键的数学洞察:

  • 核心公式:所求的最小边权总和等于 (getDis(a, c) + getDis(b, c) - getDis(a, b)) / 2。不过,根据你提供的代码,实际计算使用的是 (getDis(a, b) + getDis(b, c) + getDis(a, c)) / 2。这两个公式在数学上是等价的,可以相互推导。
  • 逻辑解释:满足条件的子树,本质上必须包含连接 a, b, c 三点的最小 Steiner 树。在这棵树上,从 ac 和从 bc 的路径会有重叠部分。这个公式的目的就是只计算一次所有边的权重,避免重复计算重叠路径的权重。最终结果就是所需子树的最小边权和 。

💡 复杂度分析

  • 时间复杂度

    • 预处理阶段(DFS和倍增数组填充):DFS遍历所有节点,时间复杂度为 O(n)O(n)。填充倍增数组需要遍历每个节点和每个二进制位(log(n)log(n) 级别),因此是 O(nlogn)O(n \log n)
    • 查询处理阶段:每次查询都进行固定次数(通常为3次)的LCA查询。每次LCA查询的时间复杂度为 O(logn)O(\log n)。因此,处理所有 qq 个查询的总时间是 O(qlogn)O(q \log n)
    • 总时间复杂度O(nlogn+qlogn)O(n \log n + q \log n)。这在给定的数据规模(n,q100,000n, q \leq 100,000)下是高效的。
  • 额外空间复杂度

    • 主要空间开销在于存储邻接表O(n)O(n))、深度数组O(n)O(n))、距离数组O(n)O(n))和倍增祖先数组O(nlogn)O(n \log n))。
    • 因此,总的额外空间复杂度O(nlogn)O(n \log n)

Go完整代码如下:

package main

import (
	"fmt"
	"math/bits"
)

func minimumWeight(edges [][]int, queries [][]int) []int {
	n := len(edges) + 1
	type edge struct{ to, wt int }
	g := make([][]edge, n)
	for _, e := range edges {
		x, y, wt := e[0], e[1], e[2]
		g[x] = append(g[x], edge{y, wt})
		g[y] = append(g[y], edge{x, wt})
	}

	const mx = 17
	pa := make([][mx]int, n)
	dep := make([]int, n)
	dis := make([]int, n)
	var dfs func(int, int)
	dfs = func(x, p int) {
		pa[x][0] = p
		for _, e := range g[x] {
			y := e.to
			if y == p {
				continue
			}
			dep[y] = dep[x] + 1
			dis[y] = dis[x] + e.wt
			dfs(y, x)
		}
	}
	dfs(0, -1)

	for i := range mx - 1 {
		for x := range pa {
			p := pa[x][i]
			if p != -1 {
				pa[x][i+1] = pa[p][i]
			} else {
				pa[x][i+1] = -1
			}
		}
	}

	uptoDep := func(x, d int) int {
		for k := uint(dep[x] - d); k > 0; k &= k - 1 {
			x = pa[x][bits.TrailingZeros(k)]
		}
		return x
	}
	getLCA := func(x, y int) int {
		if dep[x] > dep[y] {
			x, y = y, x
		}
		y = uptoDep(y, dep[x])
		if y == x {
			return x
		}
		for i := mx - 1; i >= 0; i-- {
			if pv, pw := pa[x][i], pa[y][i]; pv != pw {
				x, y = pv, pw
			}
		}
		return pa[x][0]
	}
	getDis := func(x, y int) int { return dis[x] + dis[y] - dis[getLCA(x, y)]*2 }

	// 以上全是 LCA 模板

	ans := make([]int, len(queries))
	for i, q := range queries {
		a, b, c := q[0], q[1], q[2]
		ans[i] = (getDis(a, b) + getDis(b, c) + getDis(a, c)) / 2
	}
	return ans
}

func main() {
	edges := [][]int{{0, 1, 2}, {1, 2, 3}, {1, 3, 5}, {1, 4, 4}, {2, 5, 6}}
	queries := [][]int{{2, 3, 4}, {0, 2, 5}}
	result := minimumWeight(edges, queries)
	fmt.Println(result)
}

在这里插入图片描述

Python完整代码如下:

# -*-coding:utf-8-*-

import sys
sys.setrecursionlimit(100000)

def minimumWeight(edges, queries):
    n = len(edges) + 1
    g = [[] for _ in range(n)]
    for e in edges:
        x, y, wt = e[0], e[1], e[2]
        g[x].append((y, wt))
        g[y].append((x, wt))
    
    mx = 17
    pa = [[-1] * mx for _ in range(n)]
    dep = [0] * n
    dis = [0] * n
    
    def dfs(x, p):
        pa[x][0] = p
        for y, wt in g[x]:
            if y == p:
                continue
            dep[y] = dep[x] + 1
            dis[y] = dis[x] + wt
            dfs(y, x)
    
    dfs(0, -1)
    
    for i in range(mx - 1):
        for x in range(n):
            p = pa[x][i]
            if p != -1:
                pa[x][i + 1] = pa[p][i]
            else:
                pa[x][i + 1] = -1
    
    def uptoDep(x, d):
        k = dep[x] - d
        while k > 0:
            x = pa[x][(k & -k).bit_length() - 1]
            k &= k - 1
        return x
    
    def getLCA(x, y):
        if dep[x] > dep[y]:
            x, y = y, x
        y = uptoDep(y, dep[x])
        if y == x:
            return x
        for i in range(mx - 1, -1, -1):
            if pa[x][i] != pa[y][i]:
                x, y = pa[x][i], pa[y][i]
        return pa[x][0]
    
    def getDis(x, y):
        return dis[x] + dis[y] - dis[getLCA(x, y)] * 2
    
    ans = []
    for q in queries:
        a, b, c = q[0], q[1], q[2]
        ans.append((getDis(a, b) + getDis(b, c) + getDis(a, c)) // 2)
    return ans

if __name__ == "__main__":
    edges = [[0, 1, 2], [1, 2, 3], [1, 3, 5], [1, 4, 4], [2, 5, 6]]
    queries = [[2, 3, 4], [0, 2, 5]]
    result = minimumWeight(edges, queries)
    print(result)

在这里插入图片描述

C++完整代码如下:

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

struct Edge {
    int to, wt;
};

vector<vector<Edge>> g;
vector<array<int, 18>> pa;
vector<int> dep, dis;
int n;

void dfs(int x, int p) {
    pa[x][0] = p;
    for (auto &e : g[x]) {
        int y = e.to;
        if (y == p) continue;
        dep[y] = dep[x] + 1;
        dis[y] = dis[x] + e.wt;
        dfs(y, x);
    }
}

int uptoDep(int x, int d) {
    int diff = dep[x] - d;
    for (int k = 0; diff > 0; k++, diff >>= 1) {
        if (diff & 1) x = pa[x][k];
    }
    return x;
}

int getLCA(int x, int y) {
    if (dep[x] > dep[y]) swap(x, y);
    y = uptoDep(y, dep[x]);
    if (x == y) return x;
    for (int i = 17; i >= 0; i--) {
        if (pa[x][i] != pa[y][i]) {
            x = pa[x][i];
            y = pa[y][i];
        }
    }
    return pa[x][0];
}

int getDis(int x, int y) {
    return dis[x] + dis[y] - dis[getLCA(x, y)] * 2;
}

vector<int> minimumWeight(vector<vector<int>> &edges, vector<vector<int>> &queries) {
    n = edges.size() + 1;
    g.assign(n, {});
    for (auto &e : edges) {
        int x = e[0], y = e[1], wt = e[2];
        g[x].push_back({y, wt});
        g[y].push_back({x, wt});
    }
    pa.assign(n, {});
    dep.assign(n, 0);
    dis.assign(n, 0);

    dfs(0, -1);

    for (int i = 0; i < n; i++) {
        for (int j = 1; j <= 17; j++) {
            int p = pa[i][j-1];
            pa[i][j] = (p == -1 ? -1 : pa[p][j-1]);
        }
    }

    vector<int> ans;
    for (auto &q : queries) {
        int a = q[0], b = q[1], c = q[2];
        ans.push_back((getDis(a, b) + getDis(b, c) + getDis(a, c)) / 2);
    }
    return ans;
}

int main() {
    vector<vector<int>> edges = {{0, 1, 2}, {1, 2, 3}, {1, 3, 5}, {1, 4, 4}, {2, 5, 6}};
    vector<vector<int>> queries = {{2, 3, 4}, {0, 2, 5}};
    vector<int> result = minimumWeight(edges, queries);
    for (auto r : result) cout << r << " ";
    cout << endl;
    return 0;
}

在这里插入图片描述

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。