3

I want to visualize the Dijkstra algorithm for finding the shortest path. I am inspired by this post. Sadly, I am way too bad at LaTeX. My initial idea was doing it with LuaLaTeX using Lua and I was able to code the Dijkstra algorithm using it:

-- init, graph should the an incidence list, directed a boolean
function dijkstra_initialize(graph, directed, start_node)
    -- global variables
    graph = graph
    current_node = start_node
    is_directed = directed
    not_visited_nodes = {}
    visited_nodes = {}

    -- init not visited
    for node, _ in pairs(graph) do
        table.insert(not_visited_nodes, node)
    end

    -- init distances
    distances = {}
    for node, _ in pairs(graph) do
        distances[node] = math.huge
    end
    distances[start_node] = 0

    -- init pred
    predecessors = {}
    for node, _ in pairs(graph) do
        predecessors[node] = nil
    end
end

function dijkstra_step()
    if #not_visited_nodes == 0 then
        return
    end

    local min_distance = math.huge
    local next_node
    for _, node in ipairs(not_visited_nodes) do
        if distances[node] < min_distance then
            min_distance = distances[node]
            next_node = node
        end
    end

    table.remove(not_visited_nodes, table.indexof(not_visited_nodes, next_node))
    table.insert(visited_nodes, next_node)
    current_node = next_node

    for node, weight in pairs(graph[current_node]) do
        local distance = distances[current_node] + weight
        if distance < distances[node] then
            distances[node] = distance
            predecessors[node] = current_node
        end
    end
end

I don't know whether this is helpful or not. My problem is that I have no idea how to access Lua using TikZ and visualize the variables. My idea was to have a \dijkstra_initialize() function in LaTeX to initialize everything, \dijkstra_empty_graph function to show the graph without visited nodes, \dijkstra_empty_table to show the initial empty table. Additionally, I thought about a \dijkstra_step function to print the next graph and table, which shows the current node in some color, e.g. red, the visited nodes in some color different to color of the not visited nodes. I thought, maybe it would be useful to have a function \dijkstra_step_table which makes the step but only shows the table, and \dijkstra_step_graph for the graph. Finally, I thought having \dijkstra_finish \dijkstra_finish_table \dijkstra_finish_graph would be great, which simply calls the corresponding step function until the algorithm terminated. I think this looks as follows in Lua:

function dijkstra_finish()
  while #not_visited_nodes > 0 do
    dijkstra_step()
  end
end

So far my graphs and tables are hardcoded and look like this:

