Skip to content

Commit a977548

Browse files
GiggleLiuzazabapclaude
authored
Fix #298: [Model] LengthBoundedDisjointPaths (#659)
* Add plan for #298: [Model] LengthBoundedDisjointPaths * Implement #298: [Model] LengthBoundedDisjointPaths * Fix review findings for #298 * chore: remove plan file after implementation * Fix CLI help for #298 * fix: address PR #659 review feedback - validate LengthBoundedDisjointPaths CLI inputs consistently - add regression coverage for constructor and CLI edge cases - document the LengthBoundedDisjointPaths CLI flow and local run path * fix: CLI source==sink validation, coverage tests, revert unrelated README changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zazabap <sweynan@icloud.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d922f80 commit a977548

15 files changed

Lines changed: 1071 additions & 110 deletions

File tree

docs/paper/reductions.typ

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"MaxCut": [Max-Cut],
6868
"GraphPartitioning": [Graph Partitioning],
6969
"HamiltonianPath": [Hamiltonian Path],
70+
"LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths],
7071
"IsomorphicSpanningTree": [Isomorphic Spanning Tree],
7172
"KColoring": [$k$-Coloring],
7273
"MinimumDominatingSet": [Minimum Dominating Set],
@@ -531,6 +532,67 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co
531532
caption: [Graph with $n = 6$ vertices partitioned into $A = {v_0, v_1, v_2}$ (blue) and $B = {v_3, v_4, v_5}$ (red). The 3 crossing edges $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$ are shown in bold red; internal edges are gray.],
532533
) <fig:graph-partitioning>
533534
]
535+
#{
536+
let x = load-model-example("LengthBoundedDisjointPaths")
537+
let nv = graph-num-vertices(x.instance)
538+
let ne = graph-num-edges(x.instance)
539+
let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1)))
540+
let s = x.instance.source
541+
let t = x.instance.sink
542+
let J = x.instance.num_paths_required
543+
let K = x.instance.max_length
544+
let chosen-verts = (0, 1, 2, 3, 6)
545+
let chosen-edges = ((0, 1), (1, 6), (0, 2), (2, 3), (3, 6))
546+
[
547+
#problem-def("LengthBoundedDisjointPaths")[
548+
Given an undirected graph $G = (V, E)$, distinct terminals $s, t in V$, and positive integers $J, K$, determine whether $G$ contains at least $J$ pairwise internally vertex-disjoint paths from $s$ to $t$, each using at most $K$ edges.
549+
][
550+
Length-Bounded Disjoint Paths is the bounded-routing version of the classical disjoint-path problem, with applications in network routing and VLSI where multiple connections must fit simultaneously under quality-of-service limits. Garey & Johnson list it as ND41 and summarize the sharp threshold proved by Itai, Perl, and Shiloach: the problem is NP-complete for every fixed $K >= 5$, polynomial-time solvable for $K <= 4$, and becomes polynomial again when the length bound is removed entirely @garey1979. The implementation here uses the natural $J dot |V|$ binary membership encoding, so brute-force search over configurations runs in $O^*(2^(J dot |V|))$.
551+
552+
*Example.* Consider the graph $G$ with $n = #nv$ vertices, $|E| = #ne$ edges, terminals $s = v_#s$, $t = v_#t$, $J = #J$, and $K = #K$. The two paths $P_1 = v_0 arrow v_1 arrow v_6$ and $P_2 = v_0 arrow v_2 arrow v_3 arrow v_6$ are both of length at most 3, and their internal vertex sets ${v_1}$ and ${v_2, v_3}$ are disjoint. Hence this instance is satisfying. The third branch $v_0 arrow v_4 arrow v_5 arrow v_6$ is available but unused, so the instance has multiple satisfying path-slot assignments.
553+
554+
#figure(
555+
canvas(length: 1cm, {
556+
let blue = graph-colors.at(0)
557+
let gray = luma(180)
558+
let verts = (
559+
(0, 1), // v0 = s
560+
(1.3, 1.8),
561+
(1.3, 1.0),
562+
(2.6, 1.0),
563+
(1.3, 0.2),
564+
(2.6, 0.2),
565+
(3.9, 1), // v6 = t
566+
)
567+
for (u, v) in edges {
568+
let selected = chosen-edges.any(e =>
569+
(e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u)
570+
)
571+
g-edge(verts.at(u), verts.at(v),
572+
stroke: if selected { 2pt + blue } else { 1pt + gray })
573+
}
574+
for (k, pos) in verts.enumerate() {
575+
let active = chosen-verts.contains(k)
576+
g-node(pos, name: "v" + str(k),
577+
fill: if active { blue } else { white },
578+
label: if active {
579+
text(fill: white)[
580+
#if k == s { $s$ }
581+
else if k == t { $t$ }
582+
else { $v_#k$ }
583+
]
584+
} else [
585+
#if k == s { $s$ }
586+
else if k == t { $t$ }
587+
else { $v_#k$ }
588+
])
589+
}
590+
}),
591+
caption: [A satisfying Length-Bounded Disjoint Paths instance with $s = v_0$, $t = v_6$, $J = 2$, and $K = 3$. The highlighted paths are $v_0 arrow v_1 arrow v_6$ and $v_0 arrow v_2 arrow v_3 arrow v_6$; the lower branch through $v_4, v_5$ remains unused.],
592+
) <fig:length-bounded-disjoint-paths>
593+
]
594+
]
595+
}
534596
#{
535597
let x = load-model-example("HamiltonianPath")
536598
let nv = graph-num-vertices(x.instance)

