Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"MinimumVertexCover": [Minimum Vertex Cover],
"MaxCut": [Max-Cut],
"GraphPartitioning": [Graph Partitioning],
"GeneralizedHex": [Generalized Hex],
"HamiltonianPath": [Hamiltonian Path],
"UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow],
"LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths],
Expand Down Expand Up @@ -648,6 +649,64 @@ is feasible: each set induces a connected subgraph, the component weights are $2
]
]
}
#{
let x = load-model-example("GeneralizedHex")
let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1)))
let source = x.instance.source
let target = x.instance.target
let winning-path = ((0, 1), (1, 4), (4, 5))
[
#problem-def("GeneralizedHex")[
Given an undirected graph $G = (V, E)$ and distinct terminals $s, t in V$, determine whether Player 1 has a forced win in the vertex-claiming Shannon switching game where the players alternately claim vertices of $V backslash {s, t}$, coloring them blue and red respectively, and Player 1 wins iff the final coloring contains an $s$-$t$ path whose internal vertices are all blue.
][
Generalized Hex is the vertex version of the Shannon switching game listed by Garey & Johnson (A8 GP1). Even and Tarjan proved that deciding whether the first player has a winning strategy is PSPACE-complete @evenTarjan1976. The edge-claiming Shannon switching game is a classical contrast point: Bruno and Weinberg showed that the edge version is polynomial-time solvable via matroid methods @brunoWeinberg1970.

The implementation evaluates the decision problem directly rather than searching over candidate assignments. The instance has `dims() = []`, and `evaluate([])` runs a memoized minimax search over the ternary states (unclaimed, blue, red) of the nonterminal vertices. This preserves the alternating-game semantics of the original problem instead of collapsing the game into a static coloring predicate.

*Example.* The canonical fixture uses the six-vertex graph with terminals $s = v_#source$ and $t = v_#target$, and edges #edges.map(((u, v)) => $(v_#u, v_#v)$).join(", "). Vertex $v_4$ is the unique neighbor of $t$, so Player 1 opens by claiming $v_4$. Player 2 can then block at most one of $v_1$, $v_2$, and $v_3$; Player 1 responds by claiming one of the remaining branch vertices, completing a blue path $v_0 arrow v_i arrow v_4 arrow v_5$. The fixture database therefore has exactly one satisfying configuration: the empty configuration, which triggers the internal game-tree evaluator on the initial board.

#figure(
canvas(length: 1cm, {
import draw: *
let blue = graph-colors.at(0)
let gray = luma(185)
let verts = (
(0, 1.0),
(1.6, 2.2),
(1.6, 1.0),
(1.6, -0.2),
(3.3, 1.0),
(5.0, 1.0),
)
for (u, v) in edges {
let on-path = winning-path.any(e =>
(e.at(0) == u and e.at(1) == v) or
(e.at(0) == v and e.at(1) == u)
)
g-edge(
verts.at(u),
verts.at(v),
stroke: if on-path { 2pt + blue } else { 1pt + gray },
)
}
for (k, pos) in verts.enumerate() {
let highlighted = k == source or k == 1 or k == 4 or k == target
g-node(
pos,
name: "v" + str(k),
fill: if highlighted { blue } else { white },
stroke: 1pt + if highlighted { blue } else { gray },
label: text(fill: if highlighted { white } else { black })[$v_#k$],
)
}
content((0, 1.55), text(8pt)[$s$])
content((5.0, 1.55), text(8pt)[$t$])
}),
caption: [A winning Generalized Hex instance. Player 1 first claims $v_4$, then answers any red move on $\{v_1, v_2, v_3\}$ by taking a different branch vertex and completing a blue path from $s = v_0$ to $t = v_5$.],
) <fig:generalized-hex>
]
]
}
#{
let x = load-model-example("HamiltonianPath")
let nv = graph-num-vertices(x.instance)
Expand Down
20 changes: 20 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,26 @@ @article{evenItaiShamir1976
doi = {10.1137/0205048}
}

