图解除法查询问题:用 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)),在本题约几十个节点的规模上是非常轻的。
相关推荐
youliroam12 小时前
ESP32-S3+OV2640简单推流到GO服务
开发语言·后端·golang·esp32·ov2640
BrianGriffin12 小时前
asdf 安装的 PHP 上传文件大小限制
开发语言·php
2501_9167665413 小时前
【面试题1】128陷阱、==和equals的区别
java·开发语言
a程序小傲14 小时前
蚂蚁Java面试被问:注解的工作原理及如何自定义注解
java·开发语言·python·面试
似水এ᭄往昔14 小时前
【C++】--封装红⿊树实现mymap和myset
开发语言·数据结构·c++·算法·stl
charlie11451419114 小时前
嵌入式现代C++教程:C++98——从C向C++的演化(3)
c语言·开发语言·c++·笔记·学习·嵌入式
TAEHENGV14 小时前
创建目标模块 Cordova 与 OpenHarmony 混合开发实战
android·java·开发语言
程序员zgh14 小时前
C语言 指针用法与区别(指针常量、常量指针、指针函数、函数指针、二级指针)
c语言·开发语言·jvm·c++
是一个Bug14 小时前
如何阅读JDK源码?
java·开发语言
石头dhf14 小时前
大模型配置
开发语言·python