【问题标题】:How to convert regions in figure into graph (nodes and edges) structure如何将图中的区域转换为图形(节点和边)结构
【发布时间】:2016-06-02 05:20:21
【问题描述】:

我正在解决IOI 2007 - FLOOD 中提出的问题,SPOJ 上也有。

http://www.spoj.com/OI/problems/FLOOD/
http://www.iarcs.org.in/inoi/online-study-material/problems/flood.php

我想了很多解决办法。后来才知道——

Step 0: Converting a region into a node and 
Step 1: Connecting two nodes with edges only if corresponding regions share at least a segment. 
Step 2: And then running a BFS on this will simplify the problem.

我仍然无法考虑端到端解决方案,但后来我想让我们尝试第 0 步和第 1 步。 我也失败了。如果前面有图像,我可以很容易地看到这些区域,但不是以编程方式。

The input would be end co-ordinate points of segments.

我附上一张图片以供参考。

【问题讨论】:

    标签: graph graph-algorithm breadth-first-search


    【解决方案1】:

    当然有很多不同的方法可以解决这个问题。一种可能的方法是使用来自Computational Geometry 领域的技术。特别是以下两个概念很有帮助:

    • Doubly-connected edge list(DCEL):这种数据结构在处理平面图时常用。基本思想是扩展每条边以保存对边旁边区域(也称为“面”)的引用。

      实现这一点的一种方法是将每个无向边分成两条有向边,每个方向一条。然后可以扩展每个有向边以保持对其右侧区域的一个引用(从有向边的起始顶点的角度来看)。此外,扩展每条边以存储对反向边(有时称为“对等”或“双”边)的引用也很有帮助。

    • Sweep lining:这种算法在处理平面结构时被广泛使用。从概念上讲,它通过在平面上移动“扫描线”来工作,同时保持一些中间结果。每次扫描线击中平面上的对象时,都会根据该对象更新中间结果。当最终对象被扫描后,最终结果应该可以从中间结果中推导出来。

      在更实际的术语中,这通常是通过按坐标对输入点进行排序来实现的(即首先按 y 坐标,如果两个点具有相同的 y 坐标,则按它们的 x 坐标)。然后通过迭代排序的点来完成“扫描”。

    为了给大家更多的参考,我从Voronoi diagrams的构造中熟悉了这些技巧。特别是,如果你研究Fortune's algorithm,你会发现它与我在这里描述的方法有一些相似之处(即它也使用 DCEL 和扫描衬里)。另外,我喜欢阅读有关计算几何的 book,它更详细地描述了这些技术。

    我将在下面的部分中描述如何将这些技术应用于 FLOOD 问题。完整代码在答案末尾。

    数据结构

    为了避免处理指针,我将主要使用索引来表示对象之间的引用。我使用以下别名来明确给定索引是否应该是顶点、边或面的索引:

    using v_index_t = uint32_t;
    using e_index_t = uint32_t;
    using f_index_t = uint32_t;
    
    constexpr e_index_t NULL_EDGE = 0;
    constexpr f_index_t OUTER_PLANE = 0;
    

    每个顶点存储它的位置以及哪些边与顶点相关。 由于输入图的边是轴对齐的,因此每个顶点最多可以有四个边。出于这个原因,我对四个方向中的每一个方向都使用了一个具有四个元素的固定大小的数组。如果顶点在某个方向上没有边,则使用特殊索引NULL_EDGE

    struct Vertex {
      int x;
      int y;
    
      std::array<e_index_t, 4> incident;
    };
    

    按照 DCEL 方法存储边。每条边都是有向的,并持有对其右侧面的引用和对其对等边的引用:

    struct Edge {
      v_index_t a;
      v_index_t b;
    
      e_index_t peer;
      f_index_t faceRight;
    };
    

    每个面都包含包围该面的边列表:

    struct Face {
      std::vector<e_index_t> edges;
    };
    

    完整的图包含顶点、边和面的列表:

    struct Graph {
      std::vector<Vertex> vertices;
      std::vector<Edge> edges;
      std::vector<Face> faces;
    };
    

    算法

    构建输入图后,构建人脸的基本方法是:

    • 按坐标对顶点进行排序。

    • 初始化一个union-find data structure,它最初将每个顶点分配给它自己的分区。分区用于指示哪些顶点可以相互到达。

    • 遍历已排序的顶点。对于每个顶点,检查从该顶点开始的边的方向。对于每个向左或向下的边,检查当前顶点和该边的端点是否在同一个分区中(使用并集查找数据结构的查找操作)。

      • 如果是,边关闭一个内部循环(因为如果顶点可以相互到达,它们就在同一个分区中)。创建一个新面孔。从当前边开始,始终采用最右边的后继边,沿着包围面的边。对于每个访问的边,将向右面的 DCEL 参考设置为指向新创建的面。

      • 如果否,则在 union-find 数据结构中连接两个分区,以指示两个顶点可以相互到达(因此,每个已知可以从一个顶点到达的顶点也可以从另一个顶点到达) .

    假设您已经实现了一个用于对顶点进行排序的辅助函数 getSortedVertices,一个实现联合查找数据结构的辅助类 VertexPartition 和一个辅助函数 applyFaceRightwards,它始终沿包围人脸的边缘移动最右边的后继边缘,识别人脸的主要程序如下所示:

    void Graph::createFaceGraph() {
    
      // Sort vertices by coordinate
      auto indices{getSortedVertices()};
    
      // Initialize union-find data structure with one partition for each vertex.
      VertexPartition partition(vertices.size());
    
      // Sweep over the sorted vertices
      for (v_index_t current : indices) {
    
        // Check leftwards and downwards edges. The rightwards and upwards edges
        // are handled from the vertex at the other side.
        for (Direction direction : {Direction::Left, Direction::Down}) {
          e_index_t edgeIndex{vertices[current].incident[direction]};
    
          if (edgeIndex == NULL_EDGE) {
            continue;
          }
    
          Edge &edge{edges[edgeIndex]};
          v_index_t other = edge.other(current);
    
          // Use the union-find structure to join the partitions of the current
          // vertex and the vertex at the other side of the edge.
          bool success = partition.joinPartitions(other, current);
    
          // If the partitions were joined successfully, no action is needed.
          if (success) {
            continue;
          }
    
          // If the vertices were already in the same partition,
          // a loop has been found. Create a new face and travel rightwards
          // from the current edge to apply the face index to all edge
          // that are enclosing the face.
          faces.emplace_back();
          f_index_t faceIndex = faces.size() - 1;
          applyFaceRightwards(other, current, faceIndex);
        }
      }
    
      // Add all unprocessed edges to the outer plane.
      for (size_t i = 1; i < edges.size(); i++) {
        if (edges[i].faceRight == OUTER_PLANE) {
          faces[OUTER_PLANE].edges.push_back(i);
        }
      }
    }
    

    一旦您构建了面部图,您就可以在面部图上使用 BFS,如您在问题中所述。对于每个面,您将遍历包围该面的边缘。对于每条边,您将获得孪生边并将孪生边引用的面添加到 BFS 队列中。您将记录每张脸的 BFS 等级。为了找到在洪水期间可能倒塌的墙壁,您需要识别位于具有不同 BFS 等级的面之间的边。

    请注意,上面给出的算法有一个限制:它实际上并没有构建全脸图,因为它没有考虑循环嵌套。考虑以下示例:

    全脸图将有一个顶点用于外平面,然后一个顶点用于第一个和第二个正方形之间的区域,然后一个顶点用于内部正方形中的区域:

    但是,上面给出的算法不会检查内部矩形是否被外部矩形包围。相反,它将两个矩形都指定为被外平面包围:

    所以如果你真的想构建全脸图,这个结果是错误的。例如,您可以通过添加第二个扫描通道来跟踪面嵌套来解决此问题。但是,对于 FLOOD 问题,这不是必需的:对于最终结果,我们不关心具体的 BFS 级别,而只关心相邻面之间是否存在 BFS 级别差异。要获取此信息,内部矩形的面是否附加到外部矩形的面或外部平面都没有关系。更实际地说:如果一个结构完全封闭在另一个结构内,则内部结构的墙壁将按照与将结构移动到外平面时相同的顺序破裂。将结构移动到外平面时,其墙壁当然会更快地破裂,但它们破裂的顺序是相同的。因此最终结果不会改变。

    这使我们不必编写额外的代码来处理面嵌套,并使我们能够保持代码更紧凑。

    完整代码(由 SPOJ 100% 提供):

    #include <algorithm>
    #include <array>
    #include <cstdint>
    #include <iostream>
    #include <numeric>
    #include <queue>
    #include <vector>
    
    /**
     * Aliases to make it clear which object type is referenced by an index.
     */
    using v_index_t = uint32_t;
    using e_index_t = uint32_t;
    using f_index_t = uint32_t;
    
    /**
     * The edge index 0 is used to indicate "no edge".
     */
    constexpr e_index_t NULL_EDGE = 0;
    
    /**
     * The face index 0 is used to indicate the outer plane.
     */
    constexpr f_index_t OUTER_PLANE = 0;
    
    /**
     * The four direction used in the axis-aligned input graph.
     */
    enum Direction { Up, Right, Down, Left };
    
    /**
     * Data structure for a vertex.
     */
    struct Vertex {
    
      /**
       * Position of the vertex.
       */
      const int x;
      const int y;
    
      /**
       * The edges incident to the vertex in the four direction.
       * If the vertex does not have an edge for some direction, NULL_EDGE is used.
       */
      std::array<e_index_t, 4> incident;
    
      /**
       * Create a new vertex at the given position.
       */
      Vertex(int x, int y);
    
      /**
       * Get the direction that vertex "to" is, relative to this vertex.
       */
      Direction getDirection(const Vertex &to) const;
    };
    
    /**
     * Data structure for an edge, using the DCEL approach
     */
    struct Edge {
    
      /**
       * Starting vertex of the edge.
       */
      v_index_t a;
    
      /**
       * End vertex of the edge.
       */
      v_index_t b;
    
      /**
       * The peer edge, meaning the directed edge b->a.
       */
      e_index_t peer;
    
      /**
       * The face to the right of the edge
       * (from the perspective of the starting vertex).
       */
      f_index_t faceRight{OUTER_PLANE};
    
      /**
       * Default-constructed, needed for the NULL_EDGE.
       */
      Edge() = default;
    
      /**
       * Create a new edge a->b.
       */
      Edge(v_index_t a, v_index_t b);
    
      /**
       * Get the other vertex of the edge. If "from" is "a" then "b" is returned,
       * if "from" is "b" then "a" is returned.
       */
      v_index_t other(v_index_t from) const;
    };
    
    /**
     * Data structure for a face (meaning a region in the plane).
     */
    struct Face {
      /**
       * List of edges that enclose this face.
       */
      std::vector<e_index_t> edges;
    };
    
    /**
     * Data structure for a graph.
     */
    struct Graph {
    
      /**
       * List of vertices in the graph.
       */
      std::vector<Vertex> vertices;
    
      /**
       * List of edges in the graph.
       */
      std::vector<Edge> edges;
    
      /**
       * List of faces in the graph.
       */
      std::vector<Face> faces;
    
      /**
       * Create the empty graph.
       */
      Graph();
    
      /**
       * Add an undirected edge a<->b.
       */
      void add_edge(v_index_t a, v_index_t b);
    
      /**
       * Get the right-most edge that follows the edge parent->current.
       */
      e_index_t getRightMostEdge(v_index_t current, v_index_t parent);
    
      /**
       * Sort the vertices by their coordinate.
       */
      std::vector<v_index_t> getSortedVertices();
    
      /**
       * Travel right-wards along the successor edges of from->to.
       * Apply the faceIndex to each visited edge.
       */
      void applyFaceRightwards(v_index_t to, v_index_t from, f_index_t faceIndex);
    
      /**
       * Use sweep-lining to construct the face graph.
       */
      void createFaceGraph();
    
      /**
       * Use BFS on the face graph to identify which collapsing walls.
       */
      void findCollapsedWalls();
    };
    
    /**
     * Union-find data structure.
     */
    struct VertexPartition {
    
      /**
       * Maps each vertex to the representive of its partition.
       */
      std::vector<v_index_t> parent;
    
      /**
       * Tracks the rank of each vertex in the union-find tree structure.
       */
      std::vector<int> rank;
    
      /**
       * Initialize the union-find structure such that each vertex
       * is in its own partition.
       */
      VertexPartition(size_t size);
    
      /**
       * Find the representive of the partition in which the vertex is contained.
       */
      v_index_t findPartition(v_index_t vertex);
    
      /**
       * Join the partitions containing the vertices a and b.
       */
      bool joinPartitions(v_index_t a, v_index_t b);
    };
    
    Vertex::Vertex(int x, int y) : x{x}, y{y}, incident{} {}
    
    Direction Vertex::getDirection(const Vertex &to) const {
      bool left = (x > to.x);
      bool right = (x < to.x);
      bool up = (y < to.y);
      bool down = (y > to.y);
    
      if (!up && !down) {
        if (right) {
          return Direction::Right;
        } else if (left) {
          return Direction::Left;
        }
      }
    
      if (!left && !right) {
        if (up) {
          return Direction::Up;
        } else if (down) {
          return Direction::Down;
        }
      }
    
      throw std::runtime_error{"Invalid alignment!"};
    }
    
    Edge::Edge(v_index_t a, v_index_t b) : a{a}, b{b} {}
    
    v_index_t Edge::other(v_index_t from) const {
      if (from == a) {
        return b;
      }
    
      if (from == b) {
        return a;
      }
    
      throw std::runtime_error{"Vertex not incident to edge!"};
    }
    
    VertexPartition::VertexPartition(size_t size) : parent(size), rank(size) {
      std::iota(parent.begin(), parent.end(), 0);
    }
    
    v_index_t VertexPartition::findPartition(v_index_t vertex) {
      // Pass 1: Find the representive of the partition containing vertex
      v_index_t top = vertex;
      while (parent[top] != top) {
        top = parent[top];
      }
    
      // Pass 2: Compress path to speed up future queries.
      while (parent[vertex] != vertex) {
        v_index_t next = parent[vertex];
        parent[vertex] = top;
        vertex = next;
      }
    
      return top;
    }
    
    bool VertexPartition::joinPartitions(v_index_t a, v_index_t b) {
      v_index_t aParent = findPartition(a);
      v_index_t bParent = findPartition(b);
    
      if (aParent == bParent) {
        return false;
      }
    
      // Make the representive with maximum rank the representive of the joined
      // partition.
      int aParentRank = rank[aParent];
      int bParentRank = rank[bParent];
    
      if (aParentRank < bParentRank) {
        v_index_t tmp = aParentRank;
        aParentRank = bParentRank;
        bParentRank = tmp;
      }
    
      parent[bParent] = aParent;
      if (aParentRank == bParentRank) {
        rank[aParent] += 1;
      }
    
      return true;
    }
    
    Graph::Graph() {
      faces.emplace_back(); // Create default face, representing the outer plane.
      edges.emplace_back(); // Create null edge, so that index 0 means "no edge".
    }
    
    void Graph::add_edge(v_index_t a, v_index_t b) {
      edges.emplace_back(a, b);
      edges.emplace_back(b, a);
    
      e_index_t indexAtoB = edges.size() - 2;
      e_index_t indexBtoA = edges.size() - 1;
    
      edges[indexAtoB].peer = indexBtoA;
      edges[indexBtoA].peer = indexAtoB;
    
      Direction directionAtoB{vertices[a].getDirection(vertices[b])};
      Direction directionBtoA{vertices[b].getDirection(vertices[a])};
    
      vertices[a].incident[directionAtoB] = indexAtoB;
      vertices[b].incident[directionBtoA] = indexBtoA;
    }
    
    e_index_t Graph::getRightMostEdge(v_index_t parent, v_index_t current) {
      Direction direction{vertices[current].getDirection(vertices[parent])};
    
      // Check the directions following the current direction in counter-clockwise
      // order. Note that in the last iteration of the loop, the direction
      // current->parent is checked. 
      for (int i = 1; i <= 4; i++) {
        int directionAsIntShifted{direction - i};
        if (directionAsIntShifted < 0) {
          directionAsIntShifted += 4;
        }
        Direction directionShifted{static_cast<Direction>(directionAsIntShifted)};
        e_index_t edge{vertices[current].incident[directionShifted]};
    
        if (edge != NULL_EDGE) {
          return edge;
        }
      }
    
      return NULL_EDGE;
    }
    
    std::vector<v_index_t> Graph::getSortedVertices() {
      std::vector<v_index_t> indices(vertices.size());
      std::iota(indices.begin(), indices.end(), 0);
    
      auto key = [&](v_index_t i, v_index_t j) -> bool {
        return (vertices[i].y < vertices[j].y) ||
               (vertices[i].y == vertices[j].y && vertices[i].x < vertices[j].x);
      };
    
      std::sort(indices.begin(), indices.end(), key);
    
      return indices;
    }
    
    void Graph::applyFaceRightwards(v_index_t current, v_index_t parent,
                                    f_index_t faceIndex) {
      for (;;) {
        e_index_t edgeIndex{getRightMostEdge(parent, current)};
    
        if (!edgeIndex) {
          break;
        }
    
        Edge &edge{edges[edgeIndex]};
        v_index_t next{edge.other(current)};
    
        if (edge.faceRight != OUTER_PLANE) {
          break;
        }
    
        edge.faceRight = faceIndex;
        faces[faceIndex].edges.push_back(edgeIndex);
    
        parent = current;
        current = next;
      }
    }
    
    void Graph::createFaceGraph() {
    
      // Sort vertices by coordinate
      auto indices{getSortedVertices()};
    
      // Initialize union-find data structure with one partition for each vertex.
      VertexPartition partition(vertices.size());
    
      // Sweep over the sorted vertices
      for (v_index_t current : indices) {
    
        // Check leftwards and downwards edges. The rightwards and upwards edges
        // are handled from the vertex at the other side.
        for (Direction direction : {Direction::Left, Direction::Down}) {
          e_index_t edgeIndex{vertices[current].incident[direction]};
    
          if (edgeIndex == NULL_EDGE) {
            continue;
          }
    
          Edge &edge{edges[edgeIndex]};
          v_index_t other = edge.other(current);
    
          // Use the union-find structure to join the partitions of the current
          // vertex and the vertex at the other side of the edge.
          bool success = partition.joinPartitions(other, current);
    
          // If the partitions were joined successfully, no action is needed.
          if (success) {
            continue;
          }
    
          // If the vertices were already in the same partition,
          // a loop has been found. Create a new face and travel rightwards
          // from the current edge to apply the face index to all edge
          // that are enclosing the face.
          faces.emplace_back();
          f_index_t faceIndex = faces.size() - 1;
          applyFaceRightwards(other, current, faceIndex);
        }
      }
    
      // Add all unprocessed edges to the outer plane.
      for (size_t i = 1; i < edges.size(); i++) {
        if (edges[i].faceRight == OUTER_PLANE) {
          faces[OUTER_PLANE].edges.push_back(i);
        }
      }
    }
    
    void Graph::findCollapsedWalls() {
    
      // Perform BFS on face graph
      std::queue<f_index_t> queue{};
      std::vector<bool> visited(faces.size());
      std::vector<int> rank(faces.size());
    
      auto processFace = [&](f_index_t next, int nextRank) {
        if (visited[next]) {
          return;
        }
    
        queue.push(next);
        visited[next] = true;
        rank[next] = nextRank;
      };
    
      processFace(OUTER_PLANE, 0);
    
      while (!queue.empty()) {
        f_index_t current{queue.front()};
        queue.pop();
    
        int nextRank = rank[current] + 1;
    
        for (e_index_t edge : faces[current].edges) {
          e_index_t peer = edges[edge].peer;
          processFace(edges[peer].faceRight, nextRank);
        }
      }
    
      // Check which edges are next to faces with equal BFS rank
      auto isWallStanding = [&](e_index_t i) {
        e_index_t j = edges[i].peer;
    
        return rank[edges[i].faceRight] == rank[edges[j].faceRight];
      };
    
      // Print the final result. Since the edge list contains each edge
      // twice, once for each direction, we skip over every second edge.
      // We also skip the NULL_EDGE.
      size_t wallCount = 0;
      for (size_t i = 2; i < edges.size(); i += 2) {
        if (isWallStanding(i)) {
          wallCount++;
        }
      }
    
      std::cout << wallCount << std::endl;
      for (size_t i = 2; i < edges.size(); i += 2) {
        if (isWallStanding(i)) {
          std::cout << i / 2 << std::endl;
        }
      }
    }
    
    int main() {
    
      Graph graph{};
    
      // Read input graph from stdin
      size_t corners{};
      std::cin >> corners;
      for (size_t i = 0; i < corners; i++) {
        int x;
        int y;
        std::cin >> x >> y;
        graph.vertices.emplace_back(x, y);
      }
    
      size_t walls{};
      std::cin >> walls;
      for (size_t i = 0; i < walls; i++) {
        size_t indexA;
        size_t indexB;
        std::cin >> indexA >> indexB;
        graph.add_edge(indexA - 1, indexB - 1);
      }
    
      graph.createFaceGraph();
      graph.findCollapsedWalls();
    
      return 0;
    }
    

    【讨论】:

      猜你喜欢
      • 2020-07-17
      • 2012-03-22
      • 2019-12-23
      • 2011-10-18
      • 2020-11-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-21
      • 1970-01-01
      相关资源
      最近更新 更多