Skip to content

Commit 359d0b8

Browse files
committed
Add Dijkstra algorithm
1 parent 6936b3f commit 359d0b8

File tree

5 files changed

+173
-15
lines changed

5 files changed

+173
-15
lines changed

lib/cpm_solver/solvers/dijkstra.rb

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,90 @@
11
module CpmSolver
22
module Solvers
33
class Dijkstra < Solver
4+
INF = Float::INFINITY
5+
6+
protected
7+
48
def calculate_early_times
5-
raise NotImplementedError, "#{self.class} must implement #calculate_early_times"
9+
# Initialize distances
10+
distances = {}
11+
program.activities.each_value do |activity|
12+
distances[activity.reference] = -INF
13+
end
14+
15+
# Find start activities (those with no predecessors)
16+
start_activities = program.activities.values.select { |a| a.predecessors.empty? }
17+
start_activities.each { |activity| distances[activity.reference] = 0 }
18+
19+
# Process activities in order of increasing distance
20+
queue = start_activities.dup
21+
while queue.any?
22+
current = queue.min_by { |a| distances[a.reference] }
23+
queue.delete(current)
24+
25+
# Update distances to successors
26+
current.successors.each do |succ_ref|
27+
successor = program.activities[succ_ref]
28+
new_distance = distances[current.reference] + current.duration
29+
30+
if new_distance > distances[successor.reference]
31+
distances[successor.reference] = new_distance
32+
queue << successor unless queue.include?(successor)
33+
end
34+
end
35+
end
36+
37+
# Set early times for all activities
38+
program.activities.each_value do |activity|
39+
activity.early_start = distances[activity.reference]
40+
activity.early_finish = activity.early_start + activity.duration
41+
end
642
end
743

844
def calculate_late_times
9-
raise NotImplementedError, "#{self.class} must implement #calculate_late_times"
45+
return unless program.activities.values.all? { |a| a.early_finish }
46+
47+
project_end = program.activities.values.map(&:early_finish).max
48+
distances = {}
49+
program.activities.each_value do |activity|
50+
distances[activity.reference] = INF
51+
end
52+
53+
# Find end activities (those with no successors)
54+
end_activities = program.activities.values.select { |a| a.successors.empty? }
55+
end_activities.each do |activity|
56+
# Set late finish to project end time
57+
distances[activity.reference] = project_end
58+
end
59+
60+
# Process activities in reverse order
61+
queue = end_activities.dup
62+
processed = Set.new
63+
64+
while queue.any?
65+
current = queue.min_by { |a| -distances[a.reference] }
66+
queue.delete(current)
67+
processed.add(current)
68+
69+
# Update distances to predecessors
70+
current.predecessors.each do |pred_ref|
71+
predecessor = program.activities[pred_ref]
72+
new_distance = distances[current.reference] - current.duration
73+
74+
if new_distance < distances[predecessor.reference]
75+
distances[predecessor.reference] = new_distance
76+
unless processed.include?(predecessor)
77+
queue << predecessor unless queue.include?(predecessor)
78+
end
79+
end
80+
end
81+
end
82+
83+
# Set late times for all activities
84+
program.activities.each_value do |activity|
85+
activity.late_finish = distances[activity.reference]
86+
activity.late_start = activity.late_finish - activity.duration
87+
end
1088
end
1189
end
1290
end

spec/cpm_solver/integration/house_program_spec.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ def log(message)
138138
include_examples "solver behavior", CpmSolver::Solvers::Topological
139139
end
140140

141+
context "with Dijkstra solver" do
142+
include_examples "solver behavior", CpmSolver::Solvers::Dijkstra
143+
end
144+
141145
after(:all) do
142146
if @verbose
143147
if Dir.exist?(@tmp_dir)

spec/cpm_solver/performance/solver_performance_spec.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,15 @@ def generate_large_program
147147
results = {
148148
'Bellman-Ford' => [],
149149
'Floyd-Warshall' => [],
150-
'Topological' => []
150+
'Topological' => [],
151+
'Dijkstra' => []
151152
}
152153

153154
solvers = {
154155
'Bellman-Ford' => CpmSolver::Solvers::BellmanFord,
155156
'Floyd-Warshall' => CpmSolver::Solvers::FloydWarshall,
156-
'Topological' => CpmSolver::Solvers::Topological
157+
'Topological' => CpmSolver::Solvers::Topological,
158+
'Dijkstra' => CpmSolver::Solvers::Dijkstra
157159
}
158160

159161
# Run each solver multiple times
Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,95 @@
11
require "spec_helper"
22

