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
10 changes: 10 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
"ClosestVectorProblem": [Closest Vector Problem],
"RuralPostman": [Rural Postman],
"LongestCommonSubsequence": [Longest Common Subsequence],
"SubsetSum": [Subset Sum],
"MinimumFeedbackArcSet": [Minimum Feedback Arc Set],
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
"SubgraphIsomorphism": [Subgraph Isomorphism],
"SubsetSum": [Subset Sum],
Expand Down Expand Up @@ -1015,6 +1017,14 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa
*Example.* Let $A = {3, 7, 1, 8, 2, 4}$ ($n = 6$) and target $B = 11$. Selecting $A' = {3, 8}$ gives sum $3 + 8 = 11 = B$. Another solution: $A' = {7, 4}$ with sum $7 + 4 = 11 = B$.
]

#problem-def("MinimumFeedbackArcSet")[
Given a directed graph $G = (V, A)$, find a minimum-size subset $A' subset.eq A$ such that $G - A'$ is a directed acyclic graph (DAG). Equivalently, $A'$ must contain at least one arc from every directed cycle in $G$.
][
Feedback Arc Set (FAS) is a classical NP-complete problem from Karp's original list @karp1972 (via transformation from Vertex Cover, as presented in Garey & Johnson GT8). The problem arises in ranking aggregation, sports scheduling, deadlock avoidance, and causal inference. Unlike the undirected analogue (which is trivially polynomial --- the number of non-tree edges in a spanning forest), the directed version is NP-hard due to the richer structure of directed cycles. The best known exact algorithm uses dynamic programming over vertex subsets in $O^*(2^n)$ time, generalizing the Held--Karp TSP technique to vertex ordering problems @bodlaender2012. FAS is fixed-parameter tractable with parameter $k = |A'|$: an $O(4^k dot k! dot n^(O(1)))$ algorithm exists via iterative compression @chen2008. Polynomial-time solvable for planar digraphs via the Lucchesi--Younger theorem @lucchesi1978.

*Example.* Consider $G$ with $V = {0, 1, 2, 3, 4, 5}$ and arcs $(0 arrow 1), (1 arrow 2), (2 arrow 0), (1 arrow 3), (3 arrow 4), (4 arrow 1), (2 arrow 5), (5 arrow 3), (3 arrow 0)$. This graph contains four directed cycles: $0 arrow 1 arrow 2 arrow 0$, $1 arrow 3 arrow 4 arrow 1$, $0 arrow 1 arrow 3 arrow 0$, and $2 arrow 5 arrow 3 arrow 0 arrow 1 arrow 2$. Removing $A' = {(0 arrow 1), (3 arrow 4)}$ breaks all four cycles (vertex 0 becomes a sink in the residual graph), giving a minimum FAS of size 2.
]

