LaTeX 中的浮动体:浮动算法

这是系列文章的第三篇,系列文章的前两篇分别是

此篇将讨论 LaTeX 中的浮动算法。在此之后,读者应该有能力理解 LaTeX 中浮动体各种「乱跑」的现象的由来,并且知道关于 LaTeX 中浮动体一些最佳实践。

浮动体相关的术语

浮动体类型

每个浮动体都从属于一种浮动体类型。默认情况下,LaTeX 定义了两种浮动体类型,即 figuretable。文档类和宏包的作者,可以在其中定义额外的浮动体类型(比如 listings 宏包定义了用于排版代码清单的浮动体;algorithm 宏包定义了用于排版算法的浮动体),用户也可以在导言区定义自己需要的浮动体类型(借助 float 宏包)。浮动体从属的类型,在多个方面会影响浮动体的最终位置。比如说,每个浮动体类型都有默认的位置选项,如果它们没有被浮动体本身的位置选项覆盖的话,那么就会生效。

需要特别强调的是,同一个浮动体类型中的不同浮动体,它们的相对顺序是固定的。也就是说,不管浮动体如何「乱跑」,Figure 1, Figure 2, Figure 3 这样的顺序是始终保持的。(简称「你大爷始终是你大爷」原则)不过,不同类型的浮动体之间,其顺序则可能出现穿插。比如,如果有 Table 1,则可以出现在相对上述三个图片的任意位置。

浮动区域

在同一栏(column)当中,LaTeX 设置了两个浮动区域:栏的顶部和底部。对于双栏排版来说,LaTeX 还提供了额外的区域:跨过双栏的顶部。

此外,LaTeX 种也有所谓的「浮动栏」或者「浮动页」的设定。顾名思义,浮动栏和浮动页就是「只有浮动体」的栏或者页。

最后,LaTeX 也可以将浮动体放在文本内容的中间(当然,这需要显式指定)。

浮动体位置选项

为了指定浮动体放置的位置,手稿作者需要给浮动体环境传入浮动体位置选项(通过环境的可选参数)。如果手稿作者没有显式提供位置选项,那么 LaTeX 则会使用浮动体所述类型所指定的位置选项。

1
2
3
\begin{figure}[!htbp]
% ...
\end{figure}

LaTeX 中默认的浮动体位置选项有五种,手稿作者可以以任意顺序组合使用这些选项。它们是

  • ! 表示忽略一些严格的限制条件(后文详述);
  • h 表示如有可能,则放在当前位置;
  • t 表示该浮动体允许置于栏的顶部;
  • b 表示该浮动体允许置于栏的底部;
  • p 表示该浮动体允许置于浮动栏或浮动页。

这也就是说,如果某个字符(比如 b)没有出现在浮动体位置选项中,则 LaTeX 在尝试输出该浮动体时,就不会试着将它放在栏的底部。

需要再次强调的是,浮动体位置选项的指定是一个「组合」问题,而不是「排列问题」。因此,[tb][bt] 是等效的。并不是说 [bt] 表示首先尝试放在栏的底部。

浮动算法参数

总计,大约有 20 个参数,会最终影响到 LaTeX 的浮动体算法。根本来说,这些参数限制了

  • 某个浮动区域至多允许摆放多少个浮动体,
  • 浮动区域的大小,
  • 非浮动栏或非浮动页中,文字区域的最小大小,
  • 一个浮动区域内连续浮动体之间的垂直距离,以及
  • 浮动区域与其前后文字区域的垂直距离。

浮动体参考点

显而易见,浮动体在手稿源代码中的位置,会影响最终的输出位置。因为,浮动体在手稿源代码中的位置,决定了 LaTeX 在何时第一次遇见这个浮动体。如果,浮动体在手稿的源代码中,被置于一个段落的中间,那么这个浮动体的参考点是(自源代码中观察)浮动体之后的(自最终输出文件观察)下一个断行点或者分页点。

浮动算法

至此,我们可以开始探讨 LaTeX 浮动算法的行为了。

在理解浮动算法之前,我们有必要强调一下,这个算法是在 1980 年代设计出来的。其时,计算机的算力相对现在是非常有限的。因此,浮动算法的设计中有非常多的「妥协」。

基于这一原因,LaTeX 中所有的排版算法,都在尽力避免「回溯」。这也就是说,LaTeX 读入源代码之后,会试着尽可能快地将之排版出来。这样一来,算法的复杂度就可以尽量的低——当然,它依然很复杂,并因此可以尽快输出排版结果。

