Integer Programming
Consider the following optimization problem.
\[ \begin{array}{ll} \mbox{Maximize} & x_1 + 2x_2 - 0.1x_3 - 3x_4\\ \mbox{subject to} & x_1, x_2, x_3, x_4 >= 0\\ & x_1 + x_2 <= 5\\ & 2x_1 - x_2 >= 0\\ & -x_1 + 3x_2 >= 0\\ & x_3 + x_4 >= 0.5\\ & x_3 >= 1.1\\ & x_3 \mbox{ is integer.} \end{array} \]
CVXR
provides constructors for the integer and boolean variables via
the parameter integer = TRUE
or boolean = TRUE
to the Variable()
function. These can be combined with vstack
(analog of rbind
) or
hstack
(analog of cbind
) to construct new expressions.
The above problem now in CVXR
.
y1 <- Variable(2)
y2 <- Variable(1, integer = TRUE)
y3 <- Variable(1)
x <- vstack(y1, y2, y3) ## Create x expression
C <- matrix(c(1, 2, -0.1, -3), nrow = 1)
objective <- Maximize(C %*% x)
constraints <- list(
x >= 0,
x[1] + x[2] <= 5,
2 * x[1] - x[2] >= 0,
-x[1] + 3 * x[2] >= 0,
x[3] + x[4] >= 0.5,
x[3] >= 1.1)
problem <- Problem(objective, constraints)
We can solve this problem as usual using the default ECOS
(actually
ECOS_BB
) solver and obtain the optimal value as well as the
solution.
result <- solve(problem, solver = "ECOS_BB")
cat(sprintf("Optimal value: %.3f\n", result$value))
## Optimal value: 8.133
ecos_solution <- result$getValue(x)
Alternative Solvers
We can try other solvers and compare the solutions obtained, like
LPSOLVE
and GLPK
provided the respective R packages are installed
as documented in the tutorial Using Other
Solvers.
Note: LPSOLVE
is now orphaned on CRAN and so no longer supported.
##result <- solve(problem, solver = "LPSOLVE")
##lpsolve_solution <- result$getValue(x)
result <- solve(problem, solver = "GLPK")
glpk_solution <- result$getValue(x)
We can also try a commercial solver, Gurobi, that can handle integer programs. This requires installation of the Gurobi solver, discussed in Using Other Solvers.
result <- solve(problem, solver = "GUROBI")
gurobi_solution <- result$getValue(x)
Finally, two other commercial solvers, MOSEK
and CPLEX
.
result <- solve(problem, solver = "CPLEX")
cplex_solution <- result$getValue(x)
result <- solve(problem, solver = "MOSEK")
mosek_solution <- result$getValue(x)
Below is the table is solutions from all the solvers we used.
solutions <- data.frame(ECOS = ecos_solution,
## LPSOLVE = lpsolve_solution,
GLPK = glpk_solution,
GUROBI = gurobi_solution,
MOSEK = mosek_solution,
CPLEX = cplex_solution)
row.names(solutions) <- c("$x_1$", "$x_2$", "$x_3$", "$x_4$")
knitr::kable(solutions, format = "html") %>%
kable_styling("striped") %>%
column_spec(1:4, background = "#ececec")
ECOS | GLPK | GUROBI | MOSEK | CPLEX | |
---|---|---|---|---|---|
\(x_1\) | 1.6666703 | 1.666667 | 1.666667 | 1.666667 | 1.666667 |
\(x_2\) | 3.3333291 | 3.333333 | 3.333333 | 3.333333 | 3.333333 |
\(x_3\) | 2.0000704 | 2.000000 | 2.000000 | 2.000000 | 2.000000 |
\(x_4\) | -0.0000018 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
## Testthat Results: No output is good
## Error: `ecos_solution` not identical to intprog$ecos.
## 4/4 mismatches (average diff: 2e-05)
## [1] 1.67e+00 - 1.67e+00 == 3.61e-06
## [2] 3.33e+00 - 3.33e+00 == -4.19e-06
## [3] 2.00e+00 - 2.00e+00 == 7.04e-05
## [4] -1.79e-06 - -3.34e-11 == -1.79e-06
Office Assignment Problem
For a slightly more involved example, we consider the office assignment problem.
The goal is to assign six people, Marcelo, Rakesh, Peter, Tom, Marjorie, and Mary Ann, to seven offices. Each office can have no more than one person, and each person gets exactly one office. So there will be one empty office. People can give preferences for the offices, and their preferences are considered based on their seniority. Some offices have windows, some do not, and one window is smaller than others. Additionally, Peter and Tom often work together, so should be in adjacent offices. Marcelo and Rakesh often work together, and should be in adjacent offices.
draw_office_layout()
The office layout is shown above. Offices 1, 2, 3, and 4 are inside offices (no windows). Offices 5, 6, and 7 have windows, but the window in office 5 is smaller than the other two.
We begin by recording the names of the people and offices.
people <- c('Mary Ann', 'Marjorie', 'Tom',
'Peter', 'Marcelo', 'Rakesh')
offices <- c('Office 1', 'Office 2', 'Office 3',
'Office 4','Office 5', 'Office 6', 'Office 7')
We also have the office preferences of each person for each of the seven offices along with seniority data which is used to scale the office preferences.
preference_matrix <- matrix( c(0, 0, 0, 0, 10, 40, 50,
0, 0, 0, 0, 20, 40, 40,
0, 0, 0, 0, 30, 40, 30,
1, 3, 3, 3, 10, 40, 40,
3, 4, 1, 2, 10, 40, 40,
10, 10, 10, 10, 20, 20, 20),
byrow = TRUE, nrow = length(people))
rownames(preference_matrix) <- people
colnames(preference_matrix) <- offices
seniority <- c(9, 10, 5, 3, 1.5, 2)
weightvector <- seniority / sum(seniority)
PM <- diag(weightvector) %*% preference_matrix
We define the the occupancy variable which indicates, using values 1 or 0, who occupies which office.
occupy <- Variable(length(people), length(offices), integer = TRUE)
The objective is to maximize the satisfaction of the preferences weighted by seniority constrained by the fact the a person can only occupy a single office and no office can have more than 1 person.
objective <- Maximize(sum_entries(multiply(PM, occupy)))
constraints <- list(
occupy >= 0,
occupy <= 1,
sum_entries(occupy, axis = 1) == 1,
sum_entries(occupy, axis = 2) <= 1
)
We further add the constraint that Tom (person 3) and Peter (person 4) should be no more than one office away, and ditto for Marcelo (person 5) and Rakesh (person 6).
tom_peter <- list(
occupy[3, 1] + sum_entries(occupy[4, ]) - occupy[4, 2] <= 1,
occupy[3, 2] + sum_entries(occupy[4, ]) - occupy[4, 1] - occupy[4, 3] - occupy[4, 5] <= 1,
occupy[3, 3] + sum_entries(occupy[4, ]) - occupy[4, 2] - occupy[4, 4] - occupy[4, 6] <= 1,
occupy[3, 4] + sum_entries(occupy[4, ]) - occupy[4, 3] - occupy[4, 7] <= 1,
occupy[3, 5] + sum_entries(occupy[4, ]) - occupy[4, 2] - occupy[4, 6] <= 1,
occupy[3, 6] + sum_entries(occupy[4, ]) - occupy[4, 3] - occupy[4, 5] - occupy[4, 7] <= 1,
occupy[3, 7] + sum_entries(occupy[4, ]) - occupy[4, 4] - occupy[4, 6] <= 1
)
marcelo_rakesh <- list(
occupy[5, 1] + sum_entries(occupy[6, ]) - occupy[6, 2] <= 1,
occupy[5, 2] + sum_entries(occupy[6, ]) - occupy[6, 1] - occupy[6, 3] - occupy[6, 5] <= 1,
occupy[5, 3] + sum_entries(occupy[6, ]) - occupy[6, 2] - occupy[6, 4] - occupy[6, 6] <= 1,
occupy[5, 4] + sum_entries(occupy[6, ]) - occupy[6, 3] - occupy[6, 7] <= 1,
occupy[5, 5] + sum_entries(occupy[6, ]) - occupy[6, 2] - occupy[6, 6] <= 1,
occupy[5, 6] + sum_entries(occupy[6, ]) - occupy[6, 3] - occupy[6, 5] - occupy[6, 7] <= 1,
occupy[5, 7] + sum_entries(occupy[6, ]) - occupy[6, 4] - occupy[6, 6] <= 1
)
constraints <- c(constraints, tom_peter, marcelo_rakesh)
We are now ready to solve the problem.
problem <- Problem(objective, constraints)
ecos_result <- solve(problem, solver = "ECOS_BB")
ecos_soln <- round(ecos_result$getValue(occupy), 0)
rownames(ecos_soln) <- people
colnames(ecos_soln) <- offices
We are now ready to plot the solution (after accounting for the WC).
office_assignment <- apply(ecos_soln, 1, which.max)
office_occupants <- names(office_assignment)[match(c(5:7, 1:4), office_assignment)]
office_occupants[is.na(office_occupants)] <- "Empty"
draw_office_layout(c("WC", office_occupants))
## Testthat Results: No output is good
Session Info
sessionInfo()
## R version 4.4.2 (2024-10-31)
## Platform: x86_64-apple-darwin20
## Running under: macOS Sequoia 15.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: America/Los_Angeles
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices datasets utils methods base
##
## other attached packages:
## [1] ggplot2_3.5.1 kableExtra_1.4.0 CVXR_1.0-15 testthat_3.2.1.1
## [5] here_1.0.1
##
## loaded via a namespace (and not attached):
## [1] gtable_0.3.6 xfun_0.49 bslib_0.8.0 lattice_0.22-6
## [5] vctrs_0.6.5 tools_4.4.2 Rmosek_10.2.0 generics_0.1.3
## [9] tibble_3.2.1 fansi_1.0.6 highr_0.11 pkgconfig_2.0.3
## [13] Matrix_1.7-1 desc_1.4.3 assertthat_0.2.1 lifecycle_1.0.4
## [17] farver_2.1.2 compiler_4.4.2 stringr_1.5.1 brio_1.1.5
## [21] munsell_0.5.1 gurobi_11.0-0 codetools_0.2-20 ECOSolveR_0.5.5
## [25] htmltools_0.5.8.1 sass_0.4.9 cccp_0.3-1 yaml_2.3.10
## [29] gmp_0.7-5 pillar_1.9.0 jquerylib_0.1.4 rcbc_0.1.0.9001
## [33] Rcplex_0.3-6 clarabel_0.9.0.1 cachem_1.1.0 tidyselect_1.2.1
## [37] digest_0.6.37 stringi_1.8.4 slam_0.1-54 dplyr_1.1.4
## [41] bookdown_0.41 labeling_0.4.3 rprojroot_2.0.4 fastmap_1.2.0
## [45] grid_4.4.2 colorspace_2.1-1 cli_3.6.3 magrittr_2.0.3
## [49] utf8_1.2.4 withr_3.0.2 Rmpfr_0.9-5 scales_1.3.0
## [53] bit64_4.5.2 rmarkdown_2.29 bit_4.5.0 blogdown_1.19
## [57] evaluate_1.0.1 knitr_1.48 Rglpk_0.6-5.1 viridisLite_0.4.2
## [61] rlang_1.1.4 Rcpp_1.0.13-1 glue_1.8.0 xml2_1.3.6
## [65] pkgload_1.4.0 svglite_2.1.3 rstudioapi_0.17.1 jsonlite_1.8.9
## [69] R6_2.5.1 systemfonts_1.1.0