Yog.Property.Bipartite (YogEx v0.97.1)

Copy Markdown View Source

Bipartite graph analysis and matching algorithms.

A graph is bipartite (2-colorable) if its vertices can be divided into two disjoint sets such that every edge connects a vertex in one set to a vertex in the other set. Equivalently, a bipartite graph contains no odd-length cycles.

Algorithms

ProblemAlgorithmFunctionComplexity
Bipartite checkBFS 2-coloringbipartite?/1, partition/1O(V + E)
Maximum matchingAugmenting pathsmaximum_matching/2O(VE)
Stable matchingGale-Shapleystable_marriage/2O(V²)

Key Concepts

  • Bipartite Graph: Vertices partitioned into two sets L and R, edges only go between sets
  • 2-Coloring: Adjacent vertices always have different colors (equivalent to bipartite)
  • Matching: Set of edges without common vertices
  • Maximum Matching: Matching with largest possible number of edges
  • Perfect Matching: Every vertex is matched (requires |L| = |R|)
  • Stable Matching: No unmatched pair prefers each other over current matches

Characterizations of Bipartite Graphs

A graph is bipartite if and only if:

  • It is 2-colorable
  • It contains no odd-length cycles
  • Its spectrum is symmetric about 0

König's Theorem

In bipartite graphs, the size of the maximum matching equals the size of the minimum vertex cover. This is a fundamental result that doesn't hold for general graphs.

Use Cases

  • Job assignment: Workers to tasks they can perform
  • Scheduling: Time slots to events without conflicts
  • Recommendation systems: Users to items they might like
  • Chemistry: Matching molecules for reactions
  • Stable marriage: Matching medical residents to hospitals

Bipartite Visualization

graph G { rankdir=LR; bgcolor="transparent"; node [shape=circle, fontname="inherit"]; edge [fontname="inherit", fontsize=10]; subgraph cluster_left { label="LeftSet"; color="#6366f1"; style=rounded; L1; L2; L3; } subgraph cluster_right { label="RightSet"; color="#f43f5e"; style=rounded; R1; R2; R3; } L1 -- R1; L1 -- R2; L2 -- R2; L2 -- R3; L3 -- R1; }
iex> alias Yog.Property.Bipartite
iex> graph = Yog.from_edges(:undirected, [
...>   {"L1", "R1", 1}, {"L1", "R2", 1},
...>   {"L2", "R2", 1}, {"L2", "R3", 1},
...>   {"L3", "R1", 1}
...> ])
iex> Bipartite.bipartite?(graph)
true

Maximum Matching

