算法 (algorithm2e/algpseudocode)

在搜索框里输入「latex 算法 宏包」,会返回四个几乎一样的名字——algorithmalgorithmicalgorithmicxalgorithm2e;而你此刻正在调试的导言区,很可能同时加载了其中互不相容的两个。这团乱麻正是本页的主题,因为一旦理清,用 LaTeX 排版伪代码其实很简单。要记住的关键是:伪代码不是代码的粘贴。与姊妹页讲的源代码清单不同,算法是按接近数学公式的方式排版的——变量用斜体,赋值用箭头,关键字用粗体——而且它被放进一个可以用 \ref 引用的带编号浮动体里。下面依次讲:到底该加载哪一对宏包、加载错了会看到什么真实报错、以及两种流派各自完整的写法。

伪代码与 listings 的区别:是粘贴,还是排版

一句话概括:listings 保存字符,伪代码排版含义。 交给 listingsminted 的内容按逐字环境处理,输入的每个空格和符号都会原样出现在纸面上——真实源代码正需要如此,这个取舍留给姊妹页。伪代码正好相反。写下 $y \gets 1$y 会排成斜体变量,\gets 会变成箭头 ←:正文住在数学模式里,而不是逐字环境里。只有关键字以粗体罗马体立起来,与变量在视觉上分开。「变量用斜体、关键字用粗体」这一约定,正是把高德纳《计算机程序设计艺术》以来的教科书写法照搬到纸面上。顺带一提,\gets 这个名字本身就来自计算机科学,读作「x gets 1」,即赋值。

第二个区别是位置。lstlisting 待在正文流里不动,而伪代码按惯例放进 浮动体,与 figuretable 完全一样。于是 LaTeX 会避开难看的分页把它移到合适的位置,在上方加一个带连续编号的「Algorithm 1」标题,并让它出现在 \listofalgorithms 里。只要抓住一个结构性事实——容器(浮动体)和内容(伪代码)来自不同的宏包——那四个容易混淆的名字立刻各就各位。

到底该 \usepackage 哪一个:四个名字的真面目

如果是从头开始写,答案只有两行。要么把 \usepackage{algorithm}\usepackage{algpseudocode} 并排放,要么单独加载 \usepackage{algorithm2e}。除这两种之外的组合基本都会出事。之所以令人困惑,是因为 CTAN 上的宏包名与样式文件名对不上。algorithm.sty 并非来自名叫 algorithm 的宏包,而是来自algorithms 套装(复数形式);algpseudocode.sty 则来自 algorithmicx。也就是说,你在导言区写了两个文件名,实际安装的却是两个名字完全不同的套装。

套装包含的样式文件作用
algorithmsalgorithm.styalgorithmic.sty容器(algorithm 浮动体)以及最早的内容 algorithmic;后者用全大写命令(\STATE),可追溯到 1994 年,现已被取代
algorithmicxalgorithmicx.styalgpseudocode.styalgcompatible.sty只有内容。Szász János 于 2005 年的重写,algpseudocode 是其标准布局。它完全不带容器
algorithm2ealgorithm2e.sty自成一体,容器与内容一并提供。由 Christophe Fiorio 自 1996 年起维护。切勿与 algorithm 同时加载
preamble
% Camp 1 -- two packages, two jobs. This is the usual choice.
\usepackage{algorithm}      % the float: \begin{algorithm}, \caption, \listofalgorithms
\usepackage{algpseudocode}  % the body:  \begin{algorithmic}, \State, \If, \For

% Camp 2 -- one package does everything. Do NOT also load algorithm.
\usepackage[ruled,vlined,linesnumbered]{algorithm2e}

这种分工有多彻底,打开 algorithm.sty 就一目了然:全文不到一百行,而且没有一行提到伪代码。它做的全部事情是加载 float 宏包、用 \newfloat{algorithm}{htbp}{loa} 声明一种新的浮动类型、把它命名为「Algorithm」、再定义 \listofalgorithms——整个宏包就这些(TeX Live 2024 附带的是 2009 年 8 月 24 日的 v0.1)。一个有趣的副作用是:凡是它不认识的选项,都会被当作浮动体的名字。写 \usepackage[Procedure]{algorithm},此后全文的标题就都变成「Procedure 1」「Procedure 2」。

出现 Command \algorithm already defined.

