【问题标题】:Java algorithm for finding faces in a graph用于在图中查找人脸的 Java 算法
【发布时间】:2021-07-09 19:54:45
【问题描述】:

我有一个我自己创建的平面图。我想找到这张图的面孔,但我找不到这样做的有效算法。到目前为止,我所做的是使用一种算法来查找图中的所有循环,但这给了我所有可能的循环,我已经尝试过但没有找到一种只对面进行排序的方法。我的一个想法是使用 Path2Ds contains 方法来查看另一个形状是否重叠,但由于面共享节点,这不起作用。下图展示了我想要的内容,之后的代码展示了我的可复制示例。

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

public class PolygonFinder {

    //  Graph modeled as list of edges
    static int[][] graph
            = {
                {1, 2}, {1, 6}, {1, 5}, {2, 6},
                {2, 3}, {3, 7}, {7, 4}, {3, 4},
                {5, 4}, {6, 5}
            };

    static List<int[]> cycles = new ArrayList<>();

    /**
     * @param args
     */
    public static void main(String[] args) {

        for (int[] graph1 : graph) {
            for (int j = 0; j < graph1.length; j++) {
                findNewCycles(new int[]{graph1[j]});
            }
        }

        cycles.stream().map(cy -> {
            String s = "" + cy[0];
            for (int i = 1; i < cy.length; i++) {
                s += "," + cy[i];
            }
            return s;
        }).forEachOrdered(s -> {
            System.out.println(s);
        });
    }

    static void findNewCycles(int[] path) {
        int n = path[0];
        int x;
        int[] sub = new int[path.length + 1];

        for (int[] graph1 : graph) {
            for (int y = 0; y <= 1; y++) {
                if (graph1[y] == n) {
                    x = graph1[(y + 1) % 2];
                    if (!visited(x, path)) //  neighbor node not on path yet
                    {
                        sub[0] = x;
                        System.arraycopy(path, 0, sub, 1, path.length);
                        //  explore extended path
                        findNewCycles(sub);
                    } else if ((path.length > 2) && (x == path[path.length - 1])) //  cycle found
                    {
                        int[] p = normalize(path);
                        int[] inv = invert(p);
                        if (isNew(p) && isNew(inv)) {
                            cycles.add(p);
                        }
                    }
                }
            }
        }
    }

    //  check of both arrays have same lengths and contents
    static Boolean equals(int[] a, int[] b) {
        Boolean ret = (a[0] == b[0]) && (a.length == b.length);

        for (int i = 1; ret && (i < a.length); i++) {
            if (a[i] != b[i]) {
                ret = false;
            }
        }

        return ret;
    }

    //  create a path array with reversed order
    static int[] invert(int[] path) {
        int[] p = new int[path.length];

        for (int i = 0; i < path.length; i++) {
            p[i] = path[path.length - 1 - i];
        }

        return normalize(p);
    }

    //  rotate cycle path such that it begins with the smallest node
    static int[] normalize(int[] path) {
        int[] p = new int[path.length];
        int x = smallest(path);
        int n;

        System.arraycopy(path, 0, p, 0, path.length);

        while (p[0] != x) {
            n = p[0];
            System.arraycopy(p, 1, p, 0, p.length - 1);
            p[p.length - 1] = n;
        }

        return p;
    }

    //  compare path against known cycles
    //  return true, iff path is not a known cycle
    static Boolean isNew(int[] path) {
        Boolean ret = true;

        for (int[] p : cycles) {
            if (equals(p, path)) {
                ret = false;
                break;
            }
        }

        return ret;
    }

    //  return the int of the array which is the smallest
    static int smallest(int[] path) {
        int min = path[0];

        for (int p : path) {
            if (p < min) {
                min = p;
            }
        }

        return min;
    }

    //  check if vertex n is contained in path
    static Boolean visited(int n, int[] path) {
        Boolean ret = false;

        for (int p : path) {
            if (p == n) {
                ret = true;
                break;
            }
        }

        return ret;
    }
}

