题目与核心抽象
题目给出若干等式 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)),在本题约几十个节点的规模上是非常轻的。