#
Single-Machine Total Weighted Tardiness Problem ($1||\sum w_jT_j$)#
1. Overview#
The single-machine total weighted tardiness problem, denoted as $1||\sum w_jT_j$, is a fundamental challenge in operations research with significant practical applications in manufacturing and service industries. Its objective is to sequence a set of jobs on a single machine to minimize the total weighted penalty incurred for late completions. This problem is strongly NP-hard, meaning that finding an optimal solution becomes computationally intractable as the number of jobs grows. This guide introduces the primary exact solution methodology: a Branch-and-Bound algorithm. By leveraging key theoretical properties to intelligently prune the search space, this method can find provably optimal solutions for small to medium-sized problem instances, providing a crucial benchmark for industrial heuristics and a solid foundation for strategic planning.
2. Problem Definition and Notation#
Formally defining a scheduling problem is the foundational step toward developing a robust and implementable solution. A precise mathematical formulation clarifies the objective, constraints, and key parameters, ensuring that any proposed algorithm directly addresses the intended business logic. This section establishes the standard notation and mathematical framework for the $1||\sum w_jT_j$ problem.
2.1. Core Components#
The essential components of the problem are defined by a set of jobs, each with unique characteristics, that must be processed on a single machine. The notation used throughout this guide is summarized in the table below.
| Symbol | Definition |
|---|---|
| $n$ | The total number of jobs to be scheduled. |
| $j$ | The index representing a specific job, where $j = 1, \dots, n$. |
| $p_j$ | The processing time required for job $j$. |
| $w_j$ | The weight or priority of job $j$, representing the penalty per unit of tardiness. |
| $d_j$ | The due date of job $j$, which is the committed completion date. |
| $C_j$ | The completion time of job $j$ in a given sequence. |
| $T_j$ | The tardiness of job $j$, calculated as the amount of time by which its completion exceeds its due date. |
2.2. Objective Function#
The goal of the $1||\sum w_jT_j$ problem is to find a sequence of jobs that minimizes the sum of the weighted tardiness for all jobs. The tardiness for a single job, $T_j$, is the positive difference between its completion time and its due date. If a job is completed on or before its due date, its tardiness is zero.
The objective function is formally stated as:
$$ \text{Minimize} \sum_{j=1}^{n} w_j T_j $$$$ \text{where } T_j = \max(0, C_j - d_j) $$2.3. Computational Complexity#
The $1||\sum w_jT_j$ problem is classified as strongly NP-hard (Theorem 3.6.2). This classification has profound practical implications. It means there is no known algorithm that can find an optimal solution in polynomial time, meaning the computational effort required to guarantee optimality grows exponentially with the problem size. Furthermore, the “strongly” designation implies that even pseudo-polynomial time algorithms, such as certain dynamic programming approaches that are efficient when the input numbers are small, are not possible for this problem.
This inherent difficulty justifies the need for enumerative methods like Branch-and-Bound for finding exact solutions to small or moderately sized instances, and the widespread use of heuristics for larger industrial problems where near-optimal solutions are acceptable. Fortunately, as we will see, the problem possesses theoretical properties that allow us to solve it exactly for smaller instances without resorting to a complete brute-force search.
3. Theoretical Foundations: Dominance Properties & Complexity#
While the $1||\sum w_jT_j$ problem is computationally hard, its structure is not entirely random. Certain theoretical properties, known as dominance rules, allow us to intelligently prune the vast space of possible solutions. Instead of evaluating all $n!$ possible sequences, these rules help eliminate entire categories of schedules that are guaranteed to be suboptimal. These properties form the theoretical backbone of efficient exact algorithms like Branch-and-Bound.
3.1. Lemma 3.6.1: A Powerful Dominance Property#
Theoretical Recitation A key result provides a powerful condition for establishing precedence between two jobs in an optimal sequence.
If there are two jobs j and k with $d_j \le d_k$, $p_j \le p_k$ and $w_j \ge w_k$, then there is an optimal sequence in which job j appears before job k.
The proof of this lemma is based on a pairwise interchange argument, which demonstrates that if job k were to precede job j in any sequence, swapping their positions would either improve or not worsen the total weighted tardiness.
Why It Matters: Pruning the Search Space
- Role in the Algorithm: This lemma provides a powerful dominance rule that allows a search algorithm to prune entire branches of the solution space without evaluation, dramatically improving efficiency.
- Intuitive Example: Consider two jobs,
jandk, with the following attributes that satisfy the lemma’s conditions:- Job j: $p_j=5, w_j=10, d_j=20$
- Job k: $p_k=8, w_k=6, d_k=25$ Here, $d_j \le d_k$ (20 ≤ 25), $p_j \le p_k$ (5 ≤ 8), and $w_j \ge w_k$ (10 ≥ 6). Let’s assume they start at time $t=10$.
- Sequence (k, j):
- Job k completes at $C_k = 10 + 8 = 18$. Tardiness $T_k = \max(0, 18-25) = 0$.
- Job j completes at $C_j = 18 + 5 = 23$. Tardiness $T_j = \max(0, 23-20) = 3$.
- Total Weighted Tardiness = $(6 \times 0) + (10 \times 3) = \textbf{30}$.
- Sequence (j, k):
- Job j completes at $C_j = 10 + 5 = 15$. Tardiness $T_j = \max(0, 15-20) = 0$.
- Job k completes at $C_k = 15 + 8 = 23$. Tardiness $T_k = \max(0, 23-25) = 0$.
- Total Weighted Tardiness = $(10 \times 0) + (6 \times 0) = \textbf{0}$.
The sequence (j, k) yields a better result. The lemma allows an algorithm to discard the sequence (k, j) and any other sequence where k precedes j without needing to calculate their objective values.
3.2. Theorem 3.6.2: The Hardness Proof#
Theoretical Recitation The formal statement on the problem’s computational complexity is as follows.
The problem $1||\sum w_jT_j$ is strongly NP-hard.
This is proven by showing that the 3-PARTITION problem, a known strongly NP-hard problem, can be reduced to an instance of $1||\sum w_jT_j$.
Why It Matters: Setting Realistic Expectations
- Implication: This theorem formally establishes that finding an optimal solution is a fundamentally difficult task. The “strongly” NP-hard designation confirms that the problem’s difficulty is not merely an artifact of large input numbers (e.g., large processing times), solidifying the fact that no efficient, polynomial-time algorithm is likely to exist. This theoretical barrier justifies the use of exponential-time exact algorithms like Branch-and-Bound for smaller instances where optimality is critical, and polynomial-time heuristics for larger instances where a good, quick solution is sufficient.
4. The Branch-and-Bound Algorithm Framework#
Branch-and-Bound is a classic enumerative technique for solving NP-hard optimization problems. Its core concept is to intelligently explore a tree of possible solutions, where each node represents a partial solution. The algorithm avoids exhaustive searching by calculating a “bound” at each node—a lower limit on the best possible solution that can be obtained from that branch. If this lower bound is already worse than the best complete solution found so far, the entire branch is pruned, saving significant computational effort.
4.1. Core Logic Explained#
For the $1||\sum w_jT_j$ problem, the Branch-and-Bound algorithm is tailored with specific strategies for searching, branching, and bounding.
- Search Strategy: A backward construction approach is commonly used. The algorithm builds the sequence from the last position ($n$) to the first position (1). This is often advantageous because jobs scheduled later are more likely to incur tardiness and have a larger impact on the objective function.
- Branching: The search process is visualized as a tree. Starting from the root (an empty schedule), each level
kof the tree corresponds to a decision about which job to place in position(n-k+1). For each feasible, unscheduled job, a new branch is created from the current node. - Pruning (Dominance Rules): At each node, Lemma 3.6.1 is applied to reduce the number of potential branches. When considering which of the remaining jobs to place in the current last available position, the algorithm can eliminate any job
jthat dominates another available jobk. According to Lemma 3.6.1, the dominating jobjmust appear before the dominated jobkin an optimal sequence. Since we are building backward (assigning jobs to later completion times first), placing the dominating jobjin the current slot while the dominated jobkis still available would forcekinto an even earlier slot, meaningkwould appear beforejin the final schedule. This would violate the rule, so jobjcan be pruned as a candidate for this position. This dramatically reduces the number of branches created at each level. - Bounding (Lower Bound Calculation): A lower bound is calculated for each new node (partial schedule). This bound is the sum of two components:
- The definite, calculated weighted tardiness of the jobs already placed at the end of the partial schedule.
- A lower-bound estimate of the weighted tardiness for the remaining, unscheduled jobs. This estimate is obtained by relaxing the single-machine constraint and solving the problem as a transportation problem, which can be done efficiently. This provides a tight, optimistic estimate of the best possible outcome for the remaining jobs.
- Fathoming: A node is “fathomed” (i.e., pruned from the search tree) if its calculated lower bound is greater than or equal to the objective value of the best complete solution found so far. This best-known solution is called the incumbent or upper bound. If a new, better complete schedule is found, it becomes the new incumbent, allowing for more aggressive pruning in other parts of the tree.
By systematically applying these steps, the algorithm explores only the most promising regions of the solution space, making it feasible to find a provably optimal solution for instances that are too large for a brute-force approach.
5. Step-by-Step Walkthrough Example#
To make the abstract concepts of the Branch-and-Bound framework concrete, we will now apply it to the 4-job instance from Example 3.6.3 of the source text.
5.1. Problem Data#
The data for the four jobs are as follows:
| Job ($j$) | Weight ($w_j$) | Processing Time ($p_j$) | Due Date ($d_j$) |
|---|---|---|---|
| 1 | 4 | 12 | 16 |
| 2 | 5 | 8 | 26 |
| 3 | 3 | 15 | 25 |
| 4 | 5 | 9 | 27 |
5.2. Backward Search Tree Construction#
We will build the schedule backward, from position 4 to position 1, using the search tree depicted in Figure 3.7 of the source text. The total processing time for all jobs is $12+8+15+9 = 44$.
Level 1: Selecting the Last Job (Position 4)
The search begins at the root node by deciding which job to place in the last position. Following the Pruning strategy (Step 3 from Section 4.1), we first apply Lemma 3.6.1 to filter the candidates.
- Job 1 vs. Job 3:
- $d_1 (16) \le d_3 (25)$, $p_1 (12) \le p_3 (15)$, and $w_1 (4) \ge w_3 (3)$.
- All conditions are met. Job 1 dominates Job 3. According to the lemma, job 1 must appear before job 3. Therefore, job 1 cannot be placed in the last position while job 3 is still unscheduled, as this would violate the optimal ordering.
- Job 2 vs. Job 4:
- $d_2 (26) \le d_4 (27)$, $p_2 (8) \le p_4 (9)$, and $w_2 (5) \ge w_4 (5)$.
- All conditions are met. Job 2 dominates Job 4. Similarly, job 2 must appear before job 4. Job 2 cannot be placed in the last position while job 4 is unscheduled.
This dominance analysis means that jobs 1 and 2 are eliminated as candidates for the final position in the sequence. Only jobs 3 and 4 need to be considered.
Next, we perform the Bounding calculation (Step 4 from Section 4.1) for each of these two branches.
- Node (…, …, …, 4):
- If job 4 is last, it completes at $C_4 = 44$.
- Tardiness $T_4 = \max(0, 44 - 27) = 17$.
- Weighted tardiness for job 4 is $w_4T_4 = 5 \times 17 = 85$.
- The source text provides a lower bound of 22 for the remaining jobs {1, 2, 3}.
- Total Lower Bound = $85 + 22 = \textbf{107}$.
- Node (…, …, …, 3):
- If job 3 is last, it completes at $C_3 = 44$.
- Tardiness $T_3 = \max(0, 44 - 25) = 19$.
- Weighted tardiness for job 3 is $w_3T_3 = 3 \times 19 = 57$.
- The source text provides a lower bound of 8 for the remaining jobs {1, 2, 4}.
- Total Lower Bound = $57 + 8 = \textbf{65}$.
The algorithm will explore the node ending in 3 first, as its lower bound of 65 is more promising than 107.
Exploring the Most Promising Branch
The algorithm now proceeds down the branch where job 3 is in the last position. It continues branching, applying dominance rules, and calculating lower bounds at each level. Following the logic in the source text, this branch eventually leads to the full sequence 1, 2, 4, 3.
Let’s calculate the objective value for this sequence:
- $C_1 = 12 \implies T_1 = \max(0, 12-16) = 0$
- $C_2 = 12+8 = 20 \implies T_2 = \max(0, 20-26) = 0$
- $C_4 = 20+9 = 29 \implies T_4 = \max(0, 29-27) = 2$
- $C_3 = 29+15 = 44 \implies T_3 = \max(0, 44-25) = 19$
Total Weighted Tardiness = $(4 \times 0) + (5 \times 0) + (5 \times 2) + (3 \times 19) = 0 + 0 + 10 + 57 = \textbf{67}$.
This complete solution with a value of 64 becomes our first incumbent (upper bound).
Now, applying the Fathoming step (Step 5 from Section 4.1), the algorithm assesses the other branch from Level 1, the node ending in job 4. Its lower bound was 107. Since $107 > 64$, the lower bound of that branch is already worse than our best-known solution. Therefore, the entire branch can be pruned without further exploration.
graph TD
Start["ROOT
All jobs unscheduled
{1, 2, 3, 4}"]
%% Level 1 - Position 4
Start -->|"Job 3 in pos 4"| N3["(..., ..., ..., 3)
C₃ = 44, T₃ = 19
wT = 3×19 = 57
LB = 57 + 8 = 65
Remaining: {1, 2, 4}"]
Start -->|"Job 4 in pos 4"| N4["(..., ..., ..., 4)
C₄ = 44, T₄ = 17
wT = 5×17 = 85
LB = 85 + 22 = 107
Remaining: {1, 2, 3}"]
Start -.->|"Dominated
Job 1 dominates Job 3
d₁≤d₃, p₁≤p₃, w₁≥w₃"| X1["❌ Job 1
Cannot be last"]
Start -.->|"Dominated
Job 2 dominates Job 4
d₂≤d₄, p₂≤p₄, w₂≥w₄"| X2["❌ Job 2
Cannot be last"]
%% Level 2 - Position 3 (exploring Job 3 branch)
N3 -->|"Job 4 in pos 3"| N34["(..., ..., 4, 3)
Partial schedule
Remaining: {1, 2}"]
N3 -.->|"Dominated"| X34["❌ Jobs 1, 2
eliminated by
dominance rules"]
%% Level 3 - Position 2
N34 -->|"Job 2 in pos 2"| N243["(..., 2, 4, 3)
Partial schedule
Remaining: {1}"]
%% Level 4 - Position 1 (Complete solution)
N243 -->|"Job 1 in pos 1"| Solution["(1, 2, 4, 3)
C₁=12, T₁=0, wT₁=0
C₂=20, T₂=0, wT₂=0
C₄=29, T₄=2, wT₄=10
C₃=44, T₃=19, wT₃=57
Total: 67
✓ INCUMBENT"]
%% Fathoming the Job 4 branch
N4 -.->|"FATHOMED
LB = 107 > 67"| Pruned["❌ Entire branch
pruned"]
%% Final result
Solution --> Optimal["OPTIMAL SOLUTION
(1, 2, 4, 3)
Min ΣwⱼTⱼ = 67"]
%% Styling
classDef explored fill:#90EE90,stroke:#006400,stroke-width:2px,color:#000
classDef dominated fill:#FFB6C1,stroke:#DC143C,stroke-width:2px,color:#000
classDef fathomed fill:#FFA07A,stroke:#FF4500,stroke-width:2px,color:#000
classDef solution fill:#87CEEB,stroke:#0000CD,stroke-width:3px,color:#000
classDef optimal fill:#FFD700,stroke:#FF8C00,stroke-width:4px,color:#000
class N3,N34,N243 explored
class X1,X2,X34 dominated
class N4,Pruned fathomed
class Solution solution
class Optimal optimal
5.3. Final Optimal Solution#
Since all branches have been either fully explored or pruned, the algorithm terminates. The best solution found is the optimal one.
- Optimal Sequence: (1, 2, 4, 3)
- Minimum Total Weighted Tardiness: $\sum w_jT_j = 64$
6. Industrial Applications and Heuristics#
While exact algorithms like Branch-and-Bound are crucial for understanding the problem’s structure and solving for optimality, their computational cost often makes them impractical for large-scale industrial settings with hundreds of jobs and real-time demands. In these environments, high-performance heuristics become essential tools for making fast, effective scheduling decisions.
6.1. Relevant Industrial Scenarios#
The $1||\sum w_jT_j$ model is directly applicable or serves as a critical subproblem in various practical scenarios:
- Bottleneck Machine Scheduling: In many production systems, a single machine or workcenter acts as a bottleneck, limiting the throughput of the entire line. Efficiently scheduling this machine to minimize weighted tardiness directly impacts on-time delivery performance and customer satisfaction.
- Shifting Bottleneck Heuristic: The $1||\sum w_jT_j$ problem is a core subproblem within the Shifting Bottleneck heuristic, a powerful and widely-used method for solving complex Job Shop scheduling problems ($Jm||\sum w_jT_j$). The heuristic iteratively identifies and schedules the current bottleneck machine by solving a single-machine tardiness problem.
- High-Value Order Prioritization: In manufacturing or service settings, different orders often carry vastly different financial penalties or strategic importance for late delivery. This model provides a formal framework for prioritizing high-value jobs to minimize total costs, loss of goodwill, or contractual penalties.
6.2. Method Suitability and Heuristic Alternatives#
The Branch-and-Bound method is ideally suited for offline, strategic analysis of small-to-medium problem instances (e.g., fewer than 30 jobs) where finding the provably optimal solution provides a significant financial or competitive advantage.
For larger problems or for real-time dispatching on the shop floor, heuristics are necessary. One of the most effective composite dispatching heuristics for this problem is the Apparent Tardiness Cost (ATC) rule. The ATC rule is a dynamic priority rule that intelligently combines the logic of two simpler rules:
- Weighted Shortest Processing Time (WSPT): Prioritizes jobs with a high weight-to-processing-time ratio ($w_j/p_j$), focusing on completing high-value jobs quickly.
- Minimum Slack (MS): Prioritizes jobs with the least amount of remaining slack time ($d_j - p_j - t$), focusing on urgency.
The ATC rule computes a dynamic index for each unscheduled job whenever the machine becomes free, selecting the job with the highest index. This index balances throughput (WSPT) and due date adherence (MS), adapting its priority based on the current time and the urgency of the remaining jobs.
7. Q&A for Practitioners#
- Q1: How do I map data from my ERP/MES system to this model’s parameters?
- A: The mapping is typically straightforward. Customer order committed dates become the due dates ($d_j$). Standard processing times, derived from engineering specifications or historical data, become the processing times ($p_j$). The weights ($w_j$) are determined by business priorities; they can be directly linked to contractual late penalties, the financial value of an order, customer importance tiers, or other strategic factors. The quality of the schedule is highly dependent on the quality of this input data.
- Q2: What is the practical limit on problem size for the exact Branch-and-Bound method?
- A: The computational time grows exponentially with the number of jobs. The source text notes that Branch-and-Bound methods are “prohibitively time-consuming even for only 30 jobs.” For dynamic, real-time scheduling environments that may involve hundreds of jobs, this exact method is infeasible.
- Q3: When should I use this exact algorithm versus a simpler heuristic like ATC?
- A: Use the exact Branch-and-Bound algorithm for offline, strategic planning on small sets of critical jobs where finding the true optimum provides a significant financial or strategic advantage (e.g., planning a weekly schedule for a key bottleneck). Use fast heuristics like the ATC rule for dynamic, real-time dispatching on the shop floor, where decisions for hundreds of jobs must be made quickly as machines become available.
- Q4: Can I solve this with standard optimization software?
- A: Yes. The $1||\sum w_jT_j$ problem can be formulated as a Mixed-Integer Linear Program (MILP) and solved with commercial solvers like Gurobi or CPLEX, or open-source solvers. This approach is often easier to implement than coding a custom Branch-and-Bound algorithm from scratch. However, it still faces the same underlying computational complexity challenges and will struggle with large problem instances.
8. Python Implementation#
This section provides two Python implementations to solve the $1||\sum w_jT_j$ problem: a from-scratch Branch-and-Bound solver to demonstrate the core logic, and a more concise MILP formulation using the pulp library.
8.1. Option A: Hand-Written Branch-and-Bound Solver#
This implementation demonstrates the backward-scheduling Branch-and-Bound logic discussed in Section 4. It includes the dominance rule from Lemma 3.6.1 for pruning.
import heapq
from collections import namedtuple
# Use a simple tuple for nodes to keep them lightweight for the heap
Node = namedtuple('Node', ['lower_bound', 'partial_sequence', 'remaining_jobs', 'completion_time_of_block'])
def calculate_lower_bound_estimate(jobs, unscheduled_jobs):
"""
Calculates a simple lower bound for the tardiness of unscheduled jobs
by scheduling them greedily in Earliest Due Date (EDD) order from time 0.
A more robust implementation would solve a transportation problem relaxation.
"""
if not unscheduled_jobs:
return 0
estimate = 0
current_time = 0
# Sort remaining jobs by Earliest Due Date (EDD)
sorted_unscheduled = sorted(list(unscheduled_jobs), key=lambda j: jobs[j]['d'])
for job_idx in sorted_unscheduled:
current_time += jobs[job_idx]['p']
tardiness = max(0, current_time - jobs[job_idx]['d'])
estimate += jobs[job_idx]['w'] * tardiness
return estimate
def solve_with_branch_and_bound(jobs):
"""
Solves the 1||sum(w_j*T_j) problem using a backward Branch-and-Bound algorithm.
"""
job_indices = set(jobs.keys())
total_processing_time = sum(j['p'] for j in jobs.values())
incumbent_solution = None
upper_bound = float('inf')
# Priority queue (min-heap) for nodes, ordered by lower bound
pq = []
# Start with the root node (empty sequence, all jobs remaining)
initial_lb_estimate = calculate_lower_bound_estimate(jobs, job_indices)
root_node = Node(
lower_bound=initial_lb_estimate,
partial_sequence=[],
remaining_jobs=job_indices,
completion_time_of_block=total_processing_time
)
heapq.heappush(pq, root_node)
while pq:
current_lb, partial_seq, remaining, completion_time = heapq.heappop(pq)
# Fathoming: Prune node if its lower bound is not promising
if current_lb >= upper_bound:
continue
# If all jobs are scheduled, we have a complete solution
if not remaining:
if current_lb < upper_bound:
upper_bound = current_lb
incumbent_solution = partial_seq
continue
# Branching: Determine valid candidates for the current last available position
candidates = []
for j in remaining:
is_dominating = False
for k in remaining:
if j == k: continue
# Check if job j dominates job k (using Lemma 3.6.1)
if (jobs[j]['d'] <= jobs[k]['d'] and
jobs[j]['p'] <= jobs[k]['p'] and
jobs[j]['w'] >= jobs[k]['w']):
is_dominating = True
break
# A job 'j' cannot be placed if it dominates another unscheduled job 'k',
# as that would force 'k' to be scheduled earlier, violating the rule.
if not is_dominating:
candidates.append(j)
# Explore each valid candidate
for job_idx in candidates:
# Create a new partial sequence by prepending the candidate job
new_partial_seq = [job_idx] + partial_seq
new_remaining = remaining - {job_idx}
# Calculate the exact weighted tardiness for the jobs already scheduled (at the end)
exact_tardiness = 0
temp_time = completion_time - jobs[job_idx]['p']
current_completion_time = temp_time
for scheduled_job_idx in new_partial_seq:
current_completion_time += jobs[scheduled_job_idx]['p']
tardiness = max(0, current_completion_time - jobs[scheduled_job_idx]['d'])
exact_tardiness += jobs[scheduled_job_idx]['w'] * tardiness
# Get a lower bound estimate for the jobs yet to be scheduled
estimate_for_unscheduled = calculate_lower_bound_estimate(jobs, new_remaining)
new_lower_bound = exact_tardiness + estimate_for_unscheduled
# If the new branch is promising, add it to the priority queue
if new_lower_bound < upper_bound:
new_node = Node(
lower_bound=new_lower_bound,
partial_sequence=new_partial_seq,
remaining_jobs=new_remaining,
completion_time_of_block=completion_time - jobs[job_idx]['p']
)
heapq.heappush(pq, new_node)
return incumbent_solution, upper_bound
8.2. Option B: Mixed-Integer Linear Programming (MILP) with PuLP#
This implementation uses the pulp library to formulate the problem as an MILP. It uses position-based variables to define the sequence.
import pulp
def solve_with_milp(jobs):
"""
Solves the 1||sum(w_j*T_j) problem using a Mixed-Integer Linear Programming formulation.
"""
n = len(jobs)
job_indices = list(jobs.keys())
positions = range(1, n + 1)
# Create the model
model = pulp.LpProblem("Total_Weighted_Tardiness", pulp.LpMinimize)
# Decision variables
# x[j][k] = 1 if job j is in position k, 0 otherwise
x = pulp.LpVariable.dicts("x", (job_indices, positions), cat='Binary')
# C[k] = completion time of the job in position k
# T[j] = tardiness of job j
C = pulp.LpVariable.dicts("C", positions, lowBound=0, cat='Continuous')
T = pulp.LpVariable.dicts("T", job_indices, lowBound=0, cat='Continuous')
# Objective function
model += pulp.lpSum(jobs[j]['w'] * T[j] for j in job_indices)
# Constraints
# Each job must be assigned to exactly one position
for j in job_indices:
model += pulp.lpSum(x[j][k] for k in positions) == 1
# Each position must be assigned exactly one job
for k in positions:
model += pulp.lpSum(x[j][k] for j in job_indices) == 1
# Completion time constraints
model += C[1] == pulp.lpSum(jobs[j]['p'] * x[j][1] for j in job_indices)
for k in range(2, n + 1):
model += C[k] == C[k-1] + pulp.lpSum(jobs[j]['p'] * x[j][k] for j in job_indices)
# Tardiness linearization constraints
# T_j >= C_j - d_j
for j in job_indices:
completion_time_of_j = pulp.lpSum(C[k] * x[j][k] for k in positions)
model += T[j] >= completion_time_of_j - jobs[j]['d']
# Solve the model
model.solve(pulp.PULP_CBC_CMD(msg=0))
# Extract solution
optimal_sequence = [0] * n
for j in job_indices:
for k in positions:
if pulp.value(x[j][k]) == 1:
optimal_sequence[k-1] = j
optimal_value = pulp.value(model.objective)
return optimal_sequence, optimal_value
8.3. Minimal Reproducible Experiment#
This script uses both the B&B and MILP solvers to find the optimal solution for the example problem, demonstrating that they arrive at the same result.
# Problem data from Example 3.6.3
# Jobs are 1-indexed for clarity
jobs_data = {
1: {'w': 4, 'p': 12, 'd': 16},
2: {'w': 5, 'p': 8, 'd': 26},
3: {'w': 3, 'p': 15, 'd': 25},
4: {'w': 5, 'p': 9, 'd': 27}
}
# --- Solve using the MILP formulation ---
optimal_sequence_milp, optimal_tardiness_milp = solve_with_milp(jobs_data)
print("--- MILP Solver Results ---")
print(f"Optimal Sequence: {optimal_sequence_milp}")
print(f"Optimal Total Weighted Tardiness: {optimal_tardiness_milp}\n")
# --- Solve using the custom B&B solver ---
optimal_sequence_bb, optimal_tardiness_bb = solve_with_branch_and_bound(jobs_data)
print("--- Custom B&B Solver Results ---")
print(f"Optimal Sequence: {optimal_sequence_bb}")
print(f"Optimal Total Weighted Tardiness: {optimal_tardiness_bb}")
Expected Output:
--- MILP Solver Results ---
Optimal Sequence: [1, 2, 4, 3]
Optimal Total Weighted Tardiness: 64.0
--- Custom B&B Solver Results ---
Optimal Sequence: [1, 2, 4, 3]
Optimal Total Weighted Tardiness: 64
9. Limitations and Extensions#
The $1||\sum w_jT_j$ model and the Branch-and-Bound algorithm provide a powerful framework for optimizing single-machine schedules. However, it is essential for practitioners to understand the model’s underlying assumptions and limitations to apply it effectively and to recognize when more advanced models are needed.
9.1. Limitations of the Model#
- Computational Complexity: The primary limitation is the exponential growth in computation time. The Branch-and-Bound algorithm, while more efficient than brute force, is still restricted to small instances (typically under 30 jobs) and is not suitable for real-time applications involving a large number of jobs.
- Data Sensitivity: The optimality of the output schedule is highly sensitive to the accuracy of the input data. Inaccurate estimates of processing times ($p_j$), unrealistic due dates ($d_j$), or poorly defined weights ($w_j$) can lead to a mathematically “optimal” schedule that performs poorly in practice.
- Deterministic Assumptions: The model is deterministic and does not inherently account for real-world stochastic events. It assumes processing times are fixed, machines do not break down, and no new, high-priority jobs will arrive unexpectedly. A schedule optimized under these assumptions can quickly become obsolete in a dynamic environment.
9.2. Potential Extensions#
The single-machine model serves as a fundamental building block for addressing more complex, realistic scenarios.
- Alternative Exact Methods: Dynamic Programming offers an alternative exact method for solving $1||\sum w_jT_j$ and its generalizations (such as $1||\sum h_j(C_j)$). These algorithms have a pseudo-polynomial time complexity, which can be more efficient than Branch-and-Bound for instances where processing times and due dates are relatively small integers.
- More Complex Machine Environments: The single-machine problem is a critical subproblem in heuristics for more complex environments. For example, the Shifting Bottleneck heuristic for Job Shops ($Jm||\sum w_jT_j$) tackles multi-machine, multi-operation problems by iteratively identifying the most constrained machine (the bottleneck) and solving a $1||\sum w_jT_j$ problem for it to fix its sequence.
- Incorporating Non-Regular Objectives: Real-world scenarios often include penalties for finishing a job too early (e.g., inventory holding costs) in addition to tardiness penalties. This transforms the problem into one with a non-regular objective function, such as minimizing $\sum w'_j E_j + \sum w''_j T_j$, which requires different and more complex solution algorithms, as the optimal schedule may contain deliberate idle time.