Skip to content
Merged
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
94 changes: 94 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"BiconnectivityAugmentation": [Biconnectivity Augmentation],
"HamiltonianPath": [Hamiltonian Path],
"LongestCircuit": [Longest Circuit],
"LongestPath": [Longest Path],
"ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path],
"UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow],
"LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths],
Expand Down Expand Up @@ -1072,6 +1073,65 @@ is feasible: each set induces a connected subgraph, the component weights are $2
]
]
}
#{
let x = load-model-example("LongestPath")
let nv = graph-num-vertices(x.instance)
let edges = x.instance.graph.edges
let lengths = x.instance.edge_lengths
let s = x.instance.source_vertex
let t = x.instance.target_vertex
let path-config = x.optimal_config
let path-order = (0, 1, 3, 2, 4, 5, 6)
let path-edges = edges.enumerate().filter(((idx, _)) => path-config.at(idx) == 1).map(((idx, e)) => e)
[
#problem-def("LongestPath")[
Given an undirected graph $G = (V, E)$ with positive edge lengths $l: E -> ZZ^+$ and designated vertices $s, t in V$, find a simple path $P$ from $s$ to $t$ maximizing $sum_(e in P) l(e)$.
][
Longest Path is problem ND29 in Garey & Johnson @garey1979. It bridges weighted routing and Hamiltonicity: when every edge has unit length, the optimum reaches $|V| - 1$ exactly when there is a Hamiltonian path from $s$ to $t$. The implementation catalog records the classical subset-DP exact bound $O(|V| dot 2^|V|)$, in the style of Held--Karp dynamic programming @heldkarp1962. For the parameterized $k$-path version, color-coding gives randomized $2^(O(k)) |V|^(O(1))$ algorithms @alon1995.

Variables: one binary value per edge. A configuration is valid exactly when the selected edges form a single simple $s$-$t$ path; otherwise the metric is `Invalid`. For valid selections, the metric is the total selected edge length.

*Example.* Consider the graph on #nv vertices with source $s = v_#s$ and target $t = v_#t$. The highlighted path $#path-order.map(v => $v_#v$).join($arrow$)$ uses edges ${#path-edges.map(((u, v)) => $(v_#u, v_#v)$).join(", ")}$, so its total length is $3 + 4 + 1 + 5 + 3 + 4 = 20$. Another valid path, $v_0 arrow v_2 arrow v_4 arrow v_5 arrow v_3 arrow v_1 arrow v_6$, has total length $17$, so the highlighted path is strictly better.

#pred-commands(
"pred create --example LongestPath -o longest-path.json",
"pred solve longest-path.json",
"pred evaluate longest-path.json --config " + x.optimal_config.map(str).join(","),
)

#figure({
let blue = graph-colors.at(0)
let gray = luma(200)
let verts = ((0, 1.2), (1.2, 2.0), (1.2, 0.4), (2.5, 2.0), (2.5, 0.4), (3.8, 1.2), (5.0, 1.2))
canvas(length: 1cm, {
import draw: *
for (idx, (u, v)) in edges.enumerate() {
let on-path = path-config.at(idx) == 1
g-edge(verts.at(u), verts.at(v), stroke: if on-path { 2pt + blue } else { 1pt + gray })
let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2
let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2
let dx = if idx == 0 or idx == 2 { 0 } else if idx == 1 or idx == 4 { -0.18 } else if idx == 5 or idx == 6 { 0.18 } else if idx == 8 { 0 } else { 0.16 }
let dy = if idx == 0 or idx == 2 or idx == 5 or idx == 8 { 0.18 } else if idx == 1 or idx == 4 or idx == 6 { -0.18 } else if idx == 3 { 0 } else { 0.16 }
draw.content(
(mx + dx, my + dy),
text(7pt, fill: luma(80))[#str(int(lengths.at(idx)))]
)
}
for (k, pos) in verts.enumerate() {
let on-path = path-order.any(v => v == k)
g-node(pos, name: "v" + str(k),
fill: if on-path { blue } else { white },
label: if on-path { text(fill: white)[$v_#k$] } else { [$v_#k$] })
}
content((0, 1.55), text(8pt)[$s$])
content((5.0, 1.55), text(8pt)[$t$])
})
},
caption: [Longest Path instance with edge lengths shown on the edges. The highlighted path from $s = v_0$ to $t = v_6$ has total length 20.],
) <fig:longest-path>
]
]
}
#{
let x = load-model-example("UndirectedTwoCommodityIntegralFlow")
let satisfying_count = 1
Expand Down Expand Up @@ -6944,6 +7004,40 @@ The following reductions to Integer Linear Programming are straightforward formu
#let tsp_qubo = load-example("TravelingSalesman", "QUBO")
#let tsp_qubo_sol = tsp_qubo.solutions.at(0)

