Skip to content

Commit c5d8db9

Browse files
zazabapclaudeGiggleLiu
authored
Fix #218: Add SubgraphIsomorphism model (#607)
* Add plan for #218: SubgraphIsomorphism model Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Implement #218: Add SubgraphIsomorphism model - New satisfaction problem: SubgraphIsomorphism (decision: does host graph G contain a subgraph isomorphic to pattern graph H?) - Configuration: injective mapping from pattern vertices to host vertices - Register in CLI (dispatch, alias, create with --pattern flag) - Add problem-def entry in paper - 13 unit tests covering creation, evaluation, solver, serialization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove plan files after implementation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address Copilot review comments for SubgraphIsomorphism - Short-circuit dims()/evaluate() when pattern is larger than host - Add self-loop validation in CLI pattern edge parsing - Regenerate problem_schemas.json to include new model Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: align SubgraphIsomorphism schema field names with CLI flags Schema help now suggests --graph/--pattern instead of --host-graph/--pattern-graph, matching the actual CLI argument names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: fix formatting issues caught by rustfmt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix NP-completeness attribution: cite Garey & Johnson instead of Cook 1971 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent e652ba6 commit c5d8db9

11 files changed

Lines changed: 477 additions & 3 deletions

File tree

docs/paper/reductions.typ

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@
5555
"ClosestVectorProblem": [Closest Vector Problem],
5656
"RuralPostman": [Rural Postman],
5757
"LongestCommonSubsequence": [Longest Common Subsequence],
58-
"SubsetSum": [Subset Sum],
5958
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
59+
"SubgraphIsomorphism": [Subgraph Isomorphism],
60+
"SubsetSum": [Subset Sum],
6061
)
6162

6263
// Definition label: "def:<ProblemName>" — each definition block must have a matching label
@@ -990,6 +991,14 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa
990991
*Example.* Consider a hexagonal graph with 6 vertices and 8 edges, where all outer edges have length 1 and two diagonal edges have length 2. The required edges are $E' = {(v_0, v_1), (v_2, v_3), (v_4, v_5)}$ with bound $B = 6$. The outer cycle $v_0 -> v_1 -> v_2 -> v_3 -> v_4 -> v_5 -> v_0$ covers all three required edges with total length $6 times 1 = 6 = B$, so the answer is YES.
991992
]
992993

