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