Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
initial_basis.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <algorithm>
17#include <cmath>
18#include <cstdlib>
19#include <limits>
20#include <queue>
21#include <vector>
22
23#include "absl/base/optimization.h"
24#include "absl/log/check.h"
25#include "absl/log/log.h"
31
32namespace operations_research {
33namespace glop {
34
36 const DenseRow& objective,
37 const DenseRow& lower_bound,
38 const DenseRow& upper_bound,
39 const VariableTypeRow& variable_type)
40 : max_scaled_abs_cost_(0.0),
41 bixby_column_comparator_(*this),
42 triangular_column_comparator_(*this),
43 compact_matrix_(compact_matrix),
44 objective_(objective),
45 lower_bound_(lower_bound),
46 upper_bound_(upper_bound),
47 variable_type_(variable_type) {}
48
49void InitialBasis::CompleteBixbyBasis(ColIndex num_cols,
50 RowToColMapping* basis) {
51 // Initialize can_be_replaced ('I' in Bixby's paper) and has_zero_coefficient
52 // ('r' in Bixby's paper).
53 const RowIndex num_rows = compact_matrix_.num_rows();
54 DenseBooleanColumn can_be_replaced(num_rows, false);
55 DenseBooleanColumn has_zero_coefficient(num_rows, false);
56 DCHECK_EQ(num_rows, basis->size());
57 basis->resize(num_rows, kInvalidCol);
58 for (RowIndex row(0); row < num_rows; ++row) {
59 if ((*basis)[row] == kInvalidCol) {
60 can_be_replaced[row] = true;
61 has_zero_coefficient[row] = true;
62 }
63 }
64
65 // This is 'v' in Bixby's paper.
66 DenseColumn scaled_diagonal_abs(compact_matrix_.num_rows(), kInfinity);
67
68 // Compute a list of candidate indices and sort them using the heuristic
69 // described in Bixby's paper.
70 std::vector<ColIndex> candidates;
71 ComputeCandidates(num_cols, &candidates);
72
73 // Loop over the candidate columns, and add them to the basis if the
74 // heuristics are satisfied.
75 for (int i = 0; i < candidates.size(); ++i) {
76 bool enter_basis = false;
77 const ColIndex candidate_col_index = candidates[i];
78 const auto& candidate_col = compact_matrix_.column(candidate_col_index);
79
80 // Bixby's heuristic only works with scaled columns. This should be the
81 // case by default since we only use this when the matrix is scaled, but
82 // it is not the case for our tests... The overhead for computing the
83 // infinity norm for each column should be minimal.
84 if (InfinityNorm(candidate_col) != 1.0) continue;
85
86 RowIndex candidate_row;
87 Fractional candidate_coeff = RestrictedInfinityNorm(
88 candidate_col, has_zero_coefficient, &candidate_row);
89 const Fractional kBixbyHighThreshold = 0.99;
90 if (candidate_coeff > kBixbyHighThreshold) {
91 enter_basis = true;
92 } else if (IsDominated(candidate_col, scaled_diagonal_abs)) {
93 candidate_coeff = RestrictedInfinityNorm(candidate_col, can_be_replaced,
94 &candidate_row);
95 if (candidate_coeff != 0.0) {
96 enter_basis = true;
97 }
98 }
99
100 if (enter_basis) {
101 can_be_replaced[candidate_row] = false;
102 SetSupportToFalse(candidate_col, &has_zero_coefficient);
103 const Fractional kBixbyLowThreshold = 0.01;
104 scaled_diagonal_abs[candidate_row] =
105 kBixbyLowThreshold * std::abs(candidate_coeff);
106 (*basis)[candidate_row] = candidate_col_index;
107 }
108 }
109}
110
112 RowToColMapping* basis) {
113 return GetMarosBasis<false>(num_cols, basis);
114}
115
116void InitialBasis::GetDualMarosBasis(ColIndex num_cols,
117 RowToColMapping* basis) {
118 return GetMarosBasis<true>(num_cols, basis);
119}
120
122 RowToColMapping* basis) {
123 return CompleteTriangularBasis<false>(num_cols, basis);
124}
125
127 RowToColMapping* basis) {
128 return CompleteTriangularBasis<true>(num_cols, basis);
129}
130
131template <bool only_allow_zero_cost_column>
132void InitialBasis::CompleteTriangularBasis(ColIndex num_cols,
133 RowToColMapping* basis) {
134 // Initialize can_be_replaced.
135 const RowIndex num_rows = compact_matrix_.num_rows();
136 DenseBooleanColumn can_be_replaced(num_rows, false);
137 DCHECK_EQ(num_rows, basis->size());
138 basis->resize(num_rows, kInvalidCol);
139 for (RowIndex row(0); row < num_rows; ++row) {
140 if ((*basis)[row] == kInvalidCol) {
141 can_be_replaced[row] = true;
142 }
143 }
144
145 // Initialize the residual non-zero pattern for the rows that can be replaced.
146 MatrixNonZeroPattern residual_pattern;
147 residual_pattern.Reset(num_rows, num_cols);
148 for (ColIndex col(0); col < num_cols; ++col) {
149 if (only_allow_zero_cost_column && objective_[col] != 0.0) continue;
150 for (const SparseColumn::Entry e : compact_matrix_.column(col)) {
151 if (can_be_replaced[e.row()]) {
152 residual_pattern.AddEntry(e.row(), col);
153 }
154 }
155 }
156
157 // Initialize a priority queue of residual singleton columns.
158 // Also compute max_scaled_abs_cost_ for GetColumnPenalty().
159 std::vector<ColIndex> residual_singleton_column;
160 max_scaled_abs_cost_ = 0.0;
161 for (ColIndex col(0); col < num_cols; ++col) {
162 max_scaled_abs_cost_ =
163 std::max(max_scaled_abs_cost_, std::abs(objective_[col]));
164 if (residual_pattern.ColDegree(col) == 1) {
165 residual_singleton_column.push_back(col);
166 }
167 }
168 const Fractional kBixbyWeight = 1000.0;
169 max_scaled_abs_cost_ =
170 (max_scaled_abs_cost_ == 0.0) ? 1.0 : kBixbyWeight * max_scaled_abs_cost_;
171 std::priority_queue<ColIndex, std::vector<ColIndex>,
172 InitialBasis::TriangularColumnComparator>
173 queue(residual_singleton_column.begin(), residual_singleton_column.end(),
174 triangular_column_comparator_);
175
176 // Process the residual singleton columns by priority and add them to the
177 // basis if their "diagonal" coefficient is not too small.
178 while (!queue.empty()) {
179 const ColIndex candidate = queue.top();
180 queue.pop();
181 if (residual_pattern.ColDegree(candidate) != 1) continue;
182
183 // Find the position of the singleton and compute the infinity norm of
184 // the column (note that this is always 1.0 if the problem was scaled).
185 RowIndex row(kInvalidRow);
186 Fractional coeff = 0.0;
187 Fractional max_magnitude = 0.0;
188 for (const SparseColumn::Entry e : compact_matrix_.column(candidate)) {
189 max_magnitude = std::max(max_magnitude, std::abs(e.coefficient()));
190 if (can_be_replaced[e.row()]) {
191 row = e.row();
192 coeff = e.coefficient();
193 break;
194 }
195 }
196 const Fractional kStabilityThreshold = 0.01;
197 if (std::abs(coeff) < kStabilityThreshold * max_magnitude) continue;
198 DCHECK_NE(kInvalidRow, row);
199
200 // Use this candidate column in the basis.
201 (*basis)[row] = candidate;
202 can_be_replaced[row] = false;
203 residual_pattern.DeleteRowAndColumn(row, candidate);
204 for (const ColIndex col : residual_pattern.RowNonZero(row)) {
205 if (col == candidate) continue;
206 residual_pattern.DecreaseColDegree(col);
207 if (residual_pattern.ColDegree(col) == 1) {
208 queue.push(col);
209 }
210 }
211 }
212}
213
214int InitialBasis::GetMarosPriority(ColIndex col) const {
215 // Priority values for columns as defined in Maros's book.
216 switch (variable_type_[col]) {
218 return 3;
220 return 2;
222 return 2;
224 return 1;
226 return 0;
227 }
228 LOG(FATAL) << "Invalid variable type: "
229 << static_cast<int>(variable_type_[col]);
230}
231
232int InitialBasis::GetMarosPriority(RowIndex row) const {
233 // Priority values for rows are equal to
234 // 3 - row priority values as defined in Maros's book
235 ColIndex slack_index(RowToColIndex(row) + compact_matrix_.num_cols() -
236 RowToColIndex(compact_matrix_.num_rows()));
237
238 return GetMarosPriority(slack_index);
239}
240
241template <bool only_allow_zero_cost_column>
242void InitialBasis::GetMarosBasis(ColIndex num_cols, RowToColMapping* basis) {
243 VLOG(1) << "Starting Maros crash procedure.";
244
245 // Initialize basis to the all-slack basis.
246 const RowIndex num_rows = compact_matrix_.num_rows();
247 const ColIndex first_slack = num_cols - RowToColIndex(num_rows);
248 DCHECK_EQ(num_rows, basis->size());
249 basis->resize(num_rows);
250 for (RowIndex row(0); row < num_rows; row++) {
251 (*basis)[row] = first_slack + RowToColIndex(row);
252 }
253
254 // Initialize the set of available rows and columns.
255 DenseBooleanRow available(num_cols, true);
256 for (ColIndex col(0); col < first_slack; ++col) {
257 if (variable_type_[col] == VariableType::FIXED_VARIABLE ||
258 (only_allow_zero_cost_column && objective_[col] != 0.0)) {
259 available[col] = false;
260 }
261 }
262 for (ColIndex col = first_slack; col < num_cols; ++col) {
263 if (variable_type_[col] == VariableType::UNCONSTRAINED) {
264 available[col] = false;
265 }
266 }
267
268 // Initialize the residual non-zero pattern for the active part of the matrix.
269 MatrixNonZeroPattern residual_pattern;
270 residual_pattern.Reset(num_rows, num_cols);
271 for (ColIndex col(0); col < first_slack; ++col) {
272 for (const SparseColumn::Entry e : compact_matrix_.column(col)) {
273 if (available[RowToColIndex(e.row())] && available[col]) {
274 residual_pattern.AddEntry(e.row(), col);
275 }
276 }
277 }
278
279 // Go over residual pattern and mark rows as unavailable.
280 for (RowIndex row(0); row < num_rows; row++) {
281 if (residual_pattern.RowDegree(row) == 0) {
282 available[RowToColIndex(row) + first_slack] = false;
283 }
284 }
285
286 for (;;) {
287 // Make row selection by the Row Priority Function (RPF) from Maros's
288 // book.
289 int max_row_priority_function = std::numeric_limits<int>::min();
290 RowIndex max_rpf_row = kInvalidRow;
291 for (RowIndex row(0); row < num_rows; row++) {
292 if (available[RowToColIndex(row) + first_slack]) {
293 const int rpf =
294 10 * (3 - GetMarosPriority(row)) - residual_pattern.RowDegree(row);
295 if (rpf > max_row_priority_function) {
296 max_row_priority_function = rpf;
297 max_rpf_row = row;
298 }
299 }
300 }
301 if (max_rpf_row == kInvalidRow) break;
302
303 // Trace row for nonzero entries and pick one with best Column Priority
304 // Function (cpf).
305 const Fractional kStabilityThreshold = 1e-3;
306 ColIndex max_cpf_col(kInvalidCol);
307 int max_col_priority_function(std::numeric_limits<int>::min());
308 Fractional pivot_absolute_value = 0.0;
309 for (const ColIndex col : residual_pattern.RowNonZero(max_rpf_row)) {
310 if (!available[col]) continue;
311 const int cpf =
312 10 * GetMarosPriority(col) - residual_pattern.ColDegree(col);
313 if (cpf > max_col_priority_function) {
314 // Make sure that the pivotal entry is not too small in magnitude.
315 Fractional max_magnitude = 0;
316 pivot_absolute_value = 0.0;
317 const auto& column_values = compact_matrix_.column(col);
318 for (const SparseColumn::Entry e : column_values) {
319 const Fractional absolute_value = std::fabs(e.coefficient());
320 if (e.row() == max_rpf_row) pivot_absolute_value = absolute_value;
321 max_magnitude = std::max(max_magnitude, absolute_value);
322 }
323 if (pivot_absolute_value >= kStabilityThreshold * max_magnitude) {
324 max_col_priority_function = cpf;
325 max_cpf_col = col;
326 }
327 }
328 }
329
330 if (max_cpf_col == kInvalidCol) {
331 available[RowToColIndex(max_rpf_row) + first_slack] = false;
332 continue;
333 }
334
335 // Ensure that the row leaving the basis has a lower priority than the
336 // column entering the basis. If the best column is not good enough mark
337 // row as unavailable and choose another one.
338 const int row_priority = GetMarosPriority(max_rpf_row);
339 const int column_priority = GetMarosPriority(max_cpf_col);
340 if (row_priority >= column_priority) {
341 available[RowToColIndex(max_rpf_row) + first_slack] = false;
342 continue;
343 }
344
345 // Use this candidate column in the basis. Update residual pattern and row
346 // counts list.
347 (*basis)[max_rpf_row] = max_cpf_col;
348
349 VLOG(2) << "Slack variable " << max_rpf_row << " replaced by column "
350 << max_cpf_col
351 << ". Pivot coefficient magnitude: " << pivot_absolute_value << ".";
352
353 available[max_cpf_col] = false;
354 available[first_slack + RowToColIndex(max_rpf_row)] = false;
355
356 // Maintain the invariant that all the still available columns will have
357 // zeros on the rows we already replaced. This ensures the lower-triangular
358 // nature (after permutation) of the returned basis.
359 residual_pattern.DeleteRowAndColumn(max_rpf_row, max_cpf_col);
360 for (const ColIndex col : residual_pattern.RowNonZero(max_rpf_row)) {
361 available[col] = false;
362 }
363 }
364}
365
366void InitialBasis::ComputeCandidates(ColIndex num_cols,
367 std::vector<ColIndex>* candidates) {
368 candidates->clear();
369 max_scaled_abs_cost_ = 0.0;
370 for (ColIndex col(0); col < num_cols; ++col) {
371 if (variable_type_[col] != VariableType::FIXED_VARIABLE &&
372 compact_matrix_.column(col).num_entries() > 0) {
373 candidates->push_back(col);
374 max_scaled_abs_cost_ =
375 std::max(max_scaled_abs_cost_, std::abs(objective_[col]));
376 }
377 }
378 const Fractional kBixbyWeight = 1000.0;
379 max_scaled_abs_cost_ =
380 (max_scaled_abs_cost_ == 0.0) ? 1.0 : kBixbyWeight * max_scaled_abs_cost_;
381 std::sort(candidates->begin(), candidates->end(), bixby_column_comparator_);
382}
383
384int InitialBasis::GetColumnCategory(ColIndex col) const {
385 // Only the relative position of the returned number is important, so we use
386 // 2 for the category C2 in Bixby's paper and so on.
387 switch (variable_type_[col]) {
389 return 2;
391 return 3;
393 return 3;
395 return 4;
397 return 5;
398 }
399 ABSL_UNREACHABLE();
400}
401
402Fractional InitialBasis::GetColumnPenalty(ColIndex col) const {
403 const VariableType type = variable_type_[col];
404 Fractional penalty = 0.0;
405 if (type == VariableType::LOWER_BOUNDED) {
406 penalty = lower_bound_[col];
407 }
408 if (type == VariableType::UPPER_BOUNDED) {
409 penalty = -upper_bound_[col];
410 }
412 penalty = lower_bound_[col] - upper_bound_[col];
413 }
414 return penalty + std::abs(objective_[col]) / max_scaled_abs_cost_;
415}
416
417bool InitialBasis::BixbyColumnComparator::operator()(ColIndex col_a,
418 ColIndex col_b) const {
419 if (col_a == col_b) return false;
420 const int category_a = initial_basis_.GetColumnCategory(col_a);
421 const int category_b = initial_basis_.GetColumnCategory(col_b);
422 if (category_a != category_b) {
423 return category_a < category_b;
424 } else {
425 return initial_basis_.GetColumnPenalty(col_a) <
426 initial_basis_.GetColumnPenalty(col_b);
427 }
428}
429
430bool InitialBasis::TriangularColumnComparator::operator()(
431 ColIndex col_a, ColIndex col_b) const {
432 if (col_a == col_b) return false;
433 const int category_a = initial_basis_.GetColumnCategory(col_a);
434 const int category_b = initial_basis_.GetColumnCategory(col_b);
435 if (category_a != category_b) {
436 return category_a > category_b;
437 }
438
439 // The nonzero is not in the original Bixby paper, but experiment shows it is
440 // important. It leads to sparser solves, but also sparser direction, which
441 // mean potentially less blocking variables on each pivot...
442 //
443 // TODO(user): Experiments more with this comparator or the
444 // BixbyColumnComparator.
445 if (initial_basis_.compact_matrix_.column(col_a).num_entries() !=
446 initial_basis_.compact_matrix_.column(col_b).num_entries()) {
447 return initial_basis_.compact_matrix_.column(col_a).num_entries() >
448 initial_basis_.compact_matrix_.column(col_b).num_entries();
449 }
450 return initial_basis_.GetColumnPenalty(col_a) >
451 initial_basis_.GetColumnPenalty(col_b);
452}
453
454} // namespace glop
455} // namespace operations_research
ColumnView column(ColIndex col) const
Definition sparse.h:419
void CompleteTriangularPrimalBasis(ColIndex num_cols, RowToColMapping *basis)
InitialBasis(const CompactSparseMatrix &compact_matrix, const DenseRow &objective, const DenseRow &lower_bound, const DenseRow &upper_bound, const VariableTypeRow &variable_type)
void CompleteBixbyBasis(ColIndex num_cols, RowToColMapping *basis)
void GetPrimalMarosBasis(ColIndex num_cols, RowToColMapping *basis)
void ComputeCandidates(ColIndex num_cols, std::vector< ColIndex > *candidates)
void GetDualMarosBasis(ColIndex num_cols, RowToColMapping *basis)
void CompleteTriangularDualBasis(ColIndex num_cols, RowToColMapping *basis)
double Fractional
Definition lp_types.h:81
constexpr ColIndex kInvalidCol(-1)
StrictITIVector< RowIndex, ColIndex > RowToColMapping
Definition lp_types.h:394
StrictITIVector< RowIndex, bool > DenseBooleanColumn
Definition lp_types.h:383
ColIndex RowToColIndex(RowIndex row)
Definition lp_types.h:54
constexpr RowIndex kInvalidRow(-1)
StrictITIVector< ColIndex, VariableType > VariableTypeRow
Definition lp_types.h:369
Fractional RestrictedInfinityNorm(const ColumnView &column, const DenseBooleanColumn &rows_to_consider, RowIndex *row_index)
Definition lp_utils.cc:203
Fractional InfinityNorm(const DenseColumn &v)
Definition lp_utils.cc:151
void SetSupportToFalse(const ColumnView &column, DenseBooleanColumn *b)
Definition lp_utils.cc:216
constexpr Fractional kInfinity
Definition lp_types.h:87
StrictITIVector< RowIndex, Fractional > DenseColumn
Definition lp_types.h:380
StrictITIVector< ColIndex, Fractional > DenseRow
Definition lp_types.h:351
bool IsDominated(const ColumnView &column, const DenseColumn &radius)
Definition lp_utils.cc:224
StrictITIVector< ColIndex, bool > DenseBooleanRow
Definition lp_types.h:354
OR-Tools root namespace.