在 TikZ 中绘制递归结构的迭代/使用循环中定义的节点名称

在 TikZ 中绘制递归结构的迭代/使用循环中定义的节点名称

这是我第一次使用 TikZ,我正在尝试绘制一些与希尔伯特 空间填充曲线,基于最近的一篇热门文章(HTMLPDF)具体来说,我试图画出曲线的前几次迭代,节点用四元字符串标记(字母表 {0,1,2,3} 上的字符串)。我有一些可以工作的方法,但我想知道是否有更好的方法。第一次迭代很简单——我可以得到这个图像:

希尔伯特曲线,迭代 1

使用以下代码:

  \begin{tikzpicture}
    \node (0) at (0, 0) {$0$};
    \node (1) at (0, 1) {$1$};
    \node (2) at (1, 1) {$2$};
    \node (3) at (1, 0) {$3$};
    \draw [->] (0) -- (1);
    \draw [->] (1) -- (2);
    \draw [->] (2) -- (3);
  \end{tikzpicture}

第二次迭代涉及到四个副本,其中第一个和最后一个被翻转和旋转,我得到了这个图像:

希尔伯特曲线,迭代 2

使用以下代码:

  \begin{tikzpicture}
    \foreach \quadrant / \xscale / \rotate / \xshift / \yshift in
        {0 / -1 / 90 / 0 / 0,
         1 / +1 / 00 / 0 / 2,
         2 / +1 / 00 / 2 / 2,
         3 / -1 / -90 / 3 / 1}  % (3, 1) = (1, 1) + (2, 0): the former is just to rotate
    {
      \begin{scope}[shift = {(\xshift, \yshift)}, xscale = \xscale, rotate = \rotate]
        \node (\quadrant0) at (0, 0) {$\quadrant0$};
        \node (\quadrant1) at (0, 1) {$\quadrant1$};
        \node (\quadrant2) at (1, 1) {$\quadrant2$};
        \node (\quadrant3) at (1, 0) {$\quadrant3$};
        \draw [->] (\quadrant0) -- (\quadrant1);
        \draw [->] (\quadrant1) -- (\quadrant2);
        \draw [->] (\quadrant2) -- (\quadrant3);
      \end{scope}
    }
    \draw [->] (0, 1) -- (0, 2);
    \draw [->] (1, 2) -- (2, 2);
    \draw [->] (3, 2) -- (3, 1);
  \end{tikzpicture}

存在一个实际问题和几个代码清洁度问题:

  1. 跨越不同象限的箭头(代码中的最后三行)看起来很丑陋,因为它们是在坐标之间绘制的,而不是在节点之间绘制的。我尝试使用类似代码在节点之间绘制它,\draw [->] (03) -- (10);但我收到错误消息,例如“错误:包 pgf 错误:没有已知的名为 03 的形状。”所以我猜想在 foreach 循环中,即使\quadrant值为0,该行\node (\quadrant3) at (1, 0) {$\quadrant3$};要么实际上没有创建名为 的节点03,要么在循环外无法访问。如何在循环外使用与 对应的节点03

  2. 有没有比我所做的更好的方法来绘制图形?(例如翻转+旋转的更好方法?)

  3. 在 foreach 循环中,我刚刚\quadrant在上图的代码中为每个节点添加了前缀;有没有办法重用该代码(可能首先以更通用的方式编写它,将前缀设置为空字符串)而不是重复它?


第三次迭代有第二次迭代的四个副本,除了现在丑陋的箭头数量更多之外,没有新的问题,而且我不得不基本上重复第二次迭代的所有代码(只是q在大多数变量名前面加上前缀)。

希尔伯特曲线,迭代 3

  \begin{tikzpicture}
   \foreach \qquadrant / \qxscale / \qrotate / \qxshift / \qyshift in
       {0 / -1 / 90 / 0 / 0,
        1 / +1 / 00 / 0 / 4,
        2 / +1 / 00 / 4 / 4,
        3 / -1 / -90 / 7 / 3}   % (7, 3) = (3, 3) + (4, 0)
   {
    \begin{scope}[shift = {(\qxshift, \qyshift)}, xscale = \qxscale, rotate = \qrotate]
    \foreach \quadrant / \xscale / \rotate / \xshift / \yshift in
        {0 / -1 / 90 / 0 / 0,
         1 / +1 / 00 / 0 / 2,
         2 / +1 / 00 / 2 / 2,
         3 / -1 / -90 / 3 / 1}  % (3, 1) = (1, 1) + (2, 0): the former is just to rotate
    {
      \begin{scope}[shift = {(\xshift, \yshift)}, xscale = \xscale, rotate = \rotate]
        \node (\qquadrant\quadrant0) at (0, 0) {$\qquadrant\quadrant0$};
        \node (\qquadrant\quadrant1) at (0, 1) {$\qquadrant\quadrant1$};
        \node (\qquadrant\quadrant2) at (1, 1) {$\qquadrant\quadrant2$};
        \node (\qquadrant\quadrant3) at (1, 0) {$\qquadrant\quadrant3$};
        \draw [->] (\qquadrant\quadrant0) -- (\qquadrant\quadrant1);
        \draw [->] (\qquadrant\quadrant1) -- (\qquadrant\quadrant2);
        \draw [->] (\qquadrant\quadrant2) -- (\qquadrant\quadrant3);
      \end{scope}
    }
    \draw [->] (0, 1) -- (0, 2);
    \draw [->] (1, 2) -- (2, 2);
    \draw [->] (3, 2) -- (3, 1);
    \end{scope}
   }
   \draw [->] (0, 3) -- (0, 4);
   \draw [->] (3, 4) -- (4, 4);
   \draw [->] (7, 4) -- (7, 3);
  \end{tikzpicture}

