图解除法查询问题:用 C 语言和 DFS 实现带权有向图的路径乘积

题目与核心抽象

题目给出若干等式 A / B = k,需要回答一系列除法查询 C / D 的结果,如果无法确定则返回 -1.0。leetcode

核心抽象:

把每个变量("a"、"b"、"bc"、"cd" 等)看成图中的一个节点。

每条等式 A / B = k 看成两条有向边:

  • A -> B,权重 k(表示 A / B = k);
  • B -> A,权重 1 / k(表示 B / A = 1/k)。

这样,任何一个查询 C / D 就变成:在图中找从 C 到 D 的路径,路径上所有边权重的乘积,就是 C / D 的值。如果不存在路径,就返回 -1.0。

注意:像 "bc" 和 "cd" 这种变量,仅仅是"字符串名字",算法里不要把它们当成 b * c、c * d 去约分。

数据结构设计(C 语言)

1)变量名到节点下标的映射

变量名是长度 ≤ 5 的字符串,所以不能用单个 char,需要一个 string → int 的映射。

简单做法:

  • char names[MAXN][6]; 存所有出现过的变量名。
  • int var_count = 0; 记录当前变量数。

函数 get_id(char *s):

  • 在线性扫描 names[0...var_count-1] 中用 strcmp 查找 s;
  • 找到就返回下标 i;
  • 找不到就把 s 拷贝到 names[var_count],返回 var_count,然后 var_count++。

2)邻接表表示带权有向图

使用"数组模拟链表"的邻接表:

c 复制代码
int head[MAXN];
struct Edge { int to; double w; int next; };
struct Edge edges[MAXE];
int edge_count = 0;
  • head[u] 存的是从节点 u 出发的第一条边在边数组里的下标;没边时为 -1。
  • to 是边的终点节点下标;
  • w 是边的权重(double);
  • next 是同一个起点的下一条边的下标。

加边函数伪代码(u -> v,权重 w):

c 复制代码
edges[edge_count].to = v;
edges[edge_count].w = w;
edges[edge_count].next = head[u];
head[u] = edge_count;
edge_count++;

这样就实现了典型的邻接表结构:head[u] 和 edges[e].next 都是边下标(int),不是结构体本身。

3)访问标记

DFS 需要一个 visited 数组:

c 复制代码
int visited[MAXN]; // 0 未访问,1 已访问

建图过程

对每条等式 Ai / Bi = value:

c 复制代码
int u = get_id(Ai);
int v = get_id(Bi);
add_edge(u, v, value);     // Ai / Bi = value
add_edge(v, u, 1.0 / value);  // Bi / Ai = 1/value

初始化时:

  • 所有 head[i] = -1;
  • edge_count = 0;
  • var_count = 0;

对于 "bc"、"cd" 这样的变量名,完全按普通字符串处理:

  • "bc" 可能映射为 2,"cd" 映射为 3;
  • 等式 bc / cd = 5.0 仍然只是 2 -> 3 权重 5.0,3 -> 2 权重 0.2,不做任何字符级的"约分"。

DFS 逻辑与返回 -1.0 的判定

目标:从起点 start(C)到终点 target(D),沿途乘边权,若找到路径就返回乘积,找不到就返回 -1.0。

1)DFS 函数设计

约定:

  • 如果从当前节点 u 出发能走到 target,则返回对应的 start / target 值;
  • 否则返回 -1.0。

伪代码:

c 复制代码
double dfs(int u, int target, double curr)
if (u == target):
    return curr;  // 表示 start / target 已经算完

mark visited[u] = 1;

for each outgoing edge u->v with weight w:
    if visited[v] == 1:
        skip;  // 避免环
    
    double res = dfs(v, target, curr * w);
    if res != -1.0:
        return res;  // 说明通过 v 找到了 target

return -1.0;  // 从 u 出发的所有路径都无法到达 target

其中 curr 始终代表"从原始起点 start 走到当前 u 的值 start / u"。每次走边 u->v 权重 w 时,新的乘积是 curr * w,对应公式 (start/u) ∗ (u/v) = start/v。

2)单个查询的处理

给一个查询 C / D(两个字符串):

特殊情况先处理

  • 如果 C 或 D 从未出现在 equations 中:需要一个"只查不插"的函数,比如 find_id_no_insert©;如果不存在,答案就是 -1.0。
  • 如果 C 和 D 字符串完全相同:
    • 如果它在图里出现过(有 id),返回 1.0;
    • 否则返回 -1.0(未定义变量)。

一般情况

  • 用映射拿到下标:int u = id©; int v = id(D);
  • 清空 visited:对所有节点 visited[i] = 0;
  • 调用:double ans = dfs(u, v, 1.0);
  • 如果 ans 为 -1.0,说明 start 和 target 不连通,返回 -1.0;否则返回 ans 作为该查询的结果。

3)为什么用 -1.0 表示"失败"

题目本身要求"无法确定时返回 -1.0",所以 DFS 沿用这个值刚好对应题意。

  • 某条分支找不到 target,就在那一层返回 -1.0;
  • 调用方(上一层 DFS)看到 res == -1.0,就继续尝试下一个邻居;
  • 如果所有邻居都返回 -1.0,说明从这个 u 出发无解,也返回 -1.0;
  • 最外层返回 -1.0,就可以直接填入结果数组。

小结与复杂度

抽象:

变量 → 节点,等式 → 带权有向边,查询 → 图上的路径乘积问题。

实现:

  • C 语言里用 string→id 的简单映射 + 邻接表存图;
  • 每条等式 A / B = k 建两条边(k 和 1/k);
  • 每个查询用 DFS 从 C 到 D,沿途乘边权;
  • 无法到达或变量未定义则返回 -1.0。

复杂度(在这题规模下非常够用):

  • 建图:O(E),E 是等式数量(≤ 20)。
  • 每个查询 DFS:最坏 O(V + E),V 是变量数;Q 个查询总共 O(Q × (V + E)),在本题约几十个节点的规模上是非常轻的。
相关推荐
雨落在了我的手上1 小时前
知识扩展:进制的详细介绍
c语言·学习
poggioxay1 小时前
JAVA零基础入门知识3(持续更新中)
java·开发语言·python
鹤归时起雾.1 小时前
Vue3响应式编程核心指南
开发语言·vue3
charlie1145141911 小时前
深入理解CC++的编译与链接技术8:Windows和Linux是如何搜寻动态库的?
c语言·c++·动态库·编译·编译技术
郝学胜-神的一滴1 小时前
Linux信号四要素详解:从理论到实践
linux·服务器·开发语言·网络·c++·程序人生
yangpipi-1 小时前
《C++并发编程实战》 第3章 在线程间共享数据
开发语言·c++
fish_xk1 小时前
c++基础
开发语言·c++
MoonBit月兔1 小时前
审美积累 | MoonBit LOGO 投稿作品速递
开发语言·编程·moonbit
缘三水2 小时前
【C语言】12.指针(2)
c语言·开发语言·指针