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

Source

R Markdown

References