【并查集-2】399.除法求值

题目描述:

给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。

另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。

返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。

**注意:**输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。

**注意:**未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。

示例 1:

复制代码
输入:equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]
解释:
条件:a / b = 2.0, b / c = 3.0
问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
结果:[6.0, 0.5, -1.0, 1.0, -1.0 ]
注意:x 是未定义的 => -1.0

示例 2:

复制代码
输入:equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]]
输出:[3.75000,0.40000,5.00000,0.20000]

示例 3:

复制代码
输入:equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]]
输出:[0.50000,2.00000,-1.00000,-1.00000]

解题思路:

方法一:图 + DFS/BFS

核心思路:

建图:

对于 A / B = k:

  • 从 A 到 B 有一条权重为 k 的边

  • 从 B 到 A 有一条权重为 1/k 的边

这样,A / C 就转化为从 A 到 C 的路径上所有边的权重乘积。

具体过程示例:

equations = [["a","b"],["b","c"]], values = [2.0, 3.0]

复制代码
a --2.0--> b --3.0--> c
a <--0.5-- b <--1/3-- c

a / c = a->b->c = 2.0 * 3.0 = 6.0

查询逻辑:

  • 如果 from 或 to 不在图中 → 返回 -1.0

  • 如果 from == to → 返回 1.0

  • 否则,从 from 出发 DFS/BFS 搜索到 to,沿途累乘权重

代码实现(DFS):

cpp 复制代码
class Solution {
public:
    vector<double> calcEquation(vector<vector<string>>& equations, 
                                 vector<double>& values, 
                                 vector<vector<string>>& queries) {
        // 建图:邻接表,存储 (邻居, 权重)
        unordered_map<string, vector<pair<string, double>>> graph;
        
        for (int i = 0; i < equations.size(); i++) {
            string a = equations[i][0], b = equations[i][1];
            double v = values[i];
            graph[a].push_back({b, v});
            graph[b].push_back({a, 1.0 / v});
        }
        
        vector<double> result;
        for (auto& query : queries) {
            string from = query[0], to = query[1];
            
            // 变量不存在
            if (graph.find(from) == graph.end() || graph.find(to) == graph.end()) {
                result.push_back(-1.0);
                continue;
            }
            
            // 相同变量
            if (from == to) {
                result.push_back(1.0);
                continue;
            }
            
            // DFS 搜索
            unordered_set<string> visited;
            result.push_back(dfs(graph, from, to, 1.0, visited));
        }
        
        return result;
    }
    
private:
    double dfs(unordered_map<string, vector<pair<string, double>>>& graph,
               string curr, string target, double value, unordered_set<string>& visited) {
        if (curr == target) return value;
        if (visited.count(curr)) return -1.0;
        
        visited.insert(curr);
        
        for (auto& [next, weight] : graph[curr]) {
            double res = dfs(graph, next, target, value * weight, visited);
            if (res != -1.0) return res;
        }
        
        return -1.0;
    }
};

复杂度分析:

设 E 是方程式数量,Q 是查询数量,V 是变量数量。

维度 复杂度 说明
时间复杂度 O(Q × (V + E)) 每个查询最坏遍历整个图
空间复杂度 O(V + E) 存储图和 visited

方法二:并查集

带权并查集的核心:

权重定义
cpp 复制代码
weight[x] = x / parent[x]
  • 如果 x 是根节点,weight[x] = 1.0

  • 查找时,路径压缩需要更新权重

合并逻辑:

已知 a / b = v,要把 a 和 b 合并到同一集合:

  • 找到 a 的根 rootA,b 的根 rootB

  • 如果 rootA != rootB,把 rootA 接到 rootB 上

  • 需要计算 rootA / rootB 的值

推导:

复制代码
a / b = v
a = weight[a] * rootA
b = weight[b] * rootB
所以: (weight[a] * rootA) / (weight[b] * rootB) = v
即: rootA / rootB = v * weight[b] / weight[a]

所以 weight[rootA] = v * weight[b] / weight[a]。

查询逻辑:

已知 from 和 to:

  • 如果它们不在同一集合 → 返回 -1.0

  • 如果在同一集合,from / to = weight[from] / weight[to]

代码实现:

cpp 复制代码
class Solution {
private:
    unordered_map<string, string> parent;  // 父节点
    unordered_map<string, double> weight;  // weight[x] = x / parent[x]

    // 查找根节点,路径压缩 + 更新权重
    string find(string x) {
        if (parent[x] != x) {
            string root = find(parent[x]);
            weight[x] *= weight[parent[x]];  // 更新 x / root
            parent[x] = root;
        }
        return parent[x];
    }