这条报错只有一个含义:你同时加载了 algorithmalgorithm2e。两者都想创建名为 algorithm 的环境,后到的那个就撞上了先来的。从导言区删掉其中一个即可。同理,加载顺序不同,措辞也不同:若先写 algorithm2e、后写 algorithm,得到的会是 ! LaTeX Error: Command \listofalgorithms already defined.,成因相同。另一条常见的是 ! LaTeX Error: Command \algorithmic already defined.,那是把旧的 algorithmic 与较新的 algpseudocode 一起加载的症状。algorithmicx 一系正是 algorithmic 的后继,二者不必同时使用。

preamble
% Each of these three preambles stops the compile.
\usepackage{algorithm}\usepackage{algorithm2e}
%   ! LaTeX Error: Command \algorithm already defined.

\usepackage{algorithm2e}\usepackage{algorithm}
%   ! LaTeX Error: Command \listofalgorithms already defined.

\usepackage{algorithmic}\usepackage{algpseudocode}
%   ! LaTeX Error: Command \algorithmic already defined.

% If a class or a co-author really forces algorithm2e into a document that
% already owns the algorithm float, this option renames the environment to
% algorithm2e and \listofalgorithms to \listofalgorithmes, so nothing collides.
\usepackage[algo2e]{algorithm2e}

不过,最麻烦的组合反而根本不报错。把 algorithm2ealgpseudocode 并排加载,文档会一声不吭地编译通过。原因是 algorithmicx 在定义 \For\If 这类面向用户的名字时,会先检查该名字是否已经存在,若已存在就默默放弃定义。于是 algorithm2e 的含义存活下来,直到你写下 algorithmic 环境的正文,才会在 \EndWhile 那一行冒出看似毫不相干的 ! Missing number, treated as zero.。在离病因最远的地方崩溃,某种意义上比干脆报错更难对付。algorithm2e 之所以准备了 algo2e 选项,正是为了化解这类冲突;启用它之后,\listofalgorithms 会变成拼法不同的 \listofalgorithmes——这是该宏包出自法语环境留下的痕迹。

\begin{algorithm} 是浮动体:\caption\label[H]

algorithm 环境是真正的浮动体,与 figuretable 完全同义。其默认位置参数是 htbp(依次尝试此处、页顶、页底、独立成页),目录文件扩展名是 .loa。因此图表那套熟悉的做法可以照搬:\caption{…} 生成带编号的「Algorithm 1」标题,紧随其后放 \label{alg:…},正文便能用 \ref{alg:power} 取编号、用 \pageref{alg:power} 取页码。若把 \label 写在 \caption 之前,编号会出错——要么差一,要么取到节号——所以顺序必须遵守。在文档开头写 \listofalgorithms——它是 \listoffigures\listoftables 的同胞——就会得到一份「List of Algorithms」,把编号与 \caption 的文字一一对应。和往常一样,编号、引用与目录需要编译两次才会稳定。如果恰恰不希望它浮动,写 \begin{algorithm}[H] 即可把盒子钉在原处——[H] 无需额外设置,因为 algorithm 已经加载了 float 宏包。

书写正文:\State\If\For\While\Function

内层环境名叫 algorithmic——这是命名上的最后一个陷阱:宏包叫 algpseudocode,环境却不叫这个名字。所有命令都采用首字母大写\State 开启一行语句,代码块要显式闭合,如 \If\EndIf\For\EndFor。开头的可选参数设定行号间隔:[1] 每行编号,[5] 每五行编号一次,省略则不编号。下面的例子是二分查找,涵盖了条件分支、循环、函数、前置条件和行末注释。

document.tex
\documentclass{article}
\usepackage{algorithm}
\usepackage{algpseudocode}
\begin{document}
\listofalgorithms

\begin{algorithm}
  \caption{Binary search}\label{alg:bsearch}
  \begin{algorithmic}[1]
    \Require sorted array $a[1..n]$, key $k$
    \Ensure index of $k$, or $0$
    \Function{Search}{$a,k$}
      \State $lo \gets 1$;\ $hi \gets n$ \label{ln:init}
      \While{$lo \le hi$}
        \State $m \gets \lfloor (lo+hi)/2 \rfloor$ \Comment{midpoint}
        \If{$a[m] = k$}
          \State \Return $m$
        \ElsIf{$a[m] < k$}
          \State $lo \gets m+1$
        \Else
          \State $hi \gets m-1$
        \EndIf
      \EndWhile
      \State \Return $0$
    \EndFunction
  \end{algorithmic}
