如何将图中的区域转换为图形(节点和边)结构

时间:2016-06-02 05:20:21

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

我正在解决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.

我附上图片供参考。 enter image description here

1 个答案:

答案 0 :(得分:0)

当然有很多不同的方法可以解决这个问题。一种可能的方法是使用 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 等级的面之间的边。

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

enter image description here

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

enter image description here

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

enter image description here

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

这使我们不必编写额外的代码来处理人脸嵌套,并使我们的代码更加紧凑。

完整代码(SPOJ 100%'d):

#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;
}