994+
#problem-def("SubgraphIsomorphism")[
995+
Given graphs $G = (V_1, E_1)$ (host) and $H = (V_2, E_2)$ (pattern), determine whether $G$ contains a subgraph isomorphic to $H$: does there exist an injective function $f: V_2 -> V_1$ such that ${u, v} in E_2 arrow.double {f(u), f(v)} in E_1$?
996+
][
997+
Subgraph Isomorphism (GT48 in Garey & Johnson @garey1979) is NP-complete by transformation from Clique @garey1979. It strictly generalizes Clique (where $H = K_k$) and also contains Hamiltonian Circuit ($H = C_n$) and Hamiltonian Path ($H = P_n$) as special cases. Brute-force enumeration of all injective mappings $f: V_2 -> V_1$ runs in $O(|V_1|^(|V_2|) dot |E_2|)$ time. For fixed-size patterns, the color-coding technique of Alon, Yuster, and Zwick @alon1995 gives a randomized algorithm in $2^(O(|V_2|)) dot |V_1|^(O("tw"(H)))$ time. Practical algorithms include VF2 @cordella2004 and VF2++ @juttner2018.
998+
999+
*Example.* Consider host graph $G$ with 7 vertices: a $K_4$ clique on ${0, 1, 2, 3}$ and a triangle on ${4, 5, 6}$ connected via edge $(3, 4)$. Pattern $H = K_4$ with vertices ${a, b, c, d}$. The mapping $f(a) = 0, f(b) = 1, f(c) = 2, f(d) = 3$ preserves all 6 edges of $K_4$, confirming a subgraph isomorphism exists.
1000+
]
1001+
9931002
#problem-def("LongestCommonSubsequence")[
9941003
Given $k$ strings $s_1, dots, s_k$ over a finite alphabet $Sigma$, find a longest string $w$ that is a subsequence of every $s_i$. A string $w$ is a _subsequence_ of $s$ if $w$ can be obtained by deleting zero or more characters from $s$ without changing the order of the remaining characters.
9951004
][

docs/src/reductions/problem_schemas.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,22 @@
461461
}
462462
]
463463
},
464+
{
465+
"name": "SubgraphIsomorphism",
466+
"description": "Determine if host graph G contains a subgraph isomorphic to pattern graph H",
467+
"fields": [
468+
{
469+
"name": "graph",
470+
"type_name": "SimpleGraph",
471+
"description": "The host graph G = (V_1, E_1) to search in"
472+
},
473+
{
474+
"name": "pattern",
475+
"type_name": "SimpleGraph",
476+
"description": "The pattern graph H = (V_2, E_2) to find as a subgraph"
477+
}
478+
]
479+
},
464480
{
465481
"name": "SubsetSum",
466482
"description": "Find a subset of integers that sums to exactly a target value",

problemreductions-cli/src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ Flags by problem type:
220220
BMF --matrix (0/1), --rank
221221
CVP --basis, --target-vec [--bounds]
222222
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
223+
SubgraphIsomorphism --graph (host), --pattern (pattern)
223224
LCS --strings
224225
FVS --arcs [--weights] [--num-vertices]
225226
ILP, CircuitSAT (via reduction only)
@@ -339,6 +340,9 @@ pub struct CreateArgs {
339340
/// Upper bound B for RuralPostman
340341
#[arg(long)]
341342
pub bound: Option<i32>,
343+
/// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0)
344+
#[arg(long)]
345+
pub pattern: Option<String>,
342346
/// Input strings for LCS (semicolon-separated, e.g., "ABAC;BACA")
343347
#[arg(long)]
344348
pub strings: Option<String>,

problemreductions-cli/src/commands/create.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
4949
&& args.bounds.is_none()
5050
&& args.required_edges.is_none()
5151
&& args.bound.is_none()
52+
&& args.pattern.is_none()
5253
&& args.strings.is_none()
5354
&& args.arcs.is_none()
5455
}
@@ -96,6 +97,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
9697
"RuralPostman" => {
9798
"--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4"
9899
}
100+
"SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1",
99101
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
100102
_ => "",
101103
}
@@ -540,6 +542,50 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
540542
)
541543
}
542544