// Completeness check: warn about problem types in JSON but missing from paper
#{
let json-models = {
Expand Down
33 changes: 33 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,36 @@ @article{cygan2014
note = {Conference version: STOC 2014},
doi = {10.1137/140990255}
}

@article{bodlaender2012,
author = {Hans L. Bodlaender and Fedor V. Fomin and Arie M. C. A. Koster and Dieter Kratsch and Dimitrios M. Thilikos},
title = {A Note on Exact Algorithms for Vertex Ordering Problems on Graphs},
journal = {Theory of Computing Systems},
volume = {50},
number = {3},
pages = {420--432},
year = {2012},
doi = {10.1007/s00224-011-9312-0}
}

@article{chen2008,
author = {Jianer Chen and Yang Liu and Songjian Lu and Barry O'Sullivan and Igor Razgon},
title = {A Fixed-Parameter Algorithm for the Directed Feedback Vertex Set Problem},
journal = {Journal of the ACM},
volume = {55},
number = {5},
pages = {1--19},
year = {2008},
doi = {10.1145/1411509.1411511}
}

@article{lucchesi1978,
author = {Cl\'audio L. Lucchesi and Daniel H. Younger},
title = {A Minimax Theorem for Directed Graphs},
journal = {Journal of the London Mathematical Society},
volume = {s2-17},
number = {3},
pages = {369--374},
year = {1978},
doi = {10.1112/jlms/s2-17.3.369}
}
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ Flags by problem type:
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
SubgraphIsomorphism --graph (host), --pattern (pattern)
LCS --strings
FAS --arcs [--weights] [--num-vertices]
FVS --arcs [--weights] [--num-vertices]
ILP, CircuitSAT (via reduction only)

Expand Down
129 changes: 93 additions & 36 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
"u64" => "integer",
"i64" => "integer",
"Vec<i64>" => "comma-separated integers: 3,7,1,8",
"DirectedGraph" => "directed arcs: 0>1,1>2,2>0",
_ => "value",
}
}
Expand All @@ -94,6 +95,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"KColoring" => "--graph 0-1,1-2,2-0 --k 3",
"PartitionIntoTriangles" => "--graph 0-1,1-2,0-2",
"Factoring" => "--target 15 --m 4 --n 4",
"MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"",
"RuralPostman" => {
"--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4"
}
Expand Down Expand Up @@ -122,6 +124,10 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> {
if graph_type == Some("UnitDiskGraph") {
eprintln!(" --{:<16} Distance threshold [default: 1.0]", "radius");
}
} else if field.type_name == "DirectedGraph" {
// DirectedGraph fields use --arcs, not --graph
let hint = type_format_hint(&field.type_name, graph_type);
eprintln!(" --{:<16} {} ({})", "arcs", field.description, hint);
} else {
let hint = type_format_hint(&field.type_name, graph_type);
eprintln!(
Expand Down Expand Up @@ -542,6 +548,22 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// MinimumFeedbackArcSet
"MinimumFeedbackArcSet" => {
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"MinimumFeedbackArcSet requires --arcs\n\n\
Usage: pred create FAS --arcs \"0>1,1>2,2>0\" [--weights 1,1,1] [--num-vertices N]"
)
})?;
let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?;
let weights = parse_arc_weights(args, num_arcs)?;
(
ser(MinimumFeedbackArcSet::new(graph, weights))?,
resolved_variant.clone(),
)
}