graph G { rankdir=LR; bgcolor="transparent"; node [shape=circle, fontname="inherit"]; edge [fontname="inherit", fontsize=10]; subgraph cluster_workers { label="Workers"; color="#6366f1"; style=rounded; w1; w2; w3; } subgraph cluster_tasks { label="Tasks"; color="#f43f5e"; style=rounded; t1; t2; t3; } // Matching edges (Job Assignments) w1 -- t1 [color="#6366f1", penwidth=2.5]; w2 -- t3 [color="#6366f1", penwidth=2.5]; w3 -- t2 [color="#6366f1", penwidth=2.5]; // Other potential matches w1 -- t2 [style=dashed, color="#94a3b8"]; w2 -- t1 [style=dashed, color="#94a3b8"]; }
iex> alias Yog.Property.Bipartite
iex> graph = Yog.from_edges(:undirected, [
...>   {"w1", "t1", 1}, {"w1", "t2", 1},
...>   {"w2", "t1", 1}, {"w2", "t3", 1},
...>   {"w3", "t2", 1}
...> ])
iex> {:ok, p} = Bipartite.partition(graph)
iex> matching = Bipartite.maximum_matching(graph, p)
iex> length(matching)
3

Examples

# Check if a graph is bipartite
iex> graph = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_node(4, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
...> |> Yog.add_edge_ensure(from: 3, to: 4, with: 1)
iex> Yog.Property.Bipartite.bipartite?(graph)
true

# Get the partition
iex> graph =
...>   Yog.undirected()
...>   |> Yog.add_node(1, nil)
...>   |> Yog.add_node(2, nil)
...>   |> Yog.add_node(3, nil)
...>   |> Yog.add_node(4, nil)
...>   |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...>   |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
...>   |> Yog.add_edge_ensure(from: 3, to: 4, with: 1)
iex> {:ok, %{left: left, right: right}} = Yog.Property.Bipartite.partition(graph)
iex> MapSet.size(left) + MapSet.size(right)
4

References

Summary

Types

A partition of a bipartite graph into two independent sets. In a bipartite graph, all edges connect vertices from left to right, with no edges within left or within right.

Functions

Determines if a graph is bipartite (2-colorable). Works for both directed and undirected graphs.

Finds a 2-coloring of a graph if it is bipartite.

Gets the partner of a person in a stable matching.

Finds a maximum matching in a bipartite graph.

Returns the two partitions of a bipartite graph, or an error if not bipartite.

Finds a stable matching given preference lists for two groups.

Types

partition()

@type partition() :: %{left: MapSet.t(Yog.node_id()), right: MapSet.t(Yog.node_id())}

A partition of a bipartite graph into two independent sets. In a bipartite graph, all edges connect vertices from left to right, with no edges within left or within right.

Functions

bipartite?(graph)

@spec bipartite?(Yog.graph()) :: boolean()

Determines if a graph is bipartite (2-colorable). Works for both directed and undirected graphs.

A graph is bipartite if its vertices can be divided into two disjoint sets such that every edge connects a vertex in one set to a vertex in the other set.

Examples

iex> graph = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
iex> Yog.Property.Bipartite.bipartite?(graph)
true

# Odd cycle (triangle) is not bipartite
iex> triangle = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
...> |> Yog.add_edge_ensure(from: 3, to: 1, with: 1)
iex> Yog.Property.Bipartite.bipartite?(triangle)
false

Time Complexity

O(V + E)

coloring(graph)

@spec coloring(Yog.graph()) ::
  {:ok, %{required(Yog.node_id()) => 0 | 1}} | {:error, :not_bipartite}

Finds a 2-coloring of a graph if it is bipartite.

Returns a map where each node ID maps to either 0 or 1, representing the two colors.

Examples

iex> graph = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
iex> {:ok, coloring} = Yog.Property.Bipartite.coloring(graph)
iex> # Adjacent nodes should have different colors
...> coloring[1] != coloring[2] and coloring[2] != coloring[3]
true

iex> triangle = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
...> |> Yog.add_edge_ensure(from: 3, to: 1, with: 1)
iex> Yog.Property.Bipartite.coloring(triangle)
{:error, :not_bipartite}

get_partner(matches, person)

@spec get_partner(%{required(k :: any()) => v :: any()}, any()) :: any() | nil

Gets the partner of a person in a stable matching.

Returns the partner if the person is matched, nil otherwise.

Examples

iex> residents = %{1 => [101, 102], 2 => [102, 101]}
iex> hospitals = %{101 => [1, 2], 102 => [2, 1]}
iex> matches = Yog.Property.Bipartite.stable_marriage(residents, hospitals)
iex> partner = Yog.Property.Bipartite.get_partner(matches, 1)
iex> partner in [101, 102]
true
iex> Yog.Property.Bipartite.get_partner(matches, 999)
nil

maximum_matching(graph, partition_map)

@spec maximum_matching(Yog.graph(), %{
  left: MapSet.t(Yog.node_id()),
  right: MapSet.t(Yog.node_id())
}) ::
  [{Yog.node_id(), Yog.node_id()}]

Finds a maximum matching in a bipartite graph.

A matching is a set of edges with no common vertices. A maximum matching has the largest possible number of edges.

Uses the augmenting path algorithm (also known as the Hungarian algorithm for unweighted bipartite matching).

Returns a list of matched pairs {left_node, right_node}.

Examples

# Complete bipartite graph K_{2,2}
iex> graph = Yog.undirected()
...> |> Yog.add_node(1, nil)  # left
...> |> Yog.add_node(2, nil)  # left
...> |> Yog.add_node(3, nil)  # right
...> |> Yog.add_node(4, nil)  # right
...> |> Yog.add_edge_ensure(from: 1, to: 3, with: 1)
...> |> Yog.add_edge_ensure(from: 1, to: 4, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 4, with: 1)
iex> {:ok, p} = Yog.Property.Bipartite.partition(graph)
iex> matching = Yog.Property.Bipartite.maximum_matching(graph, p)
iex> length(matching)
2

Time Complexity

O(V * E)

partition(graph)

@spec partition(Yog.graph()) ::
  {:ok, %{left: MapSet.t(Yog.node_id()), right: MapSet.t(Yog.node_id())}}
  | {:error, :not_bipartite}

Returns the two partitions of a bipartite graph, or an error if not bipartite.

Uses BFS with 2-coloring to detect bipartiteness and construct the partitions. Handles disconnected graphs by checking all components.

Examples

iex> graph = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_node(4, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
...> |> Yog.add_edge_ensure(from: 3, to: 4, with: 1)
iex> {:ok, %{left: left, right: right}} = Yog.Property.Bipartite.partition(graph)
iex> MapSet.size(left) == 2 and MapSet.size(right) == 2
true

# Not bipartite - odd cycle
iex> triangle = Yog.undirected()
...> |> Yog.add_node(1, nil)
...> |> Yog.add_node(2, nil)
...> |> Yog.add_node(3, nil)
...> |> Yog.add_edge_ensure(from: 1, to: 2, with: 1)
...> |> Yog.add_edge_ensure(from: 2, to: 3, with: 1)
...> |> Yog.add_edge_ensure(from: 3, to: 1, with: 1)
iex> Yog.Property.Bipartite.partition(triangle)
{:error, :not_bipartite}

Time Complexity

O(V + E)

stable_marriage(opts)

stable_marriage(left_prefs, right_prefs)

@spec stable_marriage(
  %{required(k1 :: any()) => [k2 :: any()]},
  %{required(k2 :: any()) => [k1 :: any()]}
) :: %{
  required(k1 :: any()) => k2 :: any(),
  required(k2 :: any()) => k1 :: any()
}

Finds a stable matching given preference lists for two groups.

Uses the Gale-Shapley algorithm to find a stable matching where no two people would both prefer each other over their current partners.

The algorithm is "proposer-optimal" - it finds the best stable matching for the proposing group (left), and the worst stable matching for the receiving group (right).

Parameters

  • left_prefs - Map where each key is a left person and the value is a list of right person preferences (most preferred first)
  • right_prefs - Map where each key is a right person and the value is a list of left person preferences (most preferred first)

Returns a map of matched pairs (bidirectional).

Examples

# Medical residency matching
iex> residents = %{1 => [101, 102], 2 => [102, 101]}
iex> hospitals = %{101 => [1, 2], 102 => [2, 1]}
iex> matches = Yog.Property.Bipartite.stable_marriage(residents, hospitals)
iex> is_map(matches)
true

Time Complexity

O(n²) where n is the size of each group