Skip to content

Commit 63b53e3

Browse files
GiggleLiuclaude
andauthored
Fix #301: [Model] MinimumDummyActivitiesPert (#751)
* Add plan for #301: [Model] MinimumDummyActivitiesPert * Implement #301: [Model] MinimumDummyActivitiesPert * chore: remove plan file after implementation * Fix formatting after merge with main * Fix dummy arc overcount on transitive arcs, add regression test, fix paper solve command - Subtract task-arc intersections from dummy count: when a non-merged arc's dummy coincides with a task arc, it doesn't add a new arc to the event network and shouldn't be counted. - Add regression test for DAG 0→1, 1→2, 0→2 (optimal = 0 dummies). - Fix paper's pred solve command to include --solver brute-force. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix formatting after merge with main --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent adfe9ae commit 63b53e3

9 files changed

Lines changed: 567 additions & 14 deletions

File tree

docs/paper/reductions.typ

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
"MinMaxMulticenter": [Min-Max Multicenter],
142142
"FlowShopScheduling": [Flow Shop Scheduling],
143143
"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets],
144+
"MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks],
144145
"MinimumSumMulticenter": [Minimum Sum Multicenter],
145146
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
146147
"MultipleChoiceBranching": [Multiple Choice Branching],
@@ -2126,6 +2127,58 @@ is feasible: each set induces a connected subgraph, the component weights are $2
21262127
]
21272128
}
21282129

2130+
#{
2131+
let x = load-model-example("MinimumDummyActivitiesPert")
2132+
let nv = x.instance.graph.num_vertices
2133+
let arcs = x.instance.graph.arcs
2134+
let ne = arcs.len()
2135+
let sol = (config: x.optimal_config, metric: x.optimal_value)
2136+
let merged = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc)
2137+
let dummy = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 0).map(((i, arc)) => arc)
2138+
let opt = sol.metric.Valid
2139+
let blue = graph-colors.at(0)
2140+
[
2141+
#problem-def("MinimumDummyActivitiesPert")[
2142+
Given a precedence DAG $G = (V, A)$, find an activity-on-arc PERT event network with one real activity arc for each task $v in V$, minimizing the number of dummy activity arcs, such that for every ordered pair of tasks $(u, v)$ there is a path from the finish event of $u$ to the start event of $v$ if and only if $v$ is reachable from $u$ in $G$.
2143+
][
2144+
The decision version of minimum dummy activities appears as ND44 in Garey and Johnson's compendium @garey1979. It arises when an activity-on-node precedence DAG must be converted into an activity-on-arc PERT chart: merging compatible finish/start events removes dummy activities, but an over-aggressive merge creates spurious precedence relations between unrelated tasks. The implementation here enumerates, for each direct precedence arc, whether it is realized as an event merge or left as a dummy activity, so brute-force over the $m = #ne$ direct precedences yields a worst-case bound of $O^*(2^m)$. #footnote[No exact algorithm improving on the direct-precedence merge encoding implemented in the codebase is claimed here.]
2145+
2146+
*Example.* Consider the canonical precedence DAG on $n = #nv$ tasks with direct precedences #arcs.map(a => $(v_#(a.at(0)), v_#(a.at(1)))$).join(", "). The optimal encoding merges the predecessor-finish/successor-start pairs #merged.map(a => $(v_#(a.at(0)), v_#(a.at(1)))$).join(", "), so those handoffs need no dummy activity at all. The remaining direct precedences #dummy.map(a => $(v_#(a.at(0)), v_#(a.at(1)))$).join(" and ") still require dummy activities, so the optimum is $#opt$. Both unresolved precedences enter $v_3$, and merging either of them would identify unrelated task completions, creating spurious reachability between the two source tasks.
2147+
2148+
#pred-commands(
2149+
"pred create --example " + problem-spec(x) + " -o minimum-dummy-activities-pert.json",
2150+
"pred solve minimum-dummy-activities-pert.json --solver brute-force",
2151+
"pred evaluate minimum-dummy-activities-pert.json --config " + x.optimal_config.map(str).join(","),
2152+
)
2153+
2154+
#figure({
2155+
let positions = ((0, 1.0), (0, -0.3), (2.0, 1.3), (2.0, 0.35), (2.0, -0.95), (4.0, 1.3))
2156+
canvas(length: 1cm, {
2157+
for (k, pos) in positions.enumerate() {
2158+
g-node(pos, name: "v" + str(k),
2159+
fill: white,
2160+
label: [$v_#k$])
2161+
}
2162+
for arc in dummy {
2163+
let (u, v) = arc
2164+
draw.line("v" + str(u), "v" + str(v),
2165+
stroke: (paint: luma(140), thickness: 1pt, dash: "dashed"),
2166+
mark: (end: "straight", scale: 0.4))
2167+
}
2168+
for arc in merged {
2169+
let (u, v) = arc
2170+
draw.line("v" + str(u), "v" + str(v),
2171+
stroke: 1.7pt + blue,
2172+
mark: (end: "straight", scale: 0.45))
2173+
}
2174+
})
2175+
},
2176+
caption: [Canonical Minimum Dummy Activities in PERT Networks instance. Blue precedence arcs are encoded by merging the predecessor finish event with the successor start event; dashed gray arcs still require dummy activities. The optimal encoding leaves exactly #opt dummy activities.],
2177+
) <fig:minimum-dummy-activities-pert>
2178+
]
2179+
]
2180+
}
2181+
21292182
#{
21302183
let x = load-model-example("MinimumFeedbackVertexSet")
21312184
let nv = graph-num-vertices(x.instance)

