为什么 TikZ 的 \foreach 不迭代列表的最后一个元素?

为什么 TikZ 的 \foreach 不迭代列表的最后一个元素?

此代码

\documentclass{report}

\usepackage{tikz}

\begin{document}

\begin{tikzpicture}

\draw[dashed] (1,-1) -- (1,2) node[above] {$1$};
\draw[dashed] (2,-1) -- (2,2) node[above] {$2$};

\foreach \x in {1,1.1,...,2}
    {\draw[fill = yellow] (\x,1) circle (0.05);}

\foreach \x in {1,1.2,...,2}
    {\draw[fill = red] (\x,0) circle (0.05);}   

\end{tikzpicture}   

\end{document}

产生以下输出。

在此处输入图片描述

第一个函数\foreach生成黄色圆圈。在集合 {1,1.1,...,2} 中迭代的最后一个元素 2 处没有圆圈。

第二个\foreach生成红色圆圈。在 2 处有一个圆圈,它是集合 {1,1.2,...,2} 中迭代的最后一个元素。

为什么这两个命令的行为不同?

答案1

简短回答

您的示例所示的问题是由于舍入误差造成的。请参阅 TikZ/PGF 文档(v2.10 中的第 56 节,第 505 页;或 v3.0 中的第 83 节,第 910 页):

[...] 对于不是 2^{-n} 倍数的小数步长,对于某些较小的 n,舍入误差很容易发生。因此,在 中\foreach \x in {0,0.1,...,0.5} {\x, }0.5应该用 代替,0.501以提高稳健性。

在您的特定示例中,对列表的最后一个元素使用略大于 2 的值(例如 2.0001),例如

\foreach \x in {1,1.1,...,2.0001}

给出一些舍入误差的余地并解决问题。

在此处输入图片描述

然而,避免舍入误差问题的最好方法是,如g.kov 的回答percusse 的评论, 和Qrrbrbirlbel 的评论,迭代整数并在其他地方计算感兴趣的值,就像这样

\foreach \xx[evaluate={\x=\xx/10}] in {10,11,...,20}

或者在正文中\foreach

\foreach \xx in {10,11,...,20}{
    \pgfmathsetmacro\x{.1*\xx}
    ...
}

有关更详细的解释,请参见下文。


TeX 的定点数字系统

一些实数具有无限的二进制展开,即它们的小数部分包含无限重复的位模式。例如,

0.1(十进制形式)

对应于

0.00011001100110011001100110011...(二进制形式)

位模式“0011”不断重复“永远”。由于有限精度机器数仅为给定实数的小数部分分配有限数量的位,因此它们无法存储无限扩展,因此必须进行一些舍入。TeX 使用 32 位定点数表示系统,其中

  • 1 位用于标记溢出(如果有),
  • 1 位用于编码数字的符号,
  • 14 位用于数字的整数部分,
  • 16 位用于数字的小数部分。

它遵循

在此处输入图片描述

分别是最大和最小TeX 数字。我所说的“TeX 数字”是指可以表示的数字确切地使用 TeX 数字表示系统。任何大于 16383.99999 的实数都会导致溢出此外,有限精度数系只能表示有限个实数,这意味着-16383.99999 +16383.99999 之间的某些实数不能被表示为确切地用 TeX 数字表示;这些实数只能表示大约通过 TeX 数字。

我不确定 TeX 中的舍入规则是什么,但似乎(正)实数会得到

  • 如果二进制小数点右边的第 17 位(二进制展开式)为 1,则向上舍入,
  • 如果二进制小数点右边的第 17 位(二进制展开式)为 0,则截断,

对于 0.1 来说,二进制小数点右边的第 17 位是 1;因此,0.1 会被向上舍入到下一个 TeX 数字,即

0.0001100110011010 (二进制形式)

(与上面写的准确值 0.1 比较。)总之,TeX 操作的数字不是 0.1,而是一个略大于 0.1 的数字。相反

0.2(十进制形式)

对应于

0.001100110011001100110011...(二进制形式)

二进制小数点后,模式“0011”会“永远”重复。二进制小数点右边的第 17 位是 0;因此,0.2 会被截断/四舍五入为前一个 TeX 数字,即

0.001100110011011 (二进制形式)

(与上面写的精确值 0.2 进行比较。)总而言之,TeX 操作的数字不是 0.2,而是一个略小于 0.2 的数字。

机制\foreach

编译

\documentclass{article}
\usepackage{pgffor}
\begin{document}
\noindent
\foreach \x in {1,1.1,...,2}{\x\\ } \\
\foreach \x in {1,1.2,...,2}{\x\\ }
\end{document}

你会得到

在此处输入图片描述

为了理解这个输出,你需要了解\foreach一下点符号有效。当\foreach遇到...列表参数中的元素时,如\foreach \i in {x,y,...,z},它会计算差值d= y-x以填充“缺失值”。根据 TikZ/PGF 手册 (p.505),

列表读数部分x,y,...,z被替换为xx + dx + 2dx + 3d, ... ,x + md其中最后的点是语义点,而不是句法点。该值m是最大数字,使得x + md<= zifd为正数,或x + md>= zifd为负数。

在这种情况下\foreach \x in {1,1.1,...,2},前两项之间的差异是TeX 编号 0.1如上所述,它略大于实数0.1。因此, <=m的最大值是 9。结果,循环访问的最终值不是 2,而是一个略大于1+0.1*m2数学数1.9.

在这种情况下\foreach \x in {1,1.2,...,2},前两项之间的差异是TeX 编号 0.2如上所述,略小于实数0.2。因此, <=m的最大值是 5。结果,循环访问的最终值不完全是 2,而是一个仅比 2 略小的 TeX 数字,因此与 相比,在这种情况下获得的输出更令人满意。1+0.2*m2\foreach \x in {1,1.1,...,2}

参考

答案2

这两个命令的行为完全相同。舍入误差是第一种情况下“遗漏”终端值的原因。此检查

\documentclass{report}    
\usepackage{tikz}    
\begin{document}    
\foreach \x in {1,1.1,...,2}
    {\number\x\ }    
\foreach \x in {1,1.2,...,2}
    {\number\x\ }    
\end{document}

结果是

1 1.1 1.20001 1.30002 1.40002 1.50003 1.60004 1.70004 1.80005 1.90005
1 1.2 1.4 1.59999 1.79999 1.99998

因此,下一个值将大于2第一种情况。

经验法则是:永远不要在循环中使用非整数作为步长值。

答案3

是的,舍入误差,但它不能解释一切。\foreach不使用省略号列表的上限(...),而是依赖于通过增量(增量)将计算值与上限进行比较。\newforeach强制执行上限,相信用户希望将上限用作明确的点/值。

\documentclass{article}
\usepackage{tikz}
\usepackage{loops}[2013/05/01]

\begin{document}
\begin{tikzpicture}
\draw[dashed] (1,-1) -- (1,2) node[above] {$1$};
\draw[dashed] (2,-1) -- (2,2) node[above] {$2$};
\newforeach \x in {1,1.1,...,2}{
  \draw[fill = yellow] (\x,1) circle (0.05);
}
\newforeach \x in {1,1.2,...,2}{
  \draw[fill = red] (\x,0) circle (0.05);
}   
\end{tikzpicture}
\end{document}

相关内容