/* Copyright 2025, Gurobi Optimization, LLC *//* Assign workers to shifts; each worker may or may not be available on a particular day. We use Pareto optimization to solve the model: first, we minimize the linear sum of the slacks. Then, we constrain the sum of the slacks, and we minimize a quadratic objective that tries to balance the workload among the workers. */importcom.gurobi.gurobi.*;publicclassWorkforce4{publicstaticvoidmain(String[]args){try{// Sample data// Sets of days and workersStringShifts[]=newString[]{"Mon1","Tue2","Wed3","Thu4","Fri5","Sat6","Sun7","Mon8","Tue9","Wed10","Thu11","Fri12","Sat13","Sun14"};StringWorkers[]=newString[]{"Amy","Bob","Cathy","Dan","Ed","Fred","Gu"};intnShifts=Shifts.length;intnWorkers=Workers.length;// Number of workers required for each shiftdoubleshiftRequirements[]=newdouble[]{3,2,4,4,5,6,5,2,2,3,4,6,7,5};// Worker availability: 0 if the worker is unavailable for a shiftdoubleavailability[][]=newdouble[][]{{0,1,1,0,1,0,1,0,1,1,1,1,1,1},{1,1,0,0,1,1,0,1,0,0,1,0,1,0},{0,0,1,1,1,0,1,1,1,1,1,1,1,1},{0,1,1,0,1,1,0,1,1,1,1,1,1,1},{1,1,1,1,1,0,1,1,1,0,1,0,1,1},{1,1,1,0,0,1,0,1,1,0,0,1,1,1},{1,1,1,0,1,1,1,1,1,1,1,1,1,1}};// ModelGRBEnvenv=newGRBEnv();GRBModelmodel=newGRBModel(env);model.set(GRB.StringAttr.ModelName,"assignment");// Assignment variables: x[w][s] == 1 if worker w is assigned// to shift s. This is no longer a pure assignment model, so we must// use binary variables.GRBVar[][]x=newGRBVar[nWorkers][nShifts];for(intw=0;w<nWorkers;++w){for(ints=0;s<nShifts;++s){x[w][s]=model.addVar(0,availability[w][s],0,GRB.BINARY,Workers[w]+"."+Shifts[s]);}}// Slack variables for each shift constraint so that the shifts can// be satisfiedGRBVar[]slacks=newGRBVar[nShifts];for(ints=0;s<nShifts;++s){slacks[s]=model.addVar(0,GRB.INFINITY,0,GRB.CONTINUOUS,Shifts[s]+"Slack");}// Variable to represent the total slackGRBVartotSlack=model.addVar(0,GRB.INFINITY,0,GRB.CONTINUOUS,"totSlack");// Variables to count the total shifts worked by each workerGRBVar[]totShifts=newGRBVar[nWorkers];for(intw=0;w<nWorkers;++w){totShifts[w]=model.addVar(0,GRB.INFINITY,0,GRB.CONTINUOUS,Workers[w]+"TotShifts");}GRBLinExprlhs;// Constraint: assign exactly shiftRequirements[s] workers// to each shift s, plus the slackfor(ints=0;s<nShifts;++s){lhs=newGRBLinExpr();lhs.addTerm(1.0,slacks[s]);for(intw=0;w<nWorkers;++w){lhs.addTerm(1.0,x[w][s]);}model.addConstr(lhs,GRB.EQUAL,shiftRequirements[s],Shifts[s]);}// Constraint: set totSlack equal to the total slacklhs=newGRBLinExpr();lhs.addTerm(-1.0,totSlack);for(ints=0;s<nShifts;++s){lhs.addTerm(1.0,slacks[s]);}model.addConstr(lhs,GRB.EQUAL,0,"totSlack");// Constraint: compute the total number of shifts for each workerfor(intw=0;w<nWorkers;++w){lhs=newGRBLinExpr();lhs.addTerm(-1.0,totShifts[w]);for(ints=0;s<nShifts;++s){lhs.addTerm(1.0,x[w][s]);}model.addConstr(lhs,GRB.EQUAL,0,"totShifts"+Workers[w]);}// Objective: minimize the total slackGRBLinExprobj=newGRBLinExpr();obj.addTerm(1.0,totSlack);model.setObjective(obj);// Optimizeintstatus=solveAndPrint(model,totSlack,nWorkers,Workers,totShifts);if(status!=GRB.Status.OPTIMAL){return;}// Constrain the slack by setting its upper and lower boundstotSlack.set(GRB.DoubleAttr.UB,totSlack.get(GRB.DoubleAttr.X));totSlack.set(GRB.DoubleAttr.LB,totSlack.get(GRB.DoubleAttr.X));// Variable to count the average number of shifts workedGRBVaravgShifts=model.addVar(0,GRB.INFINITY,0,GRB.CONTINUOUS,"avgShifts");// Variables to count the difference from average for each worker;// note that these variables can take negative values.GRBVar[]diffShifts=newGRBVar[nWorkers];for(intw=0;w<nWorkers;++w){diffShifts[w]=model.addVar(-GRB.INFINITY,GRB.INFINITY,0,GRB.CONTINUOUS,Workers[w]+"Diff");}// Constraint: compute the average number of shifts workedlhs=newGRBLinExpr();lhs.addTerm(-nWorkers,avgShifts);for(intw=0;w<nWorkers;++w){lhs.addTerm(1.0,totShifts[w]);}model.addConstr(lhs,GRB.EQUAL,0,"avgShifts");// Constraint: compute the difference from the average number of shiftsfor(intw=0;w<nWorkers;++w){lhs=newGRBLinExpr();lhs.addTerm(-1,diffShifts[w]);lhs.addTerm(-1,avgShifts);lhs.addTerm(1,totShifts[w]);model.addConstr(lhs,GRB.EQUAL,0,Workers[w]+"Diff");}// Objective: minimize the sum of the square of the difference from the// average number of shifts workedGRBQuadExprqobj=newGRBQuadExpr();for(intw=0;w<nWorkers;++w){qobj.addTerm(1.0,diffShifts[w],diffShifts[w]);}model.setObjective(qobj);// Optimizestatus=solveAndPrint(model,totSlack,nWorkers,Workers,totShifts);if(status!=GRB.Status.OPTIMAL){return;}// Dispose of model and environmentmodel.dispose();env.dispose();}catch(GRBExceptione){System.out.println("Error code: "+e.getErrorCode()+". "+e.getMessage());}}privatestaticintsolveAndPrint(GRBModelmodel,GRBVartotSlack,intnWorkers,String[]Workers,GRBVar[]totShifts)throwsGRBException{model.optimize();intstatus=model.get(GRB.IntAttr.Status);if(status==GRB.Status.INF_OR_UNBD||status==GRB.Status.INFEASIBLE||status==GRB.Status.UNBOUNDED){System.out.println("The model cannot be solved "+"because it is infeasible or unbounded");returnstatus;}if(status!=GRB.Status.OPTIMAL){System.out.println("Optimization was stopped with status "+status);returnstatus;}// Print total slack and the number of shifts worked for each workerSystem.out.println("\nTotal slack required: "+totSlack.get(GRB.DoubleAttr.X));for(intw=0;w<nWorkers;++w){System.out.println(Workers[w]+" worked "+totShifts[w].get(GRB.DoubleAttr.X)+" shifts");}System.out.println("\n");returnstatus;}}
Help and Feedback