化学方程式配平
题目背景
近日来,西西艾弗岛化学研究中心的研究员们向岛上的初中学生开展了化学科普活动。在活动中发现,初学化学的同学们十分苦恼于正确配平化学方程式。 而还有一些同学,则提出了一些稀奇古怪的方程式,让研究员们帮忙配平。在配平之前,研究员们需要先判断这个方程式是否能够配平。
一个化学方程式,也叫化学反应方程式,是用化学式表示化学反应的式子。其等号左右两侧分别列举了化学反应的全部反应物和生成物。 每种物质都用其化学式表示。一个物质的化学式,列举了构成该物质的各元素的原子数目。例如,水的化学式是
H
2
O
H_2O
H2O,表示水分子中含有两个氢原子和一个氧原子。 化学方程式中每种物质的化学式前面都有一个系数,表示参与反应或生成的物质的相对数目比例。例如,方程式
2
H
2
+
O
2
=
2
H
2
O
2H_2+O_2=2H_2O
2H2+O2=2H2O表示二分子氢气和一分子氧气反应生成二分子水。 我们称一个化学方程式是配平的,是指该方程式中的反应物和生成物中,各元素原子总数目相等。例如上述方程式中,左侧氢原子、氧原子的总数目分别为 4 和 2,右侧氢原子、氧原子的总数目分别为 4 和 2,因此该方程式是配平的。
题目描述
为了配平一个化学方程式,我们可以令方程式中各物质的系数为未知数,然后针对涉及的每一种元素,列出关于系数的方程,形成一个齐次线性方程组。然后求解这个方程组,得到各物质的系数。这样,我们就把化学方程式配平的问题,转化为了求解齐次线性方程组的问题。 如果方程组没有非零解,那么这个方程式是不可以配平的。反之,如果方程组有非零解,我们就可能得到一个配平的方程式。当然,最终得到的方程式仍然需要结合化学知识进行检验,对此我们不再进一步考虑,仅考虑非零解的存在。
例如要配平化学方程式:
A
l
2
(
S
O
4
)
3
+
N
H
3
⋅
H
2
O
→
A
l
(
O
H
)
3
+
(
N
H
4
)
2
S
O
4
Al_2(SO_4)_3+NH_3 \cdot H_2O \rarr Al(OH)_3 + (NH_4)_2SO_4
Al2(SO4)3+NH3⋅H2O→Al(OH)3+(NH4)2SO4
首先假定所有物质在方程的同一侧,即不考虑哪个是反应物,哪个是生成物,分别设这些物质的系数为
x
1
,
x
2
,
x
3
,
x
4
x_1,x_2,x_3,x_4
x1,x2,x3,x4,则可以针对出现的各个元素,列出如下的方程组:
2
x
1
+
0
x
2
+
x
3
+
0
x
4
=
0
A
l
3
x
1
+
0
x
2
+
0
x
3
+
1
x
4
=
0
S
12
x
1
+
1
x
2
+
3
x
3
+
4
x
4
=
0
O
0
x
1
+
x
2
+
0
x
3
+
2
x
4
=
0
N
0
x
1
+
5
x
2
+
3
x
3
+
8
x
4
=
0
H
\begin{align} 2x_1+0x_2+x_3+0x_4=0 &&Al\\ 3x_1+0x_2+0x_3+1x_4=0 &&S\\ 12x_1+1x_2+3x_3+4x_4=0& &O\\ 0x_1+x_2+0x_3+2x_4=0 &&N\\ 0x_1+5x_2+3x_3+8x_4=0 &&H\\ \end{align}
2x1+0x2+x3+0x4=03x1+0x2+0x3+1x4=012x1+1x2+3x3+4x4=00x1+x2+0x3+2x4=00x1+5x2+3x3+8x4=0AlSONH
用矩阵的形式表示为:
(
2
0
1
0
3
0
0
1
12
1
3
4
0
1
0
2
0
5
3
8
)
⋅
(
x
1
x
2
x
3
x
4
)
=
0
\begin{pmatrix} 2&0&1&0 \\ 3&0&0&1\\ 12&1&3&4\\ 0&1&0&2\\ 0&5&3&8 \end{pmatrix} \cdot \begin{pmatrix} x_1\\ x_2\\ x_3\\ x_4\end{pmatrix} = 0
231200001151030301428
⋅
x1x2x3x4
=0
对系数矩阵实施高斯消元,得到系数矩阵的一个行阶梯形式:
(
2
0
1
0
0
1
−
3
4
0
0
−
3
2
1
0
0
0
0
0
0
0
0
)
\begin{pmatrix} 2&0&1&0 \\ 0&1&-3&4\\ 0&0&-\frac{3}{2}&1\\ 0&0&0&0\\ 0&0&0&0 \end{pmatrix}
20000010001−3−230004100
由此可见,系数矩阵的秩为 3。根据线性代数的知识,我们知道,齐次线性方程组
A
X
=
0
AX=0
AX=0 的解空间的维数等于其未知数个数减去系数矩阵的秩
r
a
n
k
A
rankA
rankA。而要让方程式配平,即要求方程组存在非零解, 那么就需要让解空间的维数大于 0,即系数矩阵的秩小于未知数个数。因此,我们可以通过判断系数矩阵的秩是否小于未知数个数,来判断方程式是否可以配平。如果可以配平,则可以通过解的符号来判断反应物和生成物的位置。
本题中,我们将给出一些化学方程式,请你按照上述方法判断它们是否可以配平。为了便于程序处理,我们用到的化学式,会被化简为只包含小写字母和数字的字符串,不包含括号。 其中连续的字母表示一种元素,随后的数字表示原子个数。原子个数为 1 时不省略数字;一个化学式中包含的元素不重复。例如,上述方程式中的化学式可以化简为al2s3o12、n1h5o1、al1o3h3、n2h8s1o4。
输入格式
从标准输入读入数据。
输入的第一行包含一个正整数 𝑛,表示需要判断的化学方程式的个数。
接下来的 𝑛 行,每行描述了一个需要被配平的化学方程式。包含空格分隔的一个正整数和全部涉及物质的化学式。其中,正整数 𝑚 表示方程式中的物质;随后的 𝑚 个字符串,依次给出方程式中的反应物的化学式和生成物的化学式。
输出格式
输出到标准输出。
输出包含 𝑛行,每行包含字母 Y 或 N,表示按题设方法,所给待配平化学方程式能否配平。
样例1输入
6
2 o2 o3
3 c1o1 c1o2 o2
2 n2o4 n1o2
4 cu1 h1n1o3 cu1n2o6 h2o1
4 al2s3o12 n1h5o1 al1o3h3 n2h8s1o4
4 c1o1 c1o2 o2 h2o1
样例1输出
Y
Y
Y
N
Y
Y
解答
数据输入
# 需要判断的方程式个数
n = int(input())
# 存储答案
answer = []
for i in range(n):
# 将每个物质放在elements中 以字典形式存放 例如c1o1 为[{'c':1,'o':1}]
elements = []
line = list(input().split())
# 物质的个数
m = int(line[0])
data = line[1:]
for p in data:
element = dict()
ele = ''
j = 0
while j<len(p):
# ele存储每个物质中单个元素的名字
if not p[j].isdigit():
ele=ele+p[j]
j = j+1
else:
# 说明该元素读取完成,随后读取元素的个数
num = ''
while j<len(p) and p[j].isdigit():
num += p[j]
j += 1
element[ele] = int(num)
ele = ''
elements.append(element)
对于每个物质单独处理
逐字符读取,将元素作为键个数作为值放入element字典中
例如CO
[{'c':1,'o':1}]
用elements存储物质,每一项为一个字典,字典中对应该物质各元素及其个数
生成系数矩阵
# 用mat存储物质,其实只需要字典,因为几个物质就代表了方程有几行
mat = {}
for d in elements:
mat.update(d)
row = len(mat.keys())
# 如果方程的列数大于行数 则无需进行计算秩,方程必定有解
# 不加同样可以通过测试
if m>row:
answer.append('Y')
continue
# 读取系数矩阵
matrix = []
# 初始化
for i in range(row):
matrix.append([0] * m)
# 对于每一个元素属于一行,只读取对应元素的数量
for i,k in enumerate(mat.keys()):
for L in range(m):
if k in elements[L]:
matrix[i][L] = elements[L][k]
使用mat是为了得知该方程式中一共有多少种元素,不用集合的原因是我们需要遍历的顺序
此时可以判断一下方程式是否一定可以配平(为线代知识可以直接得到答案,去掉也能通过测试)
m 代表了一共有多少个物质即代表了矩阵有多少列,row代表了元素的个数即矩阵有几行
遍历mat中的元素,将matrix对应位置赋值
化行阶梯矩阵
# 计算行阶梯矩阵
j = 0
while j < m:
# 调整矩阵
change(matrix,j)
t = j+1
# 对于以下j位置非0的行进行计算,消去第j列上面的数字
while t< row and matrix[t][j] != 0:
div = matrix[t][j]/matrix[j][j]
for n in range(m):
matrix[t][n] = matrix[t][n]-div*matrix[j][n]
t = t+1
j = j+1
change函数
def change(matrix,row):
# row为需要调整的列 重构矩阵,将row列上不为0的换到矩阵的下方
# 保证matrix前面几行的dim列非0
i = row
while i < len(matrix):
# 从第一行开始如果对应位置为0即可跳过
if(matrix[i][row]!=0):
i = i+1
continue
# 如果不为0 则找到下面row不为0的行进行交换
j = i+1
while j < len(matrix) and matrix[j][row] == 0:
j = j+1
if j >= len(matrix):
# 说明没有row处值为0的行 直接返回即可 说明下面的全是0 无需调整
return 0
# 如果找到开始调整 交换两行的位置
temp = matrix[i]
matrix[i] = matrix[j]
matrix[j] = temp
i = i+1
change操作是将矩阵转换成非0行在上的操作,接受的参数为看的列
比如 对如下矩阵进行调整 输入的dim为1
(
2
0
1
0
0
0
0
0
0
1
−
3
4
0
0
−
1
1
0
0
0
0
)
\begin{pmatrix} 2&0&1&0 \\ 0&0&0&0\\ 0&1&-3&4\\ 0&0&-1&1\\ 0&0&0&0 \end{pmatrix}
200000010010−3−1000410
会从 1行开始调整(即0 0 0 0)
我们会看 1列(从0开始计数,则1列为 0 0 1 0 0)是否非0
如果 1列非0就进行调整,寻找之后的行满足 1列元素不为0 与之交换
则将(0 0 0 0)与(0 1 -3 4)交换
(
2
0
1
0
0
1
−
3
4
0
0
0
0
0
0
−
1
1
0
0
0
0
)
\begin{pmatrix} 2&0&1&0 \\ 0&1&-3&4\\ 0&0&0&0\\ 0&0&-1&1\\ 0&0&0&0 \end{pmatrix}
20000010001−30−1004010
由于之后的 1列元素均为0 则操作停止
矩阵化简
(
2
0
1
0
3
0
0
1
12
1
3
4
0
1
0
2
0
5
3
8
)
\begin{pmatrix} 2&0&1&0 \\ 3&0&0&1\\ 12&1&3&4\\ 0&1&0&2\\ 0&5&3&8 \end{pmatrix}
231200001151030301428
例如该矩阵 第一次change 0列 无需更改
随后将 0 行之后的每一行的 第0个元素消除
将每一行减去第0行的倍数即可
(
2
0
1
0
0
0
−
1.5
1
0
1
−
3
4
0
1
0
2
0
5
3
8
)
\begin{pmatrix} 2&0&1&0 \\ 0&0&-1.5&1\\ 0&1&-3&4\\ 0&1&0&2\\ 0&5&3&8 \end{pmatrix}
20000001151−1.5−30301428
随后调整 1列
(
2
0
1
0
0
1
−
3
4
0
1
0
2
0
5
3
8
0
0
−
1.5
1
)
\begin{pmatrix} 2&0&1&0 \\ 0&1&-3&4\\ 0&1&0&2\\ 0&5&3&8\\ 0&0&-1.5&1 \end{pmatrix}
20000011501−303−1.504281
然后再用第1行(0 1 -3 4)消除后面行的第1个元素即可
重复这个过程便可以得到化简完成的行阶梯矩阵
随后可以轻松得到矩阵的秩
完整代码
def change(matrix,row):
# row为需要调整的列 重构矩阵,将row列上不为0的换到矩阵的下方
# 保证matrix前面几行的dim列非0
i = row
while i < len(matrix):
# 从第一行开始如果对应位置为0即可跳过
if(matrix[i][row]!=0):
i = i+1
continue
# 如果不为0 则找到下面row不为0的行进行交换
j = i+1
while j < len(matrix) and matrix[j][row] == 0:
j = j+1
if j >= len(matrix):
# 说明没有row处值为0的行 直接返回即可 说明下面的全是0 无需调整
return 0
# 如果找到开始调整 交换两行的位置
temp = matrix[i]
matrix[i] = matrix[j]
matrix[j] = temp
i = i+1
def main():
# 需要判断的方程式个数
n = int(input())
# 存储答案
answer = []
for i in range(n):
# 将每个物质放在elements中 以字典形式存放 例如c1o1 为[{'c':1,'o':1}]
elements = []
line = list(input().split())
# 物质的个数
m = int(line[0])
data = line[1:]
for p in data:
element = dict()
ele = ''
j = 0
while j<len(p):
# ele存储每个物质中单个元素的名字
if not p[j].isdigit():
ele=ele+p[j]
j = j+1
else:
# 说明该元素读取完成,随后读取元素的个数
num = ''
while j<len(p) and p[j].isdigit():
num += p[j]
j += 1
element[ele] = int(num)
ele = ''
elements.append(element)
# 用mat存储物质,其实只需要字典,因为几个物质就代表了方程有几行
mat = {}
for d in elements:
mat.update(d)
row = len(mat.keys())
# 如果方程的列数大于行数 则无需进行计算秩,方程必定有解
# 不加同样可以通过测试
if m>row:
answer.append('Y')
continue
# 读取系数矩阵
matrix = []
# 初始化
for i in range(row):
matrix.append([0] * m)
# 对于每一个元素属于一行,只读取对应元素的数量
for i,k in enumerate(mat.keys()):
for L in range(m):
if k in elements[L]:
matrix[i][L] = elements[L][k]
# 计算行阶梯矩阵
j = 0
while j < m:
# 调整矩阵
change(matrix,j)
t = j+1
# 对于以下j位置非0的行进行计算,消去第j列上面的数字
while t< row and matrix[t][j] != 0:
div = matrix[t][j]/matrix[j][j]
for n in range(m):
matrix[t][n] = matrix[t][n]-div*matrix[j][n]
t = t+1
j = j+1
# 计算秩
rank = sum([1 for i in matrix if any(i)])
if rank < m:
answer.append('Y')
else:
answer.append('N')
for i in answer:
print(i)
if __name__ == "__main__":
main()