@article{evenTarjan1976,
author = {Shimon Even and Robert Endre Tarjan},
title = {A Combinatorial Problem Which Is Complete in Polynomial Space},
journal = {Journal of the ACM},
volume = {23},
number = {4},
pages = {710--719},
year = {1976}
}

@article{brunoWeinberg1970,
author = {John Bruno and Louis Weinberg},
title = {A Constructive Graph-Theoretic Solution of the Shannon Switching Game},
journal = {IEEE Transactions on Circuit Theory},
volume = {17},
number = {1},
pages = {74--81},
year = {1970}
}

@article{glover2019,
author = {Fred Glover and Gary Kochenberger and Yu Du},
title = {Quantum Bridge Analytics {I}: a tutorial on formulating and using {QUBO} models},
Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ Flags by problem type:
KColoring --graph, --k
PartitionIntoTriangles --graph
GraphPartitioning --graph
GeneralizedHex --graph, --source, --sink
BoundedComponentSpanningForest --graph, --weights, --k, --bound
UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
IsomorphicSpanningTree --graph, --tree
Expand Down Expand Up @@ -267,6 +268,7 @@ Examples:
pred create MIS --graph 0-1,1-2,2-3 --weights 1,1,1
pred create SAT --num-vars 3 --clauses \"1,2;-1,3\"
pred create QUBO --matrix \"1,0.5;0.5,2\"
pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5
pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10
pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\"
pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5
Expand Down
138 changes: 133 additions & 5 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ use anyhow::{bail, Context, Result};
use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample};
use problemreductions::models::algebraic::{ClosestVectorProblem, BMF};
use problemreductions::models::graph::{
GraphPartitioning, HamiltonianPath, LengthBoundedDisjointPaths, MultipleChoiceBranching,
SteinerTree,
GeneralizedHex, GraphPartitioning, HamiltonianPath, LengthBoundedDisjointPaths,
MultipleChoiceBranching, SteinerTree,
};
use problemreductions::models::misc::{
BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing,
Expand Down Expand Up @@ -233,7 +233,6 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
"Vec<Vec<usize>>" => "semicolon-separated groups: \"0,1;2,3\"",
"usize" => "integer",
"u64" => "integer",
"Vec<u64>" => "comma-separated integers: 0,0,5",
"i64" => "integer",
"BigUint" => "nonnegative decimal integer",
"Vec<BigUint>" => "comma-separated nonnegative decimal integers: 3,7,1,8",
Expand All @@ -255,6 +254,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
_ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1",
},
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
"GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5",
"BoundedComponentSpanningForest" => {
"--graph 0-1,1-2,2-3,3-4,4-5,5-6,6-7,0-7,1-5,2-6 --weights 2,3,1,2,3,1,2,1 --k 3 --bound 6"
}
Expand Down Expand Up @@ -409,6 +409,9 @@ fn problem_help_flag_name(
if canonical == "LengthBoundedDisjointPaths" && field_name == "max_length" {
return "bound".to_string();
}
if canonical == "GeneralizedHex" && field_name == "target" {
return "sink".to_string();
}
field_name.replace('_', "-")
Comment on lines 409 to 415
}

Expand Down Expand Up @@ -561,6 +564,29 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// Generalized Hex (graph + source + sink)
"GeneralizedHex" => {
let usage =
"Usage: pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5";
let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
let num_vertices = graph.num_vertices();
let source = args
.source
.ok_or_else(|| anyhow::anyhow!("GeneralizedHex requires --source\n\n{usage}"))?;
let sink = args
.sink
.ok_or_else(|| anyhow::anyhow!("GeneralizedHex requires --sink\n\n{usage}"))?;
validate_vertex_index("source", source, num_vertices, usage)?;
validate_vertex_index("sink", sink, num_vertices, usage)?;
if source == sink {
bail!("GeneralizedHex requires distinct --source and --sink\n\n{usage}");
}
(
ser(GeneralizedHex::new(graph, source, sink))?,
resolved_variant.clone(),
)
}