对于浮动体来说,这就意味着确定浮动体位置的浮动算法必须是一个贪心算法。在 LaTeX 遇见浮动体时,它就会尽可能地尝试输出它。如果 LaTeX 确定输出了一个浮动体,那么不论之后遇到什么内容,它的位置都不会发生改变。这种贪心实际上是对运行效率的妥协。因为,显而易见地,如此贪心的位置选择,可能在读入之后的数据以后,可以发现是不那么完美的。

比如说,假设一个图片允许放在栏的顶部或者底部。而后 LaTeX 可能会将其放在某一栏的顶部区域。现在假设,在这个图片之后,假设有两个表格,允许放在栏的顶部,那么这两个表格就没地方可防了——必须放在下一页。而实际上,最好的选择是,将这两个表格放在顶部,而图片放在底部。(在这个假设中,最佳的放置办法,LaTeX 是不会去尝试的)

基本流程

浮动算法的基本流程,可以大致描述如下。

  • 当 LaTeX 遇到一个浮动体,它会根据浮动算法的规则(后文详述)尽可能快地输出该浮动体。
    • 若成功,则该浮动体被输出,并且 LaTeX 再也不会改变它的位置。
    • 若失败,则该浮动体被 LaTeX 放在一个等待队列中暂存,而后在下一页开始的时候尝试输出队列中的浮动体。
  • 当一栏或者一页组装完毕,LaTeX 会检查等待队列中的浮动体,并尝试构建一个浮动栏或浮动页,输出尽可能多的浮动体。如果等待队列中的各个类型的浮动体,都不允许输出在浮动栏或浮动页中,则 LaTeX 会开始尝试将剩下的浮动体放在栏的顶部或者底部。如果当前页已无法容纳更多浮动体,则余下的浮动体又被加入等待队列,待输出下一页时再做尝试。
  • 之后,LaTeX 开始处理当前页的文本信息。当然,在这个过程中,LaTeX 可能会遇到新的浮动体。
  • 当 LaTeX 遇到 \clearpage/FloatBarrier 或者文档末尾时,LaTeX 会新建一个页面,而后将等待列表中的所有浮动体,都输出在浮动栏或浮动页中(而不论这些浮动体的位置选项是否指定了 p)。

浮动算法的规则

不论何时,当 LaTeX 读入一个浮动体时,它都会先检查等待队列中是否有与该浮动体同属一个浮动体类型的浮动体尚未输出。若等待队列中存在这样的浮动体,则该浮动体会被直接加入等待队列,而不会尝试输出。

若等待队列中,没有当前类型的浮动体,则 LaTeX 会检查该浮动体的位置选项。

  • 如果位置选项中包含 !,则在处理该浮动体时,LaTeX 会忽略一些严格的限制(当前浮动区域允许放置浮动体的最大数量、当前浮动区域的最大面积)。否则,这些限制条件就会执行,以期获得更加美观的效果。
  • 下一步,LaTeX 会检查是否存在 h 选项。
    • 如果存在,则 LaTeX 会尝试将浮动体放在当前位置。如果当前位置尚有足够的空间供放置该浮动体,则浮动体输出成功;否则,该浮动体会被加入等待队列。
    • 如果不存在,则 LaTeX 会检查是否存在 t 选项。如果存在,并且上述限制条件没能限制该浮动体,则该浮动体会被放在栏的顶部,而后终止当前的浮动体处理。
    • 最后,LaTeX 会检查是否存在 b 选项。同样的,如果没有限制条件约束该浮动体,则它会被放在栏的底部。
  • 运行至此,说明当前 LaTeX 找不到适合该浮动体的位置,则它会被加入等待队列。

注意,p 选项在此是不起作用的;它仅于分页时起作用。

分页时,尝试清空等待队列

当 LaTeX 分页时,会首先检查浮动体等待队列,并尽可能地清空它。为此,LaTeX 会尝试构建浮动栏或浮动页。

需要注意的是,在这个步骤中,必须有 p 指定的浮动体,才会被放在浮动栏或浮动页中;否则,它就会被留在等待队列里面。需要注意的是,如果一个浮动类型尚未输出的第一个浮动体不允许放在浮动栏或浮动页中,那么,该类型所有的浮动体,都只能待在等待列表中,以等待下一个处理步骤。

如果此时有浮动体可以输出,那么 LaTeX 会构建浮动栏或者浮动页,并尽可能多地输出这样的浮动体——直到当前浮动栏或浮动页已经「满了」,或者触发了某些限制条件为止。