docs/src/cli.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ Or build from source:
1515
```bash
1616
git clone https://github.com/CodingThrust/problem-reductions
1717
cd problem-reductions
18-
make cli # builds target/release/pred
18+
cargo build -p problemreductions-cli --release # builds target/release/pred
19+
cargo install --path problemreductions-cli # optional: installs `pred` to ~/.cargo/bin
1920
```
2021

2122
Verify the installation:
@@ -24,6 +25,12 @@ Verify the installation:
2425
pred --version
2526
```
2627

28+
For a workspace-local run without installing globally, use:
29+
30+
```bash
31+
cargo run -p problemreductions-cli --bin pred -- --version
32+
```
33+
2734
### ILP Backend
2835

2936
The default ILP backend is HiGHS. To use a different backend:
@@ -48,6 +55,9 @@ pred create MIS --graph 0-1,1-2,2-3 --weights 3,1,2,1 -o weighted.json
4855
# Create a Steiner Tree instance
4956
pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2,1,5,6,1 --terminals 0,2,4 -o steiner.json
5057

58+
# Create a Length-Bounded Disjoint Paths instance
59+
pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3 -o lbdp.json
60+
5161
# Or start from a canonical model example
5262
pred create --example MIS/SimpleGraph/i32 -o example.json
5363

@@ -57,12 +67,18 @@ pred create --example MVC/SimpleGraph/i32 --to MIS/SimpleGraph/i32 -o example.js
5767
# Inspect what's inside a problem file
5868
pred inspect problem.json
5969

70+
# Inspect the new path problem
71+
pred inspect lbdp.json
72+
6073
# Solve it (auto-reduces to ILP)
6174
pred solve problem.json
6275

6376
# Or solve with brute-force
6477
pred solve problem.json --solver brute-force
6578

79+
# LengthBoundedDisjointPaths currently needs brute-force
80+
pred solve lbdp.json --solver brute-force
81+
6682
# Evaluate a specific configuration (shows Valid(N) or Invalid)
6783
pred evaluate problem.json --config 1,0,1,0
6884

@@ -276,12 +292,16 @@ pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json
276292
pred create SpinGlass --graph 0-1,1-2 -o sg.json
277293
pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json
278294
pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2,1,5,6,1 --terminals 0,2,4 -o steiner.json
295+
pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3 -o lbdp.json
279296
pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json
280297
pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json
281298
pred create X3C --universe 9 --sets "0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8" -o x3c.json
282299
pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json
283300
```
284301

302+
For `LengthBoundedDisjointPaths`, the CLI flag `--bound` maps to the JSON field
303+
`max_length`.
304+
285305
Canonical examples are useful when you want a known-good instance from the paper/example database.
286306
For model examples, `pred create --example <PROBLEM_SPEC>` emits the canonical instance for that
287307
graph node.
@@ -418,8 +438,9 @@ Source evaluation: Valid(2)
418438
```
419439

420440
> **Note:** The ILP solver requires a reduction path from the target problem to ILP.
421-
> Some problems (e.g., QUBO, SpinGlass, MaxCut, CircuitSAT) do not have this path yet.
422-
> Use `--solver brute-force` for these, or reduce to a problem that supports ILP first.
441+
> `LengthBoundedDisjointPaths` does not currently have one, so use
442+
> `pred solve lbdp.json --solver brute-force`.
443+
> For other problems, use `pred path <PROBLEM> ILP` to check whether an ILP reduction path exists.
423444
424445
## Shell Completions
425446

docs/src/reductions/problem_schemas.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,37 @@
259259
}
260260
]
261261
},
262+
{
263+
"name": "LengthBoundedDisjointPaths",
264+
"description": "Find J internally vertex-disjoint s-t paths of length at most K",
265+
"fields": [
266+
{
267+
"name": "graph",
268+
"type_name": "G",
269+
"description": "The underlying graph G=(V,E)"
270+
},
271+
{
272+
"name": "source",
273+
"type_name": "usize",
274+
"description": "The shared source vertex s"
275+
},
276+
{
277+
"name": "sink",
278+
"type_name": "usize",
279+
"description": "The shared sink vertex t"
280+
},
281+
{
282+
"name": "num_paths_required",
283+
"type_name": "usize",
284+
"description": "Required number J of disjoint s-t paths"
285+
},
286+
{
287+
"name": "max_length",
288+
"type_name": "usize",
289+
"description": "Maximum path length K in edges"
290+
}
291+
]
292+
},
262293
{
263294
"name": "LongestCommonSubsequence",
264295
"description": "Find the longest string that is a subsequence of every input string",

0 commit comments

Comments
 (0)