\end{algorithm}

Algorithm~\ref{alg:bsearch} halves the interval; the bounds are set on
line~\ref{ln:init}, page~\pageref{alg:bsearch}.

\end{document}

输出是这样的:一个上下带横线的框浮动到页面上,顶部写着「Algorithm 1 Binary search」。框内的 \Require\Ensure 生成粗体标签 Require:Ensure:,二者排在编号之外。编号从 \Function 那行开始,沿左边依次是 1、2、3 …;\Comment{midpoint} 会在行末排出一个小三角:▷ midpoint。\Function{Search}{$a,k$} 中的名字用小型大写字母排版,若要从别的行调用它,就写 \Call{Search}{$a,k$}。既然很多人想换掉这些标签:Require:Ensure: 这两个字符串分别存放在 \algorithmicrequire\algorithmicensure 中,写 \renewcommand{\algorithmicrequire}{\textbf{Input:}} 就能把前者改成 Input:

命令排出的样子备注
\State一行带编号的语句每条语句一个;漏写会接到上一行
\Require / \EnsureRequire: / Ensure:前置与后置条件,排在行号之外
\If ... \ElsIf ... \Else ... \EndIfifthen / else if / else / end if命令拼作 \ElsIf,比 “else” 少一个 e,是常见的打字错误
\For ... \EndForfordoend for\ForAll{…} 排出 for all …;条件放在花括号里
\While ... \EndWhilewhiledoend while同样形式的 \Repeat\Until{…} 也可使用
\Function ... \EndFunctionfunction Name(args) … end function名字用小型大写;调用写作 \Call{Name}{args}。对应的还有 \Procedure
\Returnreturn要与 \State 搭配,如 \State \Return $y$
\Comment▷ 注释文字放在行末;三角符号存放在 \algorithmiccomment

这一派最常见的事故是漏写 \State,而且它不报错。在 \State $x \gets 1$ 之后直接写 $y \gets 2$,输出会变成「1: x ← 1 y ← 2」——两条语句挤在同一行,编号还少了一个。若发现行数不对,先怀疑这里。另外,如果在旧的会议模板里看到 \STATE\WHILE\ENDWHILE 这类全大写命令,那是源自 1994 年的旧 algorithmic 写法。想原样沿用这种稿件,就用 \usepackage{algcompatible} 代替 algpseudocode:大写命令会在 algorithmicx 的底座上照常工作。

给行编号,并引用「第 3 行」

行号是可以引用的。用 [1] 打开 algorithmic 环境,在想引用的那一行末尾\label{ln:init},正文中的 \ref{ln:init} 就会展开为该行的编号。上例中 \Function 那行是 1,因此 \ref{ln:init} 得到 2。这一点比听上去更重要:如果手写「第 3 行的判断」,日后只要多加一条语句,这句话就变成了假话。algorithm2e 那边做法相同——启用 linesnumbered,并把 \label{…} 放在结束该行的 \; 之后。只有一条注意:给没有编号的行贴标签毫无意义,因为不指定 [1]linesnumbered 时,根本不存在可指向的编号。

algorithm2e 的写法:用花括号代替 \End…,以及不能忘的 \;

另一派不用 \EndFor 之类的命令来闭合代码块,而是把正文作为花括号参数传入——\For{条件}{正文}\While{条件}{正文};if–then–else 写作 \eIf{cond}{then}{else}(没有 else 时用 \uIf,想压在一行里用 \lIf)。输入输出有专用命令 \KwIn{…}\KwOut{…},排成粗体 Input:Output:(若更喜欢 Data:Result: 的措辞,就改用 \KwData{…}\KwResult{…})。最大的坑在于每条语句必须以 \; 结尾。忘了不会报错,下一条语句会直接接在同一行。若不想让「;」出现在成品里,用 \DontPrintSemicolon 可以隐藏它(\; 本身仍然必须写)。外观完全由加载选项决定:ruled(上下横线)、boxed(整体加框)、vlinedlined(画块竖线的两种方式)、plain(默认),再加 linesnumbered 显示行号。这三个线条选项其实就是替你执行文档内的 \SetAlgoVlined\SetAlgoLined\SetAlgoNoLine。TeX Live 2024 附带的是 2017 年 7 月 18 日的 v5.2。

document.tex
\documentclass{article}
\usepackage[ruled,vlined,linesnumbered]{algorithm2e}
\begin{document}
\listofalgorithms

