Wstęp
Wyobraźmy sobie, że jesteśmy właścicielami fabryki, w której sprzedawane są dwa rodzaje mebli: krzesła i fotele. Mamy określoną ilość materiału i ograniczoną przestrzeń w magazynie. Ile krzeseł i ile foteli powinna produkować nasza fabryka, tak aby zyski były maksymalne? Aby odpowiedzieć na to pytanie wykorzystamy zagadnienie programowania liniowego. Programowanie liniowe to metoda rozwiązania problemu masymalizacji lub minimalizacji liniowej funkcji celu \(f (x_1, x_2, ..., x_n)\) przy pewnej ilości ograniczeń nałożonych na zmienne \(x_1, x_2, ... x_n\). Zagadnienie to zostało omówione na przykładzie przedstawionego zadania optymalizacyjnego.
Zadanie:
Fabryka „Wygodnie” produkuje dwa rodzaje mebli: krzesła i fotele. Do produkcji wykorzystuje drewno, materiał i metal (gwoździe, śrubki itp.). Stan zasobów w magazynie przedstawiony jest w Tabeli 1.
Tabela 1
| Tworzywo | Zasoby_kg |
|---|---|
| Drewno | 7500 |
| Materiał | 3000 |
| Metal | 500 |
Ilość zasobów potrzebnych do produkcji oraz zysk jednostkowy wytworzonego produktu został zaprezentowany w Tabeli 2.
Tabela 2
| x | Krzesło | Fotel |
|---|---|---|
| Drewno [w kg] | 3.0 | 1.0 |
| Materiał [w kg] | 0.5 | 1.0 |
| Metal [w kg] | 0.1 | 0.1 |
| Zysk jednostkowy produktu [zł za sztukę] | 200.0 | 100.0 |
Ze względu na pojemność magazynu firma może wyprodukować tylko 3000 krzeseł lub 4000 foteli. Ile krzeseł, a ile foteli powinna produkować firma, aby zmaksymalizować swój zysk?
Rozwiązanie:
Aby rozwiązać powyższe zadanie najpierw trzeba przedstawić problem za pomocą wyrażeń matematycznych:
1. Definicja zmiennych
Naszym celem jest znalezieniem optymalnych wielkości produkcji krzeseł i foteli, w związku z czym definujemy zmienne następująco:
x- liczba krzeseł
y- liczba foteli
2. Układ ograniczeń
Poniższe nierównoście przedstawiają opisane w zadaniu ograniczenia nałożone na wielkości produkcji:
Ograniczenia ze względu na dostępność zasobów:
\(3x+y \leq 7 500\)
\(0,5x+y \leq 3 000\)
\(0,1x+0,1y \leq 500\)
Ograniczenia ze względu na wielkość magazynu:
\(x/3000+y/4000 \leq 1\)
Ograniczenia brzegowe:
\(x \leq 0\)
\(y \leq 0\)
3. Funkcja celu
W tym przypadku funkcją celu jest funkcja zysku określona wzorem:
\(Z=200x+100y ~~-> max\)
Rozwiązanie numeryczne
Rozwiązanie przy użyciu pakietu lpSolve
Zacznijmy od rozwiązania zadania z wykorzystaniem pakietu lpSolve.
# do C przypisano współczynniki funkcji celu -> C
C <- c(200, 100)
# macierz ograniczeń
A <- matrix(c(3, 1,
0.5, 1,
0.1, 0.1,
4, 3), nrow=4, byrow=TRUE)
# prawe strony ogarniczeń
B <- c(7500, 3000, 500, 12000)
# znaki ograniczeń
constranints_direction <- c("<=", "<=", "<=", "<=")
# znalezienie optymalnego rozweiązania
optimum <- lp(direction="max",
objective.in = C,
const.mat = A,
const.dir = constranints_direction,
const.rhs = B,
all.int = T)
# Status: 0 = sukces, 2 = brak możliwości rozwiązania
print(optimum$status)## [1] 0
#Wyświetl optymalne wartości dla x, y
best_sol <- optimum$solution
names(best_sol) <- c("x", "y")
print(best_sol)## x y
## 2100 1200
# Sprawdź wartość funkcji celu w optymalnym punkcie
print(paste("Całkowity zysk = ", optimum$objval, sep=""))## [1] "Całkowity zysk = 540000"
Rozwiązanie przy użyciu pakietu lpSolveAPI
Spróbujmy ponownie rozwiązać ten sam problem, tym razem korzystając z pakietu lpSolveAPI.
# Ustaw 4 ograniczenia i 3 zmienne decyzyjne
lprec <- make.lp(nrow = 4, ncol = 2)
# Ustaw typ problemu, który próbujemy rozwiązać
lp.control (lprec, sense = "max")## $anti.degen
## [1] "fixedvars" "stalling"
##
## $basis.crash
## [1] "none"
##
## $bb.depthlimit
## [1] -50
##
## $bb.floorfirst
## [1] "automatic"
##
## $bb.rule
## [1] "pseudononint" "greedy" "dynamic" "rcostfixing"
##
## $break.at.first
## [1] FALSE
##
## $break.at.value
## [1] 1e+30
##
## $epsilon
## epsb epsd epsel epsint epsperturb epspivot
## 1e-10 1e-09 1e-12 1e-07 1e-05 2e-07
##
## $improve
## [1] "dualfeas" "thetagap"
##
## $infinite
## [1] 1e+30
##
## $maxpivot
## [1] 250
##
## $mip.gap
## absolute relative
## 1e-11 1e-11
##
## $negrange
## [1] -1e+06
##
## $obj.in.basis
## [1] TRUE
##
## $pivoting
## [1] "devex" "adaptive"
##
## $presolve
## [1] "none"
##
## $scalelimit
## [1] 5
##
## $scaling
## [1] "geometric" "equilibrate" "integers"
##
## $sense
## [1] "maximize"
##
## $simplextype
## [1] "dual" "primal"
##
## $timeout
## [1] 0
##
## $verbose
## [1] "neutral"
# Ustaw typ zmiennych decyzyjnych
set.type(lprec, 1:2, type=c("integer"))
# Ustaw wektor współczynników funkcji celu
set.objfn (lprec, C)
# Dodaj ograniczenia
add.constraint(lprec, A[1, ], "<=", B[1])
add.constraint(lprec, A[2, ], "<=", B[2])
add.constraint(lprec, A[3, ], "<=", B[3])
add.constraint(lprec, A[4, ], "<=", B[4])
# Rozwiąż problem
solve (lprec)## [1] 0
# Uzyskaj wartości zmiennych decyzyjnych
a <- get.variables (lprec)
a## [1] 2100 1200
# Uzyskaj wartość funkcji celu
get.objective (lprec)## [1] 540000
Wizualizacja
Rozwiązanie zadania numerycznie to nie wszystko. Wiadomo, że ludzie są z natury wzrokowcami, więc musimy zadbać także o aspekt wizualny rozwiązania. W końcu może się zdarzyć, że szukając potencjalych inwestorów, którzy pomogą nam rozwijać naszą działalność, będziemy musieli przekonać ich, że wykorzystujemy optymalnie nasze zasoby. Przejdźmy więc do prezentacji naszego rozwiązania w sposób czytelny i zrozumiały nawet dla “matematycznych ignorantów”.
Opcja 1: Wykres przy użyciu grafiki wbudowanej
Zacznijmy od grafiki wbudowanej. Korzyścią tego typu rozwiązania jest brak konieczności wyszukiwania i wgrywania specjalnych pakietów graficznych. Jeśli zależy nam na szybkim i prostym wykresie możemy skorzystać z polecenia plot().
funkcja_liniowa <- function(x,a,b){
y <- a*x + b
return(y) }
x <- seq(0,8000,by=0.1)
y <- funkcja_liniowa(x,a = -0.5,b =3000)
y1 <- funkcja_liniowa(x,a = -4/3,b =4000)
y2 <- funkcja_liniowa(x,a = -3,b =7500)
y3 <- funkcja_liniowa(x,a = -1,b =5000)
# punkty końcowe na osi poziomej
xx = c(1, x, 8000)
yy = c(0, y, 0)
yy1 = c(0, y1, 0)
yy2 = c(0, y2, 0)
yy3 = c(0, y3, 0)
plot(1,type="n",xlim=c(0,7000),ylim=c(0,8000),xlab="liczba krzeseł",ylab="liczba foteli",xaxs="i",yaxs="i",
main="Wizulizacja rozwiązania programowania liniowego")
polygon(xx,yy,col=rgb(0, 0, 1, alpha = 0.2), border=rgb(0, 0, 1, alpha = 0.5))
polygon(xx,yy1,col=rgb(1, 0, 0.5, alpha = 0.2), border = rgb(1, 0, 0.5, alpha = 0.5))
polygon(xx,yy2,col=rgb(0., 0, 0.200, alpha = 0.2), border = rgb(0, 0, 0.200, alpha = 0.5))
polygon(xx,yy3,col=rgb(1, 0, 0, alpha = 0.2), border = rgb(1, 0, 0, alpha = 0.5))
abline(2000,-2,lty=4)
abline(4000,-2,lty=4)
abline(5400,-2,lty=4)
points(a[1],a[2],pch=19)
text(4/3*a[1],a[2],paste("(" , a[1], ",", a[2],")"), col = "gray0")
text(1/2*a[1],a[2],"zbiór rozwiązań", col = "gray0")Opcja 2: Wykres przy użyciu pakietu ggplot2
Jeśli zależy nam, na czymś trochę bardziej skomplikowanym możemy skorzystać z pakietu ggplot2. Poniżej przedstawiono przykład wizualizacji, którą możemy w prosty i szybki sposób modyfikować i wykorzystywać przy nowych zadaniach.
# definiujemy funkcje dotyczące organiczeń
f1 = function(x) -1/2*x+3000
f2 = function(x) -4/3*x+4000
f3 = function(x) -3*x+7500
f4 = function(x) -x+5000
# definujemy funkcję celu przy dwóch ustalonych stałych (stałe można dobrać na końcu)
f = function(x) -2*x + 5400
ff = function(x) -2*x + 2800
# definiujemy zestaw kolorów, które wykorzystamy na wykresie
colo <- c("purple1", "royalblue4", "purple4", "purple3", "gray0")
# tworzymy wykres
ggplot(data.frame(x=c(0, 7000)), aes(x)) +
#zaciemniamy pola odpowiadające nierównościom
stat_function(fun=f1, geom="area",fill= colo[1],alpha=0.3, na.rm = TRUE) +
stat_function(fun=f2, geom="area",fill= colo[2],alpha=0.3, na.rm = TRUE) +
stat_function(fun=f3, geom="area",fill= colo[3],alpha=0.3, na.rm = TRUE) +
stat_function(fun=f4, geom="area",fill= colo[4],alpha=0.3, na.rm = TRUE) +
stat_function(fun=f1, geom="line",col= colo[1],alpha=1, na.rm = TRUE) +
stat_function(fun = f2, geom = "line", col = colo[2], alpha = 1, na.rm = TRUE) + #dodajemy linie
stat_function(fun = f3, geom = "line", col = colo[3], alpha = 1, na.rm = TRUE) +
stat_function(fun = f4, geom = "line", col = colo[4], alpha = 1, na.rm = TRUE) +
stat_function(fun = f, geom = "line", col = colo[5], alpha = 1, linetype= "dashed", na.rm = TRUE)+
stat_function(fun = ff, geom = "line", col = colo[5], alpha = 1, linetype= "dashed", na.rm = TRUE)+
geom_segment(mapping=aes(x=0,y=0,xend=6500, yend=0), arrow=arrow(), size=1) +
geom_segment(mapping=aes(x=0,y=0,xend=0, yend=8000), arrow=arrow(), size=1) +
ylab("Liczba foteli") + xlab ("Liczba krzeseł") +
ggtitle("Wizualizacja rozwiązania programowania liniowego") +
ylim(0,8000) +
xlim(0,6500) +
geom_text(aes(x=5/3*a[1],y=3/2*a[2],label=paste("Punkt optymalny", "(" , a[1], ",", a[2],")")), col=colo[5]) +
geom_point(aes(x=a[1],y=a[2]), col= colo[5]) +
geom_text(aes(x=1/2*a[1],y=a[2],label="zbiór rozwiązań"), col= colo[5]) +
theme( #określamy wygląd wykresu
panel.background = element_rect(fill = "white", color = "#cdcdcd", size = 2),
panel.grid.major = element_line(colour = "#cdcdcd", linetype = "dashed",size = 1),
plot.title = element_text(face= "bold", size = 15),
axis.title = element_text(size = 10)
)Kilka słów od autorek
Gdy napotykamy problem maksymalizacji/minimalizacji funkcji liniowej przy nałożonych ograniczeniach, nie musimy popadać w rozpacz, bowiem istnieje prosta i skuteczna metoda poradzenia sobie z tym problemem. Mamy nadzieję, że po przeczytaniu tej pracy programowanie liniowe już nie będzie dla Ciebie zagadką. Jeśli do tego dysponujesz w miarę sprawnym komputerem i potrafisz “przeklikać”" kilka linijek w R, Twoje zadanie okaże się prostsze niż myślisz.
Jak można zauważyć, każda z przedstawionych metod doprowadziła nas do tego samego wyniku, możemy więc jednoznacznie stwierdzić, że rozwiązaniem naszego zadania jest punkt (2100, 1200). Obie metody rozwiązania numerycznego, a także obie wizualizacje okazały się więc równie skuteczne. To od Ciebie zależy, którą z nich wybierzesz… Tylko wybierz jedną, bo robienie kilku wersji jest jak widać bez sensu :)
| Źródła: |