33
RSpec.describe CpmSolver::Solvers::Dijkstra do
4-
let(:program) { instance_double(CpmSolver::Core::Program) }
4+
let(:program) { CpmSolver::Core::Program.new("Test Program") }
5+
let(:solver) { described_class.new(program) }
6+
let(:activity_a) { CpmSolver::Core::Activity.new("A", "Task A", 5) }
7+
let(:activity_b) { CpmSolver::Core::Activity.new("B", "Task B", 3) }
8+
let(:activity_c) { CpmSolver::Core::Activity.new("C", "Task C", 4) }
59

6-
describe '#solve' do
7-
subject(:solver) { described_class.new(program) }
10+
before do
11+
program.add_activity(activity_a)
12+
program.add_activity(activity_b)
13+
program.add_activity(activity_c)
14+
program.add_predecessors(activity_b, [activity_a])
15+
program.add_predecessors(activity_c, [activity_b])
16+
end
17+
18+
describe "#calculate_early_times" do
19+
before { solver.send(:calculate_early_times) }
20+
21+
it "calculates early start times" do
22+
expect(activity_a.early_start).to eq(0)
23+
expect(activity_b.early_start).to eq(5)
24+
expect(activity_c.early_start).to eq(8)
25+
end
26+
27+
it "calculates early finish times" do
28+
expect(activity_a.early_finish).to eq(5)
29+
expect(activity_b.early_finish).to eq(8)
30+
expect(activity_c.early_finish).to eq(12)
31+
end
32+
end
33+
34+
describe "#calculate_late_times" do
35+
before do
36+
solver.send(:calculate_early_times)
37+
solver.send(:calculate_late_times)
38+
end
39+
40+
it "calculates late start times" do
41+
expect(activity_a.late_start).to eq(0)
42+
expect(activity_b.late_start).to eq(5)
43+
expect(activity_c.late_start).to eq(8)
44+
end
45+
46+
it "calculates late finish times" do
47+
expect(activity_a.late_finish).to eq(5)
48+
expect(activity_b.late_finish).to eq(8)
49+
expect(activity_c.late_finish).to eq(12)
50+
end
51+
end
52+
53+
describe "#solve" do
54+
before { solver.solve }
55+
56+
it "calculates slack times" do
57+
expect(activity_a.slack).to eq(0)
58+
expect(activity_b.slack).to eq(0)
59+
expect(activity_c.slack).to eq(0)
60+
end
61+
62+
it "identifies critical path" do
63+
expect(activity_a.critical).to be true
64+
expect(activity_b.critical).to be true
65+
expect(activity_c.critical).to be true
66+
end
67+
end
68+
69+
context "with parallel paths" do
70+
let(:activity_d) { CpmSolver::Core::Activity.new("D", "Task D", 2) }
71+
let(:activity_e) { CpmSolver::Core::Activity.new("E", "Task E", 3) }
872

973
before do
10-
allow(program).to receive(:validate)
11-
allow(program).to receive(:status).and_return(CpmSolver::Core::Program::STATUS[:validated])
12-
allow(program).to receive(:activities).and_return({})
74+
program.add_activity(activity_d)
75+
program.add_activity(activity_e)
76+
program.add_predecessors(activity_d, [activity_a])
77+
program.add_predecessors(activity_e, [activity_d])
78+
program.add_predecessors(activity_c, [activity_b, activity_e])
1379
end
1480

15-
it 'raises NotImplementedError when calculating early times' do
16-
expect { solver.solve }.to raise_error(
17-
NotImplementedError,
18-
"CpmSolver::Solvers::Dijkstra must implement #calculate_early_times"
19-
)
81+
it "calculates correct early and late times for parallel paths" do
82+
solver.solve
83+
84+
expect(activity_a.early_start).to eq(0)
85+
expect(activity_b.early_start).to eq(5)
86+
expect(activity_d.early_start).to eq(5)
87+
expect(activity_e.early_start).to eq(7)
88+
expect(activity_c.early_start).to eq(10)
89+
90+
expect(activity_d.critical).to be true
91+
expect(activity_e.critical).to be true
92+
expect(activity_b.slack).to be > 0
2093
end
2194
end
2295
end

spec/spec_helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require "cpm_solver/solvers/bellman_ford"
33
require "cpm_solver/solvers/floyd_warshall"
44
require "cpm_solver/solvers/topological"
5+
require "cpm_solver/solvers/dijkstra"
56
require "pry"
67

78
RSpec.configure do |config|

0 commit comments

Comments
 (0)