\begin{algorithm}
  \DontPrintSemicolon
  \KwIn{an array $a[1..n]$}
  \KwOut{the sum $s$ of its positive entries}
  $s \gets 0$\;\label{ln:zero}
  \For{$i \gets 1$ \KwTo $n$}{
    \eIf{$a[i] > 0$}{
      $s \gets s + a[i]$\;
    }{
      \tcp{negative or zero: skip}
    }
  }
  \Return $s$\;
  \caption{Sum of positive entries}\label{alg:sum}
\end{algorithm}

Algorithm~\ref{alg:sum} accumulates into $s$, initialised on line~\ref{ln:zero}.

\end{document}

输出的标题是「Algorithm 1: Sum of positive entries」——编号后面多一个冒号,这正是它与 algorithm 浮动体在外观上的区别。正文上方是 Input:Output:;正文排成 for i ← 1 to n doifthenelse,因为指定了 vlined,每个块的左侧会有一条竖线。\tcp{…} 是行内注释,输出为 // negative or zero: skip。请注意 \caption 写在环境的末尾——这是 algorithm2e 的惯例;指定了 ruled 时,即使写在末尾,标题依然出现在框的顶部。同样的文字也会进入 \listofalgorithms。另外,algorithm2ealgorithm 环境默认同样是浮动体,需要固定时可以写 \begin{algorithm}[H]

让关键字不用英语:frenchgermanonelanguage

algorithm2e 可以翻译伪代码本身的关键字。只有这一派提供该功能,algpseudocode 没有对应物。可用的选项有 frenchgermanngermanspanishitalianoportugueseczechslovakcroatianenglish。不过行为上有个台阶。只写 \usepackage[french]{algorithm2e},浮动体的名字会变成「Algorithme 1 :」,但 \For\eIf 仍然输出英语——要得到法语,得改用法语命令名 \Pour{…}{…}\Si{…}{…}\KwA。若不想改写既有稿件里的命令名,就加上 onelanguage:写 \usepackage[french,onelanguage]{algorithm2e} 后,原封不动的 \For\eIf 会排成 pour i ← 1 à n fairesialorssinonfin。德语则用 [german,onelanguage],得到 für i ← 1 bis n tuewenndannsonstEnde,标题为「Algorithmus 1:」。

preamble
% Keep the English command names, print French keywords.
\usepackage[french,onelanguage,ruled,linesnumbered]{algorithm2e}
%   \For{...}{...}  ->  pour i <- 1 a n faire ... fin

% Without onelanguage, only the float name is translated; use the
% French command names for a French body.
\usepackage[french,ruled]{algorithm2e}
%   \Pour{...}{...}, \Si{...}{...}, \KwA

究竟该选哪个:algpseudocodealgorithm2e 的比较

如果投稿模板指定了其中之一,就照办——许多会议文类是围绕某一种选择来调校页边距和字体的。若没有指定:想把伪代码写成数学式的文字,选 algpseudocode需要显式的输入输出声明、块竖线或关键字翻译,选 algorithm2e。剩下的用下表定夺。

方面algorithm + algpseudocodealgorithm2e
packages两个:容器 + 内容一个,自成一体
block syntax显式闭合:\If{…}\EndIf正文作花括号参数:\eIf{…}{…}{…}
input / output\Require / \Ensure:前置与后置条件\KwIn / \KwOut:显式的输入与输出
end of line无需标记;由 \State 开启一行必须写 \;;漏写会让下一条语句接在同一行
line numbers\begin{algorithmic}[1] 的可选参数设定间隔linesnumbered 选项(rightnl 可移到右侧)
keyword language只有英语;需逐个重定义 \algorithmicwhile 之类frenchgerman 等选项;加 onelanguage 可保留英文命令名

无论选哪一个,作为出版物的处理方式完全相同:用 \caption 给出带编号的标题,用 \label\ref 从正文指向它,用 \listofalgorithms 生成列表,并编译两次让编号稳定。最后再强调一次,因为这正是本页的全部要点:每份文档只用一个内容宏包。用 algorithm2e 就绝不加载 algorithm;用 algpseudocode 就绝不加载 algorithmic。这一行纪律,几乎可以杜绝开头那四个易混名字引发的所有事故。如果你真正想要的是带语法着色和行号的真实源代码而非伪代码,那么讲 listingsminted 的姊妹页才是目的地。