Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5b9baa6
feat: add PaintShop -> QUBO reduction (#649)
zazabap Mar 28, 2026
4c6db4d
feat: add MinimumVertexCover -> MinimumHittingSet reduction (#200)
zazabap Mar 28, 2026
ff3e708
feat: add PartitionIntoPathsOfLength2 -> BoundedComponentSpanningFore…
zazabap Mar 28, 2026
51a1d29
feat: add HamiltonianCircuit -> LongestCircuit reduction (#358)
zazabap Mar 28, 2026
179136e
feat: add Partition -> SubsetSum reduction (#387)
zazabap Mar 28, 2026
97c073e
feat: add RootedTreeArrangement -> RootedTreeStorageAssignment reduct…
zazabap Mar 28, 2026
a3e8209
feat: add SubsetSum -> CapacityAssignment reduction (#426)
zazabap Mar 28, 2026
5377f67
feat: add LongestCommonSubsequence -> MaximumIndependentSet reduction…
zazabap Mar 28, 2026
2aa02f4
feat: add MinimumVertexCover -> EnsembleComputation reduction (#204)
zazabap Mar 28, 2026
7923931
feat: add KClique -> BalancedCompleteBipartiteSubgraph reduction (#231)
zazabap Mar 28, 2026
83075ae
feat: add KColoring(K3) -> TwoDimensionalConsecutiveSets reduction (#…
zazabap Mar 28, 2026
6d6a370
fix: correct LCS->MIS example_db target_config
zazabap Mar 28, 2026
94db88f
fix: address PR #804 review comments and merge main
zazabap Mar 29, 2026
0a16b9f
fix: escape #k-clique Typst variable reference in paper
zazabap Mar 29, 2026
4d7501a
fix: add missing Streif2021 bibliography entry for PaintShop→QUBO
zazabap Mar 29, 2026
bf95960
fix: resolve merge conflicts with main
zazabap Mar 30, 2026
bc76f7c
fix: replace deprecated 'sect' with 'inter' in Typst paper
isPANN Mar 30, 2026
7494013
fix: change EnsembleComputation to optimization (Min<usize>) and fix …
isPANN Mar 30, 2026
b322ef9
feat: add default budget for EnsembleComputation, make --budget optional
isPANN Mar 30, 2026
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
361 changes: 361 additions & 0 deletions docs/paper/reductions.typ

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/models/graph/minimum_vertex_cover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
use crate::topology::{Graph, SimpleGraph};
use crate::traits::Problem;
use crate::types::{Min, WeightElement};
use crate::types::{Min, One, WeightElement};
use num_traits::Zero;
use serde::{Deserialize, Serialize};

Expand All @@ -17,7 +17,7 @@ inventory::submit! {
aliases: &["MVC"],
dimensions: &[
VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
VariantDimension::new("weight", "i32", &["i32"]),
VariantDimension::new("weight", "i32", &["i32", "One"]),
],
module_path: module_path!(),
description: "Find minimum weight vertex cover in a graph",
Expand Down Expand Up @@ -152,6 +152,7 @@ fn is_vertex_cover_config<G: Graph>(graph: &G, config: &[usize]) -> bool {

crate::declare_variants! {
default MinimumVertexCover<SimpleGraph, i32> => "1.1996^num_vertices",
MinimumVertexCover<SimpleGraph, One> => "1.1996^num_vertices",
}

#[cfg(feature = "example-db")]
Expand Down
19 changes: 19 additions & 0 deletions src/models/misc/longest_common_subsequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ impl LongestCommonSubsequence {
pub fn num_transitions(&self) -> usize {
self.max_length.saturating_sub(1)
}

/// Returns the cross-frequency product: the sum over each alphabet symbol
/// of the product of that symbol's frequency across all input strings.
///
/// Formally: Σ_{c ∈ 0..alphabet_size} Π_{i=1..k} count(c, strings\[i\])
/// where count(c, s) is the number of occurrences of symbol c in string s.
///
/// This equals the exact number of match-node vertices in the LCS → MaxIS
/// reduction graph.
pub fn cross_frequency_product(&self) -> usize {
(0..self.alphabet_size)
.map(|c| {
self.strings
.iter()
.map(|s| s.iter().filter(|&&sym| sym == c).count())
.product::<usize>()
})
.sum()
}
}

/// Check whether `candidate` is a subsequence of `target` using greedy
Expand Down
10 changes: 10 additions & 0 deletions src/models/misc/paintshop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ impl PaintShop {
&self.car_labels
}

/// Get the sequence as car indices.
pub fn sequence_indices(&self) -> &[usize] {
&self.sequence_indices
}

/// Get whether each position is the first occurrence of its car.
pub fn is_first(&self) -> &[bool] {
&self.is_first
}

/// Get the coloring of the sequence from a configuration.
///
/// Config assigns a color (0 or 1) to each car for its first occurrence.
Expand Down
69 changes: 69 additions & 0 deletions src/rules/hamiltoniancircuit_longestcircuit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//! Reduction from HamiltonianCircuit to LongestCircuit.
//!
//! Given an HC instance G = (V, E), construct an LC instance on the same graph
//! with unit edge weights. A Hamiltonian circuit exists iff the optimal circuit
//! length equals |V|.

use crate::models::graph::{HamiltonianCircuit, LongestCircuit};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};
use crate::topology::{Graph, SimpleGraph};

/// Result of reducing HamiltonianCircuit to LongestCircuit.
#[derive(Debug, Clone)]
pub struct ReductionHamiltonianCircuitToLongestCircuit {
target: LongestCircuit<SimpleGraph, i32>,
}

impl ReductionResult for ReductionHamiltonianCircuitToLongestCircuit {
type Source = HamiltonianCircuit<SimpleGraph>;
type Target = LongestCircuit<SimpleGraph, i32>;

fn target_problem(&self) -> &Self::Target {
&self.target
}

fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution)
}
}

#[reduction(
overhead = {
num_vertices = "num_vertices",
num_edges = "num_edges",
}
)]
impl ReduceTo<LongestCircuit<SimpleGraph, i32>> for HamiltonianCircuit<SimpleGraph> {
type Result = ReductionHamiltonianCircuitToLongestCircuit;

fn reduce_to(&self) -> Self::Result {
let n = self.num_vertices();
let edges = self.graph().edges();
let target = LongestCircuit::new(SimpleGraph::new(n, edges), vec![1i32; self.num_edges()]);
ReductionHamiltonianCircuitToLongestCircuit { target }
}
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
use crate::export::SolutionPair;

vec![crate::example_db::specs::RuleExampleSpec {
id: "hamiltoniancircuit_to_longestcircuit",
build: || {
let source = HamiltonianCircuit::new(SimpleGraph::cycle(4));
crate::example_db::specs::rule_example_with_witness::<_, LongestCircuit<SimpleGraph, i32>>(
source,
SolutionPair {
source_config: vec![0, 1, 2, 3],
target_config: vec![1, 1, 1, 1],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/hamiltoniancircuit_longestcircuit.rs"]
mod tests;
138 changes: 138 additions & 0 deletions src/rules/kclique_balancedcompletebipartitesubgraph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//! Reduction from KClique to BalancedCompleteBipartiteSubgraph.
//!
//! Classical reduction attributed to Garey and Johnson (GT24) and published in
//! Johnson (1987). Given a KClique instance (G, k), constructs a bipartite graph
//! where Part A = padded vertex set and Part B = edge elements + padding elements,
//! with non-incidence adjacency encoding.

use crate::models::graph::{BalancedCompleteBipartiteSubgraph, KClique};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};
use crate::topology::{BipartiteGraph, Graph, SimpleGraph};

/// Result of reducing KClique to BalancedCompleteBipartiteSubgraph.
///
/// Stores the target problem and the number of original vertices for
/// solution extraction.
#[derive(Debug, Clone)]
pub struct ReductionKCliqueToBCBS {
target: BalancedCompleteBipartiteSubgraph,
/// Number of vertices in the original graph (before padding).
num_original_vertices: usize,
}

impl ReductionResult for ReductionKCliqueToBCBS {
type Source = KClique<SimpleGraph>;
type Target = BalancedCompleteBipartiteSubgraph;

fn target_problem(&self) -> &BalancedCompleteBipartiteSubgraph {
&self.target
}

/// Extract KClique solution from BalancedCompleteBipartiteSubgraph solution.
///
/// The k-clique is S = {v in V : v not in A'}, i.e., the original vertices
/// NOT selected on the left side. For each original vertex v (0..n-1):
/// source_config[v] = 1 - target_config[v].
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
(0..self.num_original_vertices)
.map(|v| 1 - target_solution[v])
.collect()
}
}

#[reduction(
overhead = {
left_size = "num_vertices + k * (k - 1) / 2",
right_size = "num_edges + num_vertices - k",
k = "num_vertices + k * (k - 1) / 2 - k",
}
)]
impl ReduceTo<BalancedCompleteBipartiteSubgraph> for KClique<SimpleGraph> {
type Result = ReductionKCliqueToBCBS;

fn reduce_to(&self) -> Self::Result {
let n = self.num_vertices();
let k = self.k();
let edges: Vec<(usize, usize)> = self.graph().edges();
let m = edges.len();

// C(k, 2) = k*(k-1)/2 — number of edges in a k-clique
let ck2 = k * (k - 1) / 2;

// Part A (left partition): n' = n + C(k,2) vertices
let left_size = n + ck2;

// Part B (right partition): m edge elements + (n - k) padding elements
let num_padding = n - k;
let right_size = m + num_padding;

// Target biclique parameter: K' = n' - k
let target_k = left_size - k;

// Build bipartite edges using non-incidence encoding
let mut bip_edges = Vec::new();

for v in 0..left_size {
// Edge elements: add edge (v, j) if v is NOT an endpoint of edges[j]
for (j, &(u, w)) in edges.iter().enumerate() {
if v != u && v != w {
// For padded vertices (v >= n), they are never endpoints
// of any original edge, so they always connect.
bip_edges.push((v, j));
}
}

// Padding elements: always connected
for p in 0..num_padding {
bip_edges.push((v, m + p));
}
}

let graph = BipartiteGraph::new(left_size, right_size, bip_edges);
let target = BalancedCompleteBipartiteSubgraph::new(graph, target_k);

ReductionKCliqueToBCBS {
target,
num_original_vertices: n,
}
}
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
use crate::export::SolutionPair;

vec![crate::example_db::specs::RuleExampleSpec {
id: "kclique_to_balancedcompletebipartitesubgraph",
build: || {
// 4-vertex graph with edges {0,1}, {0,2}, {1,2}, {2,3}, k=3
// Known 3-clique: {0, 1, 2}
let source = KClique::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]), 3);
// Source config: vertices {0,1,2} selected = [1,1,1,0]
// Target: left_size=7, right_size=5, k'=4
// Left side: NOT selecting clique vertices -> select {3,4,5,6}
// target_config for left: [0,0,0,1,1,1,1]
// Right side: select edge elements for clique edges + padding
// e0={0,1}, e1={0,2}, e2={1,2} are clique edges -> select them
// e3={2,3} is not a clique edge -> don't select
// w0 is padding -> select
// target_config for right: [1,1,1,0,1]
// Full target config: [0,0,0,1,1,1,1, 1,1,1,0,1]
crate::example_db::specs::rule_example_with_witness::<
_,
BalancedCompleteBipartiteSubgraph,
>(
source,
SolutionPair {
source_config: vec![1, 1, 1, 0],
target_config: vec![0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/kclique_balancedcompletebipartitesubgraph.rs"]
mod tests;
Loading
Loading