Skip to content
Open
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
88 changes: 88 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -13984,6 +13984,94 @@ The following reductions to Integer Linear Programming are straightforward formu
_Solution extraction._ For each source vertex $u in V_1$, output the unique $p in V_2$ with $x_(u,p) = 1$ if such a $p$ exists, otherwise the sentinel $|V_2|$ encoding "unmatched".
]

// Reduction: MaximumCommonEdgeSubgraph -> MaximumClique
#let mces_clq = load-example(
"MaximumCommonEdgeSubgraph",
"MaximumClique",
target-variant: (graph: "SimpleGraph", weight: "One"),
)
#let mces_clq_sol = mces_clq.solutions.at(0)
#let mces_clq_arcs1 = mces_clq.source.instance.graph_1.arcs
#let mces_clq_arcs2 = mces_clq.source.instance.graph_2.arcs
#let mces_clq_n2 = mces_clq.source.instance.graph_2.num_vertices
// Retained arc pairs (i, j, assignments), recomputed from the loaded instance.
#let mces_clq_cands = {
let out = ()
for (i, a) in mces_clq_arcs1.enumerate() {
for (j, b) in mces_clq_arcs2.enumerate() {
if a.label == b.label and (a.src == a.dst) == (b.src == b.dst) {
out.push((i, j, ((a.src, b.src), (a.dst, b.dst))))
}
}
}
out
}
#let mces_clq_adjacent(p, q) = p.at(2).all(x => q.at(2).all(y => (x.at(0) == y.at(0)) == (x.at(1) == y.at(1))))
#let mces_clq_edges = {
let out = ()
for p in range(mces_clq_cands.len()) {
for q in range(p + 1, mces_clq_cands.len()) {
if mces_clq_adjacent(mces_clq_cands.at(p), mces_clq_cands.at(q)) { out.push((p, q)) }
}
}
out
}
#let mces_clq_chosen = mces_clq_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i)
#let mces_clq_map = {
let f = range(mces_clq.source.instance.graph_1.num_vertices).map(_ => mces_clq_n2)
for k in mces_clq_chosen {
for (s, t) in mces_clq_cands.at(k).at(2) { f.at(s) = t }
}
f
}
#let mces_clq_degree(v) = mces_clq_edges.filter(e => e.at(0) == v or e.at(1) == v).len()
#let mces_clq_isolated = range(mces_clq_cands.len()).filter(v => mces_clq_degree(v) == 0)
#let mces_clq_rest = range(mces_clq_cands.len()).filter(v => mces_clq_degree(v) > 0)
#assert(mces_clq_isolated.len() == 1)
#assert(mces_clq_edges.len() == mces_clq_rest.len() * (mces_clq_rest.len() - 1) / 2)
#assert(mces_clq_rest == mces_clq_chosen)
#assert(mces_clq_cands.len() == mces_clq.target.instance.graph.num_vertices)
#assert(mces_clq_edges == mces_clq.target.instance.graph.edges)
#assert(mces_clq_map == mces_clq_sol.source_config)
// NO example: a score of m_1 + 1 needs m_1 + 1 pairwise adjacent vertices.
#let mces_clq_threshold = mces_clq_arcs1.len() + 1
#assert(mces_clq_edges.len() < mces_clq_threshold * (mces_clq_threshold - 1) / 2)
#assert(mces_clq_chosen.len() == mces_clq_arcs1.len())
#reduction-rule("MaximumCommonEdgeSubgraph", "MaximumClique",
example: true,
example-target-variant: (graph: "SimpleGraph", weight: "One"),
example-caption: [Labelled 4-cycle against a 5-vertex digraph with a competing first arc],
extra: [
#pred-commands(
"pred create --example " + rule-spec(mces_clq) + " -o source.json",
"pred reduce source.json --via route.json -o bundle.json",
"pred solve bundle.json",
"pred evaluate source.json --config " + cli-config(mces_clq_sol.source_config),
)
*YES instance.* The source has $|V_1| = #mces_clq.source.instance.graph_1.num_vertices$ vertices and $|E_1| = #mces_clq_arcs1.len()$ arcs, the second graph $|V_2| = #mces_clq_n2$ vertices and $|E_2| = #mces_clq_arcs2.len()$ arcs. Exactly #mces_clq_cands.len() arc pairs have equal labels and equal loop status:
#table(
columns: 3,
[vertex], [pair $(e, e')$], [assignments],
..mces_clq_cands.enumerate().map(((k, c)) => (
[#k],
[$(e_#(c.at(0) + 1), e'_#(c.at(1) + 1))$],
[$#c.at(2).map(x => $#x.at(0) -> #x.at(1)$).join(", ")$],
)).flatten(),
)
The target has #mces_clq.target.instance.graph.num_vertices vertices and #mces_clq.target.instance.graph.edges.len() edges $#mces_clq_edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")$; vertex #mces_clq_isolated.map(str).join(", ") has degree 0 and the remaining #mces_clq_rest.len() vertices are pairwise adjacent, a $K_#mces_clq_rest.len()$. The witness clique $C = {#mces_clq_chosen.map(str).join(", ")}$ extracts to the map $(#fmt-values(mces_clq_map))$, which preserves all #mces_clq_arcs1.len() source arcs (sentinel #mces_clq_n2 means unmatched).

*NO instance.* A map preserving #mces_clq_threshold arcs would need a clique of #mces_clq_threshold vertices, hence $#(mces_clq_threshold * (mces_clq_threshold - 1) / 2)$ edges, but the target has only #mces_clq_edges.len(); the source cannot exceed $|E_1| = #mces_clq_arcs1.len()$ arcs either. Both answers are NO at threshold #mces_clq_threshold, and the optimum #mces_clq_chosen.len() is the clique number.
],
)[
Build an association graph on the label-compatible pairs of a source arc and a target arc: a clique is a set of arc correspondences whose endpoint assignments form one partial injective vertex map, so the maximum clique size equals the maximum number of preserved arcs. For the restricted class of weakly connected, simple, oriented, node-labelled digraphs, a related clique formulation through extended line digraphs is given in @NeremEtAl2020DirectedMCES; the construction here is proved directly for general edge-labelled digraphs with loops, disconnected graphs and partial maps.
][
_Construction._ Let $G_i = (V_i, E_i)$ with $E_i subset.eq V_i times Sigma times V_i$ and $E_1, E_2$ in their stored order. A pair $(e, e')$ of $e = (u, lambda, v) in E_1$ and $e' = (a, mu, b) in E_2$ is _retained_ iff $lambda = mu$ and $(u = v) <=> (a = b)$. The target graph $H$ has one unit-weight vertex per retained pair, ordered by $e$ and then $e'$, carrying the assignments $A_p = {(u, a), (v, b)}$ (a single assignment for loops). Distinct $p, q$ are adjacent iff $(s = s') <=> (t = t')$ for all $(s, t) in A_p$ and $(s', t') in A_q$, i.e. iff $A_p union A_q$ is a function and injective. $H$ has at most $|E_1| |E_2|$ vertices and at most $binom(|E_1| |E_2|, 2)$ edges.

_Correctness._ ($arrow.r.double$) Let $f$ be a partial injection preserving the arcs $e_1, dots, e_k$ and let $e'_r = (f(u_r), lambda_r, f(v_r))$ be the target arc of $e_r$ (unique, as arc sets are sets). The pair $(e_r, e'_r)$ is retained: labels agree, and $u_r = v_r <=> f(u_r) = f(v_r)$ because $f$ is injective. All assignments lie in the graph of $f$, so their unions are functions and injective; the $k$ pairs are pairwise adjacent, giving a clique of size $k$. ($arrow.l.double$) Let $C$ be a clique of size $c$ and $g = union_(p in C) A_p$. Pairwise adjacency makes $g$ a partial injection, and each selected pair $(e, e')$ has $g(u) = a$, $g(v) = b$ with $e' = (a, lambda, b) in E_2$, so $e$ is preserved. Two selected pairs cannot share the source arc $e$: agreement on $u$ and $v$ would force the same endpoints and label of the target arc, hence the same $e'$. So $g$ preserves at least $c$ arcs. Both optima coincide, and $max |C| <= min(|E_1|, |E_2|)$.

_Solution extraction._ Validate that the selection is a clique, set every source vertex to the sentinel $|V_2|$, then write the assignments of each selected vertex. A maximum clique extracts an optimal map; a non-maximum clique can extract a map that preserves more arcs than $|C|$ (three consecutive arcs of a labelled 4-cycle determine all four images), so only the completed optimum values map identically.
]

#let cmo_ilp = load-example(
"MaximumContactMapOverlap",
"ILP",
Expand Down
9 changes: 9 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -2352,3 +2352,12 @@ @article{een2006
pages = {1--26},
doi = {10.3233/SAT190014},
}

@misc{NeremEtAl2020DirectedMCES,
author = {R. R. Nerem and P. Crawford-Kahrl and B. Cummins and T. Gedeon},
title = {A poset metric from the directed maximum common edge subgraph},
year = {2020},
eprint = {1910.14638},
archivePrefix = {arXiv},
url = {https://arxiv.org/abs/1910.14638}
}
2 changes: 1 addition & 1 deletion src/rules/maximumcommonedgesubgraph_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl ReduceTo<ILP<bool>> for MaximumCommonEdgeSubgraph {
let objective: Vec<(usize, i64)> = (0..num_y).map(|seq| (y_idx(seq), 1)).collect();

let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize)
.map_err(Self::target_construction)?;
.map_err(<Self as ReduceTo<ILP<bool>>>::target_construction)?;

Ok(ReductionMCESToILP {
target,
Expand Down
178 changes: 178 additions & 0 deletions src/rules/maximumcommonedgesubgraph_maximumclique.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//! Reduction from MaximumCommonEdgeSubgraph to MaximumClique via an association graph.
//!
//! Every pair of a source arc `(u, l, v)` and a target arc `(a, l, b)` with the
//! same label and the same loop status becomes a vertex that proposes the
//! assignments `u -> a` and `v -> b`. Two vertices are adjacent exactly when the
//! union of their assignments is a partial injection. A clique therefore
//! describes a partial injective vertex map that preserves at least as many
//! arcs as the clique has vertices, and the maximum clique size equals the
//! optimum of the source problem.

use crate::models::graph::{MaximumClique, MaximumCommonEdgeSubgraph};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult, validate_target_witness};
use crate::rules::{ExtractionResult, ReductionError};
use crate::topology::SimpleGraph;
use crate::traits::Problem;
use crate::types::One;
use std::collections::HashSet;

/// Endpoint assignments `(source vertex, target vertex)` proposed by one candidate.
type Assignments = [(usize, usize); 2];

/// Result of reducing MaximumCommonEdgeSubgraph to MaximumClique.
///
/// Target vertex `k` is the `k`-th retained arc pair, ordered by source arc
/// index and then by target arc index.
#[derive(Debug, Clone)]
pub struct ReductionMCESToClique {
target: MaximumClique<SimpleGraph, One>,
num_vertices_1: usize,
num_vertices_2: usize,
assignments: Vec<Assignments>,
}

impl ReductionResult for ReductionMCESToClique {
type Source = MaximumCommonEdgeSubgraph;
type Target = MaximumClique<SimpleGraph, One>;

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

/// Start with every source vertex unmatched (sentinel `|V2|`), then write
/// the endpoint assignments of each selected candidate. Pairwise adjacency
/// guarantees that the writes agree.
fn extract_solution(
&self,
target_solution: &<Self::Target as Problem>::Solution,
) -> ExtractionResult<<Self::Source as Problem>::Solution> {
validate_target_witness(
self.target_problem(),
target_solution,
|value| value.0.is_some(),
"selected vertices are not a clique",
)?;

let mut mapping = vec![self.num_vertices_2; self.num_vertices_1];
for (selected, assignment) in target_solution.iter().zip(&self.assignments) {
if *selected {
for &(source_vertex, target_vertex) in assignment {
mapping[source_vertex] = target_vertex;
}
}
}
Ok(mapping)
}
}

#[crate::aggregate_reduction(identity)]
impl crate::rules::traits::AggregateReductionResult for ReductionMCESToClique {}

/// Two candidates are compatible when equal source vertices carry equal target
/// vertices and different source vertices carry different target vertices.
fn compatible(left: &Assignments, right: &Assignments) -> bool {
left.iter()
.all(|&(s, t)| right.iter().all(|&(s2, t2)| (s == s2) == (t == t2)))
}

// At most one vertex per (source arc, target arc) pair, and at most one edge per
// vertex pair; the label and loop filters only remove vertices and edges.
#[reduction(transform = upper_bound {
num_vertices = "num_arcs_1 * num_arcs_2",
num_edges = "num_arcs_1 * num_arcs_2 * (num_arcs_1 * num_arcs_2 - 1) / 2",
})]
impl ReduceTo<MaximumClique<SimpleGraph, One>> for MaximumCommonEdgeSubgraph {
type Result = ReductionMCESToClique;

fn reduce_to(&self) -> Result<Self::Result, ReductionError> {
type Target = MaximumClique<SimpleGraph, One>;
let invalid = |message: String| ReductionError::invalid_target::<Self, Target>(message);
let (n1, n2) = (self.num_vertices_1(), self.num_vertices_2());
for (name, graph, bound) in [
("first", self.graph_1(), n1),
("second", self.graph_2(), n2),
] {
let mut seen = HashSet::new();
for arc in graph.arcs() {
if arc.src >= bound || arc.dst >= bound {
return Err(invalid(format!(
"an arc of the {name} graph has an endpoint outside 0..{bound}"
)));
}
if !seen.insert(*arc) {
return Err(invalid(format!("the {name} graph repeats an arc")));
}
}
}

let mut assignments: Vec<Assignments> = Vec::new();
for source_arc in self.graph_1().arcs() {
for target_arc in self.graph_2().arcs() {
let same_loop_status =
(source_arc.src == source_arc.dst) == (target_arc.src == target_arc.dst);
if source_arc.label == target_arc.label && same_loop_status {
assignments.push([
(source_arc.src, target_arc.src),
(source_arc.dst, target_arc.dst),
]);
}
}
}

let mut edges = Vec::new();
for (p, left) in assignments.iter().enumerate() {
for (offset, right) in assignments[p + 1..].iter().enumerate() {
if compatible(left, right) {
edges.push((p, p + 1 + offset));
}
}
}

let graph = SimpleGraph::try_new(assignments.len(), edges)
.map_err(<Self as ReduceTo<Target>>::target_construction)?;
let weights = vec![One; assignments.len()];
Ok(ReductionMCESToClique {
target: MaximumClique::new(graph, weights),
num_vertices_1: n1,
num_vertices_2: n2,
assignments,
})
}
}

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

vec![crate::example_db::specs::RuleExampleSpec {
id: "maximumcommonedgesubgraph_to_maximumclique",
build: || {
let arcs = |triples: &[(usize, usize, usize)]| {
triples
.iter()
.map(|&(u, label, v)| LabelledArc::new(u, label, v))
.collect::<Vec<_>>()
};
let source = MaximumCommonEdgeSubgraph::new(
LabelledDigraph::new(4, arcs(&[(0, 0, 1), (1, 1, 2), (2, 2, 3), (3, 3, 0)])),
LabelledDigraph::new(
5,
arcs(&[(0, 0, 1), (1, 1, 3), (3, 2, 4), (4, 3, 0), (2, 0, 3)]),
),
);
crate::example_db::specs::rule_example_with_witness::<_, MaximumClique<SimpleGraph, One>>(
source,
SolutionPair {
source_config: serde_json::json!([0, 1, 3, 4]),
target_config: serde_json::json!([true, false, true, true, true]),
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/maximumcommonedgesubgraph_maximumclique.rs"]
mod tests;
2 changes: 2 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ pub(crate) mod maximum2satisfiability_ilp;
pub(crate) mod maximumclique_ilp;
pub(crate) mod maximumcokplex_ilp;
pub(crate) mod maximumcommonedgesubgraph_ilp;
pub(crate) mod maximumcommonedgesubgraph_maximumclique;
pub(crate) mod maximumcontactmapoverlap_ilp;
pub(crate) mod maximumdomaticnumber_ilp;
pub(crate) mod maximumedgeweightedkclique_ilp;
Expand Down Expand Up @@ -431,6 +432,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(maximum2satisfiability_maxcut::canonical_rule_example_specs());
specs.extend(maximumclique_maximumindependentset::canonical_rule_example_specs());
specs.extend(decisionmaximumindependentset_integralflowbundles::canonical_rule_example_specs());
specs.extend(maximumcommonedgesubgraph_maximumclique::canonical_rule_example_specs());
specs.extend(maximumindependentset_maximumclique::canonical_rule_example_specs());
specs.extend(maximumindependentset_maximumsetpacking::canonical_rule_example_specs());
specs.extend(maximummatching_maximumsetpacking::canonical_rule_example_specs());
Expand Down
Loading
Loading