Determine if a Graph is Toroidal

22

0

A simple graph is toroidal if it can be drawn on the surface of a torus without any edges intersecting. Your task is to take a simple undirected graph via any reasonable method (adjacency matrix, edge vertex sets, etc.) and decide whether or not it is a toroidal graph. You should output one of two distinct values for each of the two decisions. You may choose what these values are.

This is so answers will be scored in bytes with less bytes being better.

Test Cases

Here Kn is the complete graph with n vertices and Kn,m is the complete bipartite graph.

Toroidal

Not Toroidal

  • K8

Post Rock Garf Hunter

Posted 2017-08-25T18:36:42.490

Reputation: 55 382

12Actual test cases would be helpful, for instance a few adjacency matrices. People are probably able to convert it to another appropriate format if they have to. – Stewie Griffin – 2017-08-25T18:44:12.910

1@StewieGriffin I'm not going to crowd the question with hundreds of lines of test-cases that are going to have to be converted by every user that wants to use them. – Post Rock Garf Hunter – 2017-08-25T18:46:20.497

2http://web.math.ucsb.edu/~padraic/ucsb_2013_14/math137b_s2014/math137b_s2014_lecture4.pdf the theorem at the end states: "If G is toroidal, then the Euler characteristic of G is 0" – Giuseppe – 2017-08-25T19:03:00.987

6What are K₃, K₇, K₃,₃ and K₈? – Erik the Outgolfer – 2017-08-25T19:16:06.430

@EriktheOutgolfer K_n is the complete graph with n nodes. K_n,m is the complete bipartite graph with partitions n and m. – Post Rock Garf Hunter – 2017-08-25T19:18:42.913

1

Wikipedia links: complete graph, complete bipartite graph.

– Jonathan Allan – 2017-08-25T19:20:21.810

1@Giuseppe, but can we go the other way? What is a face of a graph with no embedding yet defined? – Jonathan Allan – 2017-08-25T19:27:22.780

3

Looks like the general purpose algorithm for this is quite extensive and probably beyond the scope of PPCG. See the Yu (2011) paper mentioned at: https://mathoverflow.net/questions/119493/toroidality-testing. If "maybe" is a suitable answer I've got a short 1-liner :)

– Kelly Lowder – 2017-08-25T19:48:07.203

8@KellyLowder This is not meant to be an easy question. I don't really think there is a scope of difficulty on PPCG, after all we have the implement tetris in GoL question. – Post Rock Garf Hunter – 2017-08-25T19:50:26.293

@Wheat Wizard. Fair enough. Kudos to anyone that comes up with a good solution. – Kelly Lowder – 2017-08-25T20:53:05.057

This may work (just try all possible adjacency matrix and check how many faces it have, if the genus is 0 or 1 then done -- there must be no vertices without any edges attached) – user202729 – 2018-03-10T06:14:49.123

So why can't we just calculate the Euler characteristic? I don't see why that is not a clear test of toroidality. This challenge already does that.

– dylnan – 2018-04-13T23:43:12.960

@dylnan I don't know that Euler characteristic can be used as a test of toroidality. I do know that every toroidal graph must have Euler characteristic zero, but I don't know if the converse is true. If you can prove this is the case or find a source proving it you are welcome to use that algorithm for an answer. – Post Rock Garf Hunter – 2018-04-13T23:53:39.877

I realized I was confused before you responded so I deleted the comment. Is a graph embedded on a klein bottle considered a torus when embedded in 3 space? – dylnan – 2018-04-14T00:14:01.440

@dylnan I don't believe so. – Post Rock Garf Hunter – 2018-04-14T00:17:19.307

You say that edges musnt intersect? but then they appear to for some test case examples, unless the 2d they do but don't on Taurus? – marshal craft – 2018-10-22T11:56:39.090

Cause I'd like to make sure I understand toroidal correctly, cause $k_7$ is toroidal while $k_8$ isn't and it's not easy to picture either on the torus? So my question is, $k_2$ on torus would have 3 edges instead of just 1? – marshal craft – 2018-10-23T19:05:02.053

