Skip to content

Commit 3fc6e47

Browse files
committed
Merge branch 'main' of github.com:CodingThrust/problem-reductions
2 parents f5d39fb + c5d8db9 commit 3fc6e47

15 files changed

Lines changed: 1376 additions & 4 deletions

File tree

docs/paper/reductions.typ

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,11 @@
5353
"BicliqueCover": [Biclique Cover],
5454
"BinPacking": [Bin Packing],
5555
"ClosestVectorProblem": [Closest Vector Problem],
56+
"RuralPostman": [Rural Postman],
5657
"LongestCommonSubsequence": [Longest Common Subsequence],
57-
"SubsetSum": [Subset Sum],
5858
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
59+
"SubgraphIsomorphism": [Subgraph Isomorphism],
60+
"SubsetSum": [Subset Sum],
5961
)
6062

6163
// Definition label: "def:<ProblemName>" — each definition block must have a matching label
@@ -981,6 +983,22 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa
981983
*Example.* Let $n = 4$ items with weights $(2, 3, 4, 5)$, values $(3, 4, 5, 7)$, and capacity $C = 7$. Selecting $S = {1, 2}$ (items with weights 3 and 4) gives total weight $3 + 4 = 7 lt.eq C$ and total value $4 + 5 = 9$. Selecting $S = {0, 3}$ (weights 2 and 5) gives weight $2 + 5 = 7 lt.eq C$ and value $3 + 7 = 10$, which is optimal.
982984
]
983985

986+
#problem-def("RuralPostman")[
987+
Given an undirected graph $G = (V, E)$ with edge lengths $l: E -> ZZ_(gt.eq 0)$, a subset $E' subset.eq E$ of required edges, and a bound $B in ZZ^+$, determine whether there exists a circuit (closed walk) in $G$ that traverses every edge in $E'$ and has total length at most $B$.
988+
][
989+
The Rural Postman Problem (RPP) is a fundamental NP-complete arc-routing problem @lenstra1976 that generalizes the Chinese Postman Problem. When $E' = E$, the problem reduces to finding an Eulerian circuit with minimum augmentation (polynomial-time solvable via $T$-join matching). For general $E' subset.eq E$, exact algorithms use dynamic programming over subsets of required edges in $O(n^2 dot 2^r)$ time, where $r = |E'|$ and $n = |V|$, analogous to the Held-Karp algorithm for TSP. The problem admits a $3 slash 2$-approximation for metric instances @frederickson1979.
990+
991+
*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.
992+
]
993+
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+
9841002
#problem-def("LongestCommonSubsequence")[
9851003
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.
9861004
][

docs/src/reductions/problem_schemas.json

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,17 @@
195195
}
196196
]
197197
},
198+
{
199+
"name": "LongestCommonSubsequence",
200+
"description": "Find the longest string that is a subsequence of every input string",
201+
"fields": [
202+
{
203+
"name": "strings",
204+
"type_name": "Vec<Vec<u8>>",
205+
"description": "The input strings"
206+
}
207+
]
208+
},
198209
{
199210
"name": "MaxCut",
200211
"description": "Find maximum weight cut in a graph",
@@ -387,6 +398,32 @@
387398
}
388399
]
389400
},
401+
{
402+
"name": "RuralPostman",
403+
"description": "Find a circuit covering required edges with total length at most B (Rural Postman Problem)",
404+
"fields": [
405+
{
406+
"name": "graph",
407+
"type_name": "G",
408+
"description": "The underlying graph G=(V,E)"
409+
},
410+
{
411+
"name": "edge_weights",
412+
"type_name": "Vec<W>",
413+
"description": "Edge lengths l(e) for each e in E"
414+
},
415+
{
416+
"name": "required_edges",
417+
"type_name": "Vec<usize>",
418+
"description": "Edge indices of the required subset E' ⊆ E"
419+
},
420+
{
421+
"name": "bound",
422+
"type_name": "W::Sum",
423+
"description": "Upper bound B on total circuit length"
424+
}
425+
]
426+
},
390427
{
391428
"name": "Satisfiability",
392429
"description": "Find satisfying assignment for CNF formula",
@@ -424,6 +461,22 @@
424461
}
425462
]
426463
},
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+
},
427480
{
428481
"name": "SubsetSum",
429482
"description": "Find a subset of integers that sums to exactly a target value",

problemreductions-cli/src/cli.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ Flags by problem type:
208208
QUBO --matrix
209209
SpinGlass --graph, --couplings, --fields
210210
KColoring --graph, --k
211+
PartitionIntoTriangles --graph
211212
GraphPartitioning --graph
212213
Factoring --target, --m, --n
213214
BinPacking --sizes, --capacity
@@ -218,6 +219,8 @@ Flags by problem type:
218219
BicliqueCover --left, --right, --biedges, --k
219220
BMF --matrix (0/1), --rank
220221
CVP --basis, --target-vec [--bounds]
222+
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
223+
SubgraphIsomorphism --graph (host), --pattern (pattern)
221224
LCS --strings
222225
FVS --arcs [--weights] [--num-vertices]
223226
ILP, CircuitSAT (via reduction only)
@@ -331,6 +334,15 @@ pub struct CreateArgs {
331334
/// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10]
332335
#[arg(long, allow_hyphen_values = true)]
333336
pub bounds: Option<String>,
337+
/// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4")
338+
#[arg(long)]
339+
pub required_edges: Option<String>,
340+
/// Upper bound B for RuralPostman
341+
#[arg(long)]
342+
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>,
334346
/// Input strings for LCS (semicolon-separated, e.g., "ABAC;BACA")
335347
#[arg(long)]
336348
pub strings: Option<String>,

problemreductions-cli/src/commands/create.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
4747
&& args.basis.is_none()
4848
&& args.target_vec.is_none()
4949
&& args.bounds.is_none()
50+
&& args.required_edges.is_none()
51+
&& args.bound.is_none()
52+
&& args.pattern.is_none()
5053
&& args.strings.is_none()
5154
&& args.arcs.is_none()
5255
}
@@ -89,7 +92,12 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
8992
"QUBO" => "--matrix \"1,0.5;0.5,2\"",
9093
"SpinGlass" => "--graph 0-1,1-2 --couplings 1,1",
9194
"KColoring" => "--graph 0-1,1-2,2-0 --k 3",
95+
"PartitionIntoTriangles" => "--graph 0-1,1-2,0-2",
9296
"Factoring" => "--target 15 --m 4 --n 4",
97+
"RuralPostman" => {
98+
"--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4"
99+
}
100+
"SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1",
93101
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
94102
_ => "",
95103
}
@@ -231,6 +239,38 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
231239
(data, resolved_variant.clone())
232240
}
233241

