CSE-250 Fall 2022 - Section B - Graph Exploration

Graph Exploration

CSE-250 Fall 2022 - Section B

Oct 7, 2022

Textbook: Ch. 15.3

Edge List Summary

  • addEdge, addVertex: $O(1)$
  • removeEdge: $O(1)$
  • removeVertex: $O(m)$
  • vertex.incidentEdges: $O(m)$
  • vertex.edgeTo: $O(m)$
  • Space Used: $O(n) + O(m)$

Adjacency List Summary

  • addEdge, addVertex: $O(1)$
  • removeEdge: $O(1)$
  • vertex.incidentEdges: $O(deg(vertex))$
  • removeVertex: $O(deg(vertex))$
  • vertex.edgeTo: $O(deg(vertex))$
  • Space Used: $O(n) + O(m)$

Adjacency Matrix Summary

  • addEdge, removeEdge: $O(1)$
  • addVertex, removeVertex: $O(n^2)$
  • vertex.incidentEdges: $O(n)$
  • vertex.edgeTo: $O(1)$
  • Space Used: $O(n^2)$

Ok, we have a graph, now what do we do with it?

Connectivity Problems

Given graph $G$:

  • Is vertex $u$ adjacent to $v$?
  • Is vertex $u$ connected to $v$ via some path?
  • Which vertices are connected to vertex $v$?
  • What is the shortest path from vertex $u$ to $v$?

A few more definitions...

A subgraph $S$ of a graph $G$ is a graph where...
$S$'s vertices are a subset of $G$'s vertices
$S$'s edges are a subset of $G$'s edges
A spanning subgraph of $G$...
Is a subgraph of $G$
Contains all of $G$'s vertices.

A few more definitions...

A graph is connected...
If there is a path between every pair of vertices
A connected component of $G$...
Is a maximal connected subgraph of $G$
  • "maximal" means you can't add any new vertex without breaking the property
  • Any subset of $G$'s edges that connects the subgraph is fine.

A few more definitions...

A free tree is an undirected graph $T$ such that:
There is exactly one simple path between any two nodes
  • T is connected
  • T has no cycles
A rooted tree is a directed graph $T$ such that:
One vertex of $T$ is the root
There is exactly one simple path from the root to every other vertex in the graph.
A (free/rooted) forest is a graph $F$ such that
Every connected component is a tree.

A few more definitions...

A spanning tree of a connected graph...
Is a spanning subgraph that is a tree.
Is not unique unless the graph is a tree.

Ok... back to the question: Connectivity!

The maze is a graph!

Recall

Searching the maze with a stack (Depth-First Search)
Try out every path, one at a time to the end...
... then backtrack and try another
Searching the maze with a queue (Breadth-First Search)
Try out every path in parallel...
... keep picking a path to extend by one step.

Depth-First Search

Primary Goals

  • Visit every vertex in graph $G = (V, E)$
  • Construct a spanning tree for every connected component
    • Side Effect: Compute connected components
    • Side Effect: Compute a paths between all connected vertices
    • Side Effect: Determine if the graph is connected
    • Side Effect: Identify Cycles
  • Complete in time $O(|V| + |E|)$

Depth-First Search

DFS
Input: Graph $G$
Output: Label every edge as:
  • Spanning Edge: Part of the spanning tree
  • Back Edge: Part of a cycle
DFSOne
Input: Graph $G = (V, E)$, Start Vertex $v \in V$
Output: Label every edge in $v$'s connected component

Depth-First Search

Depth-First Search

object VertexLabel extends Enumeration
  { val UNEXPLORED, VISITED = Value }

object EdgeLabel extends Enumeration
  { val UNEXPLORED, SPANNING, BACK = Value }

def DFS(graph: Graph[VertexLabel.Value, EdgeLabel.Value])
{
  for(v <- graph.vertices) { v.setLabel(VertexLabel.UNEXPLORED) }
  for(e <- graph.edges)    { e.setLabel(EdgeLabel.UNEXPLORED) }
  for(v <- graph.vertices) { 
    if(v.label == VertexLabel.UNEXPLORED){ 
      DFSOne(graph, v)
    }
  }
}  

Depth-First Search