@marshalcraft $K_7$ is toroidal and $K_8$ is not. $K_2$ has the same number of edges no matter on what surface you write it. An easier way to picture it: "A graph is toroidal if you can draw it on a piece of paper without crossings allowing you to jump from one side to the same place on the other side." – Post Rock Garf Hunter – 2018-10-23T22:52:56.843

Yeah that clears up nothing for me, sorry. I can read clearly that from the question about 7 or 8 so no new information there. My point are the graphs have like how many edges? and so like i said, it not most feasible thing to think about as an example. Again as I said it would seem to me as though some of the toroidal examples, certainly must have interescting edges on the 2 dimensional flat plane. So I assume that the torus some how this isn't necessary, jsut as it seems to me a simple undirected graph can have up to 4 non trivial edges for 2 nodes, different then in R^2. – marshal craft – 2018-10-24T10:07:29.727

Also I look at wikipedia toroidal graph page and it seems that indeed, 2 nodes with 4 edges is a simple graph on a torus. Wish I could say your comments were useful @Post Left Ghost Hunter but they weren't. No wonder there isn't an answer after a year. None of you even know what your talking about. – marshal craft – 2018-10-24T10:10:24.393

So basically valid edges are gonna be geodesics. which i would think if the torus was parameterized by angle about major/minor axis (I'm not a torus expert so forgive me if my terminology is wrong), then you have lines similar to 2d, like y=mx+b but where y and x are the angles. – marshal craft – 2018-10-24T10:14:47.827

So you have in general, all the combination of nodes, the equations of lines connecting them, there are 4 each. So 4*number of combinations if there exists coeficients such that there are no simultaneous solutions to the large list of equations they do not intersect and thus are toroidal. – marshal craft – 2018-10-24T10:16:51.117

Also I read from most searches on google, the literature on torus is concerned with embedding's in eucludian space which is really more related to differential geometry. For purposes here, which so far seem completely independent of diff. geometry what is concerened is intrinsic to the genus 1 (the torus). Thus it's a matter of system of equations not having solutions and cases for all the combinations, thus not having intersections. Seems an application for this is say some kind of network of nodes needing to be connected but constraint that connections can't take up the same space. – marshal craft – 2018-10-24T10:32:59.403

Some how using genus 1 and higher is a method of solving some kind of problem. – marshal craft – 2018-10-24T10:33:42.697

Mathematically the most general form of this question would be given a set of nodes and minimum edges, what is the minimum genus so that they do not interesect. – marshal craft – 2018-10-24T10:35:24.797

@marshalcraft Amusingly, that's the problem I solved in my answer. – isaacg – 2019-11-21T00:14:35.150

Answers

10

Rust, 1210 1200 bytes

use std::collections::HashMap as H;type I=i64;type E=Vec<(I,I)>;fn d(g:&E)->bool{let mut s:Vec<(E,Vec<I>)>=vec![];for e in g{let(v,w)=e;let f=(*v,*w);let z=|x|s.iter().position(|p|p.1.contains(x));match(z(v),z(w)){(Some(i),Some(j))=>{if i!=j{let mut p=s.remove(i);let q=s.remove(j-(i<j)as usize);p.0.extend(q.0);p.1.extend(q.1);s.push(p)}else{s[i].0.push(f)}}(Some(i),_)=>{s[i].0.push(f);s[i].1.push(*w)}(_,Some(j))=>{s[j].0.push(f);s[j].1.push(*v)}_=>{s.push((vec![f], vec![*v, *w]))}}}s.iter().map(|h|{let mut p=H::new();let mut r=H::new();let mut i=0;for e in&h.0{let(v,w)=e;i+=2;p.insert(i-1,i);p.insert(i,i-1);r.entry(v).or_insert(vec![]).push(i-1);r.entry(w).or_insert(vec![]).push(i)}let mut r:Vec<Vec<I>>=r.values().cloned().collect();r.sort();let mut x=0;let m=r.iter().flat_map(|v|1..v.len()).fold(1,|p,n|p*n);for mut w in 0..m{let mut t=H::new();for u in&r{let mut v=u.clone();let s=v.pop().unwrap();let mut f=s;while v.len()>0{let o=v.remove(w%v.len());w/=v.len()+1;t.insert(f,o);f=o}t.insert(f,s);}let mut f=vec![];let mut n=0;for s in p.keys(){if!f.contains(s){n+=1;let mut c=s;loop{f.push(*c);c=&t[&p[c]];if c==s{break}}}}x=x.max(n)}1-(r.len()as I-g.len()as I+x as I)/2}).sum::<I>()<2}

A toroidal example: Try it online!

That's this toroidal graph: enter image description here

A non-toroidal example: Try it online!

That's this non-toroidal graph: enter image description here

The original code I wrote, before golfing, was the following. It has printouts so you can see what's happening:

use std::collections::HashMap;

#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)]
struct Vertex(u64);
type Edge = (Vertex, Vertex);
type Graph = Vec<Edge>;

fn full_genus(graph: &Graph) -> usize {
    componets(graph).iter().map(|g| genus(g)).sum()
}
fn genus(graph: &Graph) -> usize {
    #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, PartialOrd, Ord)]
    struct HalfEdge(usize);
    let mut edge_pairing: HashMap<HalfEdge, HalfEdge> = HashMap::new();
    let mut vertex_groups: HashMap<&Vertex, Vec<HalfEdge>> = HashMap::new();
    let mut i = 0;
    for edge in graph {
        let (vert1, vert2) = edge;
        let half1 = HalfEdge(i);
        i += 1;
        let half2 = HalfEdge(i);
        i += 1;
        edge_pairing.insert(half1, half2);
        edge_pairing.insert(half2, half1);
        vertex_groups.entry(vert1).or_insert(vec![]).push(half1);
        vertex_groups.entry(vert2).or_insert(vec![]).push(half2);
    }
    let mut vertex_groups: Vec<Vec<HalfEdge>> = vertex_groups.values().cloned().collect();
    vertex_groups.sort();
    println!("{:?}", edge_pairing);
    println!("{:?}", vertex_groups);

    let mut max_faces = 0;
    let num_rotations = vertex_groups
        .iter()
        .map(|v| v.len() - 1)
        .map(|n| {
            let mut prod = 1;
            for i in 0..n {
                prod *= i + 1;
            }
            prod
        })
        .fold(1, |p, n| p * n);
    println!("\nNum rotations: {}\n", num_rotations);
    for rotation_index in 0..num_rotations {
        let mut working_index = rotation_index;
        let mut rotation: HashMap<HalfEdge, HalfEdge> = HashMap::new();
        let mut pretty_rotation: Vec<Vec<HalfEdge>> = vec![];
        for group in &vertex_groups {
            let mut removal_group = group.clone();
            let start = removal_group.pop().unwrap();
            let mut from = start;
            let mut pretty_group = vec![from];
            while !removal_group.is_empty() {
                let index = working_index % removal_group.len();
                working_index /= removal_group.len();
                let to = removal_group.swap_remove(index);
                rotation.insert(from, to);
                pretty_group.push(to);
                from = to;
            }
            rotation.insert(from, start);
            pretty_rotation.push(pretty_group);
        }
        let mut seen_on_face: Vec<HalfEdge> = Vec::new();
        let mut num_faces = 0;
        for start_halfedge in edge_pairing.keys() {
            if !seen_on_face.contains(start_halfedge) {
                num_faces += 1;
                let mut current_halfedge = start_halfedge;
                loop {
                    seen_on_face.push(*current_halfedge);
                    let pair_halfedge = &edge_pairing[current_halfedge];
                    current_halfedge = &rotation[pair_halfedge];
                    if current_halfedge == start_halfedge {
                        break;
                    }
                }
            }
        }
        if num_faces > max_faces {
            max_faces = num_faces;
            let euler_characteristic: isize =
                vertex_groups.len() as isize - graph.len() as isize + max_faces as isize;
            let genus_num: isize = 1 - euler_characteristic / 2;
            println!(
                "Faces: {}, Genus <= {} on rotation {}",
                max_faces, genus_num, rotation_index
            );
            println!("{:?}\n", pretty_rotation);
        }
        if rotation_index % 1e7 as usize == 0 {
            println!("Faces: {} <= {} on rotation {}", num_faces, max_faces, rotation_index);
        }
    }
    let euler_characteristic: isize =
        vertex_groups.len() as isize - graph.len() as isize + max_faces as isize;
    let genus_num: isize = 1 - euler_characteristic / 2;
    assert!(genus_num >= 0);
    genus_num as usize
}

fn componets(graph: &Graph) -> Vec<Graph> {
    let mut graphs: Vec<(Graph, Vec<Vertex>)> = vec![];
    for edge in graph {
        let (vert1, vert2) = edge;
        let g_index1 = graphs.iter().position(|(_g, h)| h.contains(&vert1));
        let g_index2 = graphs.iter().position(|(_g, h)| h.contains(&vert2));
        match (g_index1, g_index2) {
            (Some(i1), Some(i2)) => {
                if i1 != i2 {
                    let (mut graph1, mut vs1) = graphs.remove(i1);
                    let new_i2 = if i1 < i2 { i2 - 1 } else { i2 };
                    let (graph2, vs2) = graphs.remove(new_i2);
                    graph1.extend(graph2);
                    vs1.extend(vs2);
                    graphs.push((graph1, vs1));
                } else {
                    graphs[i1].0.push(edge.clone());
                }
            }
            (Some(i1), None) => {
                graphs[i1].0.push(edge.clone());
                graphs[i1].1.push(vert2.clone());
            }
            (None, Some(i2)) => {
                graphs[i2].0.push(edge.clone());
                graphs[i2].1.push(vert1.clone());
            }
            (None, None) => {
                let edges = vec![edge.clone()];
                let vs = vec!(vert1.clone(), vert2.clone());
                graphs.push((edges, vs));
            }
        }
    }
    graphs.into_iter().map(|(g, _h)| g).collect()
}

fn main() {
    let graph = vec![
        (Vertex(0), Vertex(1)),
        (Vertex(0), Vertex(2)),
        (Vertex(0), Vertex(3)),
        (Vertex(0), Vertex(4)),
        (Vertex(0), Vertex(5)),
        (Vertex(1), Vertex(2)),
        (Vertex(1), Vertex(3)),
        (Vertex(1), Vertex(4)),
        (Vertex(1), Vertex(5)),
        (Vertex(2), Vertex(3)),
        (Vertex(2), Vertex(4)),
        (Vertex(2), Vertex(5)),
        (Vertex(3), Vertex(4)), 
        (Vertex(3), Vertex(5)),
    ];  
    let result = full_genus(&graph);
    println!("Result: {}", result);
}

This program finds the minimum genus surface that the given graph can be embedded in. A graph is toroidal if that genus is at most 1, the genus of the torus.

The code works by separating the graph into its connected components, finding their genii separately, and adding up the results.

To find the genus of a connected graph, I brute force search over all possible rotation systems of the graph, and figure out which one has the most faces. A rotation system is simply an ordering on the edges emerging from each vertex, saying what order those edges are in going around the vertex. Each rotation system has a straightforward minimum genus it can be embedded in, and having more faces directly corresponds to having lower genus. Since every possible embedding corresponds to some rotation system, if there is a toroidal embedding, my program will find the corresponding rotation system and declare that the graph is toroidal, and vice versa.

This program is very slow, because it searches through a number of rotation systems equal to the product over all degrees d of the vertices of (d-1)!. However, it can search through a little over 10 million such rotation systems per minute, so it can verify the toroidality or non-toroidality of simple graphs like the ones shown above.

isaacg

Posted 2017-08-25T18:36:42.490

Reputation: 39 268

1You do know that your main function is still called is_toroidal right? Changing that to say t saves 10 bytes – caird coinheringaahing – 2019-11-17T20:50:34.683

@cairdcoinheringaahing Thanks, I feel silly for missing that – isaacg – 2019-11-17T20:54:30.247