242+
// RuralPostman
243+
"RuralPostman" => {
244+
let (graph, _) = parse_graph(args).map_err(|e| {
245+
anyhow::anyhow!(
246+
"{e}\n\nUsage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2 --bound 6"
247+
)
248+
})?;
249+
let edge_weights = parse_edge_weights(args, graph.num_edges())?;
250+
let required_edges_str = args.required_edges.as_deref().ok_or_else(|| {
251+
anyhow::anyhow!(
252+
"RuralPostman requires --required-edges\n\n\
253+
Usage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2 --bound 6"
254+
)
255+
})?;
256+
let required_edges: Vec<usize> = util::parse_comma_list(required_edges_str)?;
257+
let bound = args.bound.ok_or_else(|| {
258+
anyhow::anyhow!(
259+
"RuralPostman requires --bound\n\n\
260+
Usage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2 --bound 6"
261+
)
262+
})?;
263+
(
264+
ser(RuralPostman::new(
265+
graph,
266+
edge_weights,
267+
required_edges,
268+
bound,
269+
))?,
270+
resolved_variant.clone(),
271+
)
272+
}
273+
234274
// KColoring
235275
"KColoring" => {
236276
let (graph, _) = parse_graph(args).map_err(|e| {
@@ -502,6 +542,68 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
502542
)
503543
}
504544

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+
589+
// PartitionIntoTriangles
590+
"PartitionIntoTriangles" => {
591+
let (graph, _) = parse_graph(args).map_err(|e| {
592+
anyhow::anyhow!(
593+
"{e}\n\nUsage: pred create PartitionIntoTriangles --graph 0-1,1-2,0-2"
594+
)
595+
})?;
596+
anyhow::ensure!(
597+
graph.num_vertices() % 3 == 0,
598+
"PartitionIntoTriangles requires vertex count divisible by 3, got {}",
599+
graph.num_vertices()
600+
);
601+
(
602+
ser(PartitionIntoTriangles::new(graph))?,
603+
resolved_variant.clone(),
604+
)
605+
}
606+
505607
// MinimumFeedbackVertexSet
506608
"MinimumFeedbackVertexSet" => {
507609
let arcs_str = args.arcs.as_ref().ok_or_else(|| {

problemreductions-cli/src/dispatch.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ pub fn load_problem(
214214
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
215215
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
216216
"TravelingSalesman" => deser_opt::<TravelingSalesman<SimpleGraph, i32>>(data),
217+
"RuralPostman" => deser_sat::<RuralPostman<SimpleGraph, i32>>(data),
217218
"KColoring" => match variant.get("k").map(|s| s.as_str()) {
218219
Some("K3") => deser_sat::<KColoring<K3, SimpleGraph>>(data),
219220
_ => deser_sat::<KColoring<KN, SimpleGraph>>(data),
@@ -246,6 +247,8 @@ pub fn load_problem(
246247
_ => deser_opt::<ClosestVectorProblem<i32>>(data),
247248
},
248249
"Knapsack" => deser_opt::<Knapsack>(data),
250+
"SubgraphIsomorphism" => deser_sat::<SubgraphIsomorphism>(data),
251+
"PartitionIntoTriangles" => deser_sat::<PartitionIntoTriangles<SimpleGraph>>(data),
249252
"LongestCommonSubsequence" => deser_opt::<LongestCommonSubsequence>(data),
250253
"MinimumFeedbackVertexSet" => deser_opt::<MinimumFeedbackVertexSet<i32>>(data),
251254
"SubsetSum" => deser_sat::<SubsetSum>(data),
@@ -275,6 +278,7 @@ pub fn serialize_any_problem(
275278
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
276279
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),
277280
"TravelingSalesman" => try_ser::<TravelingSalesman<SimpleGraph, i32>>(any),
281+
"RuralPostman" => try_ser::<RuralPostman<SimpleGraph, i32>>(any),
278282
"KColoring" => match variant.get("k").map(|s| s.as_str()) {
279283
Some("K3") => try_ser::<KColoring<K3, SimpleGraph>>(any),
280284
_ => try_ser::<KColoring<KN, SimpleGraph>>(any),
@@ -310,6 +314,8 @@ pub fn serialize_any_problem(
310314
_ => try_ser::<ClosestVectorProblem<i32>>(any),
311315
},
312316
"Knapsack" => try_ser::<Knapsack>(any),
317+
"SubgraphIsomorphism" => try_ser::<SubgraphIsomorphism>(any),
318+
"PartitionIntoTriangles" => try_ser::<PartitionIntoTriangles<SimpleGraph>>(any),
313319
"LongestCommonSubsequence" => try_ser::<LongestCommonSubsequence>(any),
314320
"MinimumFeedbackVertexSet" => try_ser::<MinimumFeedbackVertexSet<i32>>(any),
315321
"SubsetSum" => try_ser::<SubsetSum>(any),

problemreductions-cli/src/problem_name.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub const ALIASES: &[(&str, &str)] = &[
2020
("KSAT", "KSatisfiability"),
2121
("TSP", "TravelingSalesman"),
2222
("CVP", "ClosestVectorProblem"),
23+
("RPP", "RuralPostman"),
2324
("LCS", "LongestCommonSubsequence"),
2425
("MaxMatching", "MaximumMatching"),
2526
("FVS", "MinimumFeedbackVertexSet"),
@@ -49,12 +50,15 @@ pub fn resolve_alias(input: &str) -> String {
4950
"kcoloring" => "KColoring".to_string(),
5051
"maximalis" => "MaximalIS".to_string(),
5152
"travelingsalesman" | "tsp" => "TravelingSalesman".to_string(),
53+
"ruralpostman" | "rpp" => "RuralPostman".to_string(),
5254
"paintshop" => "PaintShop".to_string(),
5355
"bmf" => "BMF".to_string(),
5456
"bicliquecover" => "BicliqueCover".to_string(),
5557
"binpacking" => "BinPacking".to_string(),
5658
"cvp" | "closestvectorproblem" => "ClosestVectorProblem".to_string(),
5759
"knapsack" => "Knapsack".to_string(),
60+
"subgraphisomorphism" => "SubgraphIsomorphism".to_string(),
61+
"partitionintotriangles" => "PartitionIntoTriangles".to_string(),
5862
"lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(),
5963
"fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(),
6064
"subsetsum" => "SubsetSum".to_string(),

src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,13 @@ 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,
46-
MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman,
48+
MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover,
49+
PartitionIntoTriangles, RuralPostman, TravelingSalesman,
4750
};
4851
pub use crate::models::misc::{
4952
BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum,

src/models/graph/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
//! - [`MaxCut`]: Maximum cut on weighted graphs
1111
//! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning)
1212
//! - [`KColoring`]: K-vertex coloring
13+
//! - [`PartitionIntoTriangles`]: Partition vertices into triangles
1314
//! - [`MaximumMatching`]: Maximum weight matching
1415
//! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle)
1516
//! - [`SpinGlass`]: Ising model Hamiltonian
1617
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
18+
//! - [`RuralPostman`]: Rural Postman (circuit covering required edges)
19+
//! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem)
1720
1821
pub(crate) mod biclique_cover;
1922
pub(crate) mod graph_partitioning;
@@ -26,7 +29,10 @@ pub(crate) mod maximum_matching;
2629
pub(crate) mod minimum_dominating_set;
2730
pub(crate) mod minimum_feedback_vertex_set;
2831
pub(crate) mod minimum_vertex_cover;
32+
pub(crate) mod partition_into_triangles;
33+
pub(crate) mod rural_postman;
2934
pub(crate) mod spin_glass;
35+
pub(crate) mod subgraph_isomorphism;
3036
pub(crate) mod traveling_salesman;
3137

3238
pub use biclique_cover::BicliqueCover;
@@ -40,5 +46,8 @@ pub use maximum_matching::MaximumMatching;
4046
pub use minimum_dominating_set::MinimumDominatingSet;
4147
pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet;
4248
pub use minimum_vertex_cover::MinimumVertexCover;
49+
pub use partition_into_triangles::PartitionIntoTriangles;
50+
pub use rural_postman::RuralPostman;
4351
pub use spin_glass::SpinGlass;
52+
pub use subgraph_isomorphism::SubgraphIsomorphism;
4453
pub use traveling_salesman::TravelingSalesman;

0 commit comments

Comments
 (0)