Базисные матрицы
Пример стандартной формы
Рассмотрим следующий пример из статьи в Википедии Базисные допустимые решения.
Матрица A
имеет вид:
A = [1 5 3 4 6; 0 1 3 5 6]
2×5 Matrix{Int64}:
1 5 3 4 6
0 1 3 5 6
а вектор b
правой части:
b = [14, 7]
2-element Vector{Int64}:
14
7
Задачу можно создать и оптимизировать в стандартной форме:
n = size(A, 2)
model = Model(HiGHS.Optimizer)
set_silent(model)
@variable(model, x[1:n] >= 0)
@constraint(model, A * x == b)
optimize!(model)
@assert is_solved_and_feasible(model)
У нее есть решение:
value.(x)
5-element Vector{Float64}:
0.0
2.0
0.0
1.0
0.0
Запросим статус базиса переменной с помощью MOI.VariableBasisStatus
:
get_attribute(x[1], MOI.VariableBasisStatus())
NONBASIC_AT_LOWER::BasisStatusCode = 2
Результатом является MOI.BasisStatusCode
. Запросим все статусы базиса с помощью трансляции get_attribute.(
:
get_attribute.(x, MOI.VariableBasisStatus())
5-element Vector{MathOptInterface.BasisStatusCode}:
NONBASIC_AT_LOWER::BasisStatusCode = 2
BASIC::BasisStatusCode = 0
NONBASIC_AT_LOWER::BasisStatusCode = 2
BASIC::BasisStatusCode = 0
NONBASIC_AT_LOWER::BasisStatusCode = 2
Для этой задачи значением будет MOI.NONBASIC_AT_LOWER
или MOI.BASIC
. У всех переменных MOI.NONBASIC_AT_LOWER
есть значение на нижней границе. Переменные MOI.BASIC
соответствуют столбцам оптимального базиса.
Получим столбцы:
indices = get_attribute.(x, MOI.VariableBasisStatus()) .== MOI.BASIC
5-element BitVector:
0
1
0
1
0
Отфильтруем базисную матрицу из A
:
B = A[:, indices]
2×2 Matrix{Int64}:
5 4
1 5
Так как базисная матрица невырожденная, решение системы Bx = b
должно давать оптимальное прямое решение для базисных переменных:
B \ b
2-element Vector{Float64}:
2.0
1.0
value.(x[indices])
2-element Vector{Float64}:
2.0
1.0
Более сложный пример
Часто приходится работать с базисом модели, которая не имеет удобной стандартной формы. Например:
model = Model(HiGHS.Optimizer)
set_silent(model)
@variable(model, x >= 0)
@variable(model, 0 <= y <= 3)
@variable(model, z <= 1)
@objective(model, Min, 12x + 20y - z)
@constraint(model, c1, 6x + 8y >= 100)
@constraint(model, c2, 7x + 12y >= 120)
@constraint(model, c3, x + y <= 20)
optimize!(model)
@assert is_solved_and_feasible(model)
Вот распространенный способ запроса статуса базиса каждой переменной:
v_basis = Dict(
xi => get_attribute(xi, MOI.VariableBasisStatus()) for
xi in all_variables(model)
)
Dict{VariableRef, MathOptInterface.BasisStatusCode} with 3 entries:
y => BASIC
x => BASIC
z => NONBASIC_AT_UPPER
Хотя у модели три ограничения, в ней всего две базисные переменные. Но ведь базисная матрица должна быть квадратной. Где же еще одна базисная переменная?
Ответ кроется в том, что решатели переформулируют ограничения неравенства:
в систему:
Поэтому для каждого ограничения неравенства есть ослабляющая переменная s
.
Запросить статус базиса ослабляющих переменных, связанных с ограничением, можно с помощью MOI.ConstraintBasisStatus
:
c_basis = Dict(
ci => get_attribute(ci, MOI.ConstraintBasisStatus()) for ci in
all_constraints(model; include_variable_in_set_constraints = false)
)
Dict{ConstraintRef{Model, C, ScalarShape} where C, MathOptInterface.BasisStatusCode} with 3 entries:
c2 : 7 x + 12 y ≥ 120 => NONBASIC
c1 : 6 x + 8 y ≥ 100 => NONBASIC
c3 : x + y ≤ 20 => BASIC
Таким образом, базис образуется из x
, y
и ослабляющей переменной, связанной с c3
.
Матрицу A
неструктурированной линейной программы можно легко получить с помощью lp_matrix_data
:
matrix = lp_matrix_data(model)
matrix.A
3×3 SparseMatrixCSC{Float64, Int64} with 6 stored entries:
6.0 8.0 ⋅
7.0 12.0 ⋅
1.0 1.0 ⋅
Проверить перестановку строк и столбцов можно с помощью
matrix.variables
3-element Vector{VariableRef}:
x
y
z
и
matrix.affine_constraints
3-element Vector{ConstraintRef}:
c1 : 6 x + 8 y ≥ 100
c2 : 7 x + 12 y ≥ 120
c3 : x + y ≤ 20
Построить столбец ослабляющих переменных, связанных с c3
, можно так:
s_column = zeros(size(matrix.A, 1))
s_column[3] = 1.0
1.0
Таким образом, полная базисная матрица имеет следующий вид:
B = hcat(matrix.A[:, [1, 2]], s_column)
3×3 SparseMatrixCSC{Float64, Int64} with 7 stored entries:
6.0 8.0 ⋅
7.0 12.0 ⋅
1.0 1.0 1.0
lp_matrix_data
возвращает отдельные векторы для нижней и верхней границ строк. Преобразуем их в один вектор правой части, взяв конечные элементы:
b = ifelse.(isfinite.(matrix.b_lower), matrix.b_lower, matrix.b_upper)
3-element Vector{Float64}:
100.0
120.0
20.0
Если решить базисную систему так же, как и ранее, получится следующее.
B \ b
3-element Vector{Float64}:
14.999999999999995
1.250000000000004
3.75
Это значение x
, y
и ослабляющей переменной, связанной с c3
.
Выявление вырожденных переменных
Еще одна стандартная задача — выявление вырожденных переменных. Вырожденная переменная — это базисная переменная с оптимальным значением на нижней или верхней границе.
Вот функция, которая определяет, является ли переменная вырожденной:
function is_degenerate(x)
if get_attribute(x, MOI.VariableBasisStatus()) == MOI.BASIC
return (has_lower_bound(x) && ≈(value(x), lower_bound(x))) ||
(has_upper_bound(x) && ≈(value(x), upper_bound(x)))
end
return false
end
is_degenerate (generic function with 1 method)
Простой пример линейной программы с вырожденным решением:
A, b, c = [1 1; 0 1], [1, 1], [1, 1]
model = Model(HiGHS.Optimizer);
set_silent(model)
@variable(model, x[1:2] >= 0)
@objective(model, Min, c' * x)
@constraint(model, A * x == b)
optimize!(model)
degenerate_variables = filter(is_degenerate, all_variables(model))
1-element Vector{VariableRef}:
x[1]
Решение является вырожденным, потому что:
value(x[1])
-0.0
и
get_attribute(x[1], MOI.VariableBasisStatus())
BASIC::BasisStatusCode = 0