#let lp_ilp = load-example("LongestPath", "ILP")
#let lp_ilp_sol = lp_ilp.solutions.at(0)
#reduction-rule("LongestPath", "ILP",
example: true,
example-caption: [The 3-vertex path $0 arrow 1 arrow 2$ encoded as a 7-variable ILP with optimum 5.],
extra: [
#pred-commands(
"pred create --example LongestPath -o longest-path.json",
"pred reduce longest-path.json --to " + target-spec(lp_ilp) + " -o bundle.json",
"pred solve bundle.json",
"pred evaluate longest-path.json --config " + lp_ilp_sol.source_config.map(str).join(","),
)
*Step 1 -- Orient each undirected edge.* The canonical witness has two source edges, so the reduction creates four directed-arc variables. The optimal witness sets $x_(0,1) = 1$ and $x_(1,2) = 1$, leaving the reverse directions at 0.\

*Step 2 -- Add order variables.* The target has #lp_ilp.target.instance.num_vars variables and #lp_ilp.target.instance.constraints.len() constraints in total. The order block $bold(o) = (#lp_ilp_sol.target_config.slice(4, 7).map(str).join(", "))$ certifies the increasing path positions $0 < 1 < 2$.\

*Step 3 -- Check the objective.* The target witness $bold(z) = (#lp_ilp_sol.target_config.map(str).join(", "))$ selects lengths $2$ and $3$, so the ILP objective is $5$, matching the source optimum. #sym.checkmark
],
)[
A simple $s$-$t$ path can be represented as one unit of directed flow from $s$ to $t$ on oriented copies of the undirected edges. Integer order variables then force the selected arcs to move strictly forward, which forbids detached directed cycles.
][
_Construction._ For graph $G = (V, E)$ with $n = |V|$ and $m = |E|$:

_Variables:_ For each undirected edge ${u, v} in E$, introduce two binary arc variables $x_(u,v), x_(v,u) in {0, 1}$. Interpretation: $x_(u,v) = 1$ iff the path traverses edge ${u, v}$ from $u$ to $v$. For each vertex $v in V$, add an integer order variable $o_v in {0, dots, n-1}$. Total: $2m + n$ variables.

_Constraints:_ (1) Flow balance: $sum_(w : {v,w} in E) x_(v,w) - sum_(u : {u,v} in E) x_(u,v) = 1$ at the source, equals $-1$ at the target, and equals $0$ at every other vertex. (2) Degree bounds: every vertex has at most one selected outgoing arc and at most one selected incoming arc. (3) Edge exclusivity: $x_(u,v) + x_(v,u) <= 1$ for each undirected edge. (4) Ordering: for every oriented edge $u -> v$, $o_v - o_u >= 1 - n(1 - x_(u,v))$. (5) Anchor the path at the source with $o_s = 0$.

_Objective._ Maximize $sum_({u,v} in E) l({u,v}) dot (x_(u,v) + x_(v,u))$.

_Correctness._ ($arrow.r.double$) Any simple $s$-$t$ path can be oriented from $s$ to $t$, giving exactly one outgoing arc at $s$, one incoming arc at $t$, balanced flow at every internal vertex, and strictly increasing order values along the path. ($arrow.l.double$) Any feasible ILP solution satisfies the flow equations and degree bounds, so the selected arcs form vertex-disjoint directed paths and cycles. The ordering inequalities make every selected arc increase the order value by at least 1, so directed cycles are impossible. The only remaining positive-flow component is therefore a single directed $s$-$t$ path, whose objective is exactly the total selected edge length.

_Solution extraction._ For each undirected edge ${u, v}$, select it in the source configuration iff either $x_(u,v)$ or $x_(v,u)$ is 1.
]

#reduction-rule("TravelingSalesman", "QUBO",
example: true,
example-caption: [TSP on $K_3$ with weights $w_(01) = 1$, $w_(02) = 2$, $w_(12) = 3$: the QUBO ground state encodes the optimal tour with cost $1 + 2 + 3 = 6$.],
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ TIP: Run `pred create <PROBLEM>` (no other flags) to see problem-specific help.
Flags by problem type:
MIS, MVC, MaxClique, MinDomSet --graph, --weights
MaxCut, MaxMatching, TSP, BottleneckTravelingSalesman --graph, --edge-weights
LongestPath --graph, --edge-lengths, --source-vertex, --target-vertex
ShortestWeightConstrainedPath --graph, --edge-lengths, --edge-weights, --source-vertex, --target-vertex, --length-bound, --weight-bound
MaximalIS --graph, --weights
SAT, NAESAT --num-vars, --clauses
Expand Down
154 changes: 151 additions & 3 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ use problemreductions::models::algebraic::{
use problemreductions::models::formula::Quantifier;
use problemreductions::models::graph::{
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath,
LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, MinimumMultiwayCut,
MixedChinesePostman, MultipleChoiceBranching, SteinerTree, SteinerTreeInGraphs,
StrongConnectivityAugmentation,
LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets,
MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, SteinerTree,
SteinerTreeInGraphs, StrongConnectivityAugmentation,
};
use problemreductions::models::misc::{
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery,
Expand Down Expand Up @@ -528,6 +528,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--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"
}
"HamiltonianPath" => "--graph 0-1,1-2,2-3",
"LongestPath" => {
"--graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6 --edge-lengths 3,2,4,1,5,2,3,2,4,1 --source-vertex 0 --target-vertex 6"
}
"UndirectedTwoCommodityIntegralFlow" => {
"--graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1"
},
Expand Down Expand Up @@ -1332,6 +1335,39 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
(ser(HamiltonianPath::new(graph))?, resolved_variant.clone())
}