[enter image description here enter image description here enter image description here

I would be very happy if someone could help me because I have thought about this project for a while, but I am simply not good enough in LaTeX, TikZ or Lua.

1

1 Answer 1

10

Here's the Dijkstra algorithm in TeX.

It uses the PGFFor (the .list handler) and PGFMath (the \pgfmathloop) for looping:

  • The .list handler gets used to store the weights of each edge and to do all the steps.
  • The \pgfmathloop macro (undocumented) is very similar to a LaTeX \loop but provides an additional counter \pgfmathcounter (which is not a TeX count nor a LaTeX counter).

Both could be transformed into the other with a bit more work.

The PGFMath packages also loads a small undocumented utility PGFInt which provides \pgfinteval which is almost a clone of xfp's and L3's \inteval, I'm only using it in place of \numexpr<int 1>+<int 2>\relax as an easier way to add two integers.

The PGFKeys package is used for the main interface,

  • the user-interface keys n, steps and start where
    • n is the number of nodes (1, …, n),
    • start is the number of the node where the algorithm should start and
    • steps is the maximum number of steps the algorithm should use;
  • the key weight matrix which stores a matrix of (possible directed) weights (use i for infinity);
  • the internal keys __init and __step which do the hard work;
  • a few tests
    • is calculated = {<i>}{<true>}{<false>} which test whether a distance to node i at least has been calculated (doesn't mean it's the meanimum, just means that a neighbour has been visited – this is not part of the algorithm but I like it for visualization reasons),
    • is visited = {<i>}{<true>}{<false>} which tests whether node i has been visited,
    • is current = {<i>}{<true>}{<false>} which tests whether i is the current node, i.e. the source node for current step,
    • is edge = {<i>-<j>}{<true>}{<false>} which tests wheter edge i to j has actually a weight not equal to infinity assigned and
    • is pred = {<i>}{<j>}{<true>}{<false>} which tests whether j is a predecessor of i; and
  • actually storing all the results (it's just a fancy interface for \csname and \endcsname).

All the drawing is done by TikZ, meaning:

  • placement of nodes
  • edges and weights between nodes
  • style of nodes which uses the aforementioned tests.

Technically, the dijkstra environment is only used to group the calculations (since the code overwrites itself to not do needless iterations when the algorithm finishes but there's still more steps to do according to the steps key).

Instead of a TikZ diagram, you could just put a tabular in it.


For each step, a snapshot of all these calculated distances and predecessors are stored and can be accessed later …
which is done for the tabular below the diagram. (This doesn't need to be inside the TikZ diagram, I'm only putting it inside there for this answer.)

For the table, another bunch of commands are used that test something specific for each node where the optional first argument is the step to be tested. (If no optional argument is used, the “global” value of the algorithm is used.)

Some of these tests end on TFX (instead of the normal boolean TF). These check whether the required value actually exist, if they don't the last argument will be used. (For example, in step 2 all the values of step 3 and above don't exist and thus no content should be written in the cell.)

In regards to the table creation, this might not be the optimal way to approach this.

I purposeful am using eight instead of the required 5 or 6 steps to show that the code won't break if more steps than necessary are asked for.

And yes, the initializing phase is step 0 where only the starting node get marked as visited and with distance 0.

Code

\documentclass[tikz]{standalone}
\usepackage{pgfkeys, pgfmath, pgffor}% for dij algo
\usepackage{tikz}                    % for drawing
\usepackage{booktabs, array}         % for nice tabulars
%% User Interface
\newcommand*\dijset{\pgfqkeys{/dij}}
\newenvironment*{dijkstra}[1][]{%
  \dijset{#1, __init}%
  \ifnum\pgfkeysvalueof{/dij/steps}>0
    \dijset{__step/.list/.expanded={1,...,\pgfkeysvalueof{/dij/steps}}}%
  \fi}{}
\newcommand*\DijNodeDist[1]{% return the distance to #1 or infty
  \ifnum\pgfkeysvalueof{/dij/node #1/dist}<2147483647\relax
    \pgfkeysvalueof{/dij/node #1/dist}\else\infty\fi}
\dijset{
  weight matrix/.style={
    /utils/exec=\def\listrow{0},
    /utils/rows/.style={
      /utils/exec=\def\listcol{0}%
                  \edef\listrow{\the\numexpr\listrow+1\relax},
      /utils/cols/.ecode={%
        \edef\noexpand\listcol{\noexpand\the\numexpr\noexpand\listcol+1\relax}%
        \noexpand\if i####1%
          \noexpand\pgfkeyssetevalue{/dij/weight/\listrow-\noexpand\listcol}{2147483647}%
        \noexpand\else
          \noexpand\pgfkeyssetevalue{/dij/weight/\listrow-\noexpand\listcol}{####1}%
        \noexpand\fi
      },
      /utils/cols/.list={##1},
    },
    /utils/rows/.list={#1}},
  start/.initial  =  1,
  n/.initial      = 10,
  steps/.initial  = 10}
\makeatletter
%% Algorithm
\newif\ifdij@allvisited
\newcommand*{\pgfkeysletlet}[2]{\pgfkeysgetvalue{#2}\pgfkeys@temp\pgfkeyslet{#1}\pgfkeys@temp}
\dijset{
  __init/.code={%
    \def\dij@nodecurrent{0}% for step = 0
    \pgfmathloop
      \pgfkeyssetvalue{/dij/node \pgfmathcounter/dist}{2147483647}%
      \pgfkeyssetvalue{/dij/node \pgfmathcounter/visited}{0}%
      \pgfkeyssetvalue{/dij/node \pgfmathcounter/pred}{0}%
      \pgfkeyssetvalue{/dij/node \pgfmathcounter/step 0/dist}{2147483647}%
      \pgfkeyssetvalue{/dij/node \pgfmathcounter/step 0/visited}{0}%
      \pgfkeyssetvalue{/dij/node \pgfmathcounter/step 0/pred}{0}%
      \ifnum\pgfmathcounter<\pgfkeysvalueof{/dij/n}%
    \repeatpgfmathloop
    \pgfkeyssetvalue{/dij/node \pgfkeysvalueof{/dij/start}/dist}{0}%
    \pgfkeyssetvalue{/dij/node \pgfkeysvalueof{/dij/start}/step 0/dist}{0}%
  },
  __step/.code={%
    %% Are all visited?
    %% And if not: what's the node with the min distance?
    \dij@allvisitedtrue
    \def\dij@nodecurrent{0}%
    \def\dij@nodedist{2147483647}%
    \pgfmathloop
      \ifnum\pgfkeysvalueof{/dij/node \pgfmathcounter/visited}=0\relax
        \dij@allvisitedfalse
        \pgfkeysgetvalue{/dij/node \pgfmathcounter/dist}\dij@temp
        \ifnum\dij@temp<\dij@nodedist\relax
          \let\dij@nodecurrent\pgfmathcounter
          \let\dij@nodedist\dij@temp
        \fi
      \fi
      \ifnum\pgfmathcounter<\pgfkeysvalueof{/dij/n}%
    \repeatpgfmathloop
    %% Try to visit all neighbours of min node.
    \ifdij@allvisited\dijset{__step/.code=}% don't repeat me
    \else
      \pgfkeyssetvalue{/dij/node \dij@nodecurrent/visited}{1}%
      \pgfkeyssetvalue{/dij/node \dij@nodecurrent/step #1/visited}{1}%
      \pgfkeyssetvalue{/dij/node \dij@nodecurrent/step #1}{}% mark current node
      \pgfmathloop
        \ifnum\pgfkeysvalueof{/dij/node \pgfmathcounter/visited}=0\relax % not yet visited
          \unless\ifnum\pgfkeysvalueof{/dij/weight/\dij@nodecurrent-\pgfmathcounter}=2147483647\relax
            \edef\dij@temp{\pgfinteval{\dij@nodedist+\pgfkeysvalueof{/dij/weight/\dij@nodecurrent-\pgfmathcounter}}}%
            \ifnum\dij@temp<\pgfkeysvalueof{/dij/node \pgfmathcounter/dist}\relax
              \pgfkeyslet{/dij/node \pgfmathcounter/dist}\dij@temp
              \pgfkeyslet{/dij/node \pgfmathcounter/pred}\dij@nodecurrent
            \fi
          \fi
        \fi
        \pgfkeysletlet{/dij/node \pgfmathcounter/step #1/dist}{/dij/node \pgfmathcounter/dist}%
        \pgfkeysletlet{/dij/node \pgfmathcounter/step #1/pred}{/dij/node \pgfmathcounter/pred}%
        \pgfkeysletlet{/dij/node \pgfmathcounter/step #1/visited}{/dij/node \pgfmathcounter/visited}%
        \ifnum\pgfmathcounter<\pgfkeysvalueof{/dij/n}%
      \repeatpgfmathloop
    \fi
  }
}
%% Test Keys
\dijset{
  @create test 1/.style 2 args={
    #1/.code n args={3}{#2\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
                        {\pgfkeysalso{##2}}{\pgfkeysalso{##3}}}},
  @create test 2/.style 2 args={
    #1/.code n args={4}{#2\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
                        {\pgfkeysalso{##3}}{\pgfkeysalso{##4}}}},
  @create test 1={is current}{\ifnum\dij@nodecurrent=#1\relax}}
\makeatother
\dijset{
  @create test 1={is visited}{\ifnum\pgfkeysvalueof{/dij/node #1/visited}=1\relax},
  @create test 1={is edge}{\ifnum\pgfkeysvalueof{/dij/weight/#1}<2147483647\relax},
  @create test 2={is pred}{\ifnum\pgfkeysvalueof{/dij/node #1/pred}=#2\relax},
  @create test 1={is calculated}{\ifnum\pgfkeysvalueof{/dij/node #1/dist}<2147483647\relax}
}
%% Table
\dijset{table/.cd,
  .style={/dij/table/content/.initial=, /dij/table/row/.list={#1}},
  row/.style={%
    /dij/table/content/.append={#1},
    /dij/table/col/.style={%
      /dij/table/content/.append={%
        \DijIfNumTF{0=#1}{%
          & / & $\DijIsNodeStartTF{##1}{0}{\infty}$
        }{%
          \DijIsNodeStartTF{##1}{&&}{%
            \DijIsNodeCalculatedTFX[#1]{##1}{%
              \DijIsNodeVisitedTFX[#1]{##1}{%
                \DijIsNodeCurrentTF[#1]{##1}{%
                  & $\pgfkeysvalueof{/dij/table/node function}
                    {\pgfkeysvalueof{/dij/node ##1/step #1/pred}}$
                  & $\pgfkeysvalueof{/dij/node ##1/step #1/dist}$
                }{&&}%
              }{%
                & $\pgfkeysvalueof{/dij/table/node function}
                  {\pgfkeysvalueof{/dij/node ##1/step #1/pred}}$
                & $\pgfkeysvalueof{/dij/node ##1/step #1/dist}$
              }{&&}%
            }{& / & $\infty$}{&&}%
          }%
        }%
      }%
    },
    /dij/table/col/.list/.expanded={1,...,\pgfkeysvalueof{/dij/n}},
    /dij/table/content/.append=\\}}
%% Tests
\makeatletter
\def\dij@firstofthree#1#2#3{#1}
\def\dij@secondofthree#1#2#3{#2}
\def\dij@thirdofthree#1#2#3{#3}
\dijset{table/node function/.initial=\@Alph}
\def\dij@teststep#1{\ifnum#1<0 \else step #1/\fi}
\newcommand*\dij@relaxTF[3]{%
  \pgfkeysgetvalue{/dij/node #2/\dij@teststep{#1}#3}\dij@temp
  \ifx\dij@temp\relax\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi}
\newcommand*\DijIsNodeVisitedTFX[2][-1]{%
  \dij@relaxTF{#1}{#2}{visited}{\dij@thirdofthree}{%
    \ifnum\dij@temp=1\relax
      \expandafter\dij@firstofthree\else\expandafter\dij@secondofthree\fi}}
\newcommand*\DijIsNodeCalculatedTFX[2][-1]{%
  \dij@relaxTF{#1}{#2}{dist}{\dij@thirdofthree}{%
    \ifnum\dij@temp<2147483647\relax
      \expandafter\dij@firstofthree\else\expandafter\dij@secondofthree\fi}}
\newcommand*\DijIsNodeStartTF[1]{\ifnum\pgfkeysvalueof{/dij/start}=#1\relax
  \expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi}
\newcommand*\DijIfNumTF[1]{%
  \ifnum#1\relax\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi}
\newcommand*\DijIsNodeCurrentTF[2][-1]{%
  \dij@relaxTF{#1}{#2}{}{\@secondoftwo}{\@firstoftwo}}
\makeatother
\begin{document}
\dijset{
  n = 6, start = 1,
  weight matrix={
    {0,6,5,i,i,i},
    {6,0,7,1,i,i},
    {5,7,0,8,4,i},
    {i,1,8,0,9,2},
    {i,i,4,9,0,3},
    {i,i,i,2,3,0}}}
\foreach \STEP in {0,...,8}{%
\begin{dijkstra}[steps = \STEP]
\begin{tikzpicture}[
  thick, scale=2,
  node edge 1-2/.style=swap,
  node edge 4-6/.style=swap,
]
\foreach[count=\i]\p in {(0,0), (1,1), (1,-1), (3,1), (3,-1), (4,0)}
  \node[draw=blue, circle, text width=width("$\infty$"), align=center,
    /dij/is calculated={\i}{fill=yellow!25}{},
    /dij/is visited={\i}{fill=green}{},
    /dij/is current={\i}{fill=yellow}{},
  ] (\i) at \p
    {\begin{tabular}{@{}c@{}} \pgfkeysvalueof{/dij/table/node function}{\i} \\
                              $\DijNodeDist{\i}$\end{tabular}}
    \ifnum\i>1 % there's no possible neighbour for the first node
      foreach[expand list] \j in {1,...,\pgfinteval{\i-1}}{
        [/dij/is edge={\j-\i}{insert path={
          (\i) edge[/dij/is pred={\i}{\j}{}{/dij/is pred={\j}{\i}{}{gray}}]
               node[auto, node edge \j-\i/.try]{\pgfkeysvalueof{/dij/weight/\j-\i}}
          (\j)}}{}]
      }
    \fi;
\node[anchor = north west] at (current bounding box.north west) {Step: \STEP};
\small
\node[below=+.5em, inner sep=+0pt] at (current bounding box.south) {%
  \pgfmathsetlengthmacro\dijcolleft{width("$M$")}%
  \pgfmathsetlengthmacro\dijcolright{width("$00$")}%
  \begin{tabular}{r *6{>{\centering \arraybackslash}p{\dijcolleft}
                       >{\raggedleft\arraybackslash}p{\dijcolright}}}
  \toprule
   & \multicolumn{2}{c}{$A$} & \multicolumn{2}{c}{$B$} & \multicolumn{2}{c}{$C$}
   & \multicolumn{2}{c}{$D$} & \multicolumn{2}{c}{$E$} & \multicolumn{2}{c}{$F$} \\
   \cmidrule(lr){2-3}\cmidrule(lr){4-5}  \cmidrule(lr){6-7}
   \cmidrule(lr){8-9}\cmidrule(lr){10-11}\cmidrule(lr){12-13}
   \dijset{table={0,...,8}}%
   \pgfkeysvalueof{/dij/table/content}%
   \bottomrule
  \end{tabular}
};
\end{tikzpicture}
\end{dijkstra}}
\end{document}

Output

enter image description here

2
  • I'm not sure if it's right that the table is stable with step 5 but the algorithm is still eating its way through the network for two more steps until every node is properly “visited”. For fun (and testing whether the algorithm is actually working), here's a gif of all steps for all start positions. The algorithm should work for asymmetrical/directed graphs but I haven't checked it and, of course, the drawing will need to be adjusted for that, too. Apr 25, 2023 at 0:52
  • Wow, this is a great answer. I just started building on it to create all the commands I wanted and so far it works!!! But the idea of my question was to get a bridge from lua to tex, so that I can code additional graph algorithms like bfs, page rank, etc. I try my best and may ask additional questions for that. Your answer is still awesome and helpful!!! Thanks a lot ! :D
    – Titanlord
    Apr 25, 2023 at 9:31

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .