use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use std::time::{Duration, Instant};
use super::duration_ms;
use crate::{Error, Graph};
mod cutter;
mod expanded;
mod graph;
mod result;
use cutter::*;
use expanded::*;
use graph::*;
pub use result::Separator;
const MAX_EXPANDED_BASE: u64 = u32::MAX as u64 / 2;
fn validate_graph_size(num_vertices: u32, num_edges: usize) -> Result<(), Error> {
let num_edges = u64::try_from(num_edges).unwrap_or(u64::MAX);
let expanded_base = u64::from(num_vertices).saturating_add(num_edges.saturating_mul(2));
if expanded_base > MAX_EXPANDED_BASE {
return Err(Error::TooLarge(format!(
"graph is too large for the FlowCutter separator index space ({num_vertices} vertices and {num_edges} edges)"
)));
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[must_use]
pub struct Budget {
steps: u64,
iterations: u32,
timeout: Option<Duration>,
}
impl Budget {
pub const fn new(steps: u64, iterations: u32) -> Self {
Self {
steps,
iterations,
timeout: None,
}
}
pub const fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
}
pub fn find(graph: &Graph, budget: Budget) -> Result<Option<Separator>, Error> {
if budget.steps == 0 || budget.iterations == 0 {
return Err(Error::InvalidInput(
"a FlowCutter separator search needs positive steps and iterations".into(),
));
}
if budget.timeout.is_some_and(|timeout| timeout.is_zero()) {
return Err(Error::InvalidInput(
"a FlowCutter separator timeout must be positive".into(),
));
}
if budget.steps > i64::MAX as u64 {
return Err(Error::InvalidInput(
"FlowCutter separator step budget does not fit in i64".into(),
));
}
if budget.iterations > i32::MAX as u32 {
return Err(Error::InvalidInput(
"FlowCutter separator iteration count does not fit in i32".into(),
));
}
if budget
.timeout
.is_some_and(|timeout| timeout.as_millis() > i64::MAX as u128)
{
return Err(Error::InvalidInput(
"FlowCutter separator timeout does not fit in milliseconds".into(),
));
}
validate_graph_size(graph.num_vertices, graph.edges.len())?;
let steps = budget.steps as i64;
let iterations = budget.iterations as i32;
let timeout_ms = budget.timeout.map(duration_ms).unwrap_or(0);
let separator = compute_vertices(
graph.num_vertices as usize,
&graph.edges,
steps,
iterations,
timeout_ms,
);
Ok(separator.and_then(|separator| result::with_sides(graph, separator)))
}
#[inline]
fn elapsed_since(start: Instant) -> u128 {
crate::meter::now()
.saturating_duration_since(start)
.as_millis()
}
fn compute_vertices(
n: usize,
edges: &[(u32, u32)],
steps: i64,
iters: i32,
timeout_ms: i64,
) -> Option<Vec<u32>> {
if n < 3 {
return None;
}
let g = OrigGraph::build(n as u32, edges)?;
if !is_connected(&g) {
return None;
}
let start = crate::meter::now();
let has_deadline = timeout_ms > 0;
let deadline_ms = timeout_ms as u128;
let mut outer_rng = MinstdRand::new(0);
let mut best: Option<Vec<u32>> = None;
let mut best_size = i32::MAX;
let n_orig = g.n as i64;
let m_arc = (g.tail.len() as i64).max(1);
let step_cost = (((n_orig as f64).sqrt() * (m_arc as f64).sqrt()) / 50.0).max(1.0) as i64;
let iter_cap = iters.max(1);
let mut steps_left = steps;
for i in 0..iter_cap {
if has_deadline && elapsed_since(start) >= deadline_ms {
break;
}
if steps_left <= 0 {
break;
}
steps_left -= step_cost;
let iter_units = super::native::iteration_work_units(n_orig as u64, (m_arc / 2) as u64);
crate::meter::charge(iter_units);
let min_small_side = match i % 3 {
2 => 0.2_f32,
1 => 0.1_f32,
_ => 0.0_f32,
};
let cfg = SearchConfig {
cutter_count: 1,
random_seed: outer_rng.next() as u64,
max_cut_size: 10_000,
min_small_side_size: min_small_side,
};
if let Some(sep) = compute_separator_one(&g, &cfg, deadline_ms, start, has_deadline)
&& !sep.is_empty()
&& (sep.len() as i32) < best_size
{
best_size = sep.len() as i32;
best = Some(sep);
}
}
best
}
#[derive(Clone)]
struct SearchConfig {
cutter_count: u32,
random_seed: u64,
max_cut_size: i32,
min_small_side_size: f32,
}
fn compute_separator_one(
g: &OrigGraph,
cfg: &SearchConfig,
deadline_ms: u128,
start: Instant,
has_deadline: bool,
) -> Option<Vec<u32>> {
let n_orig = g.n;
let a_orig = g.tail.len() as u32;
let n_exp_v = n_exp(n_orig);
let a_exp_v = a_exp(n_orig, a_orig);
let exp = Exp { g, a_orig };
let pairs = select_random_st_pairs(n_orig, cfg.cutter_count, cfg.random_seed);
if pairs.is_empty() {
return None;
}
let exp_pairs: Vec<(u32, u32)> = pairs
.iter()
.map(|&(s, t)| (orig_node_to_exp(s, false), orig_node_to_exp(t, true)))
.collect();
let mut multi = MultiCutter::new(n_exp_v, a_exp_v, exp_pairs.len() as u32);
multi.init(&exp, a_orig, &exp_pairs);
let mut best: Option<Vec<u32>> = None;
let mut best_score = f64::INFINITY;
let exp_node_count_f = n_exp_v as f64;
let min_balance_threshold = cfg.min_small_side_size as f64 * exp_node_count_f;
let mut iter_guard: u32 = 0;
loop {
if has_deadline && elapsed_since(start) >= deadline_ms {
break;
}
iter_guard += 1;
if iter_guard > 10_000_000 {
break;
}
let cut_size = multi.current_cut_size() as f64;
let small_side = multi.current_smaller_size() as f64;
let mut score = if small_side > 0.0 {
cut_size / small_side
} else {
f64::INFINITY
};
if multi.current_smaller_size() < min_balance_threshold as u32 {
score += 1_000_000.0;
}
if score < best_score {
best_score = score;
let sep = extract_original_separator(g, a_orig, &multi);
if (sep.len() as i32) > cfg.max_cut_size {
best = Some(sep);
break;
}
best = Some(sep);
}
let potential_best_next = (cut_size + 1.0) / (exp_node_count_f / 2.0);
if potential_best_next >= best_score {
break;
}
if !multi.advance(&exp, a_orig) {
break;
}
}
best
}
fn extract_original_separator(g: &OrigGraph, a_orig: u32, multi: &MultiCutter) -> Vec<u32> {
let mut sep: Vec<u32> = Vec::new();
let cur_cut = multi.current_cut();
for &xy in cur_cut {
if is_intra(xy, a_orig) {
sep.push(intra_to_orig_node(xy, a_orig));
}
}
let n_orig = g.n as usize;
let cur_small = multi.current_smaller_size() as i64;
let mut left_size = (cur_small - sep.len() as i64) / 2;
let mut right_size = n_orig as i64 - sep.len() as i64 - left_size;
let is_orig_left = |x: u32| -> bool {
multi.is_on_smaller_side(orig_node_to_exp(x, true))
};
for &xy in cur_cut {
if !is_intra(xy, a_orig) {
let lr = inter_to_orig_arc(xy);
let mut l = g.tail[lr as usize];
let mut r = g.head[lr as usize];
if is_orig_left(r) {
std::mem::swap(&mut l, &mut r);
}
if left_size > right_size {
sep.push(l);
left_size -= 1;
} else {
sep.push(r);
right_size -= 1;
}
}
}
sep.sort_unstable();
sep.dedup();
sep
}
fn select_random_st_pairs(n: u32, count: u32, seed: u64) -> Vec<(u32, u32)> {
let mut rng = SmallRng::seed_from_u64(seed);
let mut out = Vec::with_capacity(count as usize);
if n < 2 {
return out;
}
for _ in 0..count {
let mut s;
let mut t;
loop {
s = rng.next_u32() % n;
t = rng.next_u32() % n;
if s != t {
break;
}
}
out.push((s, t));
}
out
}
struct MinstdRand {
state: u64,
}
impl MinstdRand {
fn new(seed: u32) -> Self {
let s = if seed == 0 { 1 } else { seed };
MinstdRand { state: s as u64 }
}
fn next(&mut self) -> u32 {
self.state = (self.state * 48271) % ((1u64 << 31) - 1);
self.state as u32
}
}
#[cfg(test)]
mod tests;