上面代码运行后的结果是:

1,6,2
1,5,6,2
1,5,4,7,3,2
1,6,5,4,7,3,2
1,5,4,3,2
1,6,5,4,3,2
1,5,4,7,3,2,6
1,5,4,3,2,6
1,5,6
2,3,7,4,5,6
2,3,4,5,6
3,4,7

解决此问题的最佳尝试之一是使用以下代码。坐标来自顶部的图片。

    List<Polygon> polys = new LinkedList<>();
    Polygon p1 = new Polygon();
    p1.addPoint(new Point2D.Double(-4, 4));
    p1.addPoint(new Point2D.Double(-1, 3));
    p1.addPoint(new Point2D.Double(-1, 5));
    Polygon p2 = new Polygon();
    p2.addPoint(new Point2D.Double(-4, 4));
    p2.addPoint(new Point2D.Double(0, -2));
    p2.addPoint(new Point2D.Double(-1, 3));
    p2.addPoint(new Point2D.Double(-1, 5));
    Polygon p3 = new Polygon();
    p3.addPoint(new Point2D.Double(-4, 4));
    p3.addPoint(new Point2D.Double(0, -2));
    p3.addPoint(new Point2D.Double(4, 1));
    p3.addPoint(new Point2D.Double(2, 2));
    p3.addPoint(new Point2D.Double(3, 4));
    p3.addPoint(new Point2D.Double(-1, 5));
    Polygon p4 = new Polygon();
    p4.addPoint(new Point2D.Double(-4, 4));
    p4.addPoint(new Point2D.Double(-1, 3));
    p4.addPoint(new Point2D.Double(0, -2));
    p4.addPoint(new Point2D.Double(4, 1));
    p4.addPoint(new Point2D.Double(2, 2));
    p4.addPoint(new Point2D.Double(3, 4));
    p4.addPoint(new Point2D.Double(-1, 5));
    Polygon p5 = new Polygon();
    p5.addPoint(new Point2D.Double(-4, 4));
    p5.addPoint(new Point2D.Double(0, -2));
    p5.addPoint(new Point2D.Double(4, 1));
    p5.addPoint(new Point2D.Double(3, 4));
    p5.addPoint(new Point2D.Double(-1, 5));
    Polygon p6 = new Polygon();
    p6.addPoint(new Point2D.Double(-4, 4));
    p6.addPoint(new Point2D.Double(-1, 3));
    p6.addPoint(new Point2D.Double(0, -2));
    p6.addPoint(new Point2D.Double(4, 1));
    p6.addPoint(new Point2D.Double(3, 4));
    p6.addPoint(new Point2D.Double(-1, 5));
    Polygon p7 = new Polygon();
    p7.addPoint(new Point2D.Double(-4, 4));
    p7.addPoint(new Point2D.Double(0, -2));
    p7.addPoint(new Point2D.Double(4, 1));
    p7.addPoint(new Point2D.Double(2, 2));
    p7.addPoint(new Point2D.Double(3, 4));
    p7.addPoint(new Point2D.Double(-1, 5));
    p7.addPoint(new Point2D.Double(-1, 3));
    Polygon p8 = new Polygon();
    p8.addPoint(new Point2D.Double(-4, 4));
    p8.addPoint(new Point2D.Double(0, -2));
    p8.addPoint(new Point2D.Double(4, 1));
    p8.addPoint(new Point2D.Double(3, 4));
    p8.addPoint(new Point2D.Double(-1, 5));
    p8.addPoint(new Point2D.Double(-1, 3));
    Polygon p9 = new Polygon();
    p9.addPoint(new Point2D.Double(-4, 4));
    p9.addPoint(new Point2D.Double(0, -2));
    p9.addPoint(new Point2D.Double(-1, 3));
    Polygon p10 = new Polygon();
    p10.addPoint(new Point2D.Double(-1, 5));
    p10.addPoint(new Point2D.Double(3, 4));
    p10.addPoint(new Point2D.Double(2, 2));
    p10.addPoint(new Point2D.Double(4, 1));
    p10.addPoint(new Point2D.Double(0, -2));
    p10.addPoint(new Point2D.Double(-1, 3));
    Polygon p11 = new Polygon();
    p11.addPoint(new Point2D.Double(-1, 5));
    p11.addPoint(new Point2D.Double(3, 4));
    p11.addPoint(new Point2D.Double(4, 1));
    p11.addPoint(new Point2D.Double(0, -2));
    p11.addPoint(new Point2D.Double(-1, 3));
    Polygon p12 = new Polygon();
    p12.addPoint(new Point2D.Double(3, 4));
    p12.addPoint(new Point2D.Double(4, 1));
    p12.addPoint(new Point2D.Double(2, 2));
    polys.add(p1);
    polys.add(p2);
    polys.add(p3);
    polys.add(p4);
    polys.add(p5);
    polys.add(p6);
    polys.add(p7);
    polys.add(p8);
    polys.add(p9);
    polys.add(p10);
    polys.add(p11);
    polys.add(p12);
    Set<Integer> toRemove = new HashSet<>();
    for (Polygon polyI : polys) {
        for (Polygon polyJ : polys) {
            if (polyI.equals(polyJ)) {
                continue;
            }
            if (polyI.contains(polyJ)) {
                toRemove.add(polys.indexOf(polyI));
            }
        }
    }
    List<Integer> list = new LinkedList<>(toRemove);
    Collections.sort(list);
    Collections.reverse(list);
    list.forEach((t) -> {
        polys.remove(t.intValue());
    });

    System.out.println("");
    polys.forEach((t) -> {
        System.out.println(t.getPoints());
    });

