You are the head of a company with n employees and n distinct jobs to be completed. Every employee takes a different amount of time to complete different jobs, given in the form of a cost[][] matrix, where cost[i][j] represents the time taken by the ith person to complete the jth job. Your task is to assign the jobs in such a way that the total time taken by all employees is minimized.
Note: Each employee must be assigned exactly one job, and each job must be assigned to exactly one employee.
Examples:
Input: n = 4 cost[][] = [ [9, 2, 7, 8], [6, 4, 3, 7], [5, 8, 1, 8], [7, 6, 9, 4] ] Output: 13 Explanation: Following image depicts the cost associated with each job-worker combinations. Numbers marked in green should be opted to get minimum possible cost.
[Naive Approach] - Generate All Combinations - O(n!) Time and O(1) Space
The idea is to generate n! possible job assignments and for each such assignment, we compute its total cost and return the less expensive assignment. Since the solution is a permutation of the n jobs, its complexity is O(n!).
[Better Approach] - Using Hungarian Algorithm - O(n ^ 3) Time and O(n ^ 2) Space
The optimal assignment can be found using the Hungarian algorithm. The Hungarian algorithm has worst case run-time complexity of O(n^3). We've already discussed the approach in article Hungarian Algorithm for Assignment Problem
[Better Approach] - Using DFS/BFS on State Space Tree
A state space tree is an N-ary tree where each path from the root to a leaf node represents a potential solution to the given problem. One way to explore this tree is through Depth-First Search (DFS), which follows the leftmost path from the root. However, DFS doesn't consider whether each move brings us closer to the goalâsuccessive steps might actually lead us further away. As a result, itâs possible that a valid solution may never be found.
Alternatively, we can use Breadth-First Search (BFS), which explores the tree level by level. Like DFS, BFS also follows a fixed order of exploring nodes regardless of the initial state. Both strategies are blind to the actual direction of the goal unless guided by additional heuristics.
[Expected Approach] - Using Branch Bound - O(n ^ 2) Time and O(n) Space
In both Breadth-First Search (BFS) and Depth-First Search (DFS), the selection of the next node to explore is blindâthat is, it does not prioritize nodes that are more likely to lead to an optimal solution. These uninformed strategies treat all nodes equally, without considering their potential to reach the goal efficiently.
To overcome this limitation, we can use an informed search strategy that leverages an intelligent ranking function, often referred to as an approximate cost function. This function helps guide the search process by assigning a cost to each live node, enabling the algorithm to avoid exploring subtrees unlikely to contain optimal solutions. This approach is similar to BFS, but instead of following the strict FIFO order, it selects the live node with the lowest estimated cost.
Although this method does not guarantee the optimal solution, it significantly increases the chances of finding a near-optimal solution faster by focusing the search on more promising paths.
There are two common approaches to estimate the cost function:
Row-based Minimization: For each worker (row), select the minimum cost job from the list of unassigned jobs (i.e., take the minimum entry from each row).
Column-based Minimization: For each job (column), choose the worker with the lowest cost from the list of unassigned workers (i.e., take the minimum entry from each column).
In this article, the first approach is followed.
Letâs take below example and try to calculate promising cost when Job 2 is assigned to worker A.
Since Job 2 is assigned to worker A (marked in green), cost becomes 2 and Job 2 and worker A becomes unavailable (marked in red).
Now we assign job 3 to worker B as it has minimum cost from list of unassigned jobs. Cost becomes 2 + 3 = 5 and Job 3 and worker B also becomes unavailable.
Finally, job 1 gets assigned to worker C as it has minimum cost among unassigned jobs and job 4 gets assigned to worker D as it is only Job left. Total cost becomes 2 + 3 + 5 + 4 = 14.
Below diagram shows complete search space diagram showing optimal solution path in green:
Below given is the step-by-step approach:
Create a dummy root node using new Node(-1, -1, assigned, NULL) with assigned set to false for all jobs, and set root->pathCost and root->cost to 0.
Insert the root node into a min-heap priority queue (live nodes) ordered by node->cost.
While the priority queue is not empty:
Extract node E using Least() (i.e. pq.top() then pq.pop()).
Let worker = E->workerID + 1.
If worker equals N (all workers assigned), use printAssignments(E) to print the job assignment and return E->cost.
For each job j from 0 to N-1:
If E->assigned[j] is false:
Create child node x = new Node(worker, j, E->assigned, E).
Set x->pathCost = E->pathCost + costMatrix[worker][j].
Calculate lower bound using calculateCost(costMatrix, worker, j, x->assigned) and set x->cost = x->pathCost + lower bound.
Insert x into the priority queue using Add(x).
C++
#include<bits/stdc++.h>usingnamespacestd;// state space tree nodeclassNode{public:// stores parent node of current node// helps in tracing path when answer is foundNode*parent;// contains cost for ancestors nodes// including current nodeintpathCost;// contains least promising costintcost;// contain worker numberintworkerID;// contains Job IDintjobID;// Boolean array assigned will contains// info about available jobsvector<bool>assigned;Node(intx,inty,vector<bool>assigned,Node*parent){this->workerID=x;this->jobID=y;this->assigned=assigned;this->parent=parent;this->pathCost=0;this->cost=0;}};// Function to calculate the least promising cost// of node after worker x is assigned to job y.intcalculateCost(vector<vector<int>>&costMat,intx,inty,vector<bool>&assigned){intn=costMat.size();intcost=0;// to store unavailable jobsvector<bool>available(n,true);// start from next workerfor(inti=x+1;i<n;i++){intmin=INT_MAX,minIndex=-1;// do for each jobfor(intj=0;j<n;j++){// if job is unassignedif(!assigned[j]&&available[j]&&costMat[i][j]<min){// store job numberminIndex=j;// store costmin=costMat[i][j];}}// add cost of next workercost+=min;// job becomes unavailableavailable[minIndex]=false;}returncost;}// Comparison object to be used to order the heapstructcomp{booloperator()(constNode*lhs,constNode*rhs)const{returnlhs->cost>rhs->cost;}};// Finds minimum cost using Branch and Bound.intfindMinCost(vector<vector<int>>&costMat){intn=costMat.size();// Create a priority queue to store// live nodes of search tree;priority_queue<Node*,vector<Node*>,comp>pq;// initialize heap to dummy node with cost 0vector<bool>assigned(n,false);Node*root=newNode(-1,-1,assigned,nullptr);root->pathCost=root->cost=0;root->workerID=-1;// Add dummy node to list of live nodes;pq.push(root);while(!pq.empty()){// Find a live node with least estimated costNode*min=pq.top();// The found node is deleted from the listpq.pop();// i stores next workerinti=min->workerID+1;// if all workers are assigned a jobif(i==n){returnmin->cost;}// do for each jobfor(intj=0;j<n;j++){// If unassignedif(!min->assigned[j]){// create a new tree nodeNode*child=newNode(i,j,min->assigned,min);child->assigned[j]=true;// cost for ancestors nodes including current nodechild->pathCost=min->pathCost+costMat[i][j];// calculate its lower boundchild->cost=child->pathCost+calculateCost(costMat,i,j,child->assigned);// Add child to list of live nodes;pq.push(child);}}}// will not be usedreturn-1;}intmain(){vector<vector<int>>costMat={{9,2,7,8},{6,4,3,7},{5,8,1,8},{7,6,9,4}};cout<<findMinCost(costMat);return0;}
Java
importjava.util.*;publicclassGfG{// state space tree nodestaticclassNode{// stores parent node of current node// helps in tracing path when answer is foundNodeparent;// contains cost for ancestors nodes// including current nodeintpathCost;// contains least promising costintcost;// contain worker numberintworkerID;// contains Job IDintjobID;// Boolean array assigned will contains// info about available jobsboolean[]assigned;Node(intx,inty,boolean[]assigned,Nodeparent){this.workerID=x;this.jobID=y;this.assigned=assigned.clone();this.parent=parent;this.pathCost=0;this.cost=0;}}// Function to calculate the least promising cost// of node after worker x is assigned to job y.staticintcalculateCost(int[][]costMat,intx,inty,boolean[]assigned){intn=costMat.length;intcost=0;// to store unavailable jobsboolean[]available=newboolean[n];Arrays.fill(available,true);// start from next workerfor(inti=x+1;i<n;i++){intmin=Integer.MAX_VALUE,minIndex=-1;// do for each jobfor(intj=0;j<n;j++){// if job is unassignedif(!assigned[j]&&available[j]&&costMat[i][j]<min){// store job numberminIndex=j;// store costmin=costMat[i][j];}}// add cost of next workercost+=min;// job becomes unavailableavailable[minIndex]=false;}returncost;}// Comparison object to be used to order the heapstaticclassCompimplementsComparator<Node>{publicintcompare(Nodelhs,Noderhs){returnlhs.cost-rhs.cost;}}// Finds minimum cost using Branch and Bound.staticintfindMinCost(int[][]costMat){intn=costMat.length;// Create a priority queue to store// live nodes of search tree;PriorityQueue<Node>pq=newPriorityQueue<>(newComp());// initialize heap to dummy node with cost 0boolean[]assigned=newboolean[n];Noderoot=newNode(-1,-1,assigned,null);root.pathCost=root.cost=0;root.workerID=-1;// Add dummy node to list of live nodes;pq.add(root);while(!pq.isEmpty()){// Find a live node with least estimated costNodemin=pq.poll();// The found node is deleted from the list// i stores next workerinti=min.workerID+1;// if all workers are assigned a jobif(i==n){returnmin.cost;}// do for each jobfor(intj=0;j<n;j++){// If unassignedif(!min.assigned[j]){// create a new tree nodeNodechild=newNode(i,j,min.assigned,min);child.assigned[j]=true;// cost for ancestors nodes including current nodechild.pathCost=min.pathCost+costMat[i][j];// calculate its lower boundchild.cost=child.pathCost+calculateCost(costMat,i,j,child.assigned);// Add child to list of live nodes;pq.add(child);}}}// will not be usedreturn-1;}publicstaticvoidmain(String[]args){int[][]costMat={{9,2,7,8},{6,4,3,7},{5,8,1,8},{7,6,9,4}};System.out.println(findMinCost(costMat));}}
Python
importheapqimportmath# state space tree nodeclassNode:# stores parent node of current node# helps in tracing path when answer is founddef__init__(self,x,y,assigned,parent):self.workerID=xself.jobID=yself.assigned=assigned[:]# copy listself.parent=parentself.pathCost=0self.cost=0def__lt__(self,other):returnself.cost<other.cost# Function to calculate the least promising cost# of node after worker x is assigned to job y.defcalculateCost(costMat,x,y,assigned):n=len(costMat)cost=0# to store unavailable jobsavailable=[True]*n# start from next workerforiinrange(x+1,n):minVal=float('inf')minIndex=-1# do for each jobforjinrange(n):# if job is unassignedif(notassigned[j])andavailable[j]andcostMat[i][j]<minVal:# store job numberminIndex=j# store costminVal=costMat[i][j]# add cost of next workercost+=minVal# job becomes unavailableavailable[minIndex]=Falsereturncost# Finds minimum cost using Branch and Bound.deffindMinCost(costMat):n=len(costMat)# Create a priority queue to store# live nodes of search tree;pq=[]# initialize heap to dummy node with cost 0assigned=[False]*nroot=Node(-1,-1,assigned,None)root.pathCost=root.cost=0root.workerID=-1# Add dummy node to list of live nodes;heapq.heappush(pq,root)whilepq:# Find a live node with least estimated costminNode=heapq.heappop(pq)# The found node is deleted from the list# i stores next workeri=minNode.workerID+1# if all workers are assigned a jobifi==n:returnminNode.cost# do for each jobforjinrange(n):# If unassignedifnotminNode.assigned[j]:# create a new tree nodechild=Node(i,j,minNode.assigned,minNode)child.assigned[j]=True# cost for ancestors nodes including current nodechild.pathCost=minNode.pathCost+costMat[i][j]# calculate its lower boundchild.cost=child.pathCost+calculateCost(costMat,i,j,child.assigned)# Add child to list of live nodes;heapq.heappush(pq,child)return-1if__name__=="__main__":costMat=[[9,2,7,8],[6,4,3,7],[5,8,1,8],[7,6,9,4]]print(findMinCost(costMat))
C#
usingSystem;usingSystem.Collections.Generic;publicclassGfG{// state space tree nodepublicclassNode{// stores parent node of current node// helps in tracing path when answer is foundpublicNodeparent;// contains cost for ancestors nodes// including current nodepublicintpathCost;// contains least promising costpublicintcost;// contain worker numberpublicintworkerID;// contains Job IDpublicintjobID;// Boolean array assigned will contains// info about available jobspublicbool[]assigned;publicNode(intx,inty,bool[]assigned,Nodeparent){this.workerID=x;this.jobID=y;this.assigned=(bool[])assigned.Clone();this.parent=parent;this.pathCost=0;this.cost=0;}}// Function to calculate the least promising cost// of node after worker x is assigned to job y.publicstaticintcalculateCost(int[][]costMat,intx,inty,bool[]assigned){intn=costMat.Length;intcost=0;// to store unavailable jobsbool[]available=newbool[n];for(inti=0;i<n;i++){available[i]=true;}// start from next workerfor(inti=x+1;i<n;i++){intmin=int.MaxValue,minIndex=-1;// do for each jobfor(intj=0;j<n;j++){// if job is unassignedif(!assigned[j]&&available[j]&&costMat[i][j]<min){// store job numberminIndex=j;// store costmin=costMat[i][j];}}// add cost of next workercost+=min;// job becomes unavailableavailable[minIndex]=false;}returncost;}// Comparison object to be used to order the heappublicclassNodeComparer:IComparer<Node>{publicintCompare(Nodelhs,Noderhs){returnlhs.cost.CompareTo(rhs.cost);}}// Finds minimum cost using Branch and Bound.publicstaticintfindMinCost(int[][]costMat){intn=costMat.Length;// Create a priority queue to store// live nodes of search tree;SortedSet<Node>pq=newSortedSet<Node>(newNodeComparer());// initialize heap to dummy node with cost 0bool[]assigned=newbool[n];Noderoot=newNode(-1,-1,assigned,null);root.pathCost=root.cost=0;root.workerID=-1;// Add dummy node to list of live nodes;pq.Add(root);while(pq.Count>0){// Find a live node with least estimated costNodemin=pq.Min;pq.Remove(min);// The found node is deleted from the list// i stores next workerinti=min.workerID+1;// if all workers are assigned a jobif(i==n){returnmin.cost;}// do for each jobfor(intj=0;j<n;j++){// If unassignedif(!min.assigned[j]){// create a new tree nodeNodechild=newNode(i,j,min.assigned,min);child.assigned[j]=true;// cost for ancestors nodes including current nodechild.pathCost=min.pathCost+costMat[i][j];// calculate its lower boundchild.cost=child.pathCost+calculateCost(costMat,i,j,child.assigned);// Add child to list of live nodes;pq.Add(child);}}}// will not be usedreturn-1;}publicstaticvoidMain(string[]args){int[][]costMat=newint[][]{newint[]{9,2,7,8},newint[]{6,4,3,7},newint[]{5,8,1,8},newint[]{7,6,9,4}};Console.WriteLine(findMinCost(costMat));}}
JavaScript
// state space tree nodeclassNode{// stores parent node of current node// helps in tracing path when answer is foundconstructor(x,y,assigned,parent){this.workerID=x;this.jobID=y;this.assigned=assigned.slice();this.parent=parent;this.pathCost=0;this.cost=0;}}// Function to calculate the least promising cost// of node after worker x is assigned to job y.functioncalculateCost(costMat,x,y,assigned){letn=costMat.length;letcost=0;// to store unavailable jobsletavailable=newArray(n).fill(true);// start from next workerfor(leti=x+1;i<n;i++){letmin=Infinity,minIndex=-1;// do for each jobfor(letj=0;j<n;j++){// if job is unassignedif(!assigned[j]&&available[j]&&costMat[i][j]<min){// store job numberminIndex=j;// store costmin=costMat[i][j];}}// add cost of next workercost+=min;// job becomes unavailableavailable[minIndex]=false;}returncost;}// Comparison function to be used to order the heapfunctioncompareNodes(lhs,rhs){returnlhs.cost-rhs.cost;}// Finds minimum cost using Branch and Bound.functionfindMinCost(costMat){letn=costMat.length;// Create a priority queue to store// live nodes of search tree;letpq=[];// initialize heap to dummy node with cost 0letassigned=newArray(n).fill(false);letroot=newNode(-1,-1,assigned,null);root.pathCost=root.cost=0;root.workerID=-1;// Add dummy node to list of live nodes;pq.push(root);while(pq.length>0){// Find a live node with least estimated costpq.sort(compareNodes);letmin=pq.shift();// The found node is deleted from the list// i stores next workerleti=min.workerID+1;// if all workers are assigned a jobif(i===n){returnmin.cost;}// do for each jobfor(letj=0;j<n;j++){// If unassignedif(!min.assigned[j]){// create a new tree nodeletchild=newNode(i,j,min.assigned,min);child.assigned[j]=true;// cost for ancestors nodes including current nodechild.pathCost=min.pathCost+costMat[i][j];// calculate its lower boundchild.cost=child.pathCost+calculateCost(costMat,i,j,child.assigned);// Add child to list of live nodes;pq.push(child);}}}// will not be usedreturn-1;}letcostMat=[[9,2,7,8],[6,4,3,7],[5,8,1,8],[7,6,9,4]];console.log(findMinCost(costMat));