【问题标题】:Grouping horizontal and vertical lines into table (C#)将水平线和垂直线分组到表格中(C#)
【发布时间】:2012-09-17 21:52:03
【问题描述】:

一个简单的线类被定义为两个包含起点和终点坐标的 PointF 成员:

public class Line    {
    PointF s, e;
}

我有两个列表,其中包含出现在绘图画布上并形成一个或多个表格的所有水平和垂直线。

List<Line> AllHorizontalLines;
List<Line> AllVerticalLines;

我需要对这些行进行分组,以便将属于一个表的行捕获在一个组中,因此分组函数将具有如下签名:

List<List<Line>> GroupLines(List<Line> hor, List<Line> ver)
{ 

}

为简单起见,我们假设页面上只有“简单”表格,即没有嵌套表格。但是可以合并单元格,因此我们必须忽略小于父表全高的小的水平和垂直线。为进一步简单起见,假设两个输入列表均已排序(水平线 w.r.t. Y 轴,垂直线 w.r.t. X 轴)。

是否有任何已知的算法来解决这个问题?或者谁能​​帮我设计一个?

【问题讨论】:

  • 这是一道作业题吗?如果是这样,您可能应该这样标记它。
  • 不,不是。这是我试图解决的更大、更复杂问题的一部分。
  • 好的,你解决这个问题了吗?如果是这样,你想到了什么?
  • 我现在正在努力。尝试了几次尝试定位“角”(最左边和最右边的垂直线与水平线的交点),但到目前为止还没有产生好的结果。

标签: c# algorithm


【解决方案1】:

这是我建议的方法:

  • 清理这两个列表,这样就不会有任何完全包含的“小”行。
  • 选择任意一行。
  • 取所有接触(相交)这条线的线。
  • 对于这些线中的每一条线,取与其相接触的所有线。
  • 继续直到找不到更多的接触线。
  • 您现在有一个组。
  • 从剩下的行中挑选一行并重复,直到没有其他行为止。

代码:

public static IEnumerable<IEnumerable<Line>> Group(IEnumerable<Line> horizontalLines, IEnumerable<Line> verticalLines)
{
  // Clean the input lists here. I'll leave the implementation up to you.

  var ungroupedLines = new HashSet<Line>(horizontalLines.Concat(verticalLines));
  var groupedLines = new List<List<Line>>();

  while (ungroupedLines.Count > 0)
  {
    var group = new List<Line>();
    var unprocessedLines = new HashSet<Line>();
    unprocessedLines.Add(ungroupedLines.TakeFirst());

    while (unprocessedLines.Count > 0)
    {
      var line = unprocessedLines.TakeFirst();
      group.Add(line);
      unprocessedLines.AddRange(ungroupedLines.TakeIntersectingLines(line));
    }

    groupedLines.Add(group);
  }

  return groupedLines;
}

public static class GroupingExtensions
{
  public static T TakeFirst<T>(this HashSet<T> set)
  {
    var item = set.First();
    set.Remove(item);
    return item;
  }

  public static IEnumerable<Line> TakeIntersectingLines(this HashSet<Line> lines, Line line)
  {
    var intersectedLines = lines.Where(l => l.Intersects(line)).ToList();
    lines.RemoveRange(intersectedLines);
    return intersectedLines;
  }

  public static void RemoveRange<T>(this HashSet<T> set, IEnumerable<T> itemsToRemove)
  {
    foreach(var item in itemsToRemove)
    {
      set.Remove(item);
    }
  }

  public static void AddRange<T>(this HashSet<T> set, IEnumerable<T> itemsToAdd)
  {
    foreach(var item in itemsToAdd)
    {
      set.Add(item);
    }
  }
}

将此方法添加到 Line

public bool Intersects(Line other)
{
  // Whether this line intersects the other line or not.
  // I'll leave the implementation up to you.
}

注意事项:

如果此代码运行速度太慢,您可能需要水平扫描,边走边拾取连接的行。可能也值得关注this

专业:

public static IEnumerable<IEnumerable<Line>> Group(IEnumerable<Line> horizontalLines, IEnumerable<Line> verticalLines)
{
  // Clean the input lists here. I'll leave the implementation up to you.

  var ungroupedHorizontalLines = new HashSet<Line>(horizontalLines);
  var ungroupedVerticalLines = new HashSet<Line>(verticalLines);
  var groupedLines = new List<List<Line>>();

  while (ungroupedHorizontalLines.Count + ungroupedVerticalLines.Count > 0)
  {
    var group = new List<Line>();
    var unprocessedHorizontalLines = new HashSet<Line>();
    var unprocessedVerticalLines = new HashSet<Line>();

    if (ungroupedHorizontalLines.Count > 0)
    {
      unprocessedHorizontalLines.Add(ungroupedHorizontalLines.TakeFirst());
    }
    else
    {
      unprocessedVerticalLines.Add(ungroupedVerticalLines.TakeFirst());
    }

    while (unprocessedHorizontalLines.Count + unprocessedVerticalLines.Count > 0)
    {
      while (unprocessedHorizontalLines.Count > 0)
      {
        var line = unprocessedHorizontalLines.TakeFirst();
        group.Add(line);
                unprocessedVerticalLines.AddRange(ungroupedVerticalLines.TakeIntersectingLines(line));
      }
      while (unprocessedVerticalLines.Count > 0)
      {
        var line = unprocessedVerticalLines.TakeFirst();
        group.Add(line);
        unprocessedHorizontalLines.AddRange(ungroupedHorizontalLines.TakeIntersectingLines(line));
      }
    }
    groupedLines.Add(group);
  }

  return groupedLines;
}

这假设没有线重叠,因为它不检查水平线是否与其他水平线接触(垂直相同)。

您可能可以删除 if-else。这只是为了防止垂直线没有连接到水平线。

【讨论】:

  • 是的,请把它翻译成 C#(或 Java、C++ 或 VB.NET),这样我就知道你到底在提议什么?
  • 如果您需要我为我遗漏的部分提供实现,我可以,但它们应该不会太难。
  • 上面的代码有几个错误。更糟糕的是,在更正所有这些之后,代码会陷入(看似)无限循环。对于 45 条水平线和 19 条垂直线的输入,它已将 2021707 行添加到 group 变量中,并且在我中断执行时仍在继续。
  • 抱歉出现错误,我是用记事本写的。我将下载和编辑并修复它。听起来这些台词并没有从布景中删除,我会调查一下。
  • 现已修复。我在一些琐碎的线路上尝试了它,它对我有用。
【解决方案2】:

以下似乎有效:

  • 设置一个字典,将边界矩形映射到每个矩形中的线列表。
  • 对于两个输入列表中的每一行(我们不关心方向)
    • 在线外创建一个边界矩形
    • 检查线是否穿过任何现有的边界矩形。
      • 如果是,则合并这些矩形中的线,添加当前线,删除触摸的矩形,计算新的边界矩形,然后再次检查。
      • 否则,将此新矩形添加到字典中并删除旧矩形。
  • 返回每个矩形的线条列表。

这是我得到的代码:

public static IEnumerable<IEnumerable<Line>> GroupLines(IEnumerable<Line> lines)
{
    var grouped = new Dictionary<Rectangle, IEnumerable<Line>>();

    foreach (var line in lines)
    {
        var boundedLines = new List<Line>(new[] { line });
        IEnumerable<Rectangle> crossedRectangles;
        var boundingRectangle = CalculateRectangle(boundedLines);
        while (
            (crossedRectangles = grouped.Keys
                .Where(r => Crosses(boundingRectangle, r))
                .ToList()
            ).Any())
        {
            foreach (var crossedRectangle in crossedRectangles)
            {
                boundedLines.AddRange(grouped[crossedRectangle]);
                grouped.Remove(crossedRectangle);
            }
            boundingRectangle = CalculateRectangle(boundedLines);
        }
        grouped.Add(boundingRectangle, boundedLines);
    }
    return grouped.Values;
}

public static bool Crosses(Rectangle r1, Rectangle r2)
{
    return !(r2.Left > r1.Right ||
        r2.Right < r1.Left ||
        r2.Top > r1.Bottom ||
        r2.Bottom < r1.Top);
}

public static Rectangle CalculateRectangle(IEnumerable<Line> lines)
{
    Rectangle rtn = new Rectangle
    {
        Left = int.MaxValue,
        Right = int.MinValue,
        Top = int.MaxValue,
        Bottom = int.MinValue
    };

    foreach (var line in lines)
    {
        if (line.P1.X < rtn.Left) rtn.Left = line.P1.X;
        if (line.P2.X < rtn.Left) rtn.Left = line.P2.X;
        if (line.P1.X > rtn.Right) rtn.Right = line.P1.X;
        if (line.P2.X > rtn.Right) rtn.Right = line.P2.X;
        if (line.P1.Y < rtn.Top) rtn.Top = line.P1.Y;
        if (line.P2.Y < rtn.Top) rtn.Top = line.P2.Y;
        if (line.P1.Y > rtn.Bottom) rtn.Bottom = line.P1.Y;
        if (line.P2.Y > rtn.Bottom) rtn.Bottom = line.P2.Y;
    }

    return rtn;
}

【讨论】:

  • 很有趣,但我在伪代码中看到了一些歧义,或者我可能没有完全理解它。你能帮个忙并将算法翻译成C#吗?如果你不熟悉 C#,我什至可以翻译 Java、C++ 或 VB.NET。
  • @dotNET 完成。我应该强调我认为这行得通,所以我会为你的 cmets 感到高兴。
  • 实际上我已经可以看到一个洞了顶部和底部不接触左右,它不会合并所有四个,只有顶部和底部。该死。
  • 新算法,应该涵盖这个。基本上它反复检查矩形重叠,直到没有。作为一个加号,它甚至应该适用于对角线。
  • 您能否也为CalculateRectangle(IEnumerable ls) 编写代码?我很难掌握它。
【解决方案3】:

好的。我终于自己设计了一个有效的算法。以下是任何未来读者的步骤。

  1. 定义Tuple&lt;List&lt;Line&gt;, List&lt;Line&gt;&gt; 的大集合。此集合中的每个元组将代表一个表(该表的所有水平和垂直线)。

  2. 查找与水平线的起点在一端和另一水平线的起点在另一端的所有垂直线。这些将是每个表的最左边的行。

  3. 现在对于每个左行(称为 LV): 一个。找到所有与 LV 具有相同起始和结束 Y 并因此属于同一个表的垂直线。将这些“姐妹”行添加到名为 MyVSisters 的列表中。

    b.找到最右边的垂直姊妹线的 X 坐标(称为 RVX)。

    c。现在找到从 LV 的 X 延伸到 RVX 的所有水平线,并且它们的 Y 位于 LV 的 Y1 和 Y2 之间。将这些行添加到名为 MyHSisters 的集合中。

    d。将元组 MyHSisters 和 MyVSisters 添加到大集合中。

  4. 返回大集合。

我还没有将其标记为答案。我会等待两个人的回应,然后比较所有 3 个的性能和准确性,然后再决定哪一个是最佳答案。

【讨论】:

  • 好的。在查看了此页面上提供的所有 3 种算法之后,我的算法似乎消耗的时间最少,而所有算法的准确性似乎都一样好。所以我会把这个答案标记为正确的。
  • 如果您有两个相同的表并排,会发生什么?步骤 3a 是否正常工作还是需要太多行?此外,当您有两个相同的表时,3c 是否有效,一个在另一个之上,还是需要太多行?
  • 谢谢乔伊。两个相同的表可以出现在另一个之上,我的代码会处理这个问题。我现在修改了步骤 3c 以匹配我的实际代码。但是,在我的输入中,并排表格是不可能的。