    // 合并:a / b = v
    void unite(string a, string b, double v) {
        string rootA = find(a);
        string rootB = find(b);
        if (rootA == rootB) return;

        // rootA / rootB = v * weight[b] / weight[a]
        parent[rootA] = rootB;
        weight[rootA] = v * weight[b] / weight[a];
    }

public:
    vector<double> calcEquation(vector<vector<string>>& equations,
                                 vector<double>& values,
                                 vector<vector<string>>& queries) {
        // 初始化:每个变量是独立集合,权重为 1
        for (int i = 0; i < equations.size(); i++) {
            string a = equations[i][0], b = equations[i][1];
            if (parent.find(a) == parent.end()) {
                parent[a] = a;
                weight[a] = 1.0;
            }
            if (parent.find(b) == parent.end()) {
                parent[b] = b;
                weight[b] = 1.0;
            }
            unite(a, b, values[i]);
        }

        vector<double> result;
        for (auto& query : queries) {
            string from = query[0], to = query[1];

            // 变量不存在
            if (parent.find(from) == parent.end() ||
                parent.find(to) == parent.end()) {
                result.push_back(-1.0);
                continue;
            }

            string rootFrom = find(from);
            string rootTo = find(to);

            // 不在同一集合
            if (rootFrom != rootTo) {
                result.push_back(-1.0);
                continue;
            }

            // 在同一集合:from / to = weight[from] / weight[to]
            result.push_back(weight[from] / weight[to]);
        }

        return result;
    }
};

具体过程示例:

equations = [["a","b"],["b","c"]], values = [2.0, 3.0]

初始化:
复制代码
parent: {a:a, b:b, c:c}
weight: {a:1.0, b:1.0, c:1.0}
合并 a/b = 2.0:
复制代码
rootA = a, rootB = b
parent[a] = b
weight[a] = 2.0 * weight[b] / weight[a] = 2.0 * 1.0 / 1.0 = 2.0

parent: {a:b, b:b, c:c}
weight: {a:2.0, b:1.0, c:1.0}
合并 b/c = 3.0:
复制代码
rootB = b, rootC = c
parent[b] = c
weight[b] = 3.0 * weight[c] / weight[b] = 3.0 * 1.0 / 1.0 = 3.0

parent: {a:b, b:c, c:c}
weight: {a:2.0, b:3.0, c:1.0}
查询 a/c:
复制代码
find(a): a->b->c,路径压缩
  weight[a] = 2.0 * 3.0 = 6.0(a/c)
  parent[a] = c

find(c): c 是根,weight[c] = 1.0

a/c = weight[a] / weight[c] = 6.0 / 1.0 = 6.0 ✅

复杂度分析:

维度 复杂度 说明
时间复杂度 O((E + Q) × α) α 是阿克曼函数反函数,接近 O(1)
空间复杂度 O(V) parent 和 weight 哈希表

E 是方程式数量,Q 是查询数量,V 是变量数量。

关键细节:

1. 路径压缩时为什么要更新权重?
cpp 复制代码
string root = find(parent[x]);
weight[x] *= weight[parent[x]];
parent[x] = root;

因为 weight[x] 原来表示 x / parent[x],路径压缩后要变成 x / root,所以需要乘上 parent[x] / root(即 weight[parent[x]])。

2. 合并时 weightrootA 怎么算?
复制代码
a / b = v
a = weight[a] * rootA
b = weight[b] * rootB
(weight[a] * rootA) / (weight[b] * rootB) = v
rootA / rootB = v * weight[b] / weight[a]

所以 weight[rootA] = v * weight[b] / weight[a]。

3. 查询时为什么是 weight[from] / weight[to]?

因为:

复制代码
from / to = (weight[from] * root) / (weight[to] * root) = weight[from] / weight[to]

根节点被约掉了。

并查集 vs DFS/BFS:

对比维度 带权并查集 DFS/BFS
时间复杂度 O((E+Q) × α) O(Q × (V+E))
空间复杂度 O(V) O(V+E)
查询速度 接近 O(1) O(V+E)
代码复杂度 较高 中等
推荐度 ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐

并查集适合查询多的场景,DFS/BFS 代码更直观。

相关推荐
迪丽热爱1 小时前
一、C语言概述与程序结构
c语言·开发语言·算法
码爸1 小时前
动态规划介绍
算法·动态规划
怪奇云呼军2 小时前
教育试听预约后没到课,闪电智能 Voice Agent 如何做分层回访?
人工智能·python·算法·云计算·音视频
Nil2082 小时前
leetcode 416分割等和子集
算法·leetcode·职场和发展
云淡风轻~窗明几净2 小时前
角谷猜想的进展之三 移动了群山/沸騰的群山 2026-10-10
人工智能·算法·dubbo·图论
程序喵大人2 小时前
【C++入门】编译链接模型 - 05 ODR 为什么会让重复定义变成工程炸点
开发语言·c++·编译链接·odr
ForeverYang20153 小时前
一种无需训练缺陷数据生成算法--O2MAG
人工智能·深度学习·算法
汉克老师3 小时前
CSP-S 2026 初赛试题解析(第一部分:选择题(8-15))精讲
c++·csp-s·小学生·学c++编程
_wyt0013 小时前
洛谷 P10112 [GESP202312 八级] 奖品分配 题解
c++·数学·gesp·八级