我不期望绘制第三次迭代之后的图形(由于节点名称很长,水平箭头已经变得非常短),并且我愿意忍受代码重复,因此只需修复这三个图形的箭头就足够了。


编辑:我正在寻找一种传统的编程解决方案,但多亏了答案/评论,我了解了 Lindenmayer 系统,它看起来非常迷人。我接受了答案,并希望更详细地探索这些系统。

同时,当我尝试为上述问题再举一个最小的例子时,我自己找到了答案。结果发现答案非常简单:它与 中的数字和斜线之间的空格有关。\foreach \quadrant / \xscale [...] in  {0 / -1 [...]因此,循环内的节点被创建为0 3而不是03。将0 /etc. 更改为0/etc. 得到下图:

希尔伯特曲线,迭代 3,固定

使用下面的代码(我现在知道如何编写得更好,但为了对比或相似性,保持与上面的代码相似):

  \begin{tikzpicture}
   \foreach \qquadrant / \qxscale / \qrotate / \qxshift / \qyshift in
       {0/ -1 / 90 / 0 / 0,
        1/ +1 / 00 / 0 / 4,
        2/ +1 / 00 / 4 / 4,
        3/ -1 / -90 / 7 / 3}   % (7, 3) = (3, 3) + (4, 0)
   {
    \begin{scope}[shift = {(\qxshift, \qyshift)}, xscale = \qxscale, rotate = \qrotate]
    \foreach \quadrant / \xscale / \rotate / \xshift / \yshift in
        {0/ -1 / 90 / 0 / 0,
         1/ +1 / 00 / 0 / 2,
         2/ +1 / 00 / 2 / 2,
         3/ -1 / -90 / 3 / 1}  % (3, 1) = (1, 1) + (2, 0): the former is just to rotate
    {
      \begin{scope}[shift = {(\xshift, \yshift)}, xscale = \xscale, rotate = \rotate]
        \node (\qquadrant\quadrant0) at (0, 0) {$\qquadrant\quadrant0$};
        \node (\qquadrant\quadrant1) at (0, 1) {$\qquadrant\quadrant1$};
        \node (\qquadrant\quadrant2) at (1, 1) {$\qquadrant\quadrant2$};
        \node (\qquadrant\quadrant3) at (1, 0) {$\qquadrant\quadrant3$};
        \draw [->] (\qquadrant\quadrant0) -- (\qquadrant\quadrant1);
        \draw [->] (\qquadrant\quadrant1) -- (\qquadrant\quadrant2);
        \draw [->] (\qquadrant\quadrant2) -- (\qquadrant\quadrant3);
      \end{scope}
    }
    \draw [->] (\qquadrant03) -- (\qquadrant10);
    \draw [->] (\qquadrant13) -- (\qquadrant20);
    \draw [->] (\qquadrant23) -- (\qquadrant30);
    \end{scope}
   }
   \draw [->] (033) -- (100);
   \draw [->] (133) -- (200);
   \draw [->] (233) -- (300);
  \end{tikzpicture}

答案1

正如 Marc 指出的那样,Lindenmayer 系统(2.10 手册中的第 37 节“Lindenmayer 系统绘图库”)可以做到这一点。但是,需要进行一些调整才能使节点文本正确,为此我使用了基础转换工具(第 64.4 节“基础转换”)。

但请注意,一些多余的空格从某个地方悄悄混入了文档中,但我不确定在哪里。

\documentclass[border=0.125cm]{standalone}

\usepackage{tikz}
\usetikzlibrary{lindenmayersystems}

\newcount\hilbertnodecount
\newcount\hilbertnodepreviouscount

\pgfdeclarelindenmayersystem{hilbert curve}{
    % Symbol to initialise things
    \symbol{I}{\hilbertnodecount=0}
    % Symbol to draw node
    \symbol{X}{%
        \pgflsystemdrawforward%
        \node [every hilbert node/.try, hilbert node \the\hilbertnodecount/.try ] 
            (hilbert-\the\hilbertnodecount) {};
        \ifnum\hilbertnodecount>0\relax%
            \hilbertnodepreviouscount=\hilbertnodecount%
            \advance\hilbertnodepreviouscount by-1\relax%
            \path [hilbert node path/.try] (hilbert-\the\hilbertnodepreviouscount) -- (hilbert-\the\hilbertnodecount);
        \fi%
        \advance\hilbertnodecount by1%
    }
    % Symbol to turn left
    \symbol{+}{\pgflsystemturnright} 
    % Symbol to turn right
    \symbol{-}{\pgflsystemturnleft}
    % Initial rule (to reset \hilbertnodecount and draw first node)
    \rule{S -> IXA}
    % Rules to draw Hilbert curve
    \rule{A -> +BX-AXA-XB+}
    \rule{B -> -AX+BXB+XA-}
}

\makeatletter
% Need a teensy hack to get the order
\def\tikzlsystemorder{\pgf@lsystem@order}%
\makeatother

\tikzset{
    hilbert nodes/.style={
        lindenmayer system={hilbert curve, axiom=S, order=#1, angle=90},
        insert path={lindenmayer system}
    },
    hilbert step/.style={%
        /pgf/lindenmayer system/step=#1
    }
}




\tikzset{%
    every hilbert node/.style={
        draw=none,
        %minimum size=0.75cm, <- change minimum size
        %font=\tiny, <- change font
        execute at end node={% <- code for printing number
            % Get the number of digits
            \pgfmathparse{int(\tikzlsystemorder-1)}%
            \let\ndigits=\pgfmathresult%
            % Ensure result of base conversion are padded with digits%
            \pgfmathsetbasenumberlength{\ndigits}%
            % Convert the node number to base 4
            \pgfmathdectobase\n{\hilbertnodecount}{4}%
            \n%
        }
    },
    hilbert step=1cm,% <- distance between nodes
    hilbert node path/.style={draw, ->}% <- style for lines between nodes
}

\begin{document}
\begin{tabular}{c} %
\tikz[yscale=-1]\path [hilbert nodes=2]; \\[1cm]
\tikz[yscale=-1]\path [hilbert nodes=3]; \\[1cm]
\tikz[yscale=-1]\path [hilbert nodes=4]; 
\end{tabular}
\end{document}

在此处输入图片描述

答案2

这是一个基于此的解决方案例子texample.net 的代码没有优化,\foreach \i in {4,8,12,16} ...而且我使用了太多的计数器:(

更新

我删除了\foreach \i in {4,8,12,16} ...

\documentclass{standalone}
\usepackage{tikz}

\pgfmathsetmacro\HilbertLastX{0}
\pgfmathsetmacro\HilbertLastY{0}

\newcounter{Order}
\newcounter{Num}
\newcounter{HilbertOrder}
\newcounter{GlobalHilbertOrder}

\tikzset{hilbert/.style={minimum height=1.5ex,minimum width=3ex,inner sep=0pt,outer sep=0pt,font=\tiny}}

\def\AddNewNode#1#2{%
\pgfmathsetmacro\HilbertLastX{\HilbertLastX+#1}
\pgfmathsetmacro\HilbertLastY{\HilbertLastY+#2}
\addtocounter{Num}{1}
\node[hilbert] (tmp) at (\HilbertLastX cm,\HilbertLastY cm) {};
\draw[red,->] (last) -- (tmp);
\pgfmathsetbasenumberlength{3}%
\pgfmathdectobase\mynumber{\theNum}{4}
\node[hilbert] (last) at (\HilbertLastX cm,\HilbertLastY cm) {\mynumber};
}%

\def\Hilbert[#1,#2,#3,#4,#5,#6,#7,#8] {
  \ifnum\value{HilbertOrder} > 0%
     \addtocounter{HilbertOrder}{-1}     
     \Hilbert[#5,#6,#7,#8,#1,#2,#3,#4]
     \AddNewNode {#1} {#2}
     \Hilbert[#1,#2,#3,#4,#5,#6,#7,#8]
     \AddNewNode {#5} {#6}
     \Hilbert[#1,#2,#3,#4,#5,#6,#7,#8]
     \AddNewNode {#3} {#4}
     \Hilbert[#7,#8,#5,#6,#3,#4,#1,#2]
     \addtocounter{HilbertOrder}{1}
     \addtocounter{GlobalHilbertOrder}{1}
      \ifnum\value{Order} >1
          \pgfmathparse{mod(\value{Order},4)}
          \ifdim\pgfmathresult pt =0pt \addtocounter{GlobalHilbertOrder}{-1}\fi
    \fi
      \addtocounter{Order}{1}
  \fi
}% 

\def\hilbert((#1,#2),#3){%
 \pgfmathsetmacro\HilbertLastX{\HilbertLastX+#1}
 \pgfmathsetmacro\HilbertLastY{\HilbertLastY+#2}
 \path (\HilbertLastX,\HilbertLastY);
 \setcounter{HilbertOrder}{#3}
 \Hilbert[1,0,-1,0,0,1,0,-1]
}%

\begin{document}
\parindent=0pt

\begin{tikzpicture}[node distance=0cm,every path/.style={thin}]
    \node[hilbert] (last) at (\HilbertLastX cm,\HilbertLastY cm) {000};
    \setcounter{GlobalHilbertOrder}{0}  \setcounter{Order}{0} \setcounter{Num}{0} 
    \hilbert((0mm,0mm),3);
\end{tikzpicture}

\end{document}

在此处输入图片描述

相关内容