人民邮电出版社m.lom599公司准备重新出版《纯数学教程(纪念版)》。我于 2013 年 10 月写了一篇文章“《纯数学教程(纪念版)》中的根式”,说:书中的根式里面都有不必要的括号。

现在m.lom599来去掉这些不必要的括号。这本书是使用 LaTeX 排版的,第 31 页第 32 题的第 1 个根式的 TeX 代码如下:

\[
\sqrt {\left( {\sqrt[3]{5} - \sqrt[3]{4}} \right)} =
\frac{1}{3}\left( {\sqrt[3]{2} + \sqrt[3]{20} -
\sqrt[3]{{25}}}\right),
\]

去掉不必要的括号后变为:

\[
\sqrt { {\sqrt[3]{5} - \sqrt[3]{4}} } =
\frac{1}{3}\left( {\sqrt[3]{2} + \sqrt[3]{20} -
\sqrt[3]{{25}}}\right),
\]

第 2 个根式:

\[
\sqrt[\mbox{\raisebox{0.8ex}{\mbox{{\mbox{${}^3$}}}}}]{\left(
{\sqrt[3]{2} - 1} \right)}=
\sqrt[\mbox{\raisebox{1.5ex}{\mbox{{\mbox{${}^3$}}}}}]{\left(
{\frac{1}{9}} \right)}-
\sqrt[\mbox{\raisebox{1.5ex}{\mbox{{\mbox{${}^3$}}}}}]{\left(
{\frac{2}{9}} \right)}+
\sqrt[\mbox{\raisebox{1.5ex}{\mbox{{\mbox{${}^3$}}}}}]{\left(
{\frac{4}{9}} \right)},
\]

去掉不必要的括号后变为:

\[
\sqrt[\mbox{\raisebox{0.8ex}{\mbox{{\mbox{${}^3$}}}}}]{
{\sqrt[3]{2} - 1} }=
\sqrt[\mbox{\raisebox{1.5ex}{\mbox{{\mbox{${}^3$}}}}}]{
{\frac{1}{9}} }-
\sqrt[\mbox{\raisebox{1.5ex}{\mbox{{\mbox{${}^3$}}}}}]{
{\frac{2}{9}} }+
\sqrt[\mbox{\raisebox{1.5ex}{\mbox{{\mbox{${}^3$}}}}}]{
{\frac{4}{9}} },
\]

我写了一个 C# 程序来完成这个任务,运行结果如下:

$ mcs sqrttransformer.cs && ./sqrttransformer.exe 
   5   3 ch00.tex
 273  41 ch01.tex
 109  50 ch02.tex
  58  34 ch03.tex
  52  10 ch04.tex
  53  36 ch05.tex
 216 156 ch06.tex
  70  61 ch07.tex
 101  62 ch08.tex
 112  75 ch09.tex
  59  46 ch10.tex
   7   7 ch99.tex
1115 581 Total
$

可以看出,全书共有 1115 个根式(包含嵌套的根式),其中有 581 个根式需要去掉不必要的括号。第 6 章需要去掉不必要的括号的根式最多,有 156 个。