problemreductions-cli/src/cli.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ Flags by problem type:
302302
SCS --strings, --bound [--alphabet-size]
303303
StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size]
304304
D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
305+
MinimumDummyActivitiesPert --arcs [--num-vertices]
305306
CBQ --domain-size, --relations, --conjuncts-spec
306307
ILP, CircuitSAT (via reduction only)
307308
@@ -331,6 +332,7 @@ Examples:
331332
pred create ConsistencyOfDatabaseFrequencyTables --num-objects 6 --attribute-domains \"2,3,2\" --frequency-tables \"0,1:1,1,1|1,1,1;1,2:1,1|0,2|1,1\" --known-values \"0,0,0;3,0,1;1,2,1\"
332333
pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5
333334
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
335+
pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6
334336
pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1
335337
pred create IntegralFlowHomologousArcs --arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\"
336338
pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\"

problemreductions-cli/src/commands/create.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ use problemreductions::models::formula::Quantifier;
1414
use problemreductions::models::graph::{
1515
DisjointConnectingPaths, GeneralizedHex, GraphPartitioning, HamiltonianCircuit,
1616
HamiltonianPath, IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, LongestPath,
17-
MinimumCutIntoBoundedSets, MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching,
18-
PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
17+
MinimumCutIntoBoundedSets, MinimumDummyActivitiesPert, MinimumMultiwayCut, MixedChinesePostman,
18+
MultipleChoiceBranching, PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs,
19+
StrongConnectivityAugmentation,
1920
};
2021
use problemreductions::models::misc::{
2122
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery,
@@ -617,6 +618,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
617618
"--arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" --capacities 1,1,1,1,1,1,1,1 --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 --requirement-1 1 --requirement-2 1"
618619
}
619620
"MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"",
621+
"MinimumDummyActivitiesPert" => "--arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6",
620622
"StrongConnectivityAugmentation" => {
621623
"--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1"
622624
}
@@ -3608,6 +3610,22 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
36083610
)
36093611
}
36103612

