Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
sparse_matrix.h
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
14// Classes for modeling sparse matrices.
15#ifndef ORTOOLS_MATH_OPT_STORAGE_SPARSE_MATRIX_H_
16#define ORTOOLS_MATH_OPT_STORAGE_SPARSE_MATRIX_H_
17
18#include <algorithm>
19#include <cstdint>
20#include <tuple>
21#include <utility>
22#include <vector>
23
24#include "absl/algorithm/container.h"
25#include "absl/container/flat_hash_map.h"
26#include "absl/container/flat_hash_set.h"
27#include "absl/meta/type_traits.h"
28#include "absl/types/span.h"
33
35
36// A sparse symmetric double valued matrix over VariableIds.
37//
38// Note that the matrix is sparse in both:
39// * The IDs of the rows/columns (both VariableIds), stored as flat_hash_map.
40// * The entries with nonzero value.
41//
42// Getting/setting/clearing entries are O(1) operations. Getting a row of the
43// matrix runs in O(size of the row) if nothing has been deleted, and getting
44// all the rows runs in O(number of nonzero entries), even with deletions
45// (with deletions, accessing a particular row with many deletions may be slow).
46//
47// Implementation: The entries are stored in a
48// flat_hash_map<pair<VariableId, VariableId>, double> `values_` where for each
49// key, key.first <= key.second. Additionally, we maintain a
50// flat_hash_map<VariableId, vector<VariableId>> `related_variables_` that says
51// for each variable, which variables they have a nonzero term with. When a
52// coefficient is set to zero or a variable is deleted, we do not immediately
53// delete the data from values_ or related_variables_, we simply set the
54// coefficient to zero in values_. We track how many zeros are in values_, and
55// when more than some constant fraction of all entries are zero (see
56// kZerosCleanup in cc file), we clean up related_variables_ and values_ to
57// remove all the zeros. Iteration over the rows or total entries of the matrix
58// must check for zeros in values_ and skip these terms.
59//
60// Memory use:
61// * 3*8 bytes per nonzero plus hash capacity overhead for values_.
62// * 2*8 bytes per nonzero plus vector capacity overhead for
63// related_variables_.
64// * ~5*8 bytes per variable participating in any quadratic term; one heap
65// allocation per such variable.
67 public:
68 // Setting `value` to zero removes the value from the matrix.
69 // Returns true if `value` is different from the existing value in the matrix.
70 inline bool set(VariableId first, VariableId second, double value);
71
72 // Zero is returned if the value is not present.
73 inline double get(VariableId first, VariableId second) const;
74
75 // Zeros out all coefficients for this variable.
76 void Delete(VariableId variable);
77
78 // Returns the variables with any nonzero in the matrix.
79 //
80 // The return order is deterministic but not defined.
81 // TODO(b/233630053): expose an iterator based API to avoid making a copy.
82 std::vector<VariableId> Variables() const;
83
84 // Returns the variables that have nonzero entries with `variable`.
85 //
86 // The return order is deterministic but not defined.
87 // TODO(b/233630053): expose an iterator based API to avoid making a copy.
88 std::vector<VariableId> RelatedVariables(VariableId variable) const;
89
90 // Returns the variable value pairs (x, c) where `variable` and x have nonzero
91 // coefficient c.
92 //
93 // The return order is deterministic but not defined.
94 // TODO(b/233630053): expose an iterator based API to avoid making a copy.
95 std::vector<std::pair<VariableId, double>> Terms(VariableId variable) const;
96
97 // Returns (x, y, c) tuples where variables x and y have nonzero coefficient
98 // c, and x <= y.
99 //
100 // The return order is non-deterministic and not defined.
101 // TODO(b/233630053): expose an iterator based API to avoid making a copy.
102 std::vector<std::tuple<VariableId, VariableId, double>> Terms() const;
103
104 // Removes all terms from the matrix.
105 void Clear();
106
107 // The number of (var, var) keys with nonzero value. Note that (x, y) and
108 // (y, x) are the same key.
109 int64_t nonzeros() const { return nonzeros_; }
110
111 // For testing/debugging only, do not depend on this value, behavior may
112 // change based on implementation.
113 int64_t impl_detail_matrix_storage_size() const { return values_.size(); }
114
115 // TODO(b/233630053): do not expose values_ directly, instead offer a way to
116 // iterate over all the nonzero entries.
117 // Warning: this map will contain zeros.
118 const absl::flat_hash_map<std::pair<VariableId, VariableId>, double>& values()
119 const {
120 return values_;
121 }
122
124
126 const absl::flat_hash_set<VariableId>& deleted_variables,
127 absl::Span<const VariableId> new_variables,
128 const absl::flat_hash_set<std::pair<VariableId, VariableId>>& dirty)
129 const;
130
131 private:
132 inline std::pair<VariableId, VariableId> make_key(VariableId first,
133 VariableId second) const;
134 void CompactIfNeeded();
135
136 // The keys of values_ have key.first <= key.second.
137 absl::flat_hash_map<std::pair<VariableId, VariableId>, double> values_;
138 absl::flat_hash_map<VariableId, std::vector<VariableId>> related_variables_;
139
140 // The number of nonzero elements in values_.
141 int64_t nonzeros_ = 0;
142};
143
144// A sparse double valued matrix over int-like rows and columns.
145//
146// Note that the matrix is sparse in both:
147// * The IDs of the rows/columns, stored as flat_hash_map.
148// * The entries with nonzero value.
149//
150// Getting/setting/clearing entries are O(1) operations. Getting a row of the
151// matrix runs in O(size of the row) if nothing has been deleted, and getting
152// all the rows runs in O(number of nonzero entries), even with deletions
153// (with deletions, accessing a particular row or columns with many deletions
154// may be slow).
155//
156// Implementation: The entries are stored in a
157// flat_hash_map<pair<RowId, ColumnId>, double> `values_`. Additionally, we
158// maintain a flat_hash_map<RowId, vector<ColumnId>> `rows_` and a
159// flat_hash_map<ColumnId, vector<RowID>> `columns_` that enable efficient
160// queries of the nonzeros in any row or column. When a coefficient is set to
161// zero or a variable is deleted, we do not immediately delete the data from
162// `values_`, `rows_`, or `columns_`, we simply set the coefficient to zero in
163// `values_`. We track how many zeros are in `values_`, and when more than some
164// constant fraction of all entries are zero (see `internal::kZerosCleanup`), we
165// clean up `rows_`, `columns_`, and `values_` to remove all the zeros.
166// Iteration over the rows or total entries of the matrix must check for zeros
167// in values_ and skip these terms.
168//
169// Memory use:
170// * 3*8 bytes per nonzero plus hash capacity overhead for values_.
171// * 2*8 bytes per nonzero plus vector capacity overhead for
172// rows_ and columns_.
173// * ~5*8 bytes and one heap allocation per unique row and unique column.
174template <typename RowId, typename ColumnId>
176 public:
177 // Setting `value` to zero removes the value from the matrix.
178 // Returns true if `value` is different from the existing value in the matrix.
179 bool set(RowId row, ColumnId column, double value);
180
181 // Zero is returned if the value is not present.
182 double get(RowId row, ColumnId column) const;
183
184 // Returns true if the value is present (nonzero).
185 bool contains(RowId row, ColumnId column) const;
186
187 // Zeros out all coefficients for this variable.
188 void DeleteRow(RowId row);
189
190 // Zeros out all coefficients for this variable.
191 void DeleteColumn(ColumnId column);
192
193 // Returns the variables that have nonzero entries with `variable`.
194 // TODO(b/233630053): expose an iterator based API to avoid making a copy.
195 std::vector<ColumnId> row(RowId row_id) const;
196
197 // Returns the variables that have nonzero entries with `variable`.
198 // TODO(b/233630053): expose an iterator based API to avoid making a copy.
199 std::vector<RowId> column(ColumnId column_id) const;
200
201 // Returns the variable value pairs (x, c) where `variable` and x have nonzero
202 // coefficient c.
203 //
204 // The return order is deterministic but not defined.
205 // TODO(b/233630053): expose an iterator based API to avoid making a copy.
206 std::vector<std::pair<ColumnId, double>> RowTerms(RowId row_id) const;
207
208 // Returns the variable value pairs (x, c) where `variable` and x have nonzero
209 // coefficient c.
210 //
211 // The return order is deterministic but not defined.
212 // TODO(b/233630053): expose an iterator based API to avoid making a copy.
213 std::vector<std::pair<RowId, double>> ColumnTerms(ColumnId col_id) const;
214
215 // Returns (x, y, c) tuples where variables x and y have nonzero coefficient
216 // c, and x <= y.
217 //
218 // The return order is non-deterministic and not defined.
219 // TODO(b/233630053): expose an iterator based API to avoid making a copy.
220 std::vector<std::tuple<RowId, ColumnId, double>> Terms() const;
221
222 // Removes all terms from the matrix.
223 void Clear();
224
225 // The number of (row, column) keys with nonzero value.
226 int64_t nonzeros() const;
227
228 // For testing/debugging only, do not depend on this value, behavior may
229 // change based on implementation.
230 int64_t impl_detail_matrix_storage_size() const { return values_.size(); }
231
233
235 const absl::flat_hash_set<RowId>& deleted_rows,
236 absl::Span<const RowId> new_rows,
237 const absl::flat_hash_set<ColumnId>& deleted_columns,
238 absl::Span<const ColumnId> new_columns,
239 const absl::flat_hash_set<std::pair<RowId, ColumnId>>& dirty) const;
240
241 private:
242 void CompactIfNeeded();
243
244 // The values of the map can include zero.
245 absl::flat_hash_map<std::pair<RowId, ColumnId>, double> values_;
246 absl::flat_hash_map<RowId, std::vector<ColumnId>> rows_;
247 absl::flat_hash_map<ColumnId, std::vector<RowId>> columns_;
248
249 // The number of nonzero elements in values_.
250 int64_t nonzeros_ = 0;
251};
252
253namespace internal {
254
255// When the fraction of entries in values_ with value 0.0 is larger than
256// kZerosCleanup, we compact the data structure and remove all zero entries.
257constexpr double kZerosCleanup = 1.0 / 3.0;
258
259// entries must have unique (row, column) values but can be in any order.
260template <typename RowId, typename ColumnId>
262 std::vector<std::tuple<RowId, ColumnId, double>> entries);
263
264} // namespace internal
265
268// Inlined functions
271
272namespace internal {
273
274template <typename RowId, typename ColumnId>
276 std::vector<std::tuple<RowId, ColumnId, double>> entries) {
277 absl::c_sort(entries);
278 const int num_entries = static_cast<int>(entries.size());
280 result.mutable_row_ids()->Reserve(num_entries);
281 result.mutable_column_ids()->Reserve(num_entries);
282 result.mutable_coefficients()->Reserve(num_entries);
283 for (const auto [lin_con, var, coef] : entries) {
284 result.add_row_ids(lin_con.value());
285 result.add_column_ids(var.value());
286 result.add_coefficients(coef);
287 }
288 return result;
289}
290
291} // namespace internal
292
294// SparseSymmetricMatrix
296
297std::pair<VariableId, VariableId> SparseSymmetricMatrix::make_key(
298 const VariableId first, const VariableId second) const {
299 return {std::min(first, second), std::max(first, second)};
300}
301
302bool SparseSymmetricMatrix::set(const VariableId first, const VariableId second,
303 const double value) {
304 const std::pair<VariableId, VariableId> key = make_key(first, second);
305 auto map_iter = values_.find(key);
306
307 if (map_iter == values_.end()) {
308 if (value == 0.0) {
309 return false;
310 }
311 related_variables_[first].push_back(second);
312 if (first != second) {
313 related_variables_[second].push_back(first);
314 }
315 values_[key] = value;
316 nonzeros_++;
317 return true;
318 } else {
319 if (map_iter->second == value) {
320 return false;
321 }
322 const double old_value = map_iter->second;
323 map_iter->second = value;
324 if (value == 0.0) {
325 nonzeros_--;
326 CompactIfNeeded();
327 } else if (old_value == 0.0) {
328 nonzeros_++;
329 }
330 return true;
331 }
332}
333
334double SparseSymmetricMatrix::get(const VariableId first,
335 const VariableId second) const {
336 return gtl::FindWithDefault(values_, make_key(first, second));
337}
338
339
340// SparseMatrix
342
343template <typename RowId, typename ColumnId>
344bool SparseMatrix<RowId, ColumnId>::set(const RowId row, const ColumnId column,
345 const double value) {
346 const std::pair<RowId, ColumnId> key = {row, column};
347 const auto it = values_.find(key);
348 if (it == values_.end()) {
349 if (value == 0.0) {
350 return false;
351 }
352 rows_[row].push_back(column);
353 columns_[column].push_back(row);
354 values_[key] = value;
355 ++nonzeros_;
356 return true;
357 } else {
358 if (it->second == value) {
359 return false;
360 }
361 const double old_value = it->second;
362 it->second = value;
363 if (value == 0.0) {
364 --nonzeros_;
365 CompactIfNeeded();
366 } else if (old_value == 0.0) {
367 ++nonzeros_;
368 }
369 return true;
370 }
371}
372
373template <typename RowId, typename ColumnId>
374double SparseMatrix<RowId, ColumnId>::get(const RowId row,
375 const ColumnId column) const {
376 const auto it = values_.find({row, column});
377 if (it != values_.end()) {
378 return it->second;
379 }
380 return 0.0;
381}
382template <typename RowId, typename ColumnId>
384 const ColumnId column) const {
385 const auto it = values_.find({row, column});
386 return it != values_.end() && it->second != 0.0;
387}
388
389template <typename RowId, typename ColumnId>
390void SparseMatrix<RowId, ColumnId>::DeleteRow(const RowId row) {
391 const auto row_it = rows_.find(row);
392 if (row_it == rows_.end()) {
393 return;
394 }
395 for (const ColumnId col : row_it->second) {
396 auto val_it = values_.find({row_it->first, col});
397 if (val_it != values_.end() && val_it->second != 0.0) {
398 val_it->second = 0.0;
399 nonzeros_--;
400 }
401 }
402 CompactIfNeeded();
403}
404
405template <typename RowId, typename ColumnId>
406void SparseMatrix<RowId, ColumnId>::DeleteColumn(const ColumnId column) {
407 const auto col_it = columns_.find(column);
408 if (col_it == columns_.end()) {
409 return;
410 }
411 for (const RowId row : col_it->second) {
412 auto val_it = values_.find({row, col_it->first});
413 if (val_it != values_.end() && val_it->second != 0.0) {
414 val_it->second = 0.0;
415 nonzeros_--;
416 }
417 }
418 CompactIfNeeded();
419}
420
421template <typename RowId, typename ColumnId>
422std::vector<ColumnId> SparseMatrix<RowId, ColumnId>::row(
423 const RowId row_id) const {
424 std::vector<ColumnId> result;
425 const auto it = rows_.find(row_id);
426 if (it != rows_.end()) {
427 for (const ColumnId col : it->second) {
428 if (contains(row_id, col)) {
429 result.push_back(col);
431 }
432 }
433 return result;
434}
435
436template <typename RowId, typename ColumnId>
438 const ColumnId column_id) const {
439 std::vector<RowId> result;
440 const auto it = columns_.find(column_id);
441 if (it != columns_.end()) {
442 for (const RowId row_id : it->second) {
443 if (contains(row_id, column_id)) {
444 result.push_back(row_id);
446 }
447 }
448 return result;
449}
450
451template <typename RowId, typename ColumnId>
452std::vector<std::pair<ColumnId, double>>
453SparseMatrix<RowId, ColumnId>::RowTerms(const RowId row_id) const {
454 std::vector<std::pair<ColumnId, double>> result;
455 const auto it = rows_.find(row_id);
456 if (it != rows_.end()) {
457 for (const ColumnId col : it->second) {
458 const double val = get(row_id, col);
459 if (val != 0.0) {
460 result.push_back({col, val});
462 }
463 }
464 return result;
465}
466
467template <typename RowId, typename ColumnId>
468std::vector<std::pair<RowId, double>>
469SparseMatrix<RowId, ColumnId>::ColumnTerms(const ColumnId col_id) const {
470 std::vector<std::pair<RowId, double>> result;
471 const auto it = columns_.find(col_id);
472 if (it != columns_.end()) {
473 for (const RowId row_id : it->second) {
474 const double val = get(row_id, col_id);
475 if (val != 0.0) {
476 result.push_back({row_id, val});
478 }
479 }
480 return result;
481}
482
483template <typename RowId, typename ColumnId>
484std::vector<std::tuple<RowId, ColumnId, double>>
486 std::vector<std::tuple<RowId, ColumnId, double>> result;
487 result.reserve(nonzeros_);
488 for (const auto& [k, v] : values_) {
489 if (v != 0.0) {
490 result.push_back({k.first, k.second, v});
491 }
492 }
493 return result;
494}
495
496template <typename RowId, typename ColumnId>
498 rows_.clear();
499 columns_.clear();
500 values_.clear();
501}
502
503template <typename RowId, typename ColumnId>
505 return nonzeros_;
506}
507
508template <typename RowId, typename ColumnId>
511 std::vector<std::tuple<RowId, ColumnId, double>> terms = Terms();
512 absl::c_sort(terms);
513 for (const auto [r, c, v] : terms) {
514 result.add_row_ids(r.value());
515 result.add_column_ids(c.value());
516 result.add_coefficients(v);
518 return result;
519}
520
521template <typename RowId, typename ColumnId>
522SparseDoubleMatrixProto SparseMatrix<RowId, ColumnId>::Update(
523 const absl::flat_hash_set<RowId>& deleted_rows,
524 const absl::Span<const RowId> new_rows,
525 const absl::flat_hash_set<ColumnId>& deleted_columns,
526 const absl::Span<const ColumnId> new_columns,
527 const absl::flat_hash_set<std::pair<RowId, ColumnId>>& dirty) const {
528 // Extract changes to the matrix of linear constraint coefficients
529 std::vector<std::tuple<LinearConstraintId, VariableId, double>>
530 matrix_updates;
531 for (const auto [row, column] : dirty) {
532 // Note: it is important that we check for deleted constraints and variables
533 // here. While we generally try to remove elements from matrix_keys_ when
534 // either their variable or constraint is deleted, if a coefficient is set
535 // to zero and then the variable/constraint is deleted, we will miss it.
536 if (deleted_rows.contains(row) || deleted_columns.contains(column)) {
537 continue;
538 }
539 matrix_updates.push_back({row, column, get(row, column)});
540 }
541
542 for (const ColumnId new_col : new_columns) {
543 // TODO(b/233630053): use iterator API.
544 for (const auto [row, coef] : ColumnTerms(new_col)) {
545 matrix_updates.push_back({row, new_col, coef});
546 }
547 }
548 for (const RowId new_row : new_rows) {
549 // TODO(b/233630053) use iterator API.
550 for (const auto [col, coef] : RowTerms(new_row)) {
551 // NOTE(user): we already have the columns above the checkpoint from
552 // the loop above, but we will do at most twice as much as needed here.
553 if (new_columns.empty() || col < new_columns[0]) {
554 matrix_updates.push_back({new_row, col, coef});
555 }
556 }
557 }
558 return internal::EntriesToMatrixProto(std::move(matrix_updates));
559}
560
561template <typename RowId, typename ColumnId>
562void SparseMatrix<RowId, ColumnId>::CompactIfNeeded() {
563 const int64_t zeros = values_.size() - nonzeros_;
564 if (values_.empty() ||
565 static_cast<double>(zeros) / values_.size() <= internal::kZerosCleanup) {
566 return;
567 }
568 // Traverse the rows and remove elements where value_ is zero. Delete the row
569 // if it has no entries.
570 for (auto row_it = rows_.begin(); row_it != rows_.end();) {
571 const RowId row = row_it->first;
572 std::vector<ColumnId>& row_entries = row_it->second;
573 int64_t write = 0;
574 for (int64_t read = 0; read < row_entries.size(); ++read) {
575 const ColumnId col = row_entries[read];
576 const auto val_it = values_.find({row, col});
577 if (val_it != values_.end() && val_it->second != 0.0) {
578 row_entries[write] = col;
579 ++write;
580 }
581 }
582 if (write == 0) {
583 rows_.erase(row_it++);
584 } else {
585 row_entries.resize(write);
586 ++row_it;
587 }
588 }
589
590 // Like above, but now over the columns. Additionally, delete elements from
591 // values_ that are zero in this second pass.
592 for (auto col_it = columns_.begin(); col_it != columns_.end();) {
593 const ColumnId col = col_it->first;
594 std::vector<RowId>& col_entries = col_it->second;
595 int64_t write = 0;
596 for (int64_t read = 0; read < col_entries.size(); ++read) {
597 const RowId row = col_entries[read];
598 const auto val_it = values_.find({row, col});
599 if (val_it != values_.end()) {
600 if (val_it->second != 0.0) {
601 col_entries[write] = row;
602 ++write;
603 } else {
604 values_.erase(val_it);
605 }
606 }
607 }
608 if (write == 0) {
609 columns_.erase(col_it++);
610 } else {
611 col_entries.resize(write);
612 ++col_it;
613 }
614 }
615}
616
617} // namespace operations_research::math_opt
618
619#endif // ORTOOLS_MATH_OPT_STORAGE_SPARSE_MATRIX_H_
::google::protobuf::RepeatedField<::int64_t > *PROTOBUF_NONNULL mutable_column_ids()
::google::protobuf::RepeatedField<::int64_t > *PROTOBUF_NONNULL mutable_row_ids()
::google::protobuf::RepeatedField< double > *PROTOBUF_NONNULL mutable_coefficients()
std::vector< operations_research::math_opt::TaggedId< element_type > > column(operations_research::math_opt::TaggedId< element_type > column_id) const
bool set(RowId row, ColumnId column, double value)
SparseDoubleMatrixProto Update(const absl::flat_hash_set< RowId > &deleted_rows, absl::Span< const RowId > new_rows, const absl::flat_hash_set< ColumnId > &deleted_columns, absl::Span< const ColumnId > new_columns, const absl::flat_hash_set< std::pair< RowId, ColumnId > > &dirty) const
std::vector< operations_research::math_opt::TaggedId< element_type > > row(operations_research::math_opt::TaggedId< element_type > row_id) const
double get(RowId row, ColumnId column) const
std::vector< std::pair< RowId, double > > ColumnTerms(ColumnId col_id) const
std::vector< std::tuple< RowId, ColumnId, double > > Terms() const
bool contains(RowId row, ColumnId column) const
SparseDoubleMatrixProto Proto() const
std::vector< std::pair< ColumnId, double > > RowTerms(RowId row_id) const
SparseDoubleMatrixProto Update(const absl::flat_hash_set< VariableId > &deleted_variables, absl::Span< const VariableId > new_variables, const absl::flat_hash_set< std::pair< VariableId, VariableId > > &dirty) const
std::vector< VariableId > RelatedVariables(VariableId variable) const
std::vector< std::tuple< VariableId, VariableId, double > > Terms() const
const absl::flat_hash_map< std::pair< VariableId, VariableId >, double > & values() const
double get(VariableId first, VariableId second) const
bool set(VariableId first, VariableId second, double value)
const MapUtilMappedT< Collection > & FindWithDefault(const Collection &collection, const KeyType &key, const MapUtilMappedT< Collection > &value)
Definition map_util.h:36
SparseDoubleMatrixProto EntriesToMatrixProto(std::vector< std::tuple< RowId, ColumnId, double > > entries)
ElementId< ElementType::kVariable > VariableId
Definition elements.h:80