Skip to main content

problemreductions/models/graph/
highly_connected_deletion.rs

1//! Highly Connected Deletion problem implementation.
2//!
3//! Given a simple undirected graph `G = (V, E)`, find a minimum-cardinality
4//! edge set `F ⊆ E` such that every connected component of `G - F` is either:
5//!
6//! - an isolated vertex (singleton component), or
7//! - a highly connected graph on at least `3` vertices, i.e. with edge
8//!   connectivity `λ(H) > |V(H)| / 2` (strict inequality).
9//!
10//! Components of size `2` (isolated edges) are explicitly *not* valid clusters.
11//!
12//! Reference:
13//! - Hüffner, Komusiewicz, Liebtrau, Niedermeier, "Partitioning Biological
14//!   Networks into Highly Connected Clusters with Maximum Edge Coverage",
15//!   IEEE/ACM TCBB 11(3):455–467, 2014.
16//! - Hartuv, Shamir, "A clustering algorithm based on graph connectivity",
17//!   Information Processing Letters 76(4–6):175–181, 2000.
18
19use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
20use crate::topology::{Graph, SimpleGraph};
21use crate::traits::Problem;
22use crate::types::Min;
23use crate::variant::VariantParam;
24use serde::{Deserialize, Serialize};
25use std::collections::{HashSet, VecDeque};
26
27inventory::submit! {
28    ProblemSchemaEntry {
29        name: "HighlyConnectedDeletion",
30        display_name: "Highly Connected Deletion",
31        aliases: &[],
32        dimensions: &[
33            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
34        ],
35        category: crate::registry::ProblemCategory::Graph,
36        module_path: module_path!(),
37        description: "Minimum number of edge deletions so every component is an isolated vertex or a highly connected graph on >=3 vertices",
38        fields: &[
39            FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" },
40        ],
41    }
42}
43
44/// The Highly Connected Deletion problem.
45///
46/// Given a simple undirected graph `G = (V, E)`, find a minimum-cardinality
47/// edge set `F ⊆ E` such that every connected component of `G - F` is either
48/// an isolated vertex or a highly connected graph on at least `3` vertices.
49///
50/// A graph `H` is *highly connected* if its edge connectivity `λ(H)` is
51/// strictly greater than `|V(H)| / 2`. Components of size `2` (isolated edges)
52/// are never valid clusters.
53///
54/// # Type Parameters
55///
56/// * `G` - Graph type (currently only `SimpleGraph`).
57///
58/// # Example
59///
60/// ```
61/// use problemreductions::models::graph::HighlyConnectedDeletion;
62/// use problemreductions::topology::SimpleGraph;
63/// use problemreductions::{BruteForce, Problem};
64/// use problemreductions::types::Min;
65///
66/// // Triangle on {0,1,2} with leaf vertex 3 attached to 2.
67/// let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]);
68/// let problem = HighlyConnectedDeletion::new(graph);
69///
70/// // Optimal: delete only the leaf edge (2,3) → K3 + isolated {3}.
71/// let solution = BruteForce::new().solve(&problem).unwrap().unwrap();
72/// assert_eq!(problem.evaluate(&solution).unwrap(), Min(Some(1)));
73/// ```
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))]
76pub struct HighlyConnectedDeletion<G> {
77    /// The underlying graph.
78    graph: G,
79}
80
81impl<G: Graph> HighlyConnectedDeletion<G> {
82    /// Create a new Highly Connected Deletion instance from a graph.
83    pub fn new(graph: G) -> Self {
84        Self { graph }
85    }
86
87    /// Get a reference to the underlying graph.
88    pub fn graph(&self) -> &G {
89        &self.graph
90    }
91
92    /// Number of vertices in the underlying graph.
93    pub fn num_vertices(&self) -> usize {
94        self.graph.num_vertices()
95    }
96
97    /// Number of edges in the underlying graph.
98    pub fn num_edges(&self) -> usize {
99        self.graph.num_edges()
100    }
101
102    /// Check whether a deletion configuration leaves every component as either
103    /// an isolated vertex or a highly connected graph on at least `3` vertices.
104    pub fn is_valid_solution(&self, config: &[bool]) -> bool {
105        is_feasible_deletion(&self.graph, config)
106    }
107}
108
109impl<G> Problem for HighlyConnectedDeletion<G>
110where
111    G: Graph + VariantParam,
112{
113    const NAME: &'static str = "HighlyConnectedDeletion";
114    type Solution = Vec<bool>;
115    type Value = Min<i64>;
116
117    crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),];
118
119    fn variant() -> Vec<(&'static str, &'static str)> {
120        crate::variant_params![G]
121    }
122
123    fn evaluate(
124        &self,
125        config: &Self::Solution,
126    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
127        if config.len() != self.graph.num_edges() {
128            return Err(crate::traits::EvaluationError::InvalidConfiguration(
129                "edge-selection length does not match the graph".into(),
130            ));
131        }
132        Ok({
133            if !is_feasible_deletion(&self.graph, config) {
134                return Ok(Min(None));
135            }
136            let deleted = i64::try_from(config.iter().filter(|&&deleted| deleted).count())
137                .map_err(|_| {
138                    crate::traits::EvaluationError::IntegerOverflow(
139                        "converting deleted-edge count to i64".into(),
140                    )
141                })?;
142            Min(Some(deleted))
143        })
144    }
145}
146
147impl<G> crate::solvers::BruteForceProblem for HighlyConnectedDeletion<G>
148where
149    G: Graph + VariantParam,
150{
151    fn dimensions(&self) -> Vec<usize> {
152        vec![2; self.graph.num_edges()]
153    }
154}
155
156/// Decide feasibility of a deletion configuration.
157///
158/// `config[e] = 1` means edge `e` (in `graph.edges()` order) is deleted.
159/// The remaining graph `G - F` must have every connected component be either
160/// a singleton or a highly connected graph on at least `3` vertices.
161fn is_feasible_deletion<G: Graph>(graph: &G, config: &[bool]) -> bool {
162    let n = graph.num_vertices();
163    let edges = graph.edges();
164    if config.len() != edges.len() {
165        return false;
166    }
167
168    // Build adjacency from the surviving edges only.
169    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
170    for (i, &(u, v)) in edges.iter().enumerate() {
171        if !config.get(i).copied().unwrap_or(false) {
172            adj[u].push(v);
173            adj[v].push(u);
174        }
175    }
176
177    // Find connected components by BFS.
178    let mut visited = vec![false; n];
179    for start in 0..n {
180        if visited[start] {
181            continue;
182        }
183        let mut component: Vec<usize> = Vec::new();
184        let mut queue: VecDeque<usize> = VecDeque::new();
185        queue.push_back(start);
186        visited[start] = true;
187        while let Some(u) = queue.pop_front() {
188            component.push(u);
189            for &w in &adj[u] {
190                if !visited[w] {
191                    visited[w] = true;
192                    queue.push_back(w);
193                }
194            }
195        }
196        let size = component.len();
197        if size == 1 {
198            continue; // isolated vertex: allowed
199        }
200        if size == 2 {
201            return false; // 2-vertex component never valid
202        }
203        // size >= 3: must be highly connected.
204        let lambda = edge_connectivity(&component, &adj);
205        // Strict inequality: λ > size/2 (avoid float by using 2*λ > size).
206        if 2 * lambda <= size {
207            return false;
208        }
209    }
210    true
211}
212
213/// Compute the edge connectivity `λ(H)` of the induced subgraph on
214/// `vertices` using the surviving-edge adjacency list `adj`.
215///
216/// `λ(H) = min over distinct s, t in V(H) of max-flow(s -> t)` with unit edge
217/// capacities. We fix one source `s` (the first vertex in the component) and
218/// iterate over every other vertex `t`; by symmetry this suffices because the
219/// minimum cut separating *any* pair must also separate `s` from one of the
220/// resulting sides.
221///
222/// For each `(s, t)` pair we run Edmonds–Karp on the directed expansion of the
223/// induced subgraph (each undirected edge becomes two arcs of capacity 1).
224/// Components are small in tests so this runs well under the per-test budget.
225fn edge_connectivity(vertices: &[usize], adj: &[Vec<usize>]) -> usize {
226    let size = vertices.len();
227    if size <= 1 {
228        return 0;
229    }
230    // Index vertices locally 0..size for compact tables.
231    let mut local: std::collections::HashMap<usize, usize> =
232        std::collections::HashMap::with_capacity(size);
233    for (i, &v) in vertices.iter().enumerate() {
234        local.insert(v, i);
235    }
236
237    // Build directed-arc lists with residual capacities for Edmonds–Karp.
238    // Arc layout: arcs[2k] is forward, arcs[2k+1] is reverse for the k-th
239    // undirected edge. `head[a]` is the arc's destination; `cap[a]` is its
240    // current residual capacity.
241    let in_component: HashSet<usize> = vertices.iter().copied().collect();
242    let mut head: Vec<usize> = Vec::new();
243    let mut cap: Vec<u8> = Vec::new();
244    let mut out: Vec<Vec<usize>> = vec![Vec::new(); size];
245
246    let mut seen_edges: HashSet<(usize, usize)> = HashSet::new();
247    for &u in vertices {
248        let lu = local[&u];
249        for &v in &adj[u] {
250            if !in_component.contains(&v) {
251                continue;
252            }
253            let key = if u < v { (u, v) } else { (v, u) };
254            if !seen_edges.insert(key) {
255                continue;
256            }
257            let lv = local[&v];
258            // Forward arc u -> v.
259            let a = head.len();
260            head.push(lv);
261            cap.push(1);
262            // Reverse arc v -> u.
263            head.push(lu);
264            cap.push(1);
265            out[lu].push(a);
266            out[lv].push(a + 1);
267        }
268    }
269
270    let mut best = usize::MAX;
271    let s = 0;
272    for t in 1..size {
273        // Reset residual capacities for each (s, t) pair.
274        for c in cap.iter_mut() {
275            *c = 1;
276        }
277        let mut flow = 0usize;
278        loop {
279            // BFS to find an augmenting path with positive residual capacity.
280            let mut parent_arc: Vec<Option<usize>> = vec![None; size];
281            let mut visited = vec![false; size];
282            visited[s] = true;
283            let mut queue: VecDeque<usize> = VecDeque::new();
284            queue.push_back(s);
285            while let Some(u) = queue.pop_front() {
286                if u == t {
287                    break;
288                }
289                for &a in &out[u] {
290                    let v = head[a];
291                    if !visited[v] && cap[a] > 0 {
292                        visited[v] = true;
293                        parent_arc[v] = Some(a);
294                        queue.push_back(v);
295                    }
296                }
297            }
298            if !visited[t] {
299                break;
300            }
301            // Augment by 1 (unit capacities).
302            let mut cur = t;
303            while cur != s {
304                let a = parent_arc[cur].expect("visited vertex has a BFS parent arc");
305                cap[a] -= 1;
306                cap[a ^ 1] += 1;
307                // The originating endpoint is the head of the reverse arc.
308                cur = head[a ^ 1];
309            }
310            flow += 1;
311        }
312        if flow < best {
313            best = flow;
314            if best == 0 {
315                return 0;
316            }
317        }
318    }
319    if best == usize::MAX {
320        0
321    } else {
322        best
323    }
324}
325
326crate::declare_variants! {
327    default HighlyConnectedDeletion<SimpleGraph> => "2^num_edges",
328}
329
330crate::register_brute_force! {
331    HighlyConnectedDeletion<SimpleGraph> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
332}
333
334#[cfg(feature = "example-db")]
335pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
336    vec![crate::example_db::specs::ModelExampleSpec {
337        id: "highly_connected_deletion_simplegraph",
338        instance: Box::new(HighlyConnectedDeletion::new(SimpleGraph::new(
339            4,
340            vec![(0, 1), (0, 2), (1, 2), (2, 3)],
341        ))),
342        // Edges in input order; deleting only edge index 3 = (2,3) leaves K3 + {3}.
343        optimal_config: serde_json::json!(vec![false, false, false, true]),
344        optimal_value: serde_json::json!(1),
345    }]
346}
347
348/// Check whether a vertex subset `S` is a *feasible cluster* of `graph`.
349///
350/// A feasible cluster is either a singleton (`|S| = 1`) or a set of at least
351/// `3` vertices whose induced subgraph `G[S]` is connected and *highly
352/// connected* (edge connectivity strictly greater than `|S| / 2`).
353///
354/// This is the cluster-feasibility predicate used by the set-partitioning ILP
355/// reduction: `x_S` is allowed exactly when `is_feasible_cluster(graph, S)`.
356pub(crate) fn is_feasible_cluster<G: Graph>(graph: &G, vertices: &[usize]) -> bool {
357    let size = vertices.len();
358    if size == 0 {
359        return false;
360    }
361    if size == 1 {
362        return true;
363    }
364    if size == 2 {
365        return false;
366    }
367
368    // Build induced-subgraph adjacency restricted to `vertices`.
369    let n = graph.num_vertices();
370    let in_subset: HashSet<usize> = vertices.iter().copied().collect();
371    let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
372    for (u, v) in graph.edges() {
373        if in_subset.contains(&u) && in_subset.contains(&v) {
374            adj[u].push(v);
375            adj[v].push(u);
376        }
377    }
378
379    // The induced subgraph must itself be connected (a single component).
380    let mut visited: HashSet<usize> = HashSet::new();
381    let start = vertices[0];
382    let mut queue: VecDeque<usize> = VecDeque::new();
383    queue.push_back(start);
384    visited.insert(start);
385    while let Some(u) = queue.pop_front() {
386        for &w in &adj[u] {
387            if !visited.contains(&w) {
388                visited.insert(w);
389                queue.push_back(w);
390            }
391        }
392    }
393    if visited.len() != size {
394        return false;
395    }
396
397    // Strict inequality: λ(G[S]) > |S| / 2, equivalently 2 * λ > |S|.
398    let lambda = edge_connectivity(vertices, &adj);
399    2 * lambda > size
400}
401
402/// Count the number of induced edges of `graph` whose endpoints both lie
403/// inside `vertices`.
404pub(crate) fn induced_edge_count<G: Graph>(graph: &G, vertices: &[usize]) -> usize {
405    let in_subset: HashSet<usize> = vertices.iter().copied().collect();
406    graph
407        .edges()
408        .into_iter()
409        .filter(|(u, v)| in_subset.contains(u) && in_subset.contains(v))
410        .count()
411}
412
413#[cfg(test)]
414#[path = "../../unit_tests/models/graph/highly_connected_deletion.rs"]
415mod tests;
416
417#[cfg(test)]
418pub(crate) fn edge_connectivity_for_tests(vertices: &[usize], adj: &[Vec<usize>]) -> usize {
419    edge_connectivity(vertices, adj)
420}