这里列出了使用的多边形方法。

@Override
public boolean contains(Point2D point) {
    return getPath().contains(point);
}

@Override
public boolean contains(IPolygon polygon) {
    List<Point2D> p2Points = polygon.getPoints();
    for (Point2D point : p2Points) {
        if (getPath().contains(point)) {
            if (!points.contains(point)) {
                return true;
            }
        }
    }
    return false;
}

private Path2D getPath() {
    Path2D path = new Path2D.Double();
    path.moveTo(points.get(0).getX(), points.get(0).getY());
    for (int i = 1; i < points.size(); i++) {
        path.lineTo(points.get(i).getX(), points.get(i).getY());
    }
    path.closePath();
    return path;
}

这段代码给了我下面的结果,不需要第 2-4 个。

[Point2D.Double[-4.0, 4.0], Point2D.Double[-1.0, 3.0], Point2D.Double[-1.0, 5.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[0.0, -2.0], Point2D.Double[-1.0, 3.0], Point2D.Double[-1.0, 5.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[-1.0, 3.0], Point2D.Double[0.0, -2.0], Point2D.Double[4.0, 1.0], Point2D.Double[2.0, 2.0], Point2D.Double[3.0, 4.0], Point2D.Double[-1.0, 5.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[0.0, -2.0], Point2D.Double[4.0, 1.0], Point2D.Double[2.0, 2.0], Point2D.Double[3.0, 4.0], Point2D.Double[-1.0, 5.0], Point2D.Double[-1.0, 3.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[0.0, -2.0], Point2D.Double[-1.0, 3.0]]
[Point2D.Double[-1.0, 5.0], Point2D.Double[3.0, 4.0], Point2D.Double[2.0, 2.0], Point2D.Double[4.0, 1.0], Point2D.Double[0.0, -2.0], Point2D.Double[-1.0, 3.0]]
[Point2D.Double[3.0, 4.0], Point2D.Double[4.0, 1.0], Point2D.Double[2.0, 2.0]]

【问题讨论】:

  • 你的问题有点含糊。同一个图可以有许多不同的嵌入到平面中,它们可以有不同的面。例如,如果我们将点 7 向右移动直到它通过从 3 到 4 的线,F3 会丢失一个顶点。那么......你是否试图找到特定嵌入到平面中的面?还是别的什么?
  • 我的图实际上是嵌入的,你可以看到每个顶点的坐标
  • 好的。顺便说一句,我看到了图片中的坐标,但也注意到在您的第一次解决方案尝试中没有坐标,如果您实际上有坐标可供使用,这似乎很奇怪。这就是我问的原因:-)我的回答有帮助吗?
  • 我正在寻找特定嵌入的面孔。我已经可以保证我的图形是平面的并且在平面上有嵌入。我还不明白的部分是我如何找到它的面孔。我正在尝试在纸上使用某种方法“边缘排序”,但我不明白...有什么提示吗?

标签: java algorithm polygon graph-theory planar-graph


【解决方案1】:

在仅由直线组成的平面嵌入中,与顶点相交的面的边必须在该节点的所有边中相邻。

因此,如果给定这样的嵌入,并根据每个顶点的方向对每个顶点的边进行排序,我们可以通过将边上的每个顶点立即留在我们到达的边的右侧,从而轻松地遍历面的周长通过。

作为数据结构,我可能会选择这样的:

class Vertex {
    Edge edges;
}

class Edge {
    Vertex source;
    Vertex target;
    Edge reverse; // the same edge, seen from the other end
    Edge next; // forms a circular linked list, sorted in order of direction
}

然后我们可以像这样迭代人脸的周长:

Edge startingEdge = ...;
Edge currentEdge = startingEdge;
do {
    currentEdge = currentEdge.reverse.next;
} while (currentEdge != startingEdge);

要按方向对边进行排序,我们可以使用 a x b 为负的事实,如果 a 在 b 的左侧(从坐标系的原点看)。

boolean left(Point2D.Double a, Point2D.Double b) {
    return a.x * b.y - a.y * b.x < 0; 
}

我们可以使用简单的插入排序来按方向对边进行排序(这会足够快,因为平面图有一个有界的平均节点度,所以边列表会很短)。

【讨论】:

    【解决方案2】:

    这里有一个基于半边概念识别人脸的选项。在高层次上,该方法如下所示:

    1. 将连接两个点 u 和 v 的每条边替换为有向边 (u, v) 和 (v, u)。这些被称为半边
    2. 将半边链接在一起,以便单条半边链完美地勾勒出平面图的一个面。
    3. 遍历这些链以识别平面图的所有面。

    在视觉上,它看起来像这样。我们将从如下所示的图表开始:

    并以如下图结束:

    一旦我们有了第二张图,沿着彩色链走就可以识别出所有的面孔。

    那么,问题是如何准确地确定如何将半边链接在一起。基本思想如下:我们希望将边缘链接在一起,以便

    • 所有内面的半边沿逆时针方向缠绕(或逆时针方向,或 widdershins,取决于您来自池塘的哪一侧),
    • 外表面的半边沿顺时针方向缠绕。

    如果我们能想出一个方便的策略来链接这样的东西,我们就可以轻松地将半边粘合在一起以获得我们想要的属性。有很多方法可以做到这一点,但我想重点介绍的方法是通过在本地查看每个节点来实现。

    假设您有一些节点 X,其邻居是 A、B、C 和 D,如下所示。

    在这里,我将离开 X 的半边标记为纯蓝色,将进入 X 的半边标记为橙色虚线。

    现在,关注此图中的传出半边(X,A)。当我们将所有东西连接在一起时,其他一些半边(_,X)需要链接到(X,A)。是哪个边?从图中可以看出是半边(B,X),形成了部分链(B,X),(X,A)。

    同样,关注此图中的半边 (X, B)。和以前一样,当我们将所有半边连接成链时,我们需要一些方法来确定哪个半边 (_, X) 应该在它之前。通过检查,我们可以看到它是(C,X)。

    更一般地,请注意

    • (X, A) 之前的半边是 (B, X)。
    • (X, B) 之前的半边是 (C, X)。
    • (X, C) 之前的半边是 (D, X)。
    • (X, D) 之前的半边是 (A, X)。

    看到模式了吗?如果我们逆时针(逆时针)对该节点周围的邻居进行排序,那么可以找到在边(X,Y)之前的半边:假设Z是节点周围逆时针的下一个邻居,那么半边(X, Y) 之前的就是半边 (Z, X)。

    这为我们提供了一个非常好的策略,可以在满足上述要求的同时将边缘连接成链。这是一些伪代码:

    For each node v:
        Get v's neighbors sorted anticlockwise as u_1, u_2, u_3, ..., u_n
        For each half-edge (v, u_i):
            Update half-edge (u_{i+1 mod n}, v) to chain to (v, u_i)
    

    至此,我们已经将所有东西都连接成链,我们就完成了!

    这里有一些技术细节我已经忽略了,在您编写代码之前需要解决这些细节。例如:

    1. 如何逆时针对节点 v 的邻居进行排序?这可以通过使用Math.atan2(dy, dx) 计算 v 的每个邻居与 v 形成的角度并根据这些值进行排序来完成。
    2. 您如何跟踪哪些链接到哪些内容?如果您所做的只是识别人脸,您可以创建一个Map&lt;HalfEdge, HalfEdge&gt;,将每个半边与其后的下一个半边相关联。如果您计划在未来保留这些链,您可能希望将每个 HalfEdge 都作为链接列表的一部分,该链接列表引用序列中的下一个半边。
    3. 如何从一对节点映射到在它们之间运行的半边?这可以通过从节点对到半边的Map 来完成。您还可以构造半边并让每个半边存储一个指向另一个方向的半边的指针。

    归因:我首先从this related question on the Computer Science Stack Exchange 学到了这个算法,它询问如何从线段集合中构建doubly-connected edge list (DECL)。我的贡献在于简化算法,只返回识别面部所需的链,并添加一些视觉效果以更好地激发概念。

    【讨论】:

      【解决方案3】:
      1. 对于每条边,获取边顶点嵌入中的坐标,并使用它们通过三角法计算边的角度。

        例如从(x1, y1)到(x2, y2)的角度>) 从正 x 轴逆时针方向测量由 Math.atan2(y2-y1,x2-x1) 给出。

      2. 对于每个顶点,通过按角度对边进行排序来创建循环边排序。这可以存储为数组,也可以使用循环列表数据结构。

      3. 选择一条边,沿着它到相邻的顶点,然后沿着下一个相邻的顺时针边,重复沿着边到下一个顶点,然后再到下一个顺时针边,直到回到起始边;那么你已经找到了图形的一个面。

      4. 重复第 3 步,以与上一个相反的方向选择未访问的边缘或已访问的边缘,然后以相同的顺时针方向跟随它以找到下一个面。重复此操作,直到所有边都被访问了两次(每个方向一次),然后你就找到了所有的面。

      在 Java 中,这将是:

      import java.awt.geom.Point2D;
      import java.awt.Polygon;
      import java.util.ArrayList;
      import java.util.Comparator;
      import java.util.stream.Collectors;
      import java.text.MessageFormat;
      
      public class GraphFaces
      {
        static class Vertex
        {
          final int index;
          final Point2D point;
          final ArrayList<Edge> outboundEdges = new ArrayList<>();
          
          
          public Vertex( final int index, final Point2D point )
          {
            this.index = index;
            this.point = point;
          }
          
          public void addEdge( final Edge edge )
          {
            this.outboundEdges.add( edge );
          }
          
          public void sortEdges()
          {
            this.outboundEdges.sort((e1,e2)->Double.compare(e1.angle,e2.angle));
            
            Edge prev = this.outboundEdges.get(this.outboundEdges.size() - 1);
            for ( final Edge edge: this.outboundEdges )
            {
              edge.setNextEdge( prev );
              prev = edge;
            }
          }
          
          @Override
          public String toString()
          {
            return Integer.toString(this.index);
            // return MessageFormat.format("({0},{1})",this.point.getX(),this.point.getY());
          }
        }
        
        static class Edge
        {
          final Vertex from;
          final Vertex to;
          final double angle;
          boolean visited = false;
          Edge next = null;
          Edge reverse = null;
          
          public Edge( final Vertex from, final Vertex to )
          {
            this.from = from;
            this.to = to;
            this.angle = Math.atan2(to.point.getY() - from.point.getY(), to.point.getX() - from.point.getX());
            from.addEdge( this );
          }
          
          public Vertex getFrom()
          {
            return this.from;
          }
      
          public Vertex getTo()
          {
            return this.to;
          }
      
          public void setNextEdge( final Edge edge )
          {
            this.next = edge;
          }
      
          public void setReverseEdge( final Edge edge )
          {
            this.reverse = edge;
          }
      
          @Override
          public String toString()
          {
            return MessageFormat.format("{0} -> {1}", this.from, this.to);
          }
        }
      
        public static void main(final String[] args)
        {
          final Vertex[] vertices = {
            new Vertex( 1, new Point2D.Double(-4,+4) ),
            new Vertex( 2, new Point2D.Double(-1,+5) ),
            new Vertex( 3, new Point2D.Double(+3,+4) ),
            new Vertex( 4, new Point2D.Double(+4,+1) ),
            new Vertex( 5, new Point2D.Double(+0,-2) ),
            new Vertex( 6, new Point2D.Double(-1,+3) ),
            new Vertex( 7, new Point2D.Double(+2,+2) )
          };
           
          final int[][] graph = {
            {1, 2}, {1, 6}, {1, 5}, {2, 6}, {2, 3}, {3, 7}, {7, 4}, {3, 4}, {5, 4}, {6, 5}
          };
          
          final Edge[] edges = new Edge[2 * graph.length];
      
          for ( int i = 0; i < graph.length; i++ )
          {
            final Vertex from = vertices[graph[i][0]-1];
            final Vertex to = vertices[graph[i][1]-1];
            edges[2*i] = new Edge( from, to );
            edges[2*i+1] = new Edge( to, from );
            
            edges[2*i].setReverseEdge(edges[2*i+1]);
            edges[2*i+1].setReverseEdge(edges[2*i]);
          }
          
          
          for ( final Vertex vertex: vertices )
          {
            vertex.sortEdges();
          }
          
          final ArrayList<ArrayList<Edge>> faces = new ArrayList<>();
          for ( final Edge edge: edges )
          {
            if ( edge.visited )
            {
              continue;
            }
            final ArrayList<Edge> face = new ArrayList<>();
            faces.add( face );
            Edge e = edge;
            do
            {
              face.add(e);
              e.visited = true;
              e = e.reverse.next;
            }
            while (e != edge);
            
            System.out.println( face.stream().map(Edge::getFrom).collect(Collectors.toList()) );
          }
        }
      }
      

      哪些输出:

      [1, 2, 3, 4, 5]
      [2, 1, 6]
      [6, 1, 5]
      [2, 6, 5, 4, 7, 3]
      [3, 7, 4]
      

      注意:这包括图形的外表面。

      或者,如果您想: 测试图形的平面性;生成(双连接)图的所有可能嵌入;并为这些嵌入中的一个(或多个)生成循环边排序,然后您可以使用博士论文Planarity Testing by Path Addition,该论文在附录中包含完整的 Java 源代码。

      【讨论】:

      • 这是我解决它的方法,你的解释帮助我解决了我遇到的角度排序问题。谢谢!
      猜你喜欢
      • 1970-01-01
      • 2012-10-06
      • 1970-01-01
      • 2012-08-16
      • 2019-07-11
      • 2016-10-19
      • 2015-09-10
      • 2016-05-22
      • 2017-05-18
      相关资源
      最近更新 更多