生成浮动栏或浮动页的步骤,一直持续到等待列表中再无可用的浮动体(在每个类型的头部,再无指定了 p 选项的浮动体)。此时,LaTeX 会尝试将剩余的浮动体,放在下一页(或者栏)的栏的顶部或者底部。这一步骤和上一小节中介绍的基本相同,但有以下一些区别

  • h 选项再无作用了;
  • 此处输出的浮动体,不再来自 LaTeX 刚从源代码中读取到的,而是从等待队列中依次读取。

浮动体算法的参数与限制条件

四个计数器

  • totalnumber(默认为 3),在非浮动页上浮动体的最大数量。
  • topnumber(默认为 2),在一个栏的顶部浮动体的最大数量。
  • bottomnumber(默认为 1),在一个栏的底部浮动体的最大数量。
  • dbltopnumber (默认为 2),在双栏排版中,横跨双栏的顶部浮动体的最大数量。

五个区域比例

  • \topfraction(默认为 0.7),栏的顶部区域占据当前栏的最大比例。
  • \bottomfraction(默认为 0.3),栏的底部区域占据当前栏的最大比例。
  • \dbltopfraction(默认为 0.7),在双栏排版中,横跨双栏的顶部区域占据当前页的最大比例。
  • \textfraction(默认为 0.2),在非浮动栏或浮动页中,文字区域占据的最小比例。
  • \floatpagefraction(默认为 0.5),在浮动栏或浮动页中,浮动体至少应当占据的最小比例。

五个垂直距离,它们的默认值取决于文档类默认字号。

  • \floatsep,栏的顶部或底部区域中,连续浮动体之间的垂直距离。
  • \dblfloatsep,双栏排版中,横跨双栏的浮动区域中,浮动体之间的垂直距离。
  • \textfloatsep,栏的顶部或底部的浮动区域与文字区域之间的垂直距离。
  • \dbltextfloatsep,双栏排版中,横跨双栏的浮动区域与文字区域之间的垂直距离。
  • \intextsep,对于 h 生效的浮动体,与前后文字之间的垂直距离。

浮动算法导致的一些现象及其解释

浮动体可能出现在源代码相对位置之前

浮动体在源代码中的位置,决定了它在最终输出的文档中可能出现的最早的位置。这一最早位置是「当前栏的顶部区域」。如果你有认真阅读前面的部分,你就会发现,当等待列表中没有当前类型的浮动体,并且当前浮动体没有被 h 选项确定位置时,浮动体是允许放在当前栏的顶部的。因此,浮动体可能出现在源代码相对位置之前。

双栏排版中,跨栏浮动体总是被放入等待列表

对于双栏排版,跨栏浮动体总是被 LaTeX 立即放入等待列表当中;也因此,跨栏浮动体最在也要输出在下一页的顶部。

产生这一效果的原因在于,如果期待跨栏浮动体放在当前页的顶部区域,则若假设 LaTeX 遇到该浮动体时,第一栏已经排版完成(正在排版第二栏),那么 LaTeX 必须回溯,破坏已经排版好的第一栏。这在效率上是得不偿失的。

双栏排版中,跨栏浮动体不可以被放在底部区域

在 LaTeX 的双栏排版中,没有为跨栏浮动体设置底部区域。因此,如果你使用 \begin{figure*}[b],那么因为它不允许放在顶部区域,所以它直到遇见 \clearpage/\FloatBarrier 或者文档末尾时,才会被输出。

h 真的只表示「如果可能的话,放在这里」

如前所述,h 选项仅在一种情况下可能生效:等待列表中没有该类型的浮动体,并且当前页有足够的空间供其摆放

如果用户希望表达「我一定要放在这里」,那么需要使用 float 宏包提供的 H 选项。

浮动体选项的顺序没有意义

如前所述,浮动体选项只有组合的意义,没有排序的意义。

浮动体算法的文档

参见 source2e.pdfltoutput.dtx 部分。


您的鼓励是我写作最大的动力

俗话说,投资效率是最好的投资。 如果您感觉我的文章质量不错,读后收获很大,预计能为您提高 10% 的工作效率,不妨小额捐助我一下,让我有动力继续写出更多好文章。


撰写评论

写了这么多年博客,收到的优秀评论少之又少。在这个属于 SNS 的时代也并不缺少向作者反馈的渠道。因此,如果你希望撰写评论,请发邮件至我的邮箱并注明文章标题,我会挑选对读者有价值的评论附加到文章末尾。