problemreductions/models/graph/
minimum_dummy_activities_pert.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry};
11use crate::topology::DirectedGraph;
12use crate::traits::Problem;
13use crate::types::Min;
14use serde::{Deserialize, Deserializer, Serialize};
15use std::collections::{BTreeMap, BTreeSet};
16
17inventory::submit! {
18 ProblemSchemaEntry {
19 name: "MinimumDummyActivitiesPert",
20 display_name: "Minimum Dummy Activities in PERT Networks",
21 aliases: &[],
22 dimensions: &[],
23 module_path: module_path!(),
24 description: "Find a PERT event network for a precedence DAG minimizing dummy activities",
25 fields: &[
26 FieldInfo {
27 name: "graph",
28 type_name: "DirectedGraph",
29 description: "The precedence DAG G=(V,A) whose vertices are tasks and arcs encode direct precedence constraints",
30 },
31 ],
32 }
33}
34
35#[derive(Debug, Clone, Serialize)]
45pub struct MinimumDummyActivitiesPert {
46 graph: DirectedGraph,
47}
48
49impl MinimumDummyActivitiesPert {
50 pub fn try_new(graph: DirectedGraph) -> Result<Self, String> {
52 if !graph.is_dag() {
53 return Err("MinimumDummyActivitiesPert requires the input graph to be a DAG".into());
54 }
55 Ok(Self { graph })
56 }
57
58 pub fn new(graph: DirectedGraph) -> Self {
64 Self::try_new(graph).unwrap_or_else(|msg| panic!("{msg}"))
65 }
66
67 pub fn graph(&self) -> &DirectedGraph {
69 &self.graph
70 }
71
72 pub fn num_vertices(&self) -> usize {
74 self.graph.num_vertices()
75 }
76
77 pub fn num_arcs(&self) -> usize {
79 self.graph.num_arcs()
80 }
81
82 pub fn is_valid_solution(&self, config: &[usize]) -> bool {
84 self.evaluate(config).is_valid()
85 }
86
87 fn precedence_arcs(&self) -> Vec<(usize, usize)> {
88 self.graph.arcs()
89 }
90
91 fn build_candidate_network(&self, config: &[usize]) -> Option<CandidatePertNetwork> {
92 let num_tasks = self.num_vertices();
93 let arcs = self.precedence_arcs();
94 if config.len() != arcs.len() || config.iter().any(|&bit| bit > 1) {
95 return None;
96 }
97
98 let mut uf = UnionFind::new(2 * num_tasks);
99 for ((u, v), &merge_bit) in arcs.iter().zip(config.iter()) {
100 if merge_bit == 1 {
101 uf.union(finish_endpoint(*u), start_endpoint(*v));
102 }
103 }
104
105 let roots: Vec<usize> = (0..2 * num_tasks)
106 .map(|endpoint| uf.find(endpoint))
107 .collect();
108 let mut root_to_dense = BTreeMap::new();
109 for &root in &roots {
110 let next = root_to_dense.len();
111 root_to_dense.entry(root).or_insert(next);
112 }
113
114 let start_events: Vec<usize> = (0..num_tasks)
115 .map(|task| root_to_dense[&roots[start_endpoint(task)]])
116 .collect();
117 let finish_events: Vec<usize> = (0..num_tasks)
118 .map(|task| root_to_dense[&roots[finish_endpoint(task)]])
119 .collect();
120
121 if start_events
122 .iter()
123 .zip(finish_events.iter())
124 .any(|(start, finish)| start == finish)
125 {
126 return None;
127 }
128
129 let task_arcs: Vec<(usize, usize)> = (0..num_tasks)
130 .map(|task| (start_events[task], finish_events[task]))
131 .collect();
132
133 let dummy_arcs: BTreeSet<(usize, usize)> = arcs
134 .iter()
135 .zip(config.iter())
136 .filter_map(|((u, v), &merge_bit)| {
137 if merge_bit == 1 {
138 return None;
139 }
140 let source = finish_events[*u];
141 let target = start_events[*v];
142 (source != target).then_some((source, target))
143 })
144 .collect();
145
146 let task_arc_set: BTreeSet<(usize, usize)> = task_arcs.iter().copied().collect();
147 let num_dummy_arcs = dummy_arcs.difference(&task_arc_set).count();
148
149 let mut event_arcs = task_arcs;
150 event_arcs.extend(dummy_arcs.iter().copied());
151 let event_graph = DirectedGraph::new(root_to_dense.len(), event_arcs);
152 if !event_graph.is_dag() {
153 return None;
154 }
155
156 Some(CandidatePertNetwork {
157 event_graph,
158 start_events,
159 finish_events,
160 num_dummy_arcs,
161 })
162 }
163}
164
165impl Problem for MinimumDummyActivitiesPert {
166 const NAME: &'static str = "MinimumDummyActivitiesPert";
167 type Value = Min<i32>;
168
169 fn variant() -> Vec<(&'static str, &'static str)> {
170 crate::variant_params![]
171 }
172
173 fn dims(&self) -> Vec<usize> {
174 vec![2; self.graph.num_arcs()]
175 }
176
177 fn evaluate(&self, config: &[usize]) -> Min<i32> {
178 let Some(candidate) = self.build_candidate_network(config) else {
179 return Min(None);
180 };
181
182 let source_reachability = reachability_matrix(&self.graph);
183 let event_reachability = reachability_matrix(&candidate.event_graph);
184
185 for source in 0..self.num_vertices() {
186 for target in 0..self.num_vertices() {
187 let pert_reachable = candidate.finish_events[source]
188 == candidate.start_events[target]
189 || event_reachability[candidate.finish_events[source]]
190 [candidate.start_events[target]];
191 if source_reachability[source][target] != pert_reachable {
192 return Min(None);
193 }
194 }
195 }
196
197 Min(Some(
198 i32::try_from(candidate.num_dummy_arcs).expect("dummy activity count must fit in i32"),
199 ))
200 }
201}
202
203crate::declare_variants! {
204 default MinimumDummyActivitiesPert => "2^num_arcs",
205}
206
207#[cfg(feature = "example-db")]
208pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
209 vec![crate::example_db::specs::ModelExampleSpec {
210 id: "minimum_dummy_activities_pert",
211 instance: Box::new(MinimumDummyActivitiesPert::new(DirectedGraph::new(
212 6,
213 vec![(0, 2), (0, 3), (1, 3), (1, 4), (2, 5)],
214 ))),
215 optimal_config: vec![1, 0, 0, 1, 1],
216 optimal_value: serde_json::json!(2),
217 }]
218}
219
220#[derive(Deserialize)]
221struct MinimumDummyActivitiesPertData {
222 graph: DirectedGraph,
223}
224
225impl<'de> Deserialize<'de> for MinimumDummyActivitiesPert {
226 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
227 where
228 D: Deserializer<'de>,
229 {
230 let data = MinimumDummyActivitiesPertData::deserialize(deserializer)?;
231 Self::try_new(data.graph).map_err(serde::de::Error::custom)
232 }
233}
234
235struct CandidatePertNetwork {
236 event_graph: DirectedGraph,
237 start_events: Vec<usize>,
238 finish_events: Vec<usize>,
239 num_dummy_arcs: usize,
240}
241
242#[derive(Debug)]
243struct UnionFind {
244 parent: Vec<usize>,
245}
246
247impl UnionFind {
248 fn new(size: usize) -> Self {
249 Self {
250 parent: (0..size).collect(),
251 }
252 }
253
254 fn find(&mut self, x: usize) -> usize {
255 if self.parent[x] != x {
256 let root = self.find(self.parent[x]);
257 self.parent[x] = root;
258 }
259 self.parent[x]
260 }
261
262 fn union(&mut self, a: usize, b: usize) {
263 let root_a = self.find(a);
264 let root_b = self.find(b);
265 if root_a != root_b {
266 self.parent[root_b] = root_a;
267 }
268 }
269}
270
271fn start_endpoint(task: usize) -> usize {
272 2 * task
273}
274
275fn finish_endpoint(task: usize) -> usize {
276 2 * task + 1
277}
278
279fn reachability_matrix(graph: &DirectedGraph) -> Vec<Vec<bool>> {
280 let num_vertices = graph.num_vertices();
281 let adjacency: Vec<Vec<usize>> = (0..num_vertices)
282 .map(|vertex| graph.successors(vertex))
283 .collect();
284 let mut reachable = vec![vec![false; num_vertices]; num_vertices];
285
286 for source in 0..num_vertices {
287 let mut stack = adjacency[source].clone();
288 while let Some(vertex) = stack.pop() {
289 if reachable[source][vertex] {
290 continue;
291 }
292 reachable[source][vertex] = true;
293 stack.extend(adjacency[vertex].iter().copied());
294 }
295 }
296
297 reachable
298}
299
300#[cfg(test)]
301#[path = "../../unit_tests/models/graph/minimum_dummy_activities_pert.rs"]
302mod tests;