我从几年前第一次接触数独以来就一直喜欢它。因为我现在经常使用 LaTeX,我想找个简单的方法用 TikZ 排版数独网格,想着将来可能会写一些教程。
结果发现,已经有一个包可以做到这一点:sudoku(https://ctan.org/pkg/sudoku) 包。这个包很不错,但对我来说有两个限制。首先,你需要为网格中的每个方格写不少代码,像这样:
\begin{sudoku}
|2|5| | |3| |9| |1|.
| |1| | | |4| | | |.
|4| |7| | | |2| |8|.
| | |5|2| | | | | |.
| | | | |9|8|1| | |.
| |4| | | |3| | | |.
| | | |3|6| | |7|2|.
| |7| | | | | | |3|.
|9| |3| | | |6| |4|.
\end{sudoku}
这并不是特别麻烦(复制粘贴在这里可能会帮上忙),但要处理的代码量确实不少。其次,它没有办法给网格中的特定方格上色,如果你在写教程的话,这可能是你想要做的事情。
(我应该提到还有一个叫做 sudokubundle(https://ctan.org/pkg/sudokubundle) 的包,实际上它包含三个包:一个允许你从输入文件排版谜题,另一个尝试找到指定谜题的解决方案,还有一个生成谜题。我对这个包没有进行过深入的尝试,只发现它同样没有办法给网格中的某个单元上色。有关这个包的更多信息,请访问“数独求解器(https://tug.org/pracjourn/2008-2/distract/)”。)
对我来说,显而易见的做法就是使用TikZ,因为我们真正要画的只是一个方格网,然后添加一些节点来放置我们的数字。(是的,我可以用表格来做,但表格应该用于表格数据,而这其实并不是。我们这里并不是在组织或总结数据。)
数独网格是一个9×9的网格(因此有81个单元格),但这个网格被分成九个3×3的小网格,通常用较粗的线条来表示。所以我们将假设一个9×9的网格用于TikZ ,并开始用每第三条线的粗线来勾勒我们的九个小网格:
\begin{tikzpicture}[x=5mm,y=5mm]
% Thick horizontal lines
\draw [ultra thick](0,0)--(9,0);
\draw [ultra thick](0,3)--(9,3);
\draw [ultra thick](0,6)--(9,6);
\draw [ultra thick](0,9)--(9,9);
% Thick vertical lines
\draw [ultra thick](0,0)--(0,9);
\draw [ultra thick](3,0)--(3,9);
\draw [ultra thick](6,0)--(6,9);
\draw [ultra thick](9,0)--(9,9);
\end{tikzpicture}
现在我们可以添加更细的线条来划分单独的单元格,这样我们的代码看起来就像这样:
\begin{tikzpicture}[x=5mm,y=5mm]
% Thick horizontal lines
\draw [ultra thick](0,0)--(9,0);
\draw [ultra thick](0,3)--(9,3);
\draw [ultra thick](0,6)--(9,6);
\draw [ultra thick](0,9)--(9,9);
% Thick vertical lines
\draw [ultra thick](0,0)--(0,9);
\draw [ultra thick](3,0)--(3,9);
\draw [ultra thick](6,0)--(6,9);
\draw [ultra thick](9,0)--(9,9);
% Thin horizontal lines
\draw (0,1)--(9,1);
\draw (0,2)--(9,2);
\draw (0,4)--(9,4);
\draw (0,5)--(9,5);
\draw (0,7)--(9,7);
\draw (0,8)--(9,8);
% Thin vertical lines
\draw (1,0)--(1,9);
\draw (2,0)--(2,9);
\draw (4,0)--(4,9);
\draw (5,0)--(5,9);
\draw (7,0)--(7,9);
\draw (8,0)--(8,9);
\end{tikzpicture}
现在我们有了完整的九个子网格,每个子网格包含9个单元格,总共81个单元格:
注意:我想在这里提几点。首先,我会比较自由地使用注释。注释是免费的,我鼓励你们多多使用。(我们的代码对我们来说可能很明显,但对其他人,尤其是初学者来说,可能就不那么清楚了。而且我几乎可以保证,虽然我明天可能还记得这些,但一年后我可能就不记得了。请使用注释。)
第二,TikZ 的网格间距默认是 1 厘米,这对我来说有点大,所以我添加了 [x=5mm,y=5mm] 选项,将这个大小减半。我也可以使用缩放选项,但那样会缩放 TikZ 绘图中的所有内容,这样数字就会变得太小。我其实只是想减少行与行之间的距离,而不是其他的。
因为我们有一个基于笛卡尔坐标的9×9网格,所以很容易确定节点的位置。看看这个图示:
我们可以看到,左下角的单元格四个角的坐标是(0,0)、(0,1)、(1,1)和(1,0)。因此,为了在这个单元格的中间放置一个数字,我们可以在(0.5,0.5)放置一个节点。我们所有的节点都会以类似的方式放置。例如,C1中的所有节点的x坐标都是0.5,C2中的所有节点的x坐标都是1.5,依此类推。同样,R1中的所有节点的y坐标都是0.5,R2中的所有节点的y坐标都是1.5,依此类推。最后,R9C9中的节点坐标是(8.5,8.5)。
现在把这些结合起来,生成这个网格:
代码如下:
\begin{tikzpicture}[x=5mm,y=5mm]
% Thick horizontal lines
\draw [ultra thick](0,0)--(9,0);
\draw [ultra thick](0,3)--(9,3);
\draw [ultra thick](0,6)--(9,6);
\draw [ultra thick](0,9)--(9,9);
% Thick vertical lines
\draw [ultra thick](0,0)--(0,9);
\draw [ultra thick](3,0)--(3,9);
\draw [ultra thick](6,0)--(6,9);
\draw [ultra thick](9,0)--(9,9);
% Thin horizontal lines
\draw (0,1)--(9,1);
\draw (0,2)--(9,2);
\draw (0,4)--(9,4);
\draw (0,5)--(9,5);
\draw (0,7)--(9,7);
\draw (0,8)--(9,8);
% Thin vertical lines
\draw (1,0)--(1,9);
\draw (2,0)--(2,9);
\draw (4,0)--(4,9);
\draw (5,0)--(5,9);
\draw (7,0)--(7,9);
\draw (8,0)--(8,9);
% Nodes
\node at (0.5,7.5) {6};
\node at (1.5,7.5) {1};
\node at (4.5,7.5) {3};
\node at (7.5,8.5) {9};
\node at (8.5,8.5) {5};
\node at (5.5,8.5) {2};
\end{tikzpicture}
因为我们使用的是TikZ ,所以给特定单元格添加阴影非常简单:我们只需要定义一个路径,告诉它我们想要填充的颜色,选择0mm的线宽(因为我们已经有线条了),然后列出我们想要阴影的单元格的四个坐标。例如,要给R8C7的单元格添加浅灰色阴影,我们可以加载ninecolors包并使用以下代码:
\path [fill=gray8, line width=0mm] (6,7) -- (7,7) -- (7,8) -- (6,8);
可以得到:
这里需要注意的重要一点是,我们必须在绘制网格之前定义任何阴影。换句话说,填充方块的\path
命令必须在所有实际绘制网格的命令之前。如果不这样做,灰色方块就会覆盖在我们的网格上,部分遮挡线条,正如我们在这里看到的那样:
记住,TikZ 是按照你定义的顺序来绘制东西的,每个绘制的元素都是在之前绘制的内容上面。我们并不是在填充已经绘制的网格,而是绘制一个全新的方块并填充它。我们可以为填充路径定义线条粗细,但这会变得复杂,因为我们的一些线条比其他的更粗。更简单(也更快)的方法是先绘制填充路径。
代码汇总一起:
\begin{tikzpicture}[x=5mm,y=5mm]
% Fills (must go first to put behind the grid)
\path [fill=gray8, line width=0mm] (6,7) -- (7,7) -- (7,8) -- (6,8);
% Thick horizontal lines
\draw [ultra thick](0,0)--(9,0);
\draw [ultra thick](0,3)--(9,3);
\draw [ultra thick](0,6)--(9,6);
\draw [ultra thick](0,9)--(9,9);
% Thick vertical lines
\draw [ultra thick](0,0)--(0,9);
\draw [ultra thick](3,0)--(3,9);
\draw [ultra thick](6,0)--(6,9);
\draw [ultra thick](9,0)--(9,9);
% Thin horizontal lines
\draw (0,1)--(9,1);
\draw (0,2)--(9,2);
\draw (0,4)--(9,4);
\draw (0,5)--(9,5);
\draw (0,7)--(9,7);
\draw (0,8)--(9,8);
% Thin vertical lines
\draw (1,0)--(1,9);
\draw (2,0)--(2,9);
\draw (4,0)--(4,9);
\draw (5,0)--(5,9);
\draw (7,0)--(7,9);
\draw (8,0)--(8,9);
% Nodes
\node at (0.5,7.5) {6};
\node at (1.5,7.5) {1};
\node at (4.5,7.5) {3};
\node at (7.5,8.5) {9};
\node at (8.5,8.5) {5};
\node at (5.5,8.5) {2};
\end{tikzpicture}
到目前为止,我拥有我想要的一切:一个易于定义的网格,容易定位的节点,以及一个相对简单的方式来给单元格上色。但正如你所看到的,这仍然是很多代码,虽然我可以轻松地复制和粘贴,但复制和粘贴也是导致很多问题的原因,因为我们往往会复制错误的东西,然后把它们粘贴到错误的地方。
这里的解决方案很明显:让我们把这些东西变成宏。宏不仅能节省我们很多打字时间,还能让我们的文档保持一致。如果我决定不喜欢某个格式,我只需要修改宏,而不是逐个修改整个文档中的每一个代码实例,这样就不容易漏掉一个或多个。
创建 LaTeX 宏的基本语法如下:
\newcommand{\<name of new command>}[number of arguments]{<definition of new command>}
因为我的名字以字母“k”开头,所以我喜欢用同样的字母开头我的所有宏,这样我就能轻松识别和记住它们。
让我们先定义一个新命令——\kgrid
——它将为我们绘制网格:
% Draw a sudoku grid
\newcommand{\kgrid}{
% Thick horizontal lines
\draw [ultra thick](0,0)--(9,0);
\draw [ultra thick](0,3)--(9,3);
\draw [ultra thick](0,6)--(9,6);
\draw [ultra thick](0,9)--(9,9);
% Thick vertical lines
\draw [ultra thick](0,0)--(0,9);
\draw [ultra thick](3,0)--(3,9);
\draw [ultra thick](6,0)--(6,9);
\draw [ultra thick](9,0)--(9,9);
% Thin horizontal lines
\draw (0,1)--(9,1);
\draw (0,2)--(9,2);
\draw (0,4)--(9,4);
\draw (0,5)--(9,5);
\draw (0,7)--(9,7);
\draw (0,8)--(9,8);
% Thin vertical lines
\draw (1,0)--(1,9);
\draw (2,0)--(2,9);
\draw (4,0)--(4,9);
\draw (5,0)--(5,9);
\draw (7,0)--(7,9);
\draw (8,0)--(8,9);
}
现在每当我想画一个网格时,我只需要写一行:\kgrid
,我的空数独网格就准备好了。
我们节点的宏需要三个参数。前两个参数描述我们想要放置的位置坐标,第三个参数描述我们在该节点中放置的内容。它的格式如下:
\newcommand{\knode}[3]{\node at (#1,#2) {#3};}
这里的[3]告诉LaTeX这个宏将有三个参数;因此在调用时我们必须传递三个参数,每个参数用一对大括号括起来。一个例子看起来像这样:
\knode{0.5}{7.5}{6}
这和使用完整的表示法是一样的:
\node at (0.5,7.5) {6};
我承认,这里并没有节省太多的输入,但确实节省了一些,时间久了也会积少成多。
填充宏也会接受三个参数。前两个是我们想要填充的单元格左下角的坐标。换句话说,如果我们想填充R1C1位置的方块,我们会将这两个参数都设为零,因为它的左下角坐标在原点。
第三个参数是我使用的灰色阴影,基于 ninecolors(https://ctan.org/pkg/ninecolors) 包。(我喜欢这个包,因为我很少需要特定的颜色;我通常只需要给定颜色的浅色和深色版本,而这个包是实现这一点的最简单方法。)我可以添加一个第四个参数,让我选择不同的颜色,而不仅仅是灰色,但因为我通常在激光打印机上打印所有东西,这对我来说就没什么意义了。不过,这样做也很简单。
总之,这就是我们的填充宏:
\newcommand{\kfill}[3]{\draw [fill=gray#3, line width=0mm] (#1,#2) rectangle +(1,1);}
注意最后的rectangle +(1,1)
。这告诉 TikZ 从我们指定的坐标 (#1,#2)
开始,绘制一个矩形,结束于起点右边一单位和上边一单位的位置。这让我们在宏中省去了很多复杂的数学运算。因此,写
\kfill{6}{7}{8}
和写
\path [fill=gray8, line width=0mm] (6,7) -- (7,7) -- (7,8) -- (6,8);
是一样的。
这确实省了我们不少打字!
我想我已经给了你所有需要的东西,让你可以在自己的文档中添加这种 LaTeX + TikZ 的魔法。我们再来看看绘制这个图形需要什么,先不使用宏,然后再使用宏:
\begin{tikzpicture}[x=5mm,y=5mm]
% Fills (must go first to put behind the grid)
\path [fill=gray8, line width=0mm] (6,7) -- (7,7) -- (7,8) -- (6,8);
% Thick horizontal lines
\draw [ultra thick](0,0)--(9,0);
\draw [ultra thick](0,3)--(9,3);
\draw [ultra thick](0,6)--(9,6);
\draw [ultra thick](0,9)--(9,9);
% Thick vertical lines
\draw [ultra thick](0,0)--(0,9);
\draw [ultra thick](3,0)--(3,9);
\draw [ultra thick](6,0)--(6,9);
\draw [ultra thick](9,0)--(9,9);
% Thin horizontal lines
\draw (0,1)--(9,1);
\draw (0,2)--(9,2);
\draw (0,4)--(9,4);
\draw (0,5)--(9,5);
\draw (0,7)--(9,7);
\draw (0,8)--(9,8);
% Thin vertical lines
\draw (1,0)--(1,9);
\draw (2,0)--(2,9);
\draw (4,0)--(4,9);
\draw (5,0)--(5,9);
\draw (7,0)--(7,9);
\draw (8,0)--(8,9);
% Nodes
\node at (0.5,7.5) {6};
\node at (1.5,7.5) {1};
\node at (4.5,7.5) {3};
\node at (7.5,8.5) {9};
\node at (8.5,8.5) {5};
\node at (5.5,8.5) {2};
\end{tikzpicture}
如果用宏来写:
\begin{tikzpicture}[x=5mm,y=5mm]
\kfill{6}{7}{8}
\kgrid
\knode{0.5}{7.5}{6}
\knode{1.5}{7.5}{1}
\knode{4.5}{7.5}{3}
\knode{6.5}{1.5}{7}
\knode{6.5}{4.5}{4}
\knode{7.5}{8.5}{9}
\knode{8.5}{6.5}{2}
\knode{8.5}{8.5}{5}
\end{tikzpicture}
写到这里,我想大家肯定会同意,用宏是最好的选择!
如果你有任何问题、意见或者改进的建议,请在下面留言。谢谢你的阅读!
选自:https://techblog.kjodle.net/2025/04/19/typesetting-sudoku-grids-in-latex/