def DFSOne(graph: Graph[…], v: Graph[…]#Vertex)
{
  v.setLabel(VertexLabel.VISITED)

  for(e <- v.incident) { 
    if(e.label == EdgeLabel.UNEXPLORED){
      val w = e.getOpposite(v)

      if(w.label == VertexLabel.UNEXPLORED){
        e.setLabel(EdgeLabel.SPANNING)
        DFSOne(graph, w)
      } else {
        e.setLabel(EdgeLabel.BACK)
      }
    }
  }
}  

Depth-First Search

DFS vs Mazes

The DFS algorithm is like our stack-based maze solver

  • Mark each grid square with VISITED.
  • Mark each path with SPANNING.
  • Only visit each vertex once.
    • DFS won't necessarily find the shortest paths.

What's the complexity?

def DFS(graph: Graph[VertexLabel.Value, EdgeLabel.Value])
{
  for(v <- graph.vertices) { v.setLabel(VertexLabel.UNEXPLORED) }
  for(e <- graph.edges)    { e.setLabel(EdgeLabel.UNEXPLORED) }
  for(v <- graph.vertices) { 
    if(v.label == VertexLabel.UNEXPLORED){ 
      DFSOne(graph, v)
    }
  }
}  
def DFS(graph: Graph[VertexLabel.Value, EdgeLabel.Value])
{
  /* O(|V|) */
  for(e <- graph.edges)    { e.setLabel(EdgeLabel.UNEXPLORED) }
  for(v <- graph.vertices) { 
    if(v.label == VertexLabel.UNEXPLORED){ 
      DFSOne(graph, v)
    }
  }
}  
def DFS(graph: Graph[VertexLabel.Value, EdgeLabel.Value])
{
  /* O(|V|) */
  /* O(|E|) */
  for(v <- graph.vertices) { 
    if(v.label == VertexLabel.UNEXPLORED){ 
      DFSOne(graph, v)
    }
  }
}  
def DFS(graph: Graph[VertexLabel.Value, EdgeLabel.Value])
{
  /* O(|V|) */
  /* O(|E|) */
  /* O(|V|) Times */ { 
    if(v.label == VertexLabel.UNEXPLORED){ 
      DFSOne(graph, v)
    }
  }
}  
def DFS(graph: Graph[VertexLabel.Value, EdgeLabel.Value])
{
  /* O(|V|) */
  /* O(|E|) */
  /* O(|V|) Times */ { 
    if(v.label == VertexLabel.UNEXPLORED){ 
      /* ??? */
    }
  }
}  
def DFSOne(graph: Graph[…], v: Graph[…]#Vertex)
{
  v.setLabel(VertexLabel.VISITED)

  for(e <- v.incident) { 
    if(e.label == EdgeLabel.UNEXPLORED){
      val w = e.getOpposite(v)

      if(w.label == VertexLabel.UNEXPLORED){
        e.setLabel(EdgeLabel.SPANNING)
        DFSOne(graph, w)
      } else {
        e.setLabel(EdgeLabel.BACK)
      }
    }
  }
}  
def DFSOne(graph: Graph[…], v: Graph[…]#Vertex)
{
  /* O(1) */

  for(e <- v.incident) { 
    if(e.label == EdgeLabel.UNEXPLORED){
      val w = e.getOpposite(v)

      if(w.label == VertexLabel.UNEXPLORED){
        e.setLabel(EdgeLabel.SPANNING)
        DFSOne(graph, w)
      } else {
        e.setLabel(EdgeLabel.BACK)
      }
    }
  }
}  
def DFSOne(graph: Graph[…], v: Graph[…]#Vertex)
{
  /* O(1) */

  /* O(deg(v)) times */ { 
    if(e.label == EdgeLabel.UNEXPLORED){
      val w = e.getOpposite(v)

      if(w.label == VertexLabel.UNEXPLORED){
        e.setLabel(EdgeLabel.SPANNING)
        DFSOne(graph, w)
      } else {
        e.setLabel(EdgeLabel.BACK)
      }
    }
  }
}  
def DFSOne(graph: Graph[…], v: Graph[…]#Vertex)
{
  /* O(1) */

  /* O(deg(v)) times */ { 
    /* O(1) */ {
      /* O(1) */

      /* O(1) */ {
        /* O(1) */
        DFSOne(graph, w)
      } else {
        /* O(1) */
      }
    }
  }
}  
def DFSOne(graph: Graph[…], v: Graph[…]#Vertex)
{
  /* O(1) */

  /* O(deg(v)) times */ { 
    /* O(1) */ {
      /* O(1) */

      /* O(1) */ {
        /* O(1) */
        /* ??? */
      } else {
        /* O(1) */
      }
    }
  }
}  

Depth-First Search

Observation: DFSOne is called on each vertex at most once.

  • If v.label == VISITED, both DFS, DFSOne skip it.

$O(|V|)$ calls to DFSOne

What's the runtime of DFSOne excluding recursive calls?


def DFSOne(graph: Graph[…], v: Graph[…]#Vertex)
{
  /* O(1) */

  /* O(deg(v)) times */ { 
    /* O(1) */ {
      /* O(1) */

      /* O(1) */ {
        /* O(1) */
        DFSOne(graph, w)
      } else {
        /* O(1) */
      }
    }
  }
}  

What's the runtime of DFSOne excluding recursive calls?

$$O(deg(v))$$

Depth-First Search

What's the sum over all calls to DFSOne?

$\sum_{v \in V} O(deg(v))$

$ = O(\sum_{v \in V} deg(v))$

$ = O(2 |E|)$ (by rule)

$ = O(|E|)$

Depth-First Search

Summing up...

Mark Vertices UNVISITED $O(|V|)$
Mark Edges UNVISITED $O(|E|)$
DFS Vertex Loop $O(|V|)$
All Calls to DFSOne $O(|E|)$
$O(|V| + |E|)$