// Bounded Component Spanning Forest
"BoundedComponentSpanningForest" => {
let usage = "Usage: pred create BoundedComponentSpanningForest --graph 0-1,1-2,2-3,3-4,4-5,5-6,6-7,0-7,1-5,2-6 --weights 2,3,1,2,3,1,2,1 --k 3 --bound 6";
Expand Down Expand Up @@ -2257,6 +2283,26 @@ fn create_random(
(ser(HamiltonianPath::new(graph))?, variant)
}

// GeneralizedHex (graph only, with source/sink defaults)
"GeneralizedHex" => {
let num_vertices = num_vertices.max(2);
let edge_prob = args.edge_prob.unwrap_or(0.5);
if !(0.0..=1.0).contains(&edge_prob) {
bail!("--edge-prob must be between 0.0 and 1.0");
}
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
let source = args.source.unwrap_or(0);
let sink = args.sink.unwrap_or(num_vertices - 1);
let usage = "Usage: pred create GeneralizedHex --random --num-vertices 6 [--edge-prob 0.5] [--seed 42] [--source 0] [--sink 5]";
validate_vertex_index("source", source, num_vertices, usage)?;
validate_vertex_index("sink", sink, num_vertices, usage)?;
if source == sink {
bail!("GeneralizedHex requires distinct --source and --sink\n\n{usage}");
}
let variant = variant_map(&[("graph", "SimpleGraph")]);
(ser(GeneralizedHex::new(graph, source, sink))?, variant)
}

// LengthBoundedDisjointPaths (graph only, with path defaults)
"LengthBoundedDisjointPaths" => {
let num_vertices = if num_vertices < 2 {
Expand Down Expand Up @@ -2397,7 +2443,7 @@ fn create_random(
"Random generation is not supported for {canonical}. \
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \
SteinerTree, OptimalLinearArrangement, HamiltonianPath)"
SteinerTree, OptimalLinearArrangement, HamiltonianPath, GeneralizedHex)"
),
};

Expand All @@ -2412,7 +2458,23 @@ fn create_random(

#[cfg(test)]
mod tests {
use super::problem_help_flag_name;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use clap::Parser;

use super::{create, problem_help_flag_name};
use crate::cli::{Cli, Commands};
use crate::output::OutputConfig;

fn temp_output_path(name: &str) -> PathBuf {
let suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!("{}_{}.json", name, suffix))
}

#[test]
fn test_problem_help_uses_bound_for_length_bounded_disjoint_paths() {
Expand All @@ -2434,4 +2496,70 @@ mod tests {
"num-paths-required"
);
}

#[test]
fn test_create_generalized_hex_serializes_problem_json() {
let output = temp_output_path("generalized_hex_create");
let cli = Cli::try_parse_from([
"pred",
"-o",
output.to_str().unwrap(),
"create",
"GeneralizedHex",
"--graph",
"0-1,0-2,0-3,1-4,2-4,3-4,4-5",
"--source",
"0",
"--sink",
"5",
])
.unwrap();
let out = OutputConfig {
output: cli.output.clone(),
quiet: true,
json: false,
auto_json: false,
};
let args = match cli.command {
Commands::Create(args) => args,
_ => unreachable!(),
};

create(&args, &out).unwrap();

let json: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&output).unwrap()).unwrap();
fs::remove_file(&output).unwrap();
assert_eq!(json["type"], "GeneralizedHex");
assert_eq!(json["variant"]["graph"], "SimpleGraph");
assert_eq!(json["data"]["source"], 0);
assert_eq!(json["data"]["target"], 5);
}

#[test]
fn test_create_generalized_hex_requires_sink() {
let cli = Cli::try_parse_from([
"pred",
"create",
"GeneralizedHex",
"--graph",
"0-1,1-2,2-3",
"--source",
"0",
])
.unwrap();
let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};
let args = match cli.command {
Commands::Create(args) => args,
_ => unreachable!(),
};

let err = create(&args, &out).unwrap_err();
assert!(err.to_string().contains("GeneralizedHex requires --sink"));
}
}
Loading
Loading