3613+
// MinimumDummyActivitiesPert
3614+
"MinimumDummyActivitiesPert" => {
3615+
let usage = "Usage: pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" [--num-vertices N]";
3616+
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
3617+
anyhow::anyhow!(
3618+
"MinimumDummyActivitiesPert requires --arcs\n\n\
3619+
{usage}"
3620+
)
3621+
})?;
3622+
let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?;
3623+
(
3624+
ser(MinimumDummyActivitiesPert::try_new(graph).map_err(|e| anyhow::anyhow!(e))?)?,
3625+
resolved_variant.clone(),
3626+
)
3627+
}
3628+
36113629
// MixedChinesePostman
36123630
"MixedChinesePostman" => {
36133631
let usage = "Usage: pred create MixedChinesePostman --graph 0-2,1-3,0-4,4-2 --arcs \"0>1,1>2,2>3,3>0\" --edge-weights 2,3,1,2 --arc-costs 2,3,1,4 --bound 24 [--num-vertices N]";
@@ -7166,6 +7184,56 @@ mod tests {
71667184
assert!(err.contains("--num-vertices (5) is too small for the arcs"));
71677185
}
71687186

7187+
#[test]
7188+
fn test_create_minimum_dummy_activities_pert_json() {
7189+
use crate::dispatch::ProblemJsonOutput;
7190+
use problemreductions::models::graph::MinimumDummyActivitiesPert;
7191+
7192+
let mut args = empty_args();
7193+
args.problem = Some("MinimumDummyActivitiesPert".to_string());
7194+
args.num_vertices = Some(6);
7195+
args.arcs = Some("0>2,0>3,1>3,1>4,2>5".to_string());
7196+
7197+
let output_path = temp_output_path("minimum_dummy_activities_pert");
7198+
let out = OutputConfig {
7199+
output: Some(output_path.clone()),
7200+
quiet: true,
7201+
json: false,
7202+
auto_json: false,
7203+
};
7204+
7205+
create(&args, &out).unwrap();
7206+
7207+
let json = fs::read_to_string(&output_path).unwrap();
7208+
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
7209+
assert_eq!(created.problem_type, "MinimumDummyActivitiesPert");
7210+
assert!(created.variant.is_empty());
7211+
7212+
let problem: MinimumDummyActivitiesPert = serde_json::from_value(created.data).unwrap();
7213+
assert_eq!(problem.num_vertices(), 6);
7214+
assert_eq!(problem.num_arcs(), 5);
7215+
7216+
let _ = fs::remove_file(output_path);
7217+
}
7218+
7219+
#[test]
7220+
fn test_create_minimum_dummy_activities_pert_rejects_cycles() {
7221+
let mut args = empty_args();
7222+
args.problem = Some("MinimumDummyActivitiesPert".to_string());
7223+
args.num_vertices = Some(3);
7224+
args.arcs = Some("0>1,1>2,2>0".to_string());
7225+
7226+
let out = OutputConfig {
7227+
output: None,
7228+
quiet: true,
7229+
json: false,
7230+
auto_json: false,
7231+
};
7232+
7233+
let err = create(&args, &out).unwrap_err().to_string();
7234+
assert!(err.contains("requires the input graph to be a DAG"));
7235+
}
7236+
71697237
#[test]
71707238
fn test_create_balanced_complete_bipartite_subgraph() {
71717239
use crate::dispatch::ProblemJsonOutput;

src/lib.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,12 @@ pub mod prelude {
5959
pub use crate::models::graph::{
6060
KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet,
6161
MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet,
62-
MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter,
63-
MinimumVertexCover, MultipleChoiceBranching, MultipleCopyFileAllocation,
64-
OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles,
65-
PathConstrainedNetworkFlow, RuralPostman, ShortestWeightConstrainedPath,
66-
SteinerTreeInGraphs, TravelingSalesman, UndirectedFlowLowerBounds,
67-
UndirectedTwoCommodityIntegralFlow,
62+
MinimumDummyActivitiesPert, MinimumFeedbackArcSet, MinimumFeedbackVertexSet,
63+
MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching,
64+
MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2,
65+
PartitionIntoTriangles, PathConstrainedNetworkFlow, RuralPostman,
66+
ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman,
67+
UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow,
6868
};
6969
pub use crate::models::misc::{
7070
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation,

0 commit comments

Comments
 (0)