源程序如下:

 1 using System;
 2 using System.IO;
 3 
 4 static class SqrtTransformer
 5 {
 6   static readonly string S = "\\sqrt", L = "\\left", R = "\\right";
 7   static readonly string[] Ls = {"(","[","\\{",L+"(",L+"[",L+"\\{"};
 8   static readonly string[] Rs = {")","]","\\}",R+")",R+"]",R+"\\}"};
 9   static int n1 = 0, n2 = 0;
10 
11   static string DeleteBracket(string s)
12   {
13     if (s.Length < 5 || s[0] != '{' || s[s.Length - 1] != '}') return s;
14     var i0 = 1; while (char.IsWhiteSpace(s, i0)) i0++;
15     for (var k = 0; k < Ls.Length; k++) {
16       if (!s.Substring(i0).StartsWith(Ls[k])) continue;
17       var i1 = s.IndexOf(Rs[k], i0 += Ls[k].Length);
18       if (i1 < 0) throw new Exception("Right bracket not found");
19       var i2 = i1 + Rs[k].Length; while (char.IsWhiteSpace(s, i2)) i2++;
20       if (i2 + 1 != s.Length) return s; // right bracket isn't most right
21       return Transform("{" + s.Substring(i0, i1 - i0) + "}");
22     }
23     return s;
24   }
25 
26   static (int Left, int Right) GetBoundary(string s, int i)
27   {
28     for (i += S.Length; char.IsWhiteSpace(s, i); ) i++;
29     if (s[i] == '[') { while (s[i] != ']') i++; i++; }
30     while (char.IsWhiteSpace(s, i)) i++; int i0;
31     if (s[i0 = i] != '{') return (i, i); //TODO: s[i] == '\\'
32     for (var n=1; n>0;) if (s[++i]=='{') n++; else if (s[i]=='}') n--;
33     return (i0, i); // s[i0] == '{' && s[i] == '}'
34   }
35 
36   static string Transform(string s)
37   {
38     var t = new System.Text.StringBuilder();
39     for (int j, i = 0; i < s.Length; i++) {
40       if ((j = s.IndexOf(S, i)) < 0) { t.Append(s.Substring(i)); break; }
41       var (b, d) = GetBoundary(s, j);  t.Append(s.Substring(i, b - i));
42       string s2, s1 = s.Substring(b, (i = d) - b + 1);
43       t.Append(s2 = DeleteBracket(s1)); n1++; if (s1 != s2) n2++;
44     }
45     return t.ToString();
46   }
47 
48   static void Main()
49   {
50     int i1 = 0, i2 = 0;
51     foreach (var file in Directory.GetFiles("..", "ch??.tex")) {
52       File.WriteAllText(file, Transform(File.ReadAllText(file)));
53       Console.WriteLine("{0,4} {1,3} {2}", n1 - i1, n2 - i2, file);
54       i1 = n1; i2 = n2;
55     }
56     Console.WriteLine("{0,4} {1,3} Total", n1, n2);
57   }
58 }

简要说明:

  • 第 51 行的 foreach 循环遍历全书各章(ch00.tex, ch01.tex, ...)。
  • 第 52 行的 File.ReadAllText 方法把某一章全部读入内存。
  • 然后调用 Transform 方法进行转换(去掉不必要的括号)。
  • 接着调用 File.WriteAllText 方法把这一章写回 ch??.tex 文件。
  • 第 36 至 46 行的 Transform 方法执行转换。主要是查找 "\sqrt",然后调用 GetBoundary 方法找出其后的以 '{' 和 '}' 表示的左右边界,再去掉其中的不必要的括号。
  • 第 26 至 34 行的 GetBoundary 方法寻找 "\sqrt" 的左右边界。
  • 第 29 行跳过 "[...]"(对应于开平方以外的情况)。
  • 第 31 行对应被开方数是单个符号的情况。其中有可能是 \Delta 这样用多个字符表示的单个符号,但是对m.lom599的目的没有影响,为简单起见,直接定界为 \ 就行了。
  • 第 32 行定界 "{...}" 的情况,其中允许有嵌套的 "{}"。
  • 第 11 至 24 行的 DeleteBracket 方法删除不必要的括号。
  • 第 16 行判断根式中是不是以左括号开头。
  • 第 17 行寻找匹配的右括号。
  • 第 18 行处理找不到匹配的右括号的情况。
  • 第 20 行处理右括号后面不是紧接着右边界的情况,此时这对括号是必要的,不能删除。
  • 第 21 行递归调用 Transform 方法以删除嵌套的根式中的不必要的括号。

如果需要正确处理被开方数是 \Delta 这样的情况,可以把第 31 行替换为:

if (s[i0 = i] != '{') {
  if (s[i] != '\\') return (i, i);
  for (i0 = i++; char.IsLetter(s, i); ) i++;
  return (i0, i - 1);
}