在.dot树中强制节点水平排序

22

我正在尝试使用GraphViz重新创建一个二叉搜索树的示例图。最终效果应该如下所示:

enter image description here

这是我的第一次尝试:

digraph G {
    nodesep=0.3;
    ranksep=0.2;
    margin=0.1;
    node [shape=circle];
    edge [arrowsize=0.8];
    6 -> 4;
    6 -> 11;
    4 -> 2;
    4 -> 5;
    2 -> 1;
    2 -> 3;
    11 -> 8;
    11 -> 14;
    8 -> 7;
    8 -> 10;
    10 -> 9;
    14 -> 13;
    14 -> 16;
    13 -> 12;
    16 -> 15;
    16 -> 17;
}

但是不幸的是,GraphViz并不关心树的水平位置,所以我得到了:

enter image description here

我如何添加约束条件,使顶点的水平位置反映它们的总排序?

1个回答

28
您可以按照Graphviz关于平衡树的FAQ中提出的常规方法,添加不可见节点和不可见边,并调整边权等。在一些简单情况下,这已经足够了。
但是有一个更好的解决方案:Graphviz附带了一个名为gvpr(图形模式扫描和处理语言)的工具,它允许

将输入图形复制到其输出,可能转换其结构和属性,创建新图形或打印任意信息

由于Emden R. Gansner已经通过创建一个脚本来很好地布局二叉树,因此以下是如何执行此操作的步骤(所有功劳归功于ERG):
将以下gvpr脚本保存到名为tree.gv的文件中:
BEGIN {
  double tw[node_t];    // width of tree rooted at node
  double nw[node_t];    // width of node
  double xoff[node_t];  // x offset of root from left side of its tree
  double sp = 36;       // extra space between left and right subtrees
  double wd, w, w1, w2; 
  double x, y, z;
  edge_t e1, e2;
  node_t n;
}
BEG_G {
  $.bb = "";
  $tvtype=TV_postfwd;   // visit root after all children visited
}
N {
  sscanf ($.width, "%f", &w);
  w *= 72;  // convert inches to points
  nw[$] = w;
  if ($.outdegree == 0) {
    tw[$] = w;
    xoff[$] = w/2.0;
  }
  else if ($.outdegree == 1) {
    e1 = fstout($);
    w1 = tw[e1.head];    
    tw[$] = w1 + (sp+w)/2.0;
    if (e1.side == "left")
      xoff[$] = tw[$] - w/2.0;
    else
      xoff[$] = w/2.0;
  }
  else {
    e1 = fstout($);
    w1 = tw[e1.head];    
    e2 = nxtout(e1);
    w2 = tw[e2.head];    
    wd = w1 + w2 + sp;
    if (w > wd)
      wd = w;
    tw[$] = wd;
    xoff[$] = w1 + sp/2.0;
  }
}
BEG_G {
  $tvtype=TV_fwd;   // visit root first, then children
}
N {
  if ($.indegree == 0) {
    sscanf ($.pos, "%f,%f", &x, &y);
    $.pos = sprintf("0,%f", y);
  }
  if ($.outdegree == 0) return;
  sscanf ($.pos, "%f,%f", &x, &y);
  wd = tw[$];
  e1 = fstout($);
  n = e1.head;
  sscanf (n.pos, "%f,%f", &z, &y);
  if ($.outdegree == 1) {
    if (e1.side == "left")
      n.pos = sprintf("%f,%f",  x - tw[n] - sp/2.0 + xoff[n], y);
    else
      n.pos = sprintf("%f,%f", x + sp/2.0 + xoff[n], y);
  }
  else {
    n.pos = sprintf("%f,%f", x - tw[n] - sp/2.0 + xoff[n], y);
    e2 = nxtout(e1);
    n = e2.head;
    sscanf (n.pos, "%f,%f", &z, &y);
    n.pos = sprintf("%f,%f", x + sp/2.0 + xoff[n], y);
  }
}

假设您的包含图表的dot文件名为binarytree.gv,则可以执行以下命令:

dot binarytree.gv | gvpr -c -ftree.gv | neato -n -Tpng -o binarytree.png

结果是:
通过在脚本中切换一行或两行,甚至可以使单个子节点向左而不是右侧。 Binary tree nicely layouted with graphiv and a gvpr script thanks to Emden R. Gansner

1
我正在尝试,但是我只得到了 gvpr: "./tree.gv", line 46: _nd_a0:<<< -- syntax error(我已经验证代码已经正确复制,也尝试从Gansner的帖子中复制)。现在我不知道那意味着什么?我在第46行看不到任何可疑的地方 :-( - 0__
这是最后一个 N { } 块,如果我删除它,错误就会消失,但布局也不会完成。这可能是版本问题吗?我的 gvpr version 2.26.3 (20100126.1600) - 0__
1
我使用了2.28.0版本,它立即正常运行。Gansner的帖子是在你的Graphviz版本发布约6个月后发表的,因此建议尝试使用更新版本。 - marapet
1
我已经更新到GraphViz 2.28.0版本,脚本可以运行了 -- 太棒了,非常感谢! - 0__
当上面的树是二叉树时,节点9和12应该绘制在它们的父节点左侧。我能用tree.gv脚本实现这个吗? - Marten Bauer
@MartenBauer 是的,您只需要在脚本中切换一两行即可。 - marapet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接