分子图与SMILES的关系:从分子结构到SMILES,图论基础

做化学信息学的第一步,就是搞清楚分子结构在计算机里到底长什么样。你平时画在纸上的结构式,说白了是一张「图」——原子是节点,化学键是边。而SMILES就是这张图的一种字符串表示法。怎么从图变成字符串?这里面有套图论规则。

核心认识: 分子图是「无向标号图」,节点带元素种类,边带键级(单/双/三/芳香)。SMILES是深度优先遍历这张图得到的一条路径,外加分支和环标记。

图论概念快速扫盲

先别怕,“图论”听着高深,实际上就这么几个概念你需要掌握:

  • 节点(顶点):就是原子。每个原子除了元素符号,还可能带形式电荷、同位素、手性等属性。
  • 边(连接):化学键。单键、双键、三键、芳香键,SMILES里分别用-、=、#、:表示。
  • 度(Degree):一个原子连接的键数。比如甲烷中碳的度是4。
  • 环(Cycle):图中有闭合回路的子结构。SMILES用数字标记环的起点和终点。
  • 树(Tree):无环的连通图。直链烷烃就是树结构。

我记得有一回我让实习生手写一个SMILES解析器,他死活搞不懂「环数字」是什么意思。其实说白了,就是深度优先遍历到环闭合处时,需要对应回之前的节点,用一个数字做标签——像在图上打补丁。

分子图 vs. SMILES的映射

来看张对照表,把三种形式的同一分子放在一起,你马上就能看出规律:

分子结构(简式)分子图(节点+边)典型SMILES
甲醇 CH3OHC - O,C有3个H,O有1个H(隐式)CO
异戊烷直链C4,在2位C上接甲基,分支CC(C)C(C)C 或 C(C)(C)CC
环己烷6个C构成六元环,全部单键C1CCCCC1
苯6个C六元环,交替双键 → 芳香键c1ccccc1

注意到什么没有?苯的SMILES用小写c,表示芳香碳。环数字1成对出现,标记环的打开和闭合位置。

深度优先遍历:SMILES生成的本质

我习惯把SMILES生成理解成「深度优先走图,碰到分支就加括号」。举个例子,乙醇的分子图:

     H
     |
H — C — C — O — H
     |   |
     H   H

简化后就是两个碳,一个氧,氢全部隐式。从第一个碳开始深度优先:

  1. 访问C(节点1),写下C。
  2. 向右走到第二个C(节点2),写出CC。
  3. 从C继续向右走到O,写出CCO。
  4. O后面是终点的O-H,但H隐式,结束。

乙醇没有分支,也没有环,所以SMILES就是CCO。

一旦有环,比如环己烷,深度遍历走到最后一步发现要回到起点,于是用数字打标:先写C1作为起点,一路走完5个C,最后C1闭合。

避坑指南: 我曾经在生成多环SMILES时,把两个环用了相同数字,结果解析出来分子完全变形。记住:每个环必须用不同的数字(或成对复用数字但不要冲突),常见做法是顺次使用1, 2, 3…而且环闭合时的键类型也要一致。

从分子图到SMILES的算法流程

下面这个SVG展示了最核心的转换步骤,我把它当作课程里的「地图」用:

分子图 → SMILES 的转换流程(以乙醇为例) 步骤1: 构建分子图 节点: C, C, O 边: C-C, C-O 氢隐式省略 步骤2: 选择起始原子 通常从左往右 选C1为根 步骤3: 深度优先遍历 递归访问邻居 无环无分支:直写 遍历顺序(邻接表) C1 → C2 → O 每个节点输出元素符号 键符号默认单键可省略 输出 SMILES: CCO 💡 若分子有分支: 遍历到分支点加括号() 环闭合用数字标记

这张图展示的是最简单的直链乙醇。你想想看,如果分子有分支,比如异丙醇,深度优先走到C2时发现有两个邻居——一个甲基、一个羟基——那就要写成CC(O)C,括号里的O表示挂在第二个碳上的分支。

Python小实验:从图生成SMILES

纸上谈兵没用,我建议你动手跑一个。下面是个极简实现:用字典表示分子图,深度优先遍历生成SMILES(不考虑环和立体化学,只做演示)。

# 乙醇的分子图:邻接表表示
# 节点用元组 (元素, 附加信息)
graph = {
    'C1': [('C2', 1)],       # 编号1碳连到编号2碳,单键(1)
    'C2': [('C1', 1), ('O1', 1)],  # C2连C1和O
    'O1': [('C2', 1)]        # O1只连C2
}

def dfs(current, visited, graph, path):
    visited.add(current)
    neighbors = graph.get(current, [])
    for neighbor, _ in neighbors:
        if neighbor not in visited:
            # 获取元素符号(默认取第一个字母)
            elem = neighbor[0]  # 仅演示,真实需解析
            path.append(elem)
            dfs(neighbor, visited, graph, path)

start = 'C1'
path = ['C']  # 起始节点
dfs(start, {'C1'}, graph, path)
print(''.join(path))  # 输出 'CCO'
⚠️ 注意: 上面的代码是极度简化的玩具版本。真实SMILES生成要考虑:原子价态、芳香性、环数字分配、分支括号展开顺序、规范排序(canonical SMILES)。如果用RDKit的Chem.MolToSmiles,一切帮你搞定。但理解原理会让你在遇到SMILES解析错误时,知道问题出在哪。

图论视角下的SMILES限制

任何表示法都有边界。分子图本身是无向图,但SMILES强制指定了一个遍历起点和方向。这导致同一个分子可能有多种SMILES写法(非规范)。RDKit通过原子不变性排序输出规范SMILES,但这并非图论天生自带的。

还有一点:隐式氢。分子图中氢原子通常被省略,但SMILES必须通过化合价推导出氢个数。我这里吃过亏——有一次生成一个含N+的分子,忘了调整形式电荷,结果RDKit补氢补错了,结构完全变形。

嗯,讲到这里,你应该对「分子图→SMILES」这条路清晰了。下一环节我们会拿真正的分子文件来实战解析SMILES,到时候这些图论概念就会用上。自己动手画几个分子图,再写出对应的SMILES,比看一百遍都管用。