545+
// SubgraphIsomorphism
546+
"SubgraphIsomorphism" => {
547+
let (host_graph, _) = parse_graph(args).map_err(|e| {
548+
anyhow::anyhow!(
549+
"{e}\n\nUsage: pred create SubgraphIsomorphism --graph 0-1,1-2,2-0 --pattern 0-1"
550+
)
551+
})?;
552+
let pattern_str = args.pattern.as_deref().ok_or_else(|| {
553+
anyhow::anyhow!(
554+
"SubgraphIsomorphism requires --pattern (pattern graph edges)\n\n\
555+
Usage: pred create SubgraphIsomorphism --graph 0-1,1-2,2-0 --pattern 0-1"
556+
)
557+
})?;
558+
let pattern_edges: Vec<(usize, usize)> = pattern_str
559+
.split(',')
560+
.map(|pair| {
561+
let parts: Vec<&str> = pair.trim().split('-').collect();
562+
if parts.len() != 2 {
563+
bail!("Invalid edge '{}': expected format u-v", pair.trim());
564+
}
565+
let u: usize = parts[0].parse()?;
566+
let v: usize = parts[1].parse()?;
567+
if u == v {
568+
bail!(
569+
"Invalid edge '{}': self-loops are not allowed in simple graphs",
570+
pair.trim()
571+
);
572+
}
573+
Ok((u, v))
574+
})
575+
.collect::<Result<Vec<_>>>()?;
576+
let pattern_nv = pattern_edges
577+
.iter()
578+
.flat_map(|(u, v)| [*u, *v])
579+
.max()
580+
.map(|m| m + 1)
581+
.unwrap_or(0);
582+
let pattern_graph = SimpleGraph::new(pattern_nv, pattern_edges);
583+
(
584+
ser(SubgraphIsomorphism::new(host_graph, pattern_graph))?,
585+
resolved_variant.clone(),
586+
)
587+
}
588+
543589
// PartitionIntoTriangles
544590
"PartitionIntoTriangles" => {
545591
let (graph, _) = parse_graph(args).map_err(|e| {

problemreductions-cli/src/dispatch.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ pub fn load_problem(
247247
_ => deser_opt::<ClosestVectorProblem<i32>>(data),
248248
},
249249
"Knapsack" => deser_opt::<Knapsack>(data),
250+
"SubgraphIsomorphism" => deser_sat::<SubgraphIsomorphism>(data),
250251
"PartitionIntoTriangles" => deser_sat::<PartitionIntoTriangles<SimpleGraph>>(data),
251252
"LongestCommonSubsequence" => deser_opt::<LongestCommonSubsequence>(data),
252253
"MinimumFeedbackVertexSet" => deser_opt::<MinimumFeedbackVertexSet<i32>>(data),
@@ -313,6 +314,7 @@ pub fn serialize_any_problem(
313314
_ => try_ser::<ClosestVectorProblem<i32>>(any),
314315
},
315316
"Knapsack" => try_ser::<Knapsack>(any),
317+
"SubgraphIsomorphism" => try_ser::<SubgraphIsomorphism>(any),
316318
"PartitionIntoTriangles" => try_ser::<PartitionIntoTriangles<SimpleGraph>>(any),
317319
"LongestCommonSubsequence" => try_ser::<LongestCommonSubsequence>(any),
318320
"MinimumFeedbackVertexSet" => try_ser::<MinimumFeedbackVertexSet<i32>>(any),

problemreductions-cli/src/problem_name.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ pub fn resolve_alias(input: &str) -> String {
5757
"binpacking" => "BinPacking".to_string(),
5858
"cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(),
5959
"knapsack" => "Knapsack".to_string(),
60+
"subgraphisomorphism" => "SubgraphIsomorphism".to_string(),
6061
"partitionintotriangles" => "PartitionIntoTriangles".to_string(),
6162
"lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(),
6263
"fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(),

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ pub mod prelude {
4040
// Problem types
4141
pub use crate::models::algebraic::{BMF, QUBO};
4242
pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
43-
pub use crate::models::graph::{BicliqueCover, GraphPartitioning, SpinGlass};
43+
pub use crate::models::graph::{
44+
BicliqueCover, GraphPartitioning, SpinGlass, SubgraphIsomorphism,
45+
};
4446
pub use crate::models::graph::{
4547
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
4648
MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover,

src/models/graph/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
//! - [`SpinGlass`]: Ising model Hamiltonian
1717
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
1818
//! - [`RuralPostman`]: Rural Postman (circuit covering required edges)
19+
//! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem)
1920
2021
pub(crate) mod biclique_cover;
2122
pub(crate) mod graph_partitioning;
@@ -31,6 +32,7 @@ pub(crate) mod minimum_vertex_cover;
3132
pub(crate) mod partition_into_triangles;
3233
pub(crate) mod rural_postman;
3334
pub(crate) mod spin_glass;
35+
pub(crate) mod subgraph_isomorphism;
3436
pub(crate) mod traveling_salesman;
3537

3638
pub use biclique_cover::BicliqueCover;
@@ -47,4 +49,5 @@ pub use minimum_vertex_cover::MinimumVertexCover;
4749
pub use partition_into_triangles::PartitionIntoTriangles;
4850
pub use rural_postman::RuralPostman;
4951
pub use spin_glass::SpinGlass;
52+
pub use subgraph_isomorphism::SubgraphIsomorphism;
5053
pub use traveling_salesman::TravelingSalesman;
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//! SubgraphIsomorphism problem implementation.
2+
//!
3+
//! The Subgraph Isomorphism problem asks whether a "pattern" graph H can be
4+
//! found embedded within a "host" graph G as a subgraph — that is, whether
5+
//! there exists an injective mapping f: V(H) -> V(G) such that every edge
6+
//! {u,v} in H maps to an edge {f(u),f(v)} in G.
7+
8+
use crate::registry::{FieldInfo, ProblemSchemaEntry};
9+
use crate::topology::{Graph, SimpleGraph};
10+
use crate::traits::{Problem, SatisfactionProblem};
11+
use serde::{Deserialize, Serialize};
12+
13+
inventory::submit! {
14+
ProblemSchemaEntry {
15+
name: "SubgraphIsomorphism",
16+
module_path: module_path!(),
17+
description: "Determine if host graph G contains a subgraph isomorphic to pattern graph H",
18+
fields: &[
19+
FieldInfo { name: "graph", type_name: "SimpleGraph", description: "The host graph G = (V_1, E_1) to search in" },
20+
FieldInfo { name: "pattern", type_name: "SimpleGraph", description: "The pattern graph H = (V_2, E_2) to find as a subgraph" },
21+
],
22+
}
23+
}
24+
25+
/// The Subgraph Isomorphism problem.
26+
///
27+
/// Given a host graph G = (V_1, E_1) and a pattern graph H = (V_2, E_2),
28+
/// determine whether there exists an injective function f: V_2 -> V_1 such
29+
/// that for every edge {u,v} in E_2, {f(u), f(v)} is an edge in E_1.
30+
///
31+
/// This is a satisfaction (decision) problem: the metric is `bool`.
32+
///
33+
/// # Configuration
34+
///
35+
/// A configuration is a vector of length |V_2| where each entry is a value
36+
/// in {0, ..., |V_1|-1} representing the host vertex that each pattern
37+
/// vertex maps to. The configuration is valid (true) if:
38+
/// 1. All mapped host vertices are distinct (injective mapping)
39+
/// 2. Every edge in the pattern graph maps to an edge in the host graph
40+
///
41+
/// # Example
42+
///
43+
/// ```
44+
/// use problemreductions::models::graph::SubgraphIsomorphism;
45+
/// use problemreductions::topology::SimpleGraph;
46+
/// use problemreductions::{Problem, Solver, BruteForce};
47+
///
48+
/// // Host: K4 (complete graph on 4 vertices)
49+
/// let host = SimpleGraph::new(4, vec![(0,1),(0,2),(0,3),(1,2),(1,3),(2,3)]);
50+
/// // Pattern: triangle (K3)
51+
/// let pattern = SimpleGraph::new(3, vec![(0,1),(0,2),(1,2)]);
52+
/// let problem = SubgraphIsomorphism::new(host, pattern);
53+
///
54+
/// // Mapping [0, 1, 2] means pattern vertex 0->host 0, 1->1, 2->2
55+
/// assert!(problem.evaluate(&[0, 1, 2]));
56+
///
57+
/// let solver = BruteForce::new();
58+
/// let solution = solver.find_satisfying(&problem);
59+
/// assert!(solution.is_some());
60+
/// ```
61+
#[derive(Debug, Clone, Serialize, Deserialize)]
62+
pub struct SubgraphIsomorphism {
63+
/// The host graph G = (V_1, E_1).
64+
host_graph: SimpleGraph,
65+
/// The pattern graph H = (V_2, E_2).
66+
pattern_graph: SimpleGraph,
67+
}
68+
69+
impl SubgraphIsomorphism {
70+
/// Create a new SubgraphIsomorphism problem.
71+
///
72+
/// # Arguments
73+
/// * `host_graph` - The host graph to search in
74+
/// * `pattern_graph` - The pattern graph to find as a subgraph
75+
pub fn new(host_graph: SimpleGraph, pattern_graph: SimpleGraph) -> Self {
76+
Self {
77+
host_graph,
78+
pattern_graph,
79+
}
80+
}
81+
82+
/// Get a reference to the host graph.
83+
pub fn host_graph(&self) -> &SimpleGraph {
84+
&self.host_graph
85+
}
86+
87+
/// Get a reference to the pattern graph.
88+
pub fn pattern_graph(&self) -> &SimpleGraph {
89+
&self.pattern_graph
90+
}
91+
92+
/// Get the number of vertices in the host graph.
93+
pub fn num_host_vertices(&self) -> usize {
94+
self.host_graph.num_vertices()
95+
}
96+
97+
/// Get the number of edges in the host graph.
98+
pub fn num_host_edges(&self) -> usize {
99+
self.host_graph.num_edges()
100+
}
101+
102+
/// Get the number of vertices in the pattern graph.
103+
pub fn num_pattern_vertices(&self) -> usize {
104+
self.pattern_graph.num_vertices()
105+
}
106+
107+
/// Get the number of edges in the pattern graph.
108+
pub fn num_pattern_edges(&self) -> usize {
109+
self.pattern_graph.num_edges()
110+
}
111+
112+
/// Check if a configuration represents a valid subgraph isomorphism.
113+
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
114+
self.evaluate(config)
115+
}
116+
}
117+
118+
impl Problem for SubgraphIsomorphism {
119+
const NAME: &'static str = "SubgraphIsomorphism";
120+
type Metric = bool;
121+
122+
fn dims(&self) -> Vec<usize> {
123+
let n_host = self.host_graph.num_vertices();
124+
let n_pattern = self.pattern_graph.num_vertices();
125+
126+
if n_pattern > n_host {
127+
// No injective mapping possible: each variable gets an empty domain.
128+
vec![0; n_pattern]
129+
} else {
130+
vec![n_host; n_pattern]
131+
}
132+
}
133+
134+
fn evaluate(&self, config: &[usize]) -> bool {
135+
let n_pattern = self.pattern_graph.num_vertices();
136+
let n_host = self.host_graph.num_vertices();
137+
138+
// If the pattern has more vertices than the host, no injective mapping exists.
139+
if n_pattern > n_host {
140+
return false;
141+
}
142+
143+
// Config must have one entry per pattern vertex
144+
if config.len() != n_pattern {
145+
return false;
146+
}
147+
148+
// All values must be valid host vertex indices
149+
if config.iter().any(|&v| v >= n_host) {
150+
return false;
151+
}
152+
153+
// Check injectivity: all mapped host vertices must be distinct
154+
for i in 0..n_pattern {
155+
for j in (i + 1)..n_pattern {
156+
if config[i] == config[j] {
157+
return false;
158+
}
159+
}
160+
}
161+
162+
// Check edge preservation: every pattern edge must map to a host edge
163+
for (u, v) in self.pattern_graph.edges() {
164+
if !self.host_graph.has_edge(config[u], config[v]) {
165+
return false;
166+
}
167+
}
168+
169+
true
170+
}
171+
172+
fn variant() -> Vec<(&'static str, &'static str)> {
173+
crate::variant_params![]
174+
}
175+
}
176+
177+
impl SatisfactionProblem for SubgraphIsomorphism {}
178+
179+
crate::declare_variants! {
180+
SubgraphIsomorphism => "num_host_vertices ^ num_pattern_vertices",
181+
}
182+
183+
#[cfg(test)]
184+
#[path = "../../unit_tests/models/graph/subgraph_isomorphism.rs"]
185+
mod tests;

src/models/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
1414
pub use graph::{
1515
BicliqueCover, GraphPartitioning, KColoring, MaxCut, MaximalIS, MaximumClique,
1616
MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet,
17-
MinimumVertexCover, PartitionIntoTriangles, RuralPostman, SpinGlass, TravelingSalesman,
17+
MinimumVertexCover, PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism,
18+
TravelingSalesman,
1819
};
1920
pub use misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum};
2021
pub use set::{MaximumSetPacking, MinimumSetCovering};

0 commit comments

Comments
 (0)