We want to allocate resources to zones while minimising the total cost (e.g. travel time, travel distance, expenditure, etc.) for the entire system.
I <- 10
J <- 5
my_costs <- matrix(runif(I*J), nrow = I, ncol = J)
rownames(my_costs) <- sprintf("R_%s", seq.int(from = 1,length.out = I))
colnames(my_costs) <- sprintf("Z_%s", seq.int(from = 1,length.out = J))
my_costs
## Z_1 Z_2 Z_3 Z_4 Z_5
## R_1 0.31300760 0.13139723 0.4963993 0.7840266 0.73679765
## R_2 0.53842965 0.87764273 0.7447344 0.0672037 0.91149671
## R_3 0.25143509 0.90748595 0.1146217 0.5751526 0.53631711
## R_4 0.95993728 0.36790132 0.2225167 0.2831948 0.01454969
## R_5 0.30849284 0.74401005 0.9107487 0.3551488 0.52337242
## R_6 0.39425429 0.40168052 0.3757401 0.3075921 0.11172232
## R_7 0.04196323 0.07738462 0.7931153 0.5933098 0.76521537
## R_8 0.72600738 0.46235135 0.4789284 0.8689144 0.18859216
## R_9 0.68708525 0.88440675 0.7547065 0.2911243 0.36670425
## R_10 0.99598999 0.31697881 0.8720274 0.6002004 0.48280044
This system has I=10 resources and J=5 zones, so that the resources are denoted as {R1,R2,R3...RI} while the zones are denoted as {Z1,Z2,Z3...ZJ}.
A I×J matrix denoted as c represents the allocation cost for each resource to each zone. The value ci,j represents the cost of assigning resource Ri to zone Zj.
In this example, in order to assign resource R2 to zone Z3, a cost of c2,3=0.745 is incurred.
Map showing layout of the zones
s <- c(2, 1, 3, 2, 1)
A integer vector s of length J will specify the minimum number of resources required for each zone. In this example, s1=2 means that zone Z1 requires at least 2 resources.
The Integer Programming (IP) problem is formulated as below.
The symbol x denotes allocation matrix of size I×J, which is made up of binary values. When xij=1, it indicates resource Ri is allocated to zone Zj.
The constrains also specify that any resources can only be allocated to exactly 1 zone, and the total number of resources allocated to zone Zj must equal to or exceeding the threshold value indicated by sj.
The objective function is a function which minimises the total cost z of the system.
LetI=No. of resourcesJ=No. of zonesx=Allocation matrixc=Cost matrixs=Minimum no. of resourcesMinimisez=I∑i=1J∑j=1xijcijSubject toxij∈{0,1}∀i=1,2,3,...,Ij=1,2,3,...,JJ∑j=1xij=1∀i=1,2,3,...,II∑i=1xij≥sj∀j=1,2,3,...,J
To solve the IP problem, load the following packages in R
. We will solve this problem using the simplex algorithm.
library(dplyr)
library(ROI)
library(ROI.plugin.glpk)
library(ompr)
library(ompr.roi)
The IP problem is expressed in R
in the following code chunk. The computation time is usually significantly longer in a complex, real-world problem.
cost_func <- function(i,j){
vapply(seq_along(i), function(k) my_costs[i[k], j[k]], numeric(1L))
}
model <- MILPModel() %>%
add_variable(x[i, j], i = 1:I, j = 1:J, type = "binary") %>%
add_constraint(sum_expr(x[i, j], j = 1:J) == 1, i = 1:I) %>%
add_constraint(sum_expr(x[i, j], i = 1:I) >= s[j], j = 1:J) %>%
set_objective(sum_expr(x[i, j] * colwise(cost_func(i,j)), i = 1:I, j = 1:J), sense = "min")
my_solution <- model %>%
solve_model(with_ROI("glpk", verbose = TRUE))
## <SOLVER MSG> ----
## GLPK Simplex Optimizer, v4.47
## 15 rows, 50 columns, 100 non-zeros
## 0: obj = 0.000000000e+000 infeas = 1.900e+001 (10)
## * 24: obj = 5.697057522e+000 infeas = 0.000e+000 (1)
## * 45: obj = 2.058630769e+000 infeas = 0.000e+000 (0)
## OPTIMAL SOLUTION FOUND
## GLPK Integer Optimizer, v4.47
## 15 rows, 50 columns, 100 non-zeros
## 50 integer variables, all of which are binary
## Integer optimization begins...
## + 45: mip = not found yet >= -inf (1; 0)
## + 45: >>>>> 2.058630769e+000 >= 2.058630769e+000 0.0% (1; 0)
## + 45: mip = 2.058630769e+000 >= tree is empty 0.0% (0; 1)
## INTEGER OPTIMAL SOLUTION FOUND
## <!SOLVER MSG> ----
Extracting the variable xi,j will return the optimal allocation results.
my_allocation_results <- my_solution %>%
get_solution(x[i,j]) %>%
as_tibble() %>%
filter(value == 1) %>%
select(i, j) %>%
arrange(i)
my_allocation_results
## # A tibble: 10 x 2
## i j
## <int> <int>
## 1 1 2
## 2 2 4
## 3 3 3
## 4 4 3
## 5 5 1
## 6 6 3
## 7 7 1
## 8 8 5
## 9 9 4
## 10 10 2
We can count the number of resources allocated to each zone, which is indicated in column n
below. Compare that with the target threshold column s
.
my_allocation_results %>%
group_by(j) %>%
count() %>%
ungroup() %>%
arrange(j) %>%
bind_cols(s = s)
## # A tibble: 5 x 3
## j n s
## <int> <int> <dbl>
## 1 1 2 2
## 2 2 2 1
## 3 3 3 3
## 4 4 2 2
## 5 5 1 1