Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@
"MinMaxMulticenter": [Min-Max Multicenter],
"FlowShopScheduling": [Flow Shop Scheduling],
"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets],
"MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
"MinimumTardinessSequencing": [Minimum Tardiness Sequencing],
"MultipleChoiceBranching": [Multiple Choice Branching],
Expand Down Expand Up @@ -2126,6 +2127,58 @@ is feasible: each set induces a connected subgraph, the component weights are $2
]
}

#{
let x = load-model-example("MinimumDummyActivitiesPert")
let nv = x.instance.graph.num_vertices
let arcs = x.instance.graph.arcs
let ne = arcs.len()
let sol = (config: x.optimal_config, metric: x.optimal_value)
let merged = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc)
let dummy = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 0).map(((i, arc)) => arc)
let opt = sol.metric.Valid
let blue = graph-colors.at(0)
[
#problem-def("MinimumDummyActivitiesPert")[
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$.
][
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.]

*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.

#pred-commands(
"pred create --example " + problem-spec(x) + " -o minimum-dummy-activities-pert.json",
"pred solve minimum-dummy-activities-pert.json --solver brute-force",
"pred evaluate minimum-dummy-activities-pert.json --config " + x.optimal_config.map(str).join(","),
)

#figure({
let positions = ((0, 1.0), (0, -0.3), (2.0, 1.3), (2.0, 0.35), (2.0, -0.95), (4.0, 1.3))
canvas(length: 1cm, {
for (k, pos) in positions.enumerate() {
g-node(pos, name: "v" + str(k),
fill: white,
label: [$v_#k$])
}
for arc in dummy {
let (u, v) = arc
draw.line("v" + str(u), "v" + str(v),
stroke: (paint: luma(140), thickness: 1pt, dash: "dashed"),
mark: (end: "straight", scale: 0.4))
}
for arc in merged {
let (u, v) = arc
draw.line("v" + str(u), "v" + str(v),
stroke: 1.7pt + blue,
mark: (end: "straight", scale: 0.45))
}
})
},
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.],
) <fig:minimum-dummy-activities-pert>
]
]
}

#{
let x = load-model-example("MinimumFeedbackVertexSet")
let nv = graph-num-vertices(x.instance)
Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ Flags by problem type:
SCS --strings, --bound [--alphabet-size]
StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size]
D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
MinimumDummyActivitiesPert --arcs [--num-vertices]
CBQ --domain-size, --relations, --conjuncts-spec
ILP, CircuitSAT (via reduction only)

Expand Down Expand Up @@ -331,6 +332,7 @@ Examples:
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\"
pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6
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
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\"
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\"
Expand Down
72 changes: 70 additions & 2 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ use problemreductions::models::formula::Quantifier;
use problemreductions::models::graph::{
DisjointConnectingPaths, GeneralizedHex, GraphPartitioning, HamiltonianCircuit,
HamiltonianPath, IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, LongestPath,
MinimumCutIntoBoundedSets, MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching,
PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
MinimumCutIntoBoundedSets, MinimumDummyActivitiesPert, MinimumMultiwayCut, MixedChinesePostman,
MultipleChoiceBranching, PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs,
StrongConnectivityAugmentation,
};
use problemreductions::models::misc::{
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery,
Expand Down Expand Up @@ -617,6 +618,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--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"
}
"MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"",
"MinimumDummyActivitiesPert" => "--arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6",
"StrongConnectivityAugmentation" => {
"--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1"
}
Expand Down Expand Up @@ -3608,6 +3610,22 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// MinimumDummyActivitiesPert
"MinimumDummyActivitiesPert" => {
let usage = "Usage: pred create MinimumDummyActivitiesPert --arcs \"0>2,0>3,1>3,1>4,2>5\" [--num-vertices N]";
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"MinimumDummyActivitiesPert requires --arcs\n\n\
{usage}"
)
})?;
let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?;
(
ser(MinimumDummyActivitiesPert::try_new(graph).map_err(|e| anyhow::anyhow!(e))?)?,
resolved_variant.clone(),
)
}

// MixedChinesePostman
"MixedChinesePostman" => {
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]";
Expand Down Expand Up @@ -7166,6 +7184,56 @@ mod tests {
assert!(err.contains("--num-vertices (5) is too small for the arcs"));
}

#[test]
fn test_create_minimum_dummy_activities_pert_json() {
use crate::dispatch::ProblemJsonOutput;
use problemreductions::models::graph::MinimumDummyActivitiesPert;

let mut args = empty_args();
args.problem = Some("MinimumDummyActivitiesPert".to_string());
args.num_vertices = Some(6);
args.arcs = Some("0>2,0>3,1>3,1>4,2>5".to_string());

let output_path = temp_output_path("minimum_dummy_activities_pert");
let out = OutputConfig {
output: Some(output_path.clone()),
quiet: true,
json: false,
auto_json: false,
};

create(&args, &out).unwrap();

let json = fs::read_to_string(&output_path).unwrap();
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
assert_eq!(created.problem_type, "MinimumDummyActivitiesPert");
assert!(created.variant.is_empty());

let problem: MinimumDummyActivitiesPert = serde_json::from_value(created.data).unwrap();
assert_eq!(problem.num_vertices(), 6);
assert_eq!(problem.num_arcs(), 5);

let _ = fs::remove_file(output_path);
}

#[test]
fn test_create_minimum_dummy_activities_pert_rejects_cycles() {
let mut args = empty_args();
args.problem = Some("MinimumDummyActivitiesPert".to_string());
args.num_vertices = Some(3);
args.arcs = Some("0>1,1>2,2>0".to_string());

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("requires the input graph to be a DAG"));
}

#[test]
fn test_create_balanced_complete_bipartite_subgraph() {
use crate::dispatch::ProblemJsonOutput;
Expand Down
12 changes: 6 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ pub mod prelude {
pub use crate::models::graph::{
KColoring, LongestCircuit, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet,
MaximumMatching, MinMaxMulticenter, MinimumCutIntoBoundedSets, MinimumDominatingSet,
MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter,
MinimumVertexCover, MultipleChoiceBranching, MultipleCopyFileAllocation,
OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles,
PathConstrainedNetworkFlow, RuralPostman, ShortestWeightConstrainedPath,
SteinerTreeInGraphs, TravelingSalesman, UndirectedFlowLowerBounds,
UndirectedTwoCommodityIntegralFlow,
MinimumDummyActivitiesPert, MinimumFeedbackArcSet, MinimumFeedbackVertexSet,
MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching,
MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2,
PartitionIntoTriangles, PathConstrainedNetworkFlow, RuralPostman,
ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman,
UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow,
};
pub use crate::models::misc::{
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation,
Expand Down
Loading
Loading