Code to generate test graphs
This commit is contained in:
parent
4203b1e5cc
commit
38374e293f
|
@ -0,0 +1,66 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Generate a set of test graphs.
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SRCDIR="$(dirname $0)"
|
||||||
|
|
||||||
|
# Usage: gen_random SEED N M
|
||||||
|
gen_random() {
|
||||||
|
python3 $SRCDIR/make_random_graph.py --seed $1 $2 $3 \
|
||||||
|
> graph_rnd_s${1}_n${2}_m${3}.gr
|
||||||
|
}
|
||||||
|
|
||||||
|
# Usage: gen_slow STRUCTURE N M
|
||||||
|
gen_slow() {
|
||||||
|
python3 $SRCDIR/make_slow_graph.py --structure $1 $2 \
|
||||||
|
> graph_slow_${1}_n${2}_m${3}.gr
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
gen_random 101 250 31125
|
||||||
|
gen_random 102 250 31125
|
||||||
|
gen_random 103 250 3952
|
||||||
|
gen_random 104 250 3952
|
||||||
|
gen_random 105 250 2500
|
||||||
|
gen_random 106 250 2500
|
||||||
|
|
||||||
|
gen_random 111 500 124750
|
||||||
|
gen_random 112 500 124750
|
||||||
|
gen_random 113 500 11180
|
||||||
|
gen_random 114 500 11180
|
||||||
|
gen_random 115 500 5000
|
||||||
|
gen_random 116 500 5000
|
||||||
|
|
||||||
|
gen_random 121 1000 499500
|
||||||
|
gen_random 122 1000 499500
|
||||||
|
gen_random 123 1000 31622
|
||||||
|
gen_random 124 1000 31622
|
||||||
|
gen_random 125 1000 10000
|
||||||
|
gen_random 126 1000 10000
|
||||||
|
|
||||||
|
gen_random 133 2000 89442
|
||||||
|
gen_random 134 2000 89442
|
||||||
|
gen_random 135 2000 20000
|
||||||
|
gen_random 136 2000 20000
|
||||||
|
|
||||||
|
gen_random 143 5000 353553
|
||||||
|
gen_random 144 5000 353553
|
||||||
|
gen_random 145 5000 50000
|
||||||
|
gen_random 146 5000 50000
|
||||||
|
|
||||||
|
gen_random 155 10000 100000
|
||||||
|
gen_random 156 10000 100000
|
||||||
|
|
||||||
|
gen_slow dense 252 4095
|
||||||
|
gen_slow dense 500 15875
|
||||||
|
gen_slow dense 1000 63000
|
||||||
|
gen_slow sparse 252 312
|
||||||
|
gen_slow sparse 500 622
|
||||||
|
gen_slow sparse 1004 1252
|
||||||
|
gen_slow sparse 2004 2502
|
||||||
|
gen_slow sparse 5004 6252
|
||||||
|
gen_slow sparse 10004 12502
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Generate a random graph in DIMACS format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import random
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
|
||||||
|
def write_dimacs_graph(
|
||||||
|
f: TextIO,
|
||||||
|
edges: list[tuple[int, int, int|float]]
|
||||||
|
) -> None:
|
||||||
|
"""Write a graph in DIMACS edge list format."""
|
||||||
|
|
||||||
|
num_vertex = 1 + max(max(x, y) for (x, y, _w) in edges)
|
||||||
|
num_edge = len(edges)
|
||||||
|
|
||||||
|
print(f"p edge {num_vertex} {num_edge}", file=f)
|
||||||
|
|
||||||
|
integer_weights = all(isinstance(w, int) for (_x, _y, w) in edges)
|
||||||
|
|
||||||
|
for (x, y, w) in edges:
|
||||||
|
if integer_weights:
|
||||||
|
print(f"e {x+1} {y+1} {w}", file=f)
|
||||||
|
else:
|
||||||
|
print(f"e {x+1} {y+1} {w:.12g}", file=f)
|
||||||
|
|
||||||
|
|
||||||
|
def make_random_graph(
|
||||||
|
n: int,
|
||||||
|
m: int,
|
||||||
|
max_weight: float,
|
||||||
|
float_weights: bool,
|
||||||
|
rng: random.Random
|
||||||
|
) -> list[tuple[int, int, int|float]]:
|
||||||
|
"""Generate a random graph with random edge weights."""
|
||||||
|
|
||||||
|
edge_set: set[tuple[int, int]] = set()
|
||||||
|
|
||||||
|
if 3 * m < n * (n - 2) // 2:
|
||||||
|
# Simply add random edges until we have enough.
|
||||||
|
while len(edge_set) < m:
|
||||||
|
x = rng.randint(0, n - 2)
|
||||||
|
y = rng.randint(x + 1, n - 1)
|
||||||
|
edge_set.add((x, y))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# We need a very dense graph.
|
||||||
|
# Generate all edge candidates and choose a random subset.
|
||||||
|
edge_candidates = [(x, y)
|
||||||
|
for x in range(n - 1)
|
||||||
|
for y in range(x + 1, n)]
|
||||||
|
rng.shuffle(edge_candidates)
|
||||||
|
edge_set.update(edge_candidates[:m])
|
||||||
|
|
||||||
|
edges: list[tuple[int, int, int|float]] = []
|
||||||
|
for (x, y) in sorted(edge_set):
|
||||||
|
w: int|float
|
||||||
|
if float_weights:
|
||||||
|
w = rng.uniform(1.0e-8, max_weight)
|
||||||
|
else:
|
||||||
|
w = rng.randint(1, int(max_weight))
|
||||||
|
edges.append((x, y, w))
|
||||||
|
|
||||||
|
return edges
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main program."""
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.description = "Generate a random graph in DIMACS format."
|
||||||
|
|
||||||
|
parser.add_argument("--seed",
|
||||||
|
action="store",
|
||||||
|
type=int,
|
||||||
|
help="random seed")
|
||||||
|
parser.add_argument("--maxweight",
|
||||||
|
action="store",
|
||||||
|
type=float,
|
||||||
|
default=1000000,
|
||||||
|
help="maximum edge weight")
|
||||||
|
parser.add_argument("--float",
|
||||||
|
action="store_true",
|
||||||
|
help="use floating point edge weights")
|
||||||
|
parser.add_argument("n",
|
||||||
|
action="store",
|
||||||
|
type=int,
|
||||||
|
help="number of vertices")
|
||||||
|
parser.add_argument("m",
|
||||||
|
action="store",
|
||||||
|
type=int,
|
||||||
|
help="number of edges")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.n < 2:
|
||||||
|
print("ERROR: Number of vertices must be >= 2", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.m < 1:
|
||||||
|
print("ERROR: Number of edges must be >= 1", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.m > args.n * (args.n - 1) // 2:
|
||||||
|
print("ERROR: Too many edges", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.maxweight < 1.0e-6:
|
||||||
|
print("ERROR: Invalid maximum edge weight", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.seed is None:
|
||||||
|
rng = random.Random()
|
||||||
|
else:
|
||||||
|
rng = random.Random(args.seed)
|
||||||
|
|
||||||
|
edges = make_random_graph(args.n, args.m, args.maxweight, args.float, rng)
|
||||||
|
write_dimacs_graph(sys.stdout, edges)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
|
@ -0,0 +1,301 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Generate a graph that is "difficult" for the O(n**3) matching algorithm.
|
||||||
|
|
||||||
|
Output in DIMACS format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
|
||||||
|
count_make_blossom = [0]
|
||||||
|
count_delta_step = [0]
|
||||||
|
|
||||||
|
|
||||||
|
def patch_matching_code() -> None:
|
||||||
|
"""Patch the matching code to count events."""
|
||||||
|
|
||||||
|
import max_weight_matching
|
||||||
|
|
||||||
|
orig_make_blossom = max_weight_matching._MatchingContext.make_blossom
|
||||||
|
orig_substage_calc_dual_delta = (
|
||||||
|
max_weight_matching._MatchingContext.substage_calc_dual_delta)
|
||||||
|
|
||||||
|
def stub_make_blossom(*args, **kwargs):
|
||||||
|
count_make_blossom[0] += 1
|
||||||
|
return orig_make_blossom(*args, **kwargs)
|
||||||
|
|
||||||
|
def stub_substage_calc_dual_delta(*args, **kwargs):
|
||||||
|
count_delta_step[0] += 1
|
||||||
|
ret = orig_substage_calc_dual_delta(*args, **kwargs)
|
||||||
|
# print("DELTA", ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
max_weight_matching._MatchingContext.make_blossom = stub_make_blossom
|
||||||
|
max_weight_matching._MatchingContext.substage_calc_dual_delta = (
|
||||||
|
stub_substage_calc_dual_delta)
|
||||||
|
|
||||||
|
|
||||||
|
def run_max_weight_matching(
|
||||||
|
edges: list[tuple[int, int, int]]
|
||||||
|
) -> tuple[list[tuple[int, int]], int, int]:
|
||||||
|
"""Run the matching algorithm and count subroutine calls."""
|
||||||
|
import max_weight_matching
|
||||||
|
|
||||||
|
count_make_blossom[0] = 0
|
||||||
|
count_delta_step[0] = 0
|
||||||
|
|
||||||
|
pairs = max_weight_matching.maximum_weight_matching(edges)
|
||||||
|
return (pairs, count_make_blossom[0], count_delta_step[0])
|
||||||
|
|
||||||
|
|
||||||
|
def write_dimacs_graph(
|
||||||
|
f: TextIO,
|
||||||
|
edges: list[tuple[int, int, int]]
|
||||||
|
) -> None:
|
||||||
|
"""Write a graph in DIMACS edge list format."""
|
||||||
|
|
||||||
|
num_vertex = 1 + max(max(x, y) for (x, y, _w) in edges)
|
||||||
|
num_edge = len(edges)
|
||||||
|
|
||||||
|
print(f"p edge {num_vertex} {num_edge}", file=f)
|
||||||
|
|
||||||
|
for (x, y, w) in edges:
|
||||||
|
print(f"e {x+1} {y+1} {w}", file=f)
|
||||||
|
|
||||||
|
|
||||||
|
def make_dense_slow_graph(n: int) -> list[tuple[int, int, int]]:
|
||||||
|
"""Generate a dense (not complete) graph with N vertices.
|
||||||
|
|
||||||
|
N must be divisible by 4.
|
||||||
|
|
||||||
|
Number of edges = M = (N**2/16 + N/2).
|
||||||
|
Number of delta steps required to solve the matching = (M - 1).
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert n % 4 == 0
|
||||||
|
|
||||||
|
edges: list[tuple[int, int, int]] = []
|
||||||
|
|
||||||
|
num_peripheral_pairs = n // 4
|
||||||
|
num_central_pairs = n // 4
|
||||||
|
max_weight = 2 * num_central_pairs * (num_peripheral_pairs + 1)
|
||||||
|
|
||||||
|
# Peripheral pairs will be matched up first.
|
||||||
|
for i in range(num_peripheral_pairs):
|
||||||
|
x = 2 * i
|
||||||
|
y = x + 1
|
||||||
|
w = max_weight - i
|
||||||
|
edges.append((x, y, w))
|
||||||
|
|
||||||
|
# Then for each central pair:
|
||||||
|
for k in range(num_central_pairs):
|
||||||
|
|
||||||
|
x = 2 * num_peripheral_pairs + 2 * k
|
||||||
|
|
||||||
|
# Central pair discovers all peripheral pairs.
|
||||||
|
for i in range(num_peripheral_pairs):
|
||||||
|
y = 2 * i
|
||||||
|
if k % 2 == 0:
|
||||||
|
w = max_weight - (1 + k // 2) * (num_peripheral_pairs + 1) + 1
|
||||||
|
else:
|
||||||
|
w = max_weight - (1 + k // 2) * (num_peripheral_pairs + 1) - i
|
||||||
|
edges.append((x, y, w))
|
||||||
|
|
||||||
|
# Then this central pair gets matched.
|
||||||
|
y = x + 1
|
||||||
|
w = max_weight - (k + 1) * (2 * num_peripheral_pairs + 1)
|
||||||
|
edges.append((x, y, w))
|
||||||
|
|
||||||
|
return edges
|
||||||
|
|
||||||
|
|
||||||
|
def make_sparse_slow_graph(n: int) -> list[tuple[int, int, int]]:
|
||||||
|
"""Generate a sparse graph with N vertices.
|
||||||
|
|
||||||
|
N must be 4 modulo 8.
|
||||||
|
|
||||||
|
Number of edges = M = (5/4 * N - 3).
|
||||||
|
Number of delta steps required to solve the matching ~ (N**2 / 16).
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert n >= 12
|
||||||
|
assert n % 8 == 4
|
||||||
|
|
||||||
|
num_p_pairs = (n - 4) // 4
|
||||||
|
num_q_pairs = num_p_pairs // 2
|
||||||
|
|
||||||
|
#
|
||||||
|
# Graph structure:
|
||||||
|
#
|
||||||
|
# O O O O O
|
||||||
|
# | | | | | (P pairs of nodes)
|
||||||
|
# | | | | | (one edge in each pair)
|
||||||
|
# O___ O O O ___O
|
||||||
|
# \_ \ |\ / \ _/| / /
|
||||||
|
# \ | _/ __/ | _/ (2 * P edges)
|
||||||
|
# \_ | _/ __/ | _/ (connecting both coupling pairs
|
||||||
|
# \ | / __/ \ | / to each top-layer pair)
|
||||||
|
# \|/__/ \ \|/
|
||||||
|
# O------/ \--\--O
|
||||||
|
# | | (2 coupling pairs)
|
||||||
|
# | | (1 edge in each pair)
|
||||||
|
# O O
|
||||||
|
# / \ / \ (2 * Q edges)
|
||||||
|
# / \ / \ (connecting each coupling pair
|
||||||
|
# / \ / \ to the Q pairs below it)
|
||||||
|
# O O O O
|
||||||
|
# | | | | (2 groups of Q pairs of nodes)
|
||||||
|
# | | | | (1 edge in each pair)
|
||||||
|
# O O O O
|
||||||
|
#
|
||||||
|
# Plan:
|
||||||
|
# - First, match each pair in the top layer.
|
||||||
|
# - Then, pull all edges between the top-layer and the
|
||||||
|
# left coupling pair tight (without matching anything).
|
||||||
|
# - Then match the left coupling pair.
|
||||||
|
# - Then pull all edges between the top-layer and the
|
||||||
|
# right coupling pair tight. (This will loosen the edges
|
||||||
|
# between the top-layer and the left coupling pair.)
|
||||||
|
# - Then match the right coupling pair.
|
||||||
|
# - Then alternate between the bottom left group and bottom right group
|
||||||
|
# of pairs:
|
||||||
|
# - Pick a new pair from the selected bottom group.
|
||||||
|
# - Pull the edge to its coupling pair tight.
|
||||||
|
# - Pull all edges between the coupling pair and the top-layer tight.
|
||||||
|
# (This will loosen the edges between the top-layer and the other
|
||||||
|
# coupling pair.)
|
||||||
|
# - Match the selected bottom pair.
|
||||||
|
#
|
||||||
|
# The trick is to assign edge weights that force the matching
|
||||||
|
# algorithm to execute the plan as descibed above.
|
||||||
|
#
|
||||||
|
|
||||||
|
edges: list[tuple[int, int, int]] = []
|
||||||
|
|
||||||
|
max_weight = 16 * (num_q_pairs + 1) * (num_p_pairs + 2)
|
||||||
|
|
||||||
|
# Make the top pairs.
|
||||||
|
for i in range(num_p_pairs):
|
||||||
|
x = 2 * i
|
||||||
|
y = 2 * i + 1
|
||||||
|
w = max_weight - i
|
||||||
|
edges.append((x, y, w))
|
||||||
|
|
||||||
|
# Make the coupling pairs.
|
||||||
|
for k in range(2):
|
||||||
|
|
||||||
|
# Connect the coupling pair to the top layer.
|
||||||
|
for i in range(num_p_pairs):
|
||||||
|
x = 2 * i + 1
|
||||||
|
y = 2 * num_p_pairs + 2 * k
|
||||||
|
if k == 0:
|
||||||
|
w = max_weight - num_p_pairs
|
||||||
|
else:
|
||||||
|
w = max_weight - num_p_pairs - 1 - i
|
||||||
|
edges.append((x, y, w))
|
||||||
|
|
||||||
|
# Make the internal edge in the coupling pair.
|
||||||
|
x = 2 * num_p_pairs + 2 * k
|
||||||
|
y = 2 * num_p_pairs + 2 * k + 1
|
||||||
|
if k == 0:
|
||||||
|
w = max_weight - 2 * num_p_pairs - 1
|
||||||
|
else:
|
||||||
|
w = max_weight - 4 * num_p_pairs - 2
|
||||||
|
edges.append((x, y, w))
|
||||||
|
|
||||||
|
# Make the bottom groups.
|
||||||
|
for k in range(2):
|
||||||
|
|
||||||
|
# Connect the coupling pair to the bottom layer.
|
||||||
|
for i in range(num_q_pairs):
|
||||||
|
x = 2 * num_p_pairs + 2 * k + 1
|
||||||
|
y = 2 * num_p_pairs + 4 + 2 * num_q_pairs * k + 2 * i
|
||||||
|
w = (max_weight
|
||||||
|
- (2 * i + k) * (2 * num_p_pairs + 4)
|
||||||
|
- 4 * num_p_pairs + 1)
|
||||||
|
edges.append((x, y, w))
|
||||||
|
|
||||||
|
# Make the pairs in this half of the bottom layer.
|
||||||
|
for i in range(num_q_pairs):
|
||||||
|
x = 2 * num_p_pairs + 4 + 2 * num_q_pairs * k + 2 * i
|
||||||
|
y = 2 * num_p_pairs + 4 + 2 * num_q_pairs * k + 2 * i + 1
|
||||||
|
w = (max_weight
|
||||||
|
- (2 * i + k + 1) * 8 * num_p_pairs
|
||||||
|
- (2 * i + k) * 12)
|
||||||
|
edges.append((x, y, w))
|
||||||
|
|
||||||
|
return edges
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Main program."""
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.description = "Generate a difficult graph."
|
||||||
|
|
||||||
|
parser.add_argument("--structure",
|
||||||
|
action="store",
|
||||||
|
choices=("sparse", "dense"),
|
||||||
|
default="sparse",
|
||||||
|
help="choose graph structure")
|
||||||
|
parser.add_argument("--check",
|
||||||
|
action="store_true",
|
||||||
|
help="solve the matching and count delta steps")
|
||||||
|
parser.add_argument("n",
|
||||||
|
action="store",
|
||||||
|
type=int,
|
||||||
|
help="number of vertices")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.check:
|
||||||
|
patch_matching_code()
|
||||||
|
|
||||||
|
if args.structure == "sparse":
|
||||||
|
|
||||||
|
if args.n < 12:
|
||||||
|
print("ERROR: Number of vertices must be >= 12", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.n % 8 != 4:
|
||||||
|
print("ERROR: Number of vertices must be 4 modulo 8",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
edges = make_sparse_slow_graph(args.n)
|
||||||
|
|
||||||
|
elif args.structure == "dense":
|
||||||
|
|
||||||
|
if args.n < 4:
|
||||||
|
print("ERROR: Number of vertices must be >= 4", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.n % 4 != 0:
|
||||||
|
print("ERROR: Number of vertices must be divisible by 4",
|
||||||
|
file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
edges = make_dense_slow_graph(args.n)
|
||||||
|
|
||||||
|
else:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
if args.check:
|
||||||
|
(pairs, num_blossom, num_delta) = run_max_weight_matching(edges)
|
||||||
|
print(f"n={args.n} m={len(edges)} "
|
||||||
|
f"nblossom={num_blossom} ndelta={num_delta}",
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
write_dimacs_graph(sys.stdout, edges)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
Loading…
Reference in New Issue