Skip to content

Commit 9ace016

Browse files
zazabapclaudeGiggleLiu
authored
Fix #141: Add MinimumFeedbackVertexSet to ILP reduction (#624)
* Add plan for #141: MinimumFeedbackVertexSet to ILP reduction * Implement #141: MinimumFeedbackVertexSet to ILP reduction Add MTZ-style topological ordering reduction from MinimumFeedbackVertexSet to ILP<i32>. Uses 2n variables (n binary + n integer ordering) and m + 2n constraints. Includes unit tests, example program, and paper documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove plan file after implementation * chore: fix formatting after merge Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: migrate MFVS example to example-db pattern, add canonical specs Remove old-style example binary and add canonical_rule_example_specs() to the MFVS-to-ILP rule, matching the current project convention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: support ILP<i32> in CLI solve_ilp dispatch The solve_ilp function only downcasted to ILP<bool>, causing `pred solve` to fail for problems that reduce to ILP<i32> (e.g., MinimumFeedbackVertexSet). Now tries both ILP<bool> and ILP<i32>. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Regenerate fixtures and fix formatting after merge with main - examples.json now includes MFVS-to-ILP canonical example - create.rs: remove extra blank line (rustfmt) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent 9640b88 commit 9ace016

7 files changed

Lines changed: 371 additions & 24 deletions

File tree

docs/paper/reductions.typ

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3286,6 +3286,22 @@ The following reductions to Integer Linear Programming are straightforward formu
32863286
_Solution extraction._ $D = {v : x_v = 1}$.
32873287
]
32883288

3289+
#reduction-rule("MinimumFeedbackVertexSet", "ILP")[
3290+
A directed graph is a DAG iff it admits a topological ordering. MTZ-style ordering variables enforce this: for each kept vertex, an integer position variable must increase strictly along every arc. Removed vertices relax the ordering constraints via big-$M$ terms.
3291+
][
3292+
_Construction._ Given directed graph $G = (V, A)$ with $n = |V|$, $m = |A|$, and weights $w_v$:
3293+
3294+
_Variables:_ Binary $x_v in {0, 1}$ for each $v in V$: $x_v = 1$ iff $v$ is removed. Integer $o_v in {0, dots, n-1}$ for each $v in V$: topological order position. Total: $2n$ variables.
3295+
3296+
_Constraints:_ (1) For each arc $(u -> v) in A$: $o_v - o_u >= 1 - n(x_u + x_v)$. When both endpoints are kept ($x_u = x_v = 0$), this forces $o_v > o_u$ (strict topological order). When either is removed, the constraint relaxes to $o_v - o_u >= 1 - n$ (trivially satisfied). (2) Binary bounds: $x_v <= 1$. (3) Order bounds: $o_v <= n - 1$. Total: $m + 2n$ constraints.
3297+
3298+
_Objective:_ Minimize $sum_v w_v x_v$.
3299+
3300+
_Correctness._ ($arrow.r.double$) If $S$ is a feedback vertex set, then $G[V backslash S]$ is a DAG with a topological ordering. Set $x_v = 1$ for $v in S$, $o_v$ to the topological position for kept vertices, and $o_v = 0$ for removed vertices. All constraints are satisfied. ($arrow.l.double$) If the ILP is feasible with all arc constraints satisfied, no directed cycle can exist among kept vertices: a cycle $v_1 -> dots -> v_k -> v_1$ would require $o_(v_1) < o_(v_2) < dots < o_(v_k) < o_(v_1)$, a contradiction.
3301+
3302+
_Solution extraction._ $S = {v : x_v = 1}$.
3303+
]
3304+
32893305
#reduction-rule("MaximumClique", "ILP")[
32903306
A clique requires every pair of selected vertices to be adjacent; equivalently, no two selected vertices may share a _non_-edge. This is the independent set formulation on the complement graph $overline(G)$.
32913307
][

problemreductions-cli/src/commands/create.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1721,7 +1721,6 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
17211721
emit_problem_output(&output, out)
17221722
}
17231723