// LongestPath
"LongestPath" => {
let usage = "pred create LongestPath --graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6 --edge-lengths 3,2,4,1,5,2,3,2,4,1 --source-vertex 0 --target-vertex 6";
let (graph, _) =
parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\nUsage: {usage}"))?;
if args.weights.is_some() {
bail!("LongestPath uses --edge-lengths, not --weights\n\nUsage: {usage}");
}
let edge_lengths_raw = args.edge_lengths.as_ref().ok_or_else(|| {
anyhow::anyhow!("LongestPath requires --edge-lengths\n\nUsage: {usage}")
})?;
let edge_lengths =
parse_i32_edge_values(Some(edge_lengths_raw), graph.num_edges(), "edge length")?;
ensure_positive_i32_values(&edge_lengths, "edge lengths")?;
let source_vertex = args.source_vertex.ok_or_else(|| {
anyhow::anyhow!("LongestPath requires --source-vertex\n\nUsage: {usage}")
})?;
let target_vertex = args.target_vertex.ok_or_else(|| {
anyhow::anyhow!("LongestPath requires --target-vertex\n\nUsage: {usage}")
})?;
ensure_vertex_in_bounds(source_vertex, graph.num_vertices(), "source_vertex")?;
ensure_vertex_in_bounds(target_vertex, graph.num_vertices(), "target_vertex")?;
(
ser(LongestPath::new(
graph,
edge_lengths,
source_vertex,
target_vertex,
))?,
resolved_variant.clone(),
)
}

// ShortestWeightConstrainedPath
"ShortestWeightConstrainedPath" => {
let usage = "pred create ShortestWeightConstrainedPath --graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,1-4 --edge-lengths 2,4,3,1,5,4,2,6 --edge-weights 5,1,2,3,2,3,1,1 --source-vertex 0 --target-vertex 5 --length-bound 10 --weight-bound 8";
Expand Down Expand Up @@ -6069,6 +6105,118 @@ mod tests {
assert!(err.to_string().contains("GeneralizedHex requires --sink"));
}

#[test]
fn test_create_longest_path_serializes_problem_json() {
let output = temp_output_path("longest_path_create");
let cli = Cli::try_parse_from([
"pred",
"-o",
output.to_str().unwrap(),
"create",
"LongestPath",
"--graph",
"0-1,0-2,1-3,2-3,2-4,3-5,4-5,4-6,5-6,1-6",
"--edge-lengths",
"3,2,4,1,5,2,3,2,4,1",
"--source-vertex",
"0",
"--target-vertex",
"6",
])
.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"], "LongestPath");
assert_eq!(json["variant"]["graph"], "SimpleGraph");
assert_eq!(json["variant"]["weight"], "i32");
assert_eq!(json["data"]["source_vertex"], 0);
assert_eq!(json["data"]["target_vertex"], 6);
assert_eq!(
json["data"]["edge_lengths"],
serde_json::json!([3, 2, 4, 1, 5, 2, 3, 2, 4, 1])
);
}

#[test]
fn test_create_longest_path_requires_edge_lengths() {
let cli = Cli::try_parse_from([
"pred",
"create",
"LongestPath",
"--graph",
"0-1,1-2",
"--source-vertex",
"0",
"--target-vertex",
"2",
])
.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("LongestPath requires --edge-lengths"));
}

#[test]
fn test_create_longest_path_rejects_weights_flag() {
let cli = Cli::try_parse_from([
"pred",
"create",
"LongestPath",
"--graph",
"0-1,1-2",
"--weights",
"1,1,1",
"--source-vertex",
"0",
"--target-vertex",
"2",
"--edge-lengths",
"5,7",
])
.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("LongestPath uses --edge-lengths, not --weights"));
}

fn empty_args() -> CreateArgs {
CreateArgs {
problem: Some("BiconnectivityAugmentation".to_string()),
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ pub mod prelude {
DirectedTwoCommodityIntegralFlow, GeneralizedHex, GraphPartitioning, HamiltonianCircuit,
HamiltonianPath, IntegralFlowHomologousArcs, IntegralFlowWithMultipliers,
IsomorphicSpanningTree, KClique, KthBestSpanningTree, LengthBoundedDisjointPaths,
MixedChinesePostman, SpinGlass, SteinerTree, StrongConnectivityAugmentation,
LongestPath, MixedChinesePostman, SpinGlass, SteinerTree, StrongConnectivityAugmentation,
SubgraphIsomorphism,
};
pub use crate::models::graph::{
Expand Down
Loading
Loading