4

How to go from an edge list or adjacency list representation of a planar graph to a face list?

For example, with this graph (which is not 0-indexed, oddly enough):

A planar graph.

I'd want a list that looks something like this:

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

It wouldn't have to be in that format or order, but it should be some kind of list (or set) of all faces. The outer face is included.

For a graph with V vertices (E=O(V) because planar), the algorithm should generate this list in O(V).

3 Answers3

2

You need to generate a planar embedding of the graph. One issue is that often a biconnected graph can generate multiple planar embeddings.

One planarity testing and embedding algorithm is given in the "Planarity Testing by Path Addition" PhD thesis and this solves the problem of generating all possible planar embeddings of a graph (in O(V+E) time and memory for a single embedding for a biconnected graph with V vertices and E edges, and in O(P(V+E)) time and O(V+E) memory to generate all possible unique planar embeddings, where P is the number of permutations of embeddings).

Chapter 5 details the algorithm needed to test a graph for planarity and then to generate a cyclic edge order for each vertex (and also how to iterate to the next permutation of the embedding).

Given a cyclic edge order, you can generate the faces of the graph by taking each edge and following the next clockwise (or anti-clockwise) edge in the cyclic edge ordering as you come to each successive vertex.

MT0
  • 143,790
  • 11
  • 59
  • 117
  • I think the op specifies that the input graph is guaranteed to be planar, so we don't have to test it. – Yadli May 11 '17 at 14:46
  • 1
    @Yadli Almost all planarity tests tend to test for planarity by constructing a planar embedding of the graph - it is much more efficient than looking for sub-graphs isomorphic to K5 or K3,3. So if you want to get a planar embedding of the graph you run it through a planarity testing algorithm - you can ignore the test part as you just want the embedding. – MT0 May 11 '17 at 17:22
  • Thanks for the clarification. I understand that embedding algorithms have better ways to test planarity than looking for forbidden patterns, but my point is that, I quickly skimmed at the thesis you referenced, and it shall enumerate all the embeddings. We can easily construct planar graphs with exponentially many embeddings (the O(P(V+E)) part in your answer, I don't think that is polynomial). Can we break at the moment when the first embedding is found, or the embeddings are constructed in parallel and we can only obtain the result later than the none-polynomial part? – Yadli May 12 '17 at 01:12
  • 1
    @Yadli The mechanism to generate all the embeddings is created with the first embedding in O(V+E) time and memory. To then iterate to each successive permutation of embedding takes O(KV+KE) time (and no additional memory) where K averages to a constant when considered over the total number of permutations (see page 94 for a detailed breakdown of the time complexity of the Steinhaus-Johnson-Trotter permuation algorithm used to efficiently iterate over all the permuations). So for P permutations the algorithm would take O(PV+PE) time. – MT0 May 12 '17 at 01:38
1

The short answer is : you have to actually layout the graph! More exactly, you have to find an embedding of the graph in the plane - assuming that there is one without edge crossings.

So, your embedding above is :

1: [2, 7, 3]
2: [1, 4, 8]
3: [1, 5, 6, 4]
...

which is each vertex with an ordering on its neighbour set. You have to specify if that ordering is clockwise or anti-clockwise, but otherwise that should be all.

Once you have the embedding, it is possible to recover the faces using a combinatorial map. This looks trickier than it really is, although it does involve darts (or flags).

First, break each edge into flags (vertex + half-edge) and make a permutation (sigma in the wiki description) that stores the map. For example, we could label the flags in the same order as the map - then 1: [2, 7, 3] becomes {1->2 : 1, 1->7 : 2, 1->3 : 3} and so on.

For example, a cube (note : removed the middle edge!):

enter image description here

Then calculate alpha (the involution permutation) which just maps flags to the other flag across the edge. Finally, phi is the product of these two permutations, and the cycles of phi gives you the faces.

So, from the phi in the image, we have (1, 6, 24, 19) which is the outer face (note that these are darts, so we consider the vertex it starts from).

gilleain
  • 621
  • 3
  • 11
  • 24
0

As @gilleain mentioned in his answer, you have to actually layout the graph, as there could be multiple layouts if you just give the topology. Here I'll give some algorithmic analysis and then a straightforward solution.

Lemma 1: An edge is involved in at least one face, and at most two faces.

Proof: We're drawing in 2D space so a line splits a plane into two halves.

This means that we can attach a "capacity" to each of the edges, initialized to 2. When we find an face involving an edge, we subtract 1 from it.

As a face is a loop in the planar graph, the problem turns to find loops given the aforementioned constraint.

Lemma 2: If we examine the difference between two valid solutions, they differ in edges that have complementary capacities (1 vs. 2 and 2 vs. 1).

Proof: See the figure. enter image description here

As a consequence, when you drain the capacity for an edge when finding a solution, you automatically exclude the ambiguity that leads to another solution.

So the straightforward solution is to conduct DFS in the graph to find loops. When you find one, subtract the capacities of the involved edges. When the capacity reaches 0, consider the edge removed when DFS further. Note, when a loop is found, we must check if all the edges within have capacity 1. If so, then this loop must be skipped, as including this loop into our result leads to repeated counting.

def DFS-Tarjan(v, capacities, stack)
    for e in N(v):
        if capacity[e] != 0:
            if stack.contains[e.target] AND NOT all of them have capacity 1:
                output-loop(stack, e.target)
                for e2 in stack:
                    capacity[e2] -= 1
            else:
                stack.push(e.target)
                DFS-Tarjan(v, capacities, stack)
                stack.pop(e.target)
        else:
            pass # capacity drained

def find-faces(g):
    initialize capacities of g.E to [2...2]
    for v in unvisited(g.V):
        DFS-Tarjan(v, capacities, [])

Multiple solutions can be found if you alter the order for DFS. For a single solution, the algorithm is O(V) as each edge is consumed no more than twice.

Yadli
  • 381
  • 2
  • 12
  • Interesting answer. I'm not sure about the diagram - what do the colours mean? Also - maybe you know this, but - putting "@" before a username doesn't do anything when in the body of an answer :) – gilleain May 11 '17 at 09:24
  • @gilleain yeah I just hoped that one day they'll add "@" feature to the answer text. In the figure, green=edges for face 1, red=edges for face 2. For the graph in the middle, the two faces seem overlapped, but by adjusting the layout (move the bottom vertex to the top) we can see this is still a valid solution -- I think this is part of why this problem is interesting :). – Yadli May 11 '17 at 10:27
  • Finding cycles via a DFS does not equate to finding faces. You can find a cycle in a DFS and then have the region defined to be inside this cycle be bisected by other paths within the DFS such that the the faces are actually the two sub-regions defined by the original cycle and the bisection. This can be repeated with successive sub-regions. – MT0 May 11 '17 at 11:43
  • @MT0 Please see lemma 2. Although it is possible that we count the bigger, outer cycle, the algorithm makes sure that it does not further count _ALL_ the cycles inside the bigger cycle. Thus by adjusting the layout, we make the bigger cycle smaller, and the smaller one bigger, so that it looks like two disjoint faces. – Yadli May 11 '17 at 13:24
  • 1
    Please go into a **lot** more detail in your explanation as it currently does not appear to make sense and it is very unclear what is being proved in your "lemma 2". You appear to be heavily relying on the order in which the DFS processes the edges - and if this is non-deterministic then your algorithm cannot work in the general case without a lot of preparatory work re-ordering the priority of the edges. – MT0 May 11 '17 at 13:33
  • Lemma 1 is just a statement of the Jordan Curve Theorem. However, an edge is always adjacent to exactly two faces - you just appear to be ignoring the exterior face of the embedding. – MT0 May 11 '17 at 13:35
  • Also, your algorithm is incomplete - how is `e.target` defined? When you check that not all the members of the stack have capacity 1 do you mean all or do you just mean to backtrack down the trunk of the DFS tree until the lowest element of the cycle is found? Same for decrementing the capacity? And `output-loop()` is not defined. – MT0 May 11 '17 at 14:32
  • `e.target` is pseudo-code like `AND NOT all of them have capacity 1:`, I think it's better to keep just the core logic and strip the details. – Yadli May 11 '17 at 14:36
  • @MT0 the order of edges during DFS is not important. Different ordering simply leads to different solutions, not errors. Or, could you please raise an example of how the algorithm would fail given a particular visiting order? For the stack capacity check issue, I've got to improve that. What I actually mean is to check those in the loop (as done in standard `Tarjan` algorithm). – Yadli May 11 '17 at 14:41
  • As for lemma 1: yes I am ignoring the exterritory, as we can add it back anytime we want (or not). – Yadli May 11 '17 at 14:49
  • `e.target` is pseudo-code... okay but what does it represent? Is it an adjacent vertex, is it an ancestor in the DFS tree, is it something else? At the moment I am being defeated by the lack of clarity in your pseudo-code and cannot begin to implement your algorithm to properly verify it. – MT0 May 11 '17 at 17:31
  • ah ok this is undirected graph so `e.target` does not make sense. I mean the other vertex incident to it. I shall revise that part a bit. For the `Tarjan` part I mean the topological sorting algorithm, got to also update this I guess. :) – Yadli May 12 '17 at 01:46