1724-
17251724
/// Reject non-unit weights when the resolved variant uses `weight=One`.
17261725
fn reject_nonunit_weights_for_one_variant(
17271726
canonical: &str,

problemreductions-cli/src/dispatch.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,17 +165,27 @@ pub struct SolveResult {
165165
pub evaluation: String,
166166
}
167167

168-
/// Solve an ILP problem directly. The input must be an `ILP` instance.
168+
/// Solve an ILP problem directly. The input must be an `ILP<bool>` or `ILP<i32>` instance.
169169
fn solve_ilp(any: &dyn Any) -> Result<SolveResult> {
170-
let problem = any
171-
.downcast_ref::<ILP>()
172-
.ok_or_else(|| anyhow::anyhow!("Internal error: expected ILP problem instance"))?;
173-
let solver = ILPSolver::new();
174-
let config = solver
175-
.solve(problem)
176-
.ok_or_else(|| anyhow::anyhow!("ILP solver found no feasible solution"))?;
177-
let evaluation = format!("{:?}", problem.evaluate(&config));
178-
Ok(SolveResult { config, evaluation })
170+
if let Some(problem) = any.downcast_ref::<ILP<bool>>() {
171+
let solver = ILPSolver::new();
172+
let config = solver
173+
.solve(problem)
174+
.ok_or_else(|| anyhow::anyhow!("ILP solver found no feasible solution"))?;
175+
let evaluation = format!("{:?}", problem.evaluate(&config));
176+
return Ok(SolveResult { config, evaluation });
177+
}
178+
if let Some(problem) = any.downcast_ref::<ILP<i32>>() {
179+
let solver = ILPSolver::new();
180+
let config = solver
181+
.solve(problem)
182+
.ok_or_else(|| anyhow::anyhow!("ILP solver found no feasible solution"))?;
183+
let evaluation = format!("{:?}", problem.evaluate(&config));
184+
return Ok(SolveResult { config, evaluation });
185+
}
186+
Err(anyhow::anyhow!(
187+
"Internal error: expected ILP<bool> or ILP<i32> problem instance"
188+
))
179189
}
180190

181191
#[cfg(test)]

src/example_db/fixtures/examples.json

Lines changed: 14 additions & 13 deletions
Large diffs are not rendered by default.
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//! Reduction from MinimumFeedbackVertexSet to ILP (Integer Linear Programming).
2+
//!
3+
//! Uses MTZ-style topological ordering constraints:
4+
//! - Variables: n binary x_i (vertex removal) + n integer o_i (topological order) = 2n total
5+
//! - Constraints: For each arc (u->v): o_v - o_u >= 1 - n*(x_u + x_v)
6+
//! Plus binary bounds (x_i <= 1) and order bounds (o_i <= n-1)
7+
//! - Objective: Minimize the weighted sum of removed vertices
8+
9+
use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP};
10+
use crate::models::graph::MinimumFeedbackVertexSet;
11+
use crate::reduction;
12+
use crate::rules::traits::{ReduceTo, ReductionResult};
13+
14+
/// Result of reducing MinimumFeedbackVertexSet to ILP.
15+
///
16+
/// The ILP uses integer variables (`ILP<i32>`) because it needs both
17+
/// binary selection variables (x_i) and integer ordering variables (o_i).
18+
///
19+
/// Variable layout:
20+
/// - `x_i` at index `i` for `i in 0..n`: binary (0 or 1), vertex removal indicator
21+
/// - `o_i` at index `n + i` for `i in 0..n`: integer in {0, ..., n-1}, topological order
22+
#[derive(Debug, Clone)]
23+
pub struct ReductionMFVSToILP {
24+
target: ILP<i32>,
25+
/// Number of vertices in the source graph (needed for solution extraction).
26+
num_vertices: usize,
27+
}
28+
29+
impl ReductionResult for ReductionMFVSToILP {
30+
type Source = MinimumFeedbackVertexSet<i32>;
31+
type Target = ILP<i32>;
32+
33+
fn target_problem(&self) -> &ILP<i32> {
34+
&self.target
35+
}
36+
37+
/// Extract solution from ILP back to MinimumFeedbackVertexSet.
38+
///
39+
/// The first n variables of the ILP solution are the binary x_i values,
40+
/// which directly correspond to the FVS configuration (1 = removed).
41+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
42+
target_solution[..self.num_vertices].to_vec()
43+
}
44+
}
45+
46+
#[reduction(
47+
overhead = {
48+
num_vars = "2 * num_vertices",
49+
num_constraints = "num_arcs + 2 * num_vertices",
50+
}
51+
)]
52+
impl ReduceTo<ILP<i32>> for MinimumFeedbackVertexSet<i32> {
53+
type Result = ReductionMFVSToILP;
54+
55+
fn reduce_to(&self) -> Self::Result {
56+
let n = self.graph().num_vertices();
57+
let arcs = self.graph().arcs();
58+
let num_vars = 2 * n;
59+
60+
// Variable indices:
61+
// x_i = i (binary: vertex i removed?)
62+
// o_i = n + i (integer: topological order of vertex i)
63+
64+
let mut constraints = Vec::new();
65+
66+
// Binary bounds: x_i <= 1 for i in 0..n
67+
for i in 0..n {
68+
constraints.push(LinearConstraint::le(vec![(i, 1.0)], 1.0));
69+
}
70+
71+
// Order bounds: o_i <= n - 1 for i in 0..n
72+
for i in 0..n {
73+
constraints.push(LinearConstraint::le(vec![(n + i, 1.0)], (n - 1) as f64));
74+
}
75+
76+
// Arc constraints: for each arc (u -> v):
77+
// o_v - o_u >= 1 - n * (x_u + x_v)
78+
// Rearranged: o_v - o_u + n*x_u + n*x_v >= 1
79+
let n_f64 = n as f64;
80+
for &(u, v) in &arcs {
81+
let terms = vec![
82+
(n + v, 1.0), // o_v
83+
(n + u, -1.0), // -o_u
84+
(u, n_f64), // n * x_u
85+
(v, n_f64), // n * x_v
86+
];
87+
constraints.push(LinearConstraint::ge(terms, 1.0));
88+
}
89+
90+
// Objective: minimize sum w_i * x_i
91+
let objective: Vec<(usize, f64)> = self
92+
.weights()
93+
.iter()
94+
.enumerate()
95+
.map(|(i, &w)| (i, w as f64))
96+
.collect();
97+
98+
let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize);
99+
100+
ReductionMFVSToILP {
101+
target,
102+
num_vertices: n,
103+
}
104+
}
105+
}
106+
107+
#[cfg(feature = "example-db")]
108+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
109+
use crate::topology::DirectedGraph;
110+
vec![crate::example_db::specs::RuleExampleSpec {
111+
id: "minimumfeedbackvertexset_to_ilp",
112+
build: || {
113+
// Simple cycle: 0 -> 1 -> 2 -> 0 (FVS = 1 vertex)
114+
let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]);
115+
let source = MinimumFeedbackVertexSet::new(graph, vec![1i32; 3]);
116+
crate::example_db::specs::direct_ilp_example::<_, i32, _>(source, |_, _| true)
117+
},
118+
}]
119+
}
120+
121+
#[cfg(test)]
122+
#[path = "../unit_tests/rules/minimumfeedbackvertexset_ilp.rs"]
123+
mod tests;

src/rules/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ pub(crate) mod maximumsetpacking_ilp;
6464
#[cfg(feature = "ilp-solver")]
6565
pub(crate) mod minimumdominatingset_ilp;
6666
#[cfg(feature = "ilp-solver")]
67+
pub(crate) mod minimumfeedbackvertexset_ilp;
68+
#[cfg(feature = "ilp-solver")]
6769
pub(crate) mod minimumsetcovering_ilp;
6870
#[cfg(feature = "ilp-solver")]
6971
pub(crate) mod qubo_ilp;
@@ -112,6 +114,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
112114
specs.extend(maximummatching_ilp::canonical_rule_example_specs());
113115
specs.extend(maximumsetpacking_ilp::canonical_rule_example_specs());
114116
specs.extend(minimumdominatingset_ilp::canonical_rule_example_specs());
117+
specs.extend(minimumfeedbackvertexset_ilp::canonical_rule_example_specs());
115118
specs.extend(minimumsetcovering_ilp::canonical_rule_example_specs());
116119
specs.extend(qubo_ilp::canonical_rule_example_specs());
117120
specs.extend(travelingsalesman_ilp::canonical_rule_example_specs());

0 commit comments

Comments
 (0)