You could formulate the problem in CP Optimizer as follows. It even generalizes the problem to the case of tasks whose start date is not fixed but has to be decided in a time window.
from docplex.cp.model import *
# Time hh:mm to minute
def mn(time):
h,m = time.split(':')
return 60*int(h)+int(m)
T = [
# Length, Earliest start, Latest start
(45, mn("6:30"), mn("6:30")),
(45, mn("7:00"), mn("7:10")),
#...
]
# Cost of crew (per minute)
C = [ 10, 12, 11, 12, 13 ]
N = range(len(T)) # Number of nbTasks
M = range(len(C)) # Number of resources (crews)
# CP Optimizer formulation
model = CpoModel()
# Decision variables:
# task[i] : interval variable representing task i
# alloc[i][j]: optional interval variable representing task i allocated to crew j
task = [ interval_var(size=T[i][0], start=[T[i][1],T[i][2]], name="T{}".format(i)) for i in N]
alloc = [ [interval_var(optional=True, name="T{}R{}".format(i,j)) for j in M] for i in N]
# Each task must be allocated one and only one crew:
model.add([alternative(task[i], [alloc[i][j] for j in M]) for i in N])
# Tasks performed by a crew do not overlap
model.add([no_overlap([alloc[i][j] for i in N]) for j in M])
# No more than M tasks executed in parallel (this is a redundant constraint)
model.add(sum(pulse(task[i],1) for i in N) <= len(C))
# Minimize total allocation cost
cost = sum(C[j]*length_of(alloc[i][j]) for i in N for j in M)
model.add(minimize(cost))
# CP Optimizer resolution
solution = model.solve(LogPeriod=1000000, TimeLimit=30)
# Display solution
for i in N:
for j in M:
s = solution.get_var_solution(alloc[i][j])
if s.is_present():
print("Task {} scheduled on {}-{} with crew {}".format(i,s.get_start(),s.get_end(),j))
Will display something like:
Task 0 scheduled on 390-435 with crew 0
Task 1 scheduled on 420-465 with crew 2