// SubgraphIsomorphism
"SubgraphIsomorphism" => {
let (host_graph, _) = parse_graph(args).map_err(|e| {
Expand Down Expand Up @@ -606,47 +628,14 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {

// MinimumFeedbackVertexSet
"MinimumFeedbackVertexSet" => {
let arcs_str = args.arcs.as_ref().ok_or_else(|| {
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"MinimumFeedbackVertexSet requires --arcs\n\n\
Usage: pred create FVS --arcs \"0>1,1>2,2>0\" [--weights 1,1,1] [--num-vertices N]"
)
})?;
let arcs: Vec<(usize, usize)> = arcs_str
.split(',')
.map(|s| {
let parts: Vec<&str> = s.split('>').collect();
anyhow::ensure!(
parts.len() == 2,
"Invalid arc format '{}', expected 'u>v'",
s
);
Ok((
parts[0].trim().parse::<usize>()?,
parts[1].trim().parse::<usize>()?,
))
})
.collect::<Result<Vec<_>>>()?;
let inferred_num_v = arcs
.iter()
.flat_map(|&(u, v)| [u, v])
.max()
.map(|m| m + 1)
.unwrap_or(0);
let num_v = match args.num_vertices {
Some(user_num_v) => {
anyhow::ensure!(
user_num_v >= inferred_num_v,
"--num-vertices ({}) is too small for the arcs: need at least {} to cover vertices up to {}",
user_num_v,
inferred_num_v,
inferred_num_v.saturating_sub(1),
);
user_num_v
}
None => inferred_num_v,
};
let graph = DirectedGraph::new(num_v, arcs);
let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?;
let num_v = graph.num_vertices();
let weights = parse_vertex_weights(args, num_v)?;
(
ser(MinimumFeedbackVertexSet::new(graph, weights))?,
Expand Down Expand Up @@ -1035,6 +1024,74 @@ fn parse_matrix(args: &CreateArgs) -> Result<Vec<Vec<f64>>> {
.collect()
}

/// Parse `--arcs` as directed arc pairs and build a `DirectedGraph`.
///
/// Returns `(graph, num_arcs)`. Infers vertex count from arc endpoints
/// unless `num_vertices` is provided (which must be >= inferred count).
/// E.g., "0>1,1>2,2>0"
fn parse_directed_graph(
arcs_str: &str,
num_vertices: Option<usize>,
) -> Result<(DirectedGraph, usize)> {
let arcs: Vec<(usize, usize)> = arcs_str
.split(',')
.map(|pair| {
let parts: Vec<&str> = pair.trim().split('>').collect();
if parts.len() != 2 {
bail!(
"Invalid arc '{}': expected format u>v (e.g., 0>1)",
pair.trim()
);
}
let u: usize = parts[0].parse()?;
let v: usize = parts[1].parse()?;
Ok((u, v))
})
.collect::<Result<Vec<_>>>()?;
let inferred_num_v = arcs
.iter()
.flat_map(|&(u, v)| [u, v])
.max()
.map(|m| m + 1)
.unwrap_or(0);
let num_v = match num_vertices {
Some(user_num_v) => {
anyhow::ensure!(
user_num_v >= inferred_num_v,
"--num-vertices ({}) is too small for the arcs: need at least {} to cover vertices up to {}",
user_num_v,
inferred_num_v,
inferred_num_v.saturating_sub(1),
);
user_num_v
}
None => inferred_num_v,
};
let num_arcs = arcs.len();
Ok((DirectedGraph::new(num_v, arcs), num_arcs))
}

/// Parse `--weights` as arc weights (i32), defaulting to all 1s.
fn parse_arc_weights(args: &CreateArgs, num_arcs: usize) -> Result<Vec<i32>> {
match &args.weights {
Some(w) => {
let weights: Vec<i32> = w
.split(',')
.map(|s| s.trim().parse::<i32>())
.collect::<std::result::Result<Vec<_>, _>>()?;
if weights.len() != num_arcs {
bail!(
"Expected {} arc weights but got {}",
num_arcs,
weights.len()
);
}
Ok(weights)
}
None => Ok(vec![1i32; num_arcs]),
}
}

/// Handle `pred create <PROBLEM> --random ...`
fn create_random(
args: &CreateArgs,
Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ pub fn load_problem(
"LongestCommonSubsequence" => deser_opt::<LongestCommonSubsequence>(data),
"MinimumFeedbackVertexSet" => deser_opt::<MinimumFeedbackVertexSet<i32>>(data),
"SubsetSum" => deser_sat::<SubsetSum>(data),
"MinimumFeedbackArcSet" => deser_opt::<MinimumFeedbackArcSet<i32>>(data),
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
}
}
Expand Down Expand Up @@ -319,6 +320,7 @@ pub fn serialize_any_problem(
"LongestCommonSubsequence" => try_ser::<LongestCommonSubsequence>(any),
"MinimumFeedbackVertexSet" => try_ser::<MinimumFeedbackVertexSet<i32>>(any),
"SubsetSum" => try_ser::<SubsetSum>(any),
"MinimumFeedbackArcSet" => try_ser::<MinimumFeedbackArcSet<i32>>(any),
_ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)),
}
}
Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub const ALIASES: &[(&str, &str)] = &[
("LCS", "LongestCommonSubsequence"),
("MaxMatching", "MaximumMatching"),
("FVS", "MinimumFeedbackVertexSet"),
("FAS", "MinimumFeedbackArcSet"),
];

/// Resolve a short alias to the canonical problem name.
Expand Down Expand Up @@ -61,6 +62,7 @@ pub fn resolve_alias(input: &str) -> String {
"partitionintotriangles" => "PartitionIntoTriangles".to_string(),
"lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(),
"fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(),
"fas" | "minimumfeedbackarcset" => "MinimumFeedbackArcSet".to_string(),
"subsetsum" => "SubsetSum".to_string(),
_ => input.to_string(), // pass-through for exact names
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub mod prelude {
};
pub use crate::models::graph::{
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover,
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumVertexCover,
PartitionIntoTriangles, RuralPostman, TravelingSalesman,
};
pub use crate::models::misc::{
Expand Down
Loading
Loading