Google OR-Tools v9.14
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
set_cover_cft.cc
Go to the documentation of this file.
1// Copyright 2025 Francesco Cavaliere
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 <absl/algorithm/container.h>
17#include <absl/log/globals.h>
18#include <absl/log/log.h>
19#include <absl/random/random.h>
20#include <absl/status/status.h>
21#include <absl/strings/str_join.h>
22#include <absl/time/time.h>
23#include <absl/types/span.h>
24
25#include <iostream>
26#include <limits>
27#include <random>
28
34
36
37// Minimum distance between lower and upper bounds to consider them different.
38// If cost are all integral, can be set neear to 1.0
39#define CFT_BOUND_EPSILON .999
40#define CFT_MAX_MULTIPLIER 1e9
41#define CFT_MEASURE_TIME
42
46namespace {
47
48#ifdef CFT_MEASURE_TIME
49#define CFT_MEASURE_SCOPE_DURATION(Timer) \
50 Timer.Start(); \
51 Defer pause_timer = [&] { Timer.Stop(); };
52
53thread_local WallTimer subgradient_time;
54thread_local WallTimer greedy_time;
55thread_local WallTimer three_phase_time;
56thread_local WallTimer refinement_time;
57#else
58#define CFT_MEASURE_SCOPE_DURATION(Timer)
59#endif
60
61// StopWatch does not add up duration of multiple invocations, Defer is a lower
62// level construct useful in this case.
63template <typename T>
64struct Defer : T {
65 Defer(T&& t) : T(std::forward<T>(t)) {}
66 ~Defer() { T::operator()(); }
67};
68
69template <typename IndexT = ElementIndex>
70class CoverCountersImpl {
71 public:
72 CoverCountersImpl(BaseInt nelems = 0) : cov_counters(nelems, 0) {}
73 void Reset(BaseInt nelems) { cov_counters.assign(nelems, 0); }
74 BaseInt Size() const { return cov_counters.size(); }
75 BaseInt operator[](IndexT i) const {
76 assert(i < IndexT(cov_counters.size()));
77 return cov_counters[i];
78 }
79
80 template <typename IterableT>
81 BaseInt Cover(const IterableT& subset) {
82 BaseInt covered = 0;
83 for (IndexT i : subset) {
84 covered += cov_counters[i] == 0 ? 1ULL : 0ULL;
85 cov_counters[i]++;
86 }
87 return covered;
88 }
89
90 template <typename IterableT>
91 BaseInt Uncover(const IterableT& subset) {
92 BaseInt uncovered = 0;
93 for (IndexT i : subset) {
94 --cov_counters[i];
95 uncovered += cov_counters[i] == 0 ? 1ULL : 0ULL;
96 }
97 return uncovered;
98 }
99
100 // Check if all the elements of a subset are already covered.
101 template <typename IterableT>
102 bool IsRedundantCover(IterableT const& subset) const {
103 return absl::c_all_of(subset,
104 [&](IndexT i) { return cov_counters[i] > 0; });
105 }
106
107 // Check if all the elements would still be covered if the subset was removed.
108 template <typename IterableT>
109 bool IsRedundantUncover(IterableT const& subset) const {
110 return absl::c_all_of(subset,
111 [&](IndexT i) { return cov_counters[i] > 1; });
112 }
113
114 private:
115 util_intops::StrongVector<IndexT, BaseInt> cov_counters;
116};
117
118using CoverCounters = CoverCountersImpl<ElementIndex>;
119using FullCoverCounters = CoverCountersImpl<FullElementIndex>;
120} // namespace
121
122Solution::Solution(const SubModel& model,
123 const std::vector<SubsetIndex>& core_subsets)
124 : cost_(), subsets_() {
125 subsets_.reserve(core_subsets.size() + model.fixed_columns().size());
126 cost_ = model.fixed_cost();
127 for (FullSubsetIndex full_j : model.fixed_columns()) {
128 subsets_.push_back(full_j);
129 }
130 for (SubsetIndex core_j : core_subsets) {
131 FullSubsetIndex full_j = model.MapCoreToFullSubsetIndex(core_j);
132 AddSubset(full_j, model.subset_costs()[core_j]);
133 }
134}
135
137
139
140BoundCBs::BoundCBs(const SubModel& model)
141 : squared_norm_(static_cast<Cost>(model.num_elements())),
142 direction_(ElementCostVector(model.num_elements(), .0)),
143 prev_best_lb_(std::numeric_limits<Cost>::lowest()),
144 max_iter_countdown_(10 *
145 model.num_focus_elements()), // Arbitrary from [1]
146 exit_test_countdown_(300), // Arbitrary from [1]
147 exit_test_period_(300), // Arbitrary from [1]
148 unfixed_run_extension_(0),
149 step_size_(0.1), // Arbitrary from [1]
150 last_min_lb_seen_(std::numeric_limits<Cost>::max()),
151 last_max_lb_seen_(.0),
152 step_size_update_countdown_(20), // Arbitrary from [1]
153 step_size_update_period_(20) // Arbitrary from [1]
154{}
155
156bool BoundCBs::ExitCondition(const SubgradientContext& context) {
157 Cost best_lb = context.best_lower_bound;
158 Cost best_ub = context.best_solution.cost() - context.model.fixed_cost();
159 if (--max_iter_countdown_ <= 0 || squared_norm_ <= kTol) {
160 return true;
161 }
162 if (--exit_test_countdown_ > 0) {
163 return false;
164 }
165 if (prev_best_lb_ >= best_ub - CFT_BOUND_EPSILON) {
166 return true;
167 }
168 exit_test_countdown_ = exit_test_period_;
169 Cost abs_improvement = best_lb - prev_best_lb_;
170 Cost rel_improvement = DivideIfGE0(abs_improvement, best_lb);
171 prev_best_lb_ = best_lb;
172
173 if (abs_improvement >= 1.0 || rel_improvement >= .001) {
174 return false;
175 }
176
177 // (Not in [1]): During the first unfixed iteration we want to converge closer
178 // to the optimum
179 VLOG(3) << "[SUBG] First iteration extension " << unfixed_run_extension_;
180 BaseInt extension = (context.model.fixed_columns().empty() ? 2 : 1);
181 return unfixed_run_extension_++ >= extension;
182}
183
185 ElementCostVector& delta_mults) {
186 direction_ = context.subgradient;
187 BaseInt extension = (context.model.fixed_columns().empty() ? 2 : 1);
188 if (unfixed_run_extension_ < extension) {
189 MakeMinimalCoverageSubgradient(context, direction_);
190 }
191
192 squared_norm_ = .0;
193 for (ElementIndex i : context.model.ElementRange()) {
194 Cost curr_i_mult = context.current_dual_state.multipliers()[i];
195 if ((curr_i_mult <= .0 && direction_[i] < .0) ||
196 (curr_i_mult > CFT_MAX_MULTIPLIER && direction_[i] > .0)) {
197 direction_[i] = 0;
198 }
199
200 squared_norm_ += direction_[i] * direction_[i];
201 }
202
203 if (squared_norm_ <= kTol) {
204 delta_mults.assign(context.model.num_elements(), .0);
205 return;
206 }
207
208 UpdateStepSize(context);
209 Cost upper_bound = context.best_solution.cost() - context.model.fixed_cost();
210 Cost lower_bound = context.current_dual_state.lower_bound();
211 Cost delta = upper_bound - lower_bound;
212 Cost step_constant = (step_size_ * delta) / squared_norm_;
213
214 for (ElementIndex i : context.model.ElementRange()) {
215 delta_mults[i] = step_constant * direction_[i];
216 DCHECK(std::isfinite(delta_mults[i]));
217 }
218}
219
220void BoundCBs::UpdateStepSize(SubgradientContext context) {
221 Cost lower_bound = context.current_dual_state.lower_bound();
222 last_min_lb_seen_ = std::min(last_min_lb_seen_, lower_bound);
223 last_max_lb_seen_ = std::max(last_max_lb_seen_, lower_bound);
224
225 if (--step_size_update_countdown_ <= 0) {
226 step_size_update_countdown_ = step_size_update_period_;
227
228 Cost delta = last_max_lb_seen_ - last_min_lb_seen_;
229 Cost gap = DivideIfGE0(delta, last_max_lb_seen_);
230 if (gap <= .001) { // Arbitray from [1]
231 step_size_ *= 1.5; // Arbitray from [1]
232 VLOG(4) << "[SUBG] Sep size set at " << step_size_;
233 } else if (gap > .01) { // Arbitray from [1]
234 step_size_ /= 2.0; // Arbitray from [1]
235 VLOG(4) << "[SUBG] Sep size set at " << step_size_;
236 }
237 last_min_lb_seen_ = std::numeric_limits<Cost>::max();
238 last_max_lb_seen_ = .0;
239 // Not described in the paper, but in rare cases the subgradient diverges
240 step_size_ = std::clamp(step_size_, 1e-6, 10.0); // Arbitrary from c4v4
241 }
242}
243
244void BoundCBs::MakeMinimalCoverageSubgradient(const SubgradientContext& context,
245 ElementCostVector& subgradient) {
246 lagrangian_solution_.clear();
247 const auto& reduced_costs = context.current_dual_state.reduced_costs();
248 for (SubsetIndex j : context.model.SubsetRange()) {
249 if (reduced_costs[j] < .0) {
250 lagrangian_solution_.push_back(j);
251 }
252 }
253
254 absl::c_sort(lagrangian_solution_, [&](SubsetIndex j1, SubsetIndex j2) {
255 return reduced_costs[j1] > reduced_costs[j2];
256 });
257
258 const auto& cols = context.model.columns();
259 for (SubsetIndex j : lagrangian_solution_) {
260 if (absl::c_all_of(cols[j], [&](auto i) { return subgradient[i] < 0; })) {
261 absl::c_for_each(cols[j], [&](auto i) { subgradient[i] += 1.0; });
262 }
263 }
264}
265
267 CoreModel& core_model, bool force) {
268 if (core_model.UpdateCore(context.best_lower_bound, context.best_multipliers,
269 context.best_solution, force)) {
270 prev_best_lb_ = std::numeric_limits<Cost>::lowest();
271 // Grant at least `min_iters` iterations before the next exit test
272 constexpr BaseInt min_iters = 10;
273 exit_test_countdown_ = std::max<BaseInt>(exit_test_countdown_, min_iters);
274 max_iter_countdown_ = std::max<BaseInt>(max_iter_countdown_, min_iters);
275 return true;
276 }
277 return false;
278}
279
280void SubgradientOptimization(SubModel& model, SubgradientCBs& cbs,
281 PrimalDualState& best_state) {
282 CFT_MEASURE_SCOPE_DURATION(subgradient_time);
283 DCHECK(ValidateSubModel(model));
285 ElementCostVector subgradient = ElementCostVector(model.num_elements(), .0);
286 DualState dual_state = best_state.dual_state;
287 Cost best_lower_bound = dual_state.lower_bound();
288 ElementCostVector best_multipliers = dual_state.multipliers();
290
291 ElementCostVector multipliers_delta(model.num_elements()); // to avoid allocs
292 SubgradientContext context = {.model = model,
293 .current_dual_state = dual_state,
294 .best_lower_bound = best_lower_bound,
295 .best_multipliers = best_multipliers,
296 .best_solution = best_state.solution,
297 .subgradient = subgradient};
298
299 for (size_t iter = 1; !cbs.ExitCondition(context); ++iter) {
300 // Poor multipliers can lead to wasted iterations or stagnation in the
301 // subgradient method. To address this, we adjust the multipliers to
302 // get closer the trivial lower bound (= 0).
303 if (dual_state.lower_bound() < .0) {
304 VLOG(4) << "[SUBG] Dividing multipliers by 10";
305 dual_state.DualUpdate(
306 model, [&](ElementIndex i, Cost& i_mult) { i_mult /= 10.0; });
307 }
308
309 // Compute subgradient (O(nnz))
310 subgradient.assign(model.num_elements(), 1.0);
311 for (SubsetIndex j : model.SubsetRange()) {
312 if (dual_state.reduced_costs()[j] < .0) {
313 for (ElementIndex i : model.columns()[j]) {
314 subgradient[i] -= 1.0;
315 }
316 }
317 }
318
319 cbs.ComputeMultipliersDelta(context, multipliers_delta);
320 dual_state.DualUpdate(model, [&](ElementIndex i, Cost& i_mult) {
321 i_mult = std::clamp<Cost>(i_mult + multipliers_delta[i], .0,
323 });
324 if (dual_state.lower_bound() > best_lower_bound) {
325 best_lower_bound = dual_state.lower_bound();
326 best_multipliers = dual_state.multipliers();
327 }
328
329 cbs.RunHeuristic(context, solution);
330 if (!solution.subsets().empty() &&
331 solution.cost() < best_state.solution.cost()) {
332 best_state.solution = solution;
333 }
334
335 VLOG_EVERY_N(4, 100) << "[SUBG] " << iter << ": Bounds: Lower "
336 << dual_state.lower_bound() << ", best "
337 << best_lower_bound << " - Upper "
338 << best_state.solution.cost() - model.fixed_cost()
339 << ", global " << best_state.solution.cost();
340
341 if (cbs.UpdateCoreModel(context, model)) {
342 dual_state.DualUpdate(model, [&](ElementIndex i, Cost& i_mult) {
343 i_mult = best_multipliers[i];
344 });
345 best_lower_bound = dual_state.lower_bound();
346 }
347 }
348
349 if (cbs.UpdateCoreModel(context, model, /*force=*/true)) {
350 dual_state.DualUpdate(model, [&](ElementIndex i, Cost& i_mult) {
351 i_mult = best_multipliers[i];
352 });
353 best_lower_bound = dual_state.lower_bound();
354 }
355 best_state.dual_state.DualUpdate(model, [&](ElementIndex i, Cost& i_mult) {
356 i_mult = best_multipliers[i];
357 });
358 DCHECK_EQ(best_state.dual_state.lower_bound(), best_lower_bound);
359
360 VLOG(3) << "[SUBG] End - Bounds: Lower " << dual_state.lower_bound()
361 << ", best " << best_lower_bound << " - Upper "
362 << best_state.solution.cost() - model.fixed_cost() << ", global "
363 << best_state.solution.cost();
364}
365
369namespace {
370struct Score {
371 Cost score;
372 SubsetIndex idx;
373};
374
375class GreedyScores {
376 public:
377 static constexpr BaseInt removed_idx = -1;
378 static constexpr Cost max_score = std::numeric_limits<Cost>::max();
379
380 GreedyScores(const SubModel& model, const DualState& dual_state)
381 : bad_size_(),
382 worst_good_score_(std::numeric_limits<Cost>::lowest()),
383 scores_(),
384 reduced_costs_(dual_state.reduced_costs()),
385 covering_counts_(model.num_subsets()),
386 score_map_(model.num_subsets()) {
387 BaseInt s = 0;
388 for (SubsetIndex j : model.SubsetRange()) {
389 DCHECK(model.column_size(j) > 0);
390 covering_counts_[j] = model.column_size(j);
391 Cost j_score = ComputeScore(reduced_costs_[j], covering_counts_[j]);
392 scores_.push_back({j_score, j});
393 score_map_[j] = s++;
394 DCHECK(std::isfinite(reduced_costs_[j]));
395 DCHECK(std::isfinite(j_score));
396 }
397 bad_size_ = scores_.size();
398 }
399
400 SubsetIndex FindMinScoreColumn(const SubModel& model,
401 const DualState& dual_state) {
402 // Check if the bad/good partition should be updated
403 if (bad_size_ == scores_.size()) {
404 if (bad_size_ > model.num_focus_elements()) {
405 bad_size_ = bad_size_ - model.num_focus_elements();
406 absl::c_nth_element(scores_, scores_.begin() + bad_size_,
407 [](Score a, Score b) { return a.score > b.score; });
408 worst_good_score_ = scores_[bad_size_].score;
409 for (BaseInt s = 0; s < scores_.size(); ++s) {
410 score_map_[scores_[s].idx] = s;
411 }
412 } else {
413 bad_size_ = 0;
414 worst_good_score_ = max_score;
415 }
416 DCHECK(bad_size_ > 0 || worst_good_score_ == max_score);
417 }
418
419 Score min_score =
420 *std::min_element(scores_.begin() + bad_size_, scores_.end(),
421 [](Score a, Score b) { return a.score < b.score; });
422 SubsetIndex j_star = min_score.idx;
423 DCHECK_LT(min_score.score, max_score);
424 return j_star;
425 }
426
427 // For each row in the given set, if `cond` returns true, the row is
428 // considered newly covered. The function then iterates over the columns of
429 // that row, updating the scores of the columns accordingly.
430 template <typename RowT, typename ElementSpanT, typename CondT>
431 BaseInt UpdateColumnsScoreOfRowsIf(const RowT& rows,
432 const ElementCostVector& multipliers,
433 const ElementSpanT& row_idxs, CondT cond) {
434 BaseInt processed_rows_count = 0;
435 for (ElementIndex i : row_idxs) {
436 if (!cond(i)) {
437 continue;
438 }
439
440 ++processed_rows_count;
441 for (SubsetIndex j : rows[i]) {
442 covering_counts_[j] -= 1;
443 reduced_costs_[j] += multipliers[i];
444
445 BaseInt s = score_map_[j];
446 DCHECK_NE(s, removed_idx) << "Column is not in the score map";
447 scores_[s].score = ComputeScore(reduced_costs_[j], covering_counts_[j]);
448
449 if (covering_counts_[j] == 0) {
450 // Column is redundant: its score can be removed
451 if (s < bad_size_) {
452 // Column is bad: promote to good partition before removal
453 SwapScores(s, bad_size_ - 1);
454 s = --bad_size_;
455 }
456 SwapScores(s, scores_.size() - 1);
457 scores_.pop_back();
458 } else if (s >= bad_size_ && scores_[s].score > worst_good_score_) {
459 // Column not good anymore: move it into bad partition
460 SwapScores(s, bad_size_++);
461 }
462 }
463 }
464 return processed_rows_count;
465 }
466
467 private:
468 void SwapScores(BaseInt s1, BaseInt s2) {
469 SubsetIndex j1 = scores_[s1].idx, j2 = scores_[s2].idx;
470 std::swap(scores_[s1], scores_[s2]);
471 std::swap(score_map_[j1], score_map_[j2]);
472 }
473
474 // Score computed as described in [1]
475 static Cost ComputeScore(Cost adjusted_reduced_cost,
476 BaseInt num_rows_covered) {
477 DCHECK(std::isfinite(adjusted_reduced_cost)) << "Gamma is not finite";
478 return num_rows_covered == 0
479 ? max_score
480 : (adjusted_reduced_cost > .0
481 ? adjusted_reduced_cost / num_rows_covered
482 : adjusted_reduced_cost * num_rows_covered);
483 }
484
485 private:
486 // scores_ is partitioned into bad-scores / good-scores
487 BaseInt bad_size_;
488
489 // sentinel level to trigger a partition update of the scores
490 Cost worst_good_score_;
491
492 // column scores kept updated
493 std::vector<Score> scores_;
494
495 // reduced costs adjusted to currently uncovered rows (size=n)
496 SubsetCostVector reduced_costs_;
497
498 // number of uncovered rows covered by each column (size=n)
499 SubsetToIntVector covering_counts_;
500
501 // position of each column score into the scores_
502 SubsetToIntVector score_map_;
503};
504
505// Stores the redundancy set and related information
506class RedundancyRemover {
507 public:
508 RedundancyRemover(const SubModel& model, CoverCounters& total_coverage)
509 : redund_set_(),
510 total_coverage_(total_coverage),
511 partial_coverage_(model.num_elements()),
512 partial_cost_(.0),
513 partial_size_(0),
514 partial_cov_count_(0),
515 cols_to_remove_() {}
516
517 Cost TryRemoveRedundantCols(const SubModel& model, Cost cost_cutoff,
518 std::vector<SubsetIndex>& sol_subsets) {
519 for (SubsetIndex j : sol_subsets) {
520 if (total_coverage_.IsRedundantUncover(model.columns()[j]))
521 redund_set_.push_back({model.subset_costs()[j], j});
522 else {
523 ++partial_size_;
524 partial_cost_ += model.subset_costs()[j];
525 partial_cov_count_ += partial_coverage_.Cover(model.columns()[j]);
526 }
527 if (partial_cost_ >= cost_cutoff) {
528 return partial_cost_;
529 }
530 }
531 if (redund_set_.empty()) {
532 return partial_cost_;
533 }
534 absl::c_sort(redund_set_,
535 [](Score a, Score b) { return a.score < b.score; });
536
537 if (partial_cov_count_ < model.num_focus_elements()) {
538 // Complete partial solution heuristically
539 HeuristicRedundancyRemoval(model, cost_cutoff);
540 } else {
541 // All redundant columns can be removed
542 for (Score redund_col : redund_set_) {
543 cols_to_remove_.push_back(redund_col.idx);
544 }
545 }
546
547 // Note: In [1], an enumeration to selected the best redundant columns to
548 // remove is performed when the number of redundant columns is <= 10.
549 // However, based on experiments with github.com/c4v4/cft/, it appears
550 // that this enumeration does not provide significant benefits to justify
551 // the added complexity.
552
553 if (partial_cost_ < cost_cutoff) {
554 gtl::STLEraseAllFromSequenceIf(&sol_subsets, [&](auto j) {
555 return absl::c_any_of(cols_to_remove_,
556 [j](auto j_r) { return j_r == j; });
557 });
558 }
559 return partial_cost_;
560 }
561
562 private:
563 // Remove redundant columns from the redundancy set using a heuristic
564 void HeuristicRedundancyRemoval(const SubModel& model, Cost cost_cutoff) {
565 while (!redund_set_.empty()) {
566 if (partial_cov_count_ == model.num_focus_elements()) {
567 return;
568 }
569 SubsetIndex j = redund_set_.back().idx;
570 const auto& j_col = model.columns()[j];
571 redund_set_.pop_back();
572
573 if (total_coverage_.IsRedundantUncover(j_col)) {
574 total_coverage_.Uncover(j_col);
575 cols_to_remove_.push_back(j);
576 } else {
577 partial_cost_ += model.subset_costs()[j];
578 partial_cov_count_ += partial_coverage_.Cover(j_col);
579 }
580 }
581 }
582
583 private:
584 // redundant columns + their cost
585 std::vector<Score> redund_set_;
586
587 // row-cov if all the remaining columns are selected
588 CoverCounters total_coverage_;
589
590 // row-cov if we selected the current column
591 CoverCounters partial_coverage_;
592
593 // current partial solution cost
594 Cost partial_cost_;
595
596 // current partial solution size
597 BaseInt partial_size_;
598
599 // number of covered rows
600 Cost partial_cov_count_;
601
602 // list of columns to remove
603 std::vector<SubsetIndex> cols_to_remove_;
604};
605
606} // namespace
607
608Solution RunMultiplierBasedGreedy(const SubModel& model,
609 const DualState& dual_state,
610 Cost cost_cutoff) {
611 std::vector<SubsetIndex> sol_subsets;
612 CoverGreedly(model, dual_state, cost_cutoff,
613 std::numeric_limits<BaseInt>::max(), sol_subsets);
614 return Solution(model, sol_subsets);
615}
616
617Cost CoverGreedly(const SubModel& model, const DualState& dual_state,
618 Cost cost_cutoff, BaseInt stop_size,
619 std::vector<SubsetIndex>& sol_subsets) {
620 CFT_MEASURE_SCOPE_DURATION(greedy_time);
621
622 Cost sol_cost = .0;
623 for (SubsetIndex j : sol_subsets) {
624 sol_cost += model.subset_costs()[j];
625 }
626 if (sol_cost >= cost_cutoff) {
627 sol_subsets.clear();
628 return std::numeric_limits<Cost>::max();
629 }
630 if (sol_subsets.size() >= stop_size) {
631 // Solution already has required size -> early exit
632 return sol_cost;
633 }
634
635 // Process input solution (if not empty)
636 BaseInt num_rows_to_cover = model.num_focus_elements();
637 CoverCounters covered_rows(model.num_elements());
638 for (SubsetIndex j : sol_subsets) {
639 num_rows_to_cover -= covered_rows.Cover(model.columns()[j]);
640 if (num_rows_to_cover == 0) {
641 return sol_cost;
642 }
643 }
644
645 // Initialize column scores taking into account rows already covered
646 GreedyScores scores(model, dual_state); // TODO(?): cache it!
647 if (!sol_subsets.empty()) {
648 scores.UpdateColumnsScoreOfRowsIf(
649 model.rows(), dual_state.multipliers(), model.ElementRange(),
650 [&](auto i) { return covered_rows[i] > 0; });
651 }
652
653 // Fill up partial solution
654 while (num_rows_to_cover > 0 && sol_subsets.size() < stop_size) {
655 SubsetIndex j_star = scores.FindMinScoreColumn(model, dual_state);
656 const auto& column = model.columns()[j_star];
657 num_rows_to_cover -= scores.UpdateColumnsScoreOfRowsIf(
658 model.rows(), dual_state.multipliers(), column,
659 [&](auto i) { return covered_rows[i] == 0; });
660 sol_subsets.push_back(j_star);
661 covered_rows.Cover(column);
662 }
663
664 // Either remove redundant columns or discard solution
665 RedundancyRemover remover(model, covered_rows); // TODO(?): cache it!
666
667 return remover.TryRemoveRedundantCols(model, cost_cutoff, sol_subsets);
668}
669
673namespace {
674
675DualState MakeTentativeDualState(const SubModel& model) {
676 DualState tentative_dual_state(model);
677 tentative_dual_state.DualUpdate(
678 model, [&](ElementIndex i, Cost& i_multiplier) {
679 i_multiplier = std::numeric_limits<Cost>::max();
680 for (SubsetIndex j : model.rows()[i]) {
681 Cost candidate = model.subset_costs()[j] / model.column_size(j);
682 i_multiplier = std::min(i_multiplier, candidate);
683 }
684 });
685 return tentative_dual_state;
686}
687
688void FixBestColumns(SubModel& model, PrimalDualState& state) {
689 auto& [best_sol, dual_state] = state;
690
691 // This approach is not the most efficient but prioritizes clarity and the
692 // current abstraction system. We save the current core multipliers, mapped to
693 // the full model's element indices. By organizing the multipliers using the
694 // full model indices, we can easily map them to the new core model indices
695 // after fixing columns. Note: This mapping isn't strictly necessary because
696 // fixing columns effectively removes rows, and the remaining multipliers
697 // naturally shift to earlier positions. A simple iteration would suffice to
698 // discard multipliers for rows no longer in the new core model.
699 FullElementCostVector full_multipliers(model.num_elements(), .0);
700 for (ElementIndex core_i : model.ElementRange()) {
701 FullElementIndex full_i = model.MapCoreToFullElementIndex(core_i);
702 full_multipliers[full_i] = dual_state.multipliers()[core_i];
703 }
704
705 std::vector<SubsetIndex> cols_to_fix;
706 CoverCounters row_coverage(model.num_elements());
707 for (SubsetIndex j : model.SubsetRange()) {
708 if (dual_state.reduced_costs()[j] < -0.001) {
709 cols_to_fix.push_back(j);
710 row_coverage.Cover(model.columns()[j]);
711 }
712 }
713
714 // Remove columns that overlap between each other
715 gtl::STLEraseAllFromSequenceIf(&cols_to_fix, [&](SubsetIndex j) {
716 return absl::c_any_of(model.columns()[j],
717 [&](ElementIndex i) { return row_coverage[i] > 1; });
718 });
719
720 // Ensure at least a minimum number of columns are fixed
721 BaseInt fix_at_least =
722 cols_to_fix.size() + std::max<BaseInt>(1, model.num_elements() / 200);
723 CoverGreedly(model, state.dual_state, std::numeric_limits<Cost>::max(),
724 fix_at_least, cols_to_fix);
725
726 // Fix columns and update the model
727 Cost fixed_cost_delta = model.FixMoreColumns(cols_to_fix);
728
729 VLOG(3) << "[3FIX] Fixed " << cols_to_fix.size()
730 << " new columns with cost: " << fixed_cost_delta;
731 VLOG(3) << "[3FIX] Globally fixed " << model.fixed_columns().size()
732 << " columns, with cost " << model.fixed_cost();
733
734 // Update multipliers for the reduced model
735 dual_state.DualUpdate(model, [&](ElementIndex core_i, Cost& multiplier) {
736 // Note: if SubModelView is used as CoreModel, then this mapping is always
737 // the identity mapping and can be removed
738 multiplier = full_multipliers[model.MapCoreToFullElementIndex(core_i)];
739 });
740}
741
742void RandomizeDualState(const SubModel& model, DualState& dual_state,
743 std::mt19937& rnd) {
744 // In [1] this step is described, not completely sure if it actually helps
745 // or not. Seems to me one of those "throw in some randomness, it never hurts"
746 // thing.
747 dual_state.DualUpdate(model, [&](ElementIndex, Cost& i_multiplier) {
748 i_multiplier *= absl::Uniform(rnd, 0.9, 1.1);
749 });
750}
751} // namespace
752
753void HeuristicCBs::RunHeuristic(const SubgradientContext& context,
754 Solution& solution) {
756 context.model, context.current_dual_state,
757 context.best_solution.cost() - context.model.fixed_cost());
758}
759
760void HeuristicCBs::ComputeMultipliersDelta(const SubgradientContext& context,
761 ElementCostVector& delta_mults) {
762 Cost squared_norm = .0;
763 for (ElementIndex i : context.model.ElementRange()) {
764 squared_norm += context.subgradient[i] * context.subgradient[i];
765 }
766
767 Cost lower_bound = context.current_dual_state.lower_bound();
768 Cost upper_bound = context.best_solution.cost() - context.model.fixed_cost();
769 DCHECK_GE(upper_bound, lower_bound);
770 Cost delta = upper_bound - lower_bound;
771 Cost step_constant = step_size_ * delta / squared_norm;
772 for (ElementIndex i : context.model.ElementRange()) {
773 delta_mults[i] = step_constant * context.subgradient[i];
774 }
775}
776
777PrimalDualState RunThreePhase(SubModel& model, const Solution& init_solution) {
778 CFT_MEASURE_SCOPE_DURATION(three_phase_time);
779 DCHECK(ValidateSubModel(model));
780
781 PrimalDualState best_state = {.solution = init_solution,
782 .dual_state = MakeTentativeDualState(model)};
783 if (best_state.solution.Empty()) {
784 best_state.solution =
785 RunMultiplierBasedGreedy(model, best_state.dual_state);
786 }
787 VLOG(2) << "[3PHS] Initial lower bound: "
788 << best_state.dual_state.lower_bound()
789 << ", Initial solution cost: " << best_state.solution.cost()
790 << ", Starting 3-phase algorithm";
791
792 PrimalDualState curr_state = best_state;
793 BaseInt iter_count = 0;
794 std::mt19937 rnd(0xcf7);
795 while (model.num_focus_elements() > 0) {
796 ++iter_count;
797 VLOG(2) << "[3PHS] 3Phase iteration: " << iter_count;
798 VLOG(2) << "[3PHS] Active size: rows " << model.num_focus_elements() << "/"
799 << model.num_elements() << ", columns " << model.num_focus_subsets()
800 << "/" << model.num_subsets();
801
802 // Phase 1: refine the current dual_state and model
803 BoundCBs dual_bound_cbs(model);
804 VLOG(2) << "[3PHS] Subgradient Phase:";
805 SubgradientOptimization(model, dual_bound_cbs, curr_state);
806 if (iter_count == 1) {
807 best_state.dual_state = curr_state.dual_state;
808 }
809 if (curr_state.dual_state.lower_bound() >=
810 best_state.solution.cost() - model.fixed_cost() - CFT_BOUND_EPSILON) {
811 break;
812 }
813
814 // Phase 2: search for good solutions
815 HeuristicCBs heuristic_cbs;
816 heuristic_cbs.set_step_size(dual_bound_cbs.step_size());
817 VLOG(2) << "[3PHS] Heuristic Phase:";
818 SubgradientOptimization(model, heuristic_cbs, curr_state);
819 if (iter_count == 1 && best_state.dual_state.lower_bound() <
820 curr_state.dual_state.lower_bound()) {
821 best_state.dual_state = curr_state.dual_state;
822 }
823 if (curr_state.solution.cost() < best_state.solution.cost()) {
824 best_state.solution = curr_state.solution;
825 }
826 if (curr_state.dual_state.lower_bound() >=
827 best_state.solution.cost() - model.fixed_cost() - CFT_BOUND_EPSILON) {
828 break;
829 }
830
831 VLOG(2) << "[3PHS] 3Phase Bounds: Residual Lower "
832 << curr_state.dual_state.lower_bound() + model.fixed_cost()
833 << ", Upper " << best_state.solution.cost();
834
835 // Phase 3: Fix the best columns (diving)
836 FixBestColumns(model, curr_state);
837 RandomizeDualState(model, curr_state.dual_state, rnd);
838 }
839
840 VLOG(2) << "[3PHS] 3Phase End: ";
841 VLOG(2) << "[3PHS] Bounds: Residual Lower "
842 << curr_state.dual_state.lower_bound() + model.fixed_cost()
843 << ", Upper " << best_state.solution.cost();
844
845 return best_state;
846}
847
851
852namespace {
853
854struct GapContribution {
855 Cost gap;
856 FullSubsetIndex idx;
857};
858
859std::vector<FullSubsetIndex> SelectColumnByGapContribution(
860 const SubModel& model, const PrimalDualState& best_state,
861 BaseInt nrows_to_fix) {
862 const auto& [solution, dual_state] = best_state;
863
864 FullCoverCounters row_coverage(model.num_elements());
865 auto full_model = model.StrongTypedFullModelView();
866
867 for (FullSubsetIndex j : solution.subsets()) {
868 row_coverage.Cover(full_model.columns()[j]);
869 }
870
871 std::vector<GapContribution> gap_contributions;
872 for (FullSubsetIndex j : solution.subsets()) {
873 Cost j_gap = .0;
874 Cost reduced_cost = dual_state.reduced_costs()[static_cast<SubsetIndex>(j)];
875 for (FullElementIndex i : full_model.columns()[j]) {
876 Cost i_mult = dual_state.multipliers()[static_cast<ElementIndex>(i)];
877 j_gap += i_mult * (1.0 - 1.0 / row_coverage[i]);
878 reduced_cost -= i_mult;
879 }
880 j_gap += std::max<Cost>(reduced_cost, 0.0);
881 gap_contributions.push_back({j_gap, j});
882 }
883 absl::c_sort(gap_contributions, [](GapContribution g1, GapContribution g2) {
884 return g1.gap < g2.gap;
885 });
886
887 BaseInt covered_rows = 0;
888 row_coverage.Reset(model.num_elements());
889 std::vector<FullSubsetIndex> cols_to_fix;
890 for (auto [j_gap, j] : gap_contributions) {
891 covered_rows += row_coverage.Cover(full_model.columns()[j]);
892 if (covered_rows > nrows_to_fix) {
893 break;
894 }
895 cols_to_fix.push_back(static_cast<FullSubsetIndex>(j));
896 }
897 return cols_to_fix;
898}
899} // namespace
900
901PrimalDualState RunCftHeuristic(SubModel& model,
902 const Solution& init_solution) {
903 CFT_MEASURE_SCOPE_DURATION(refinement_time);
904
905 PrimalDualState best_state = {.solution = init_solution,
906 .dual_state = MakeTentativeDualState(model)};
907 if (best_state.solution.Empty()) {
908 best_state.solution =
909 RunMultiplierBasedGreedy(model, best_state.dual_state);
910 }
912 Cost cost_cutoff = std::numeric_limits<Cost>::lowest();
913 double fix_minimum = .3; // Arbitrary from [1]
914 double fix_increment = 1.1; // Arbitrary from [1]
915 double fix_fraction = fix_minimum;
916
917 for (BaseInt iter_counter = 0; model.num_elements() > 0; ++iter_counter) {
918 VLOG(1) << "[CFTH] Refinement iteration: " << iter_counter;
919 Cost fixed_cost_before = model.fixed_cost();
920 auto [solution, dual_state] = RunThreePhase(model, best_state.solution);
921 if (iter_counter == 0) {
922 best_state.dual_state = std::move(dual_state);
923 }
924 if (solution.cost() < best_state.solution.cost()) {
925 best_state.solution = solution;
926 fix_fraction = fix_minimum;
927 }
928 cost_cutoff = std::max<Cost>(cost_cutoff,
929 fixed_cost_before + dual_state.lower_bound());
930 VLOG(1) << "[CFTH] Refinement Bounds: Residual Lower " << cost_cutoff
931 << ", Real Lower " << best_state.dual_state.lower_bound()
932 << ", Upper " << best_state.solution.cost();
933 if (best_state.solution.cost() - CFT_BOUND_EPSILON <= cost_cutoff) {
934 break;
935 }
936
937 fix_fraction = std::min(1.0, fix_fraction * fix_increment);
938 std::vector<FullSubsetIndex> cols_to_fix = SelectColumnByGapContribution(
939 model, best_state, model.num_elements() * fix_fraction);
940
941 if (!cols_to_fix.empty()) {
942 model.ResetColumnFixing(cols_to_fix, best_state.dual_state);
943 }
944 VLOG(1) << "[CFTH] Fixed " << cols_to_fix.size()
945 << " new columns with cost: " << model.fixed_cost();
946 VLOG(1) << "[CFTH] Model sizes: rows " << model.num_focus_elements() << "/"
947 << model.num_elements() << ", columns " << model.num_focus_subsets()
948 << "/" << model.num_subsets();
949 }
950
951#ifdef CFT_MEASURE_TIME
952 double subg_t = subgradient_time.Get();
953 double greedy_t = greedy_time.Get();
954 double three_phase_t = three_phase_time.Get();
955 double refinement_t = refinement_time.Get();
956
957 printf("Subgradient time: %8.2f (%.1f%%)\n", subg_t,
958 100 * subg_t / refinement_t);
959 printf("Greedy Heur time: %8.2f (%.1f%%)\n", greedy_t,
960 100 * greedy_t / refinement_t);
961 printf("SubG - Greedy time: %8.2f (%.1f%%)\n", subg_t - greedy_t,
962 100 * (subg_t - greedy_t) / refinement_t);
963 printf("3Phase time: %8.2f (%.1f%%)\n", three_phase_t,
964 100 * three_phase_t / refinement_t);
965 printf("3Phase - Subg time: %8.2f (%.1f%%)\n", three_phase_t - subg_t,
966 100 * (three_phase_t - subg_t) / refinement_t);
967 printf("Total CFT time: %8.2f (%.1f%%)\n", refinement_t, 100.0);
968#endif
969
970 return best_state;
971}
972
976
977namespace {
978std::vector<FullSubsetIndex> ComputeTentativeFocus(StrongModelView full_model) {
979 std::vector<FullSubsetIndex> columns_focus;
980
981 if (full_model.num_subsets() <= 2 * kMinCov * full_model.num_elements()) {
982 columns_focus.resize(full_model.num_subsets());
983 absl::c_iota(columns_focus, FullSubsetIndex(0));
984 return columns_focus;
985 }
986
987 columns_focus.reserve(full_model.num_elements() * kMinCov);
988 FullSubsetBoolVector selected(full_model.num_subsets(), false);
989
990 // Select the first min_row_coverage columns for each row
991 for (const auto& row : full_model.rows()) {
992 BaseInt countdown = kMinCov;
993 for (FullSubsetIndex j : row) {
994 if (--countdown <= 0) {
995 break;
996 }
997 if (!selected[j]) {
998 selected[j] = true;
999 columns_focus.push_back(j);
1000 }
1001 }
1002 }
1003 // NOTE: unnecessary, but it keeps equivalence between SubModelView/SubModel
1004 absl::c_sort(columns_focus);
1005 return columns_focus;
1006}
1007} // namespace
1008
1009const std::vector<FullSubsetIndex>&
1010FullToCoreModel::ColumnSelector::ComputeNewSelection(
1011 FilterModelView full_model,
1012 const std::vector<FullSubsetIndex>& forced_columns,
1013 const SubsetCostVector& reduced_costs) {
1014 selected_.assign(full_model.num_subsets(), false);
1015 row_cover_counts_.assign(full_model.num_elements(), 0);
1016 rows_left_to_cover_ = full_model.num_focus_elements();
1017 selection_.clear();
1018 candidates_.clear();
1019
1020 // Always retain best solution in the core model (if possible)
1021 for (FullSubsetIndex full_j : forced_columns) {
1022 SubsetIndex j = static_cast<SubsetIndex>(full_j);
1023 if (full_model.IsFocusCol(j)) {
1024 SelectColumn(full_model, j);
1025 }
1026 }
1027
1028 auto subset_range = full_model.SubsetRange();
1029 candidates_ = {subset_range.begin(), subset_range.end()};
1030 absl::c_sort(candidates_, [&](auto j1, auto j2) {
1031 return reduced_costs[j1] < reduced_costs[j2];
1032 });
1033 first_unselected_ = candidates_.begin();
1034
1035 SelecteMinRedCostColumns(full_model, reduced_costs);
1036 SelectMinRedCostByRow(full_model, reduced_costs);
1037 absl::c_sort(selection_);
1038 return selection_;
1039}
1040
1041bool FullToCoreModel::ColumnSelector::SelectColumn(FilterModelView full_model,
1042 SubsetIndex j) {
1043 if (selected_[j]) {
1044 return false;
1045 }
1046 for (ElementIndex i : full_model.columns()[j]) {
1047 selected_[j] = true; // Detect empty columns
1048 if (++row_cover_counts_[i] == kMinCov) {
1049 --rows_left_to_cover_;
1050 }
1051 }
1052 if (selected_[j]) { // Skip empty comlumns
1053 selection_.push_back(static_cast<FullSubsetIndex>(j));
1054 }
1055 return selected_[j];
1056}
1057
1058void FullToCoreModel::ColumnSelector::SelecteMinRedCostColumns(
1059 FilterModelView full_model, const SubsetCostVector& reduced_costs) {
1060 BaseInt selected_size = 0;
1061 BaseInt max_size = kMinCov * full_model.num_elements();
1062 auto it = first_unselected_;
1063 while (it != candidates_.end() && reduced_costs[*it] < 0.1 &&
1064 selected_size < max_size) {
1065 selected_size += SelectColumn(full_model, *it++) ? 1 : 0;
1066 }
1067 first_unselected_ = it;
1068}
1069
1070void FullToCoreModel::ColumnSelector::SelectMinRedCostByRow(
1071 FilterModelView full_model, const SubsetCostVector& reduced_costs) {
1072 auto it = first_unselected_;
1073 while (it != candidates_.end() && rows_left_to_cover_ > 0) {
1074 SubsetIndex j = *it++;
1075 if (selected_[j]) {
1076 continue;
1077 }
1078 for (ElementIndex i : full_model.columns()[j]) {
1079 ++row_cover_counts_[i];
1080 rows_left_to_cover_ += (row_cover_counts_[i] == kMinCov ? 1 : 0);
1081 selected_[j] = selected_[j] || (row_cover_counts_[i] <= kMinCov);
1082 }
1083 if (selected_[j]) {
1084 selection_.push_back(static_cast<FullSubsetIndex>(j));
1085 }
1086 }
1087}
1088
1089FullToCoreModel::FullToCoreModel(const Model* full_model)
1090 : SubModel(full_model, ComputeTentativeFocus(StrongModelView(full_model))),
1091 full_model_(full_model),
1092 is_focus_col_(full_model->num_subsets(), true),
1093 is_focus_row_(full_model->num_elements(), true),
1094 prev_best_lower_bound_(std::numeric_limits<Cost>::lowest()),
1095 full_dual_state_(*full_model),
1096 best_dual_state_(full_dual_state_) {
1097 ResetPricingPeriod();
1098 DCHECK(ValidateSubModel(*this));
1099 DCHECK(FullToSubModelInvariantCheck());
1100}
1103 update_countdown_ = 10;
1104 update_period_ = 10;
1105 update_max_period_ = std::min<BaseInt>(1000, full_model_->num_elements() / 3);
1106}
1107
1109 const std::vector<SubsetIndex>& columns_to_fix) {
1110 StrongModelView typed_full_model = StrongTypedFullModelView();
1111 for (SubsetIndex core_j : columns_to_fix) {
1112 FullSubsetIndex full_j = SubModel::MapCoreToFullSubsetIndex(core_j);
1113 DCHECK(IsFocusCol(full_j));
1114 IsFocusCol(full_j) = false;
1115 bool any_active = false;
1116 for (FullElementIndex full_i : typed_full_model.columns()[full_j]) {
1117 any_active |= IsFocusRow(full_i);
1118 IsFocusRow(full_i) = false;
1119 }
1120 DCHECK(any_active);
1121 }
1122 for (FullSubsetIndex full_j : typed_full_model.SubsetRange()) {
1123 if (!IsFocusCol(full_j)) {
1124 continue;
1125 }
1126 IsFocusCol(full_j) = false;
1127 for (FullElementIndex full_i : typed_full_model.columns()[full_j]) {
1128 if (IsFocusRow(full_i)) {
1129 IsFocusCol(full_j) = true;
1130 break;
1131 }
1132 }
1133 }
1134
1135 ResetPricingPeriod();
1136 Cost fixed_cost = base::FixMoreColumns(columns_to_fix);
1137 DCHECK(FullToSubModelInvariantCheck());
1138 return fixed_cost;
1139}
1140
1141void FullToCoreModel::ResetColumnFixing(
1142 const std::vector<FullSubsetIndex>& full_columns_to_fix,
1143 const DualState& dual_state) {
1144 is_focus_col_.assign(full_model_->num_subsets(), true);
1145 is_focus_row_.assign(full_model_->num_elements(), true);
1146
1147 full_dual_state_ = dual_state;
1148
1149 // We could implement and in-place core-model update that removes old fixings,
1150 // set the new one while also updating the column focus. This solution is much
1151 // simpler. It just create a new core-model object from scratch and then uses
1152 // the existing interface.
1153 const auto& selection = col_selector_.ComputeNewSelection(
1154 FixingFullModelView(), full_columns_to_fix,
1155 full_dual_state_.reduced_costs());
1156 static_cast<SubModel&>(*this) = SubModel(full_model_, selection);
1157
1158 // TODO(anyone): Improve this. It's Inefficient but hardly a botleneck and it
1159 // also avoid storing a full->core column map.
1160 std::vector<SubsetIndex> columns_to_fix;
1161 for (SubsetIndex core_j : SubsetRange()) {
1162 for (FullSubsetIndex full_j : full_columns_to_fix) {
1163 if (full_j == MapCoreToFullSubsetIndex(core_j)) {
1164 columns_to_fix.push_back(core_j);
1165 break;
1166 }
1167 }
1168 }
1169 DCHECK_EQ(columns_to_fix.size(), full_columns_to_fix.size());
1170 FixMoreColumns(columns_to_fix);
1171 DCHECK(FullToSubModelInvariantCheck());
1172}
1173
1174void FullToCoreModel::SizeUpdate() {
1175 is_focus_col_.resize(full_model_->num_subsets(), true);
1176}
1177
1178bool FullToCoreModel::IsTimeToUpdate(Cost best_lower_bound, bool force) {
1179 if (!force && --update_countdown_ > 0) {
1180 return false;
1181 }
1182 if (best_lower_bound == prev_best_lower_bound_) {
1183 return false;
1184 }
1185 prev_best_lower_bound_ = best_lower_bound;
1186 return true;
1187}
1188
1189void FullToCoreModel::ComputeAndSetFocus(Cost best_lower_bound,
1190 const Solution& best_solution) {
1191 const auto& selection = col_selector_.ComputeNewSelection(
1192 FixingFullModelView(), best_solution.subsets(),
1193 full_dual_state_.reduced_costs());
1194 base::SetFocus(selection);
1195 UpdatePricingPeriod(full_dual_state_, best_lower_bound,
1196 best_solution.cost() - fixed_cost());
1197 VLOG(3) << "[F2CU] Core-update: Lower bounds: real "
1198 << full_dual_state_.lower_bound() << ", core " << best_lower_bound
1199 << ", core size: " << num_focus_elements() << "x"
1200 << num_focus_subsets();
1202
1203bool FullToCoreModel::UpdateCore(Cost best_lower_bound,
1204 const ElementCostVector& best_multipliers,
1205 const Solution& best_solution, bool force) {
1206 if (!IsTimeToUpdate(best_lower_bound, force)) {
1207 return false;
1208 }
1209 UpdateMultipliers(best_multipliers);
1210 ComputeAndSetFocus(best_lower_bound, best_solution);
1211 DCHECK(FullToSubModelInvariantCheck());
1212 return true;
1213}
1214
1216 Cost core_lower_bound,
1217 Cost core_upper_bound) {
1218 DCHECK_GE(core_lower_bound + 1e-6, full_dual_state.lower_bound());
1219 DCHECK_GE(core_upper_bound, .0);
1220
1221 Cost delta = core_lower_bound - full_dual_state.lower_bound();
1222 Cost ratio = DivideIfGE0(delta, core_upper_bound);
1223 if (ratio <= 1e-6) {
1224 update_period_ = std::min<BaseInt>(update_max_period_, 10 * update_period_);
1225 } else if (ratio <= 0.02) {
1226 update_period_ = std::min<BaseInt>(update_max_period_, 5 * update_period_);
1227 } else if (ratio <= 0.2) {
1228 update_period_ = std::min<BaseInt>(update_max_period_, 2 * update_period_);
1229 } else {
1230 update_period_ = 10;
1231 }
1232 update_countdown_ = update_period_;
1233}
1234
1235Cost FullToCoreModel::UpdateMultipliers(
1236 const ElementCostVector& core_multipliers) {
1237 auto fixing_full_model = FixingFullModelView();
1238 full_dual_state_.DualUpdate(
1239 fixing_full_model, [&](ElementIndex full_i, Cost& i_mult) {
1240 ElementIndex core_i =
1241 MapFullToCoreElementIndex(static_cast<FullElementIndex>(full_i));
1242 i_mult = core_multipliers[core_i];
1243 });
1244
1245 // Here, we simply check if any columns have been fixed, and only update the
1246 // best dual state when no fixing is in place.
1248 // Mapping a "local" dual state to a global one is possible. This would
1249 // involve keeping the multipliers for non-focused elements fixed, updating
1250 // the multipliers for focused elements, and then computing the dual state for
1251 // the entire model. However, this approach is not implemented here. Such a
1252 // strategy is unlikely to improve the best dual state unless the focus is
1253 // *always* limited to a small subset of elements (and therefore the LB sucks
1254 // and it is easy to improve) and the CFT is applied exclusively within that
1255 // narrow scope, but this falls outside the current scope of this project.
1256 if (fixed_columns().empty() &&
1257 full_dual_state_.lower_bound() > best_dual_state_.lower_bound()) {
1258 best_dual_state_ = full_dual_state_;
1259 }
1260 return full_dual_state_.lower_bound();
1261}
1262
1263bool FullToCoreModel::FullToSubModelInvariantCheck() {
1264 const SubModel& sub_model = *this;
1265 StrongModelView typed_full_model = StrongTypedFullModelView();
1266
1267 if (typed_full_model.num_subsets() < sub_model.num_subsets()) {
1268 std::cerr << absl::StrCat("SubModelView has ", sub_model.num_subsets(),
1269 " subsets, but the full model has ",
1270 typed_full_model.num_subsets(), " subsets.\n");
1271 return false;
1272 }
1273 if (typed_full_model.num_elements() != sub_model.num_elements()) {
1274 std::cerr << absl::StrCat("SubModelView has ", sub_model.num_elements(),
1275 " elements, but the full model has ",
1276 typed_full_model.num_elements(), " elements.\n");
1277 return false;
1278 }
1279 for (SubsetIndex core_j : sub_model.SubsetRange()) {
1280 FullSubsetIndex full_j = sub_model.MapCoreToFullSubsetIndex(core_j);
1281 if (!is_focus_col_[static_cast<SubsetIndex>(full_j)]) {
1282 std::cerr << absl::StrCat("Subset ", core_j,
1283 " in sub-model but its mapped subset ", full_j,
1284 " not found in full model view.\n");
1285 return false;
1286 }
1287
1288 const auto& core_column = sub_model.columns()[core_j];
1289 if (core_column.begin() == core_column.end()) {
1290 std::cerr << absl::StrCat("Core subset ", core_j, " empty.\n");
1291 return false;
1292 }
1293
1294 const auto& full_column = typed_full_model.columns()[full_j];
1295 if (full_column.begin() == full_column.end()) {
1296 std::cerr << absl::StrCat("Full subset ", full_j, " empty.\n");
1297 return false;
1298 }
1299
1300 // Assumes corresponding elements have the same order in both models
1301 auto core_it = core_column.begin();
1302 for (FullElementIndex full_i : typed_full_model.columns()[full_j]) {
1303 if (sub_model.MapFullToCoreElementIndex(full_i) != *core_it) {
1304 continue;
1305 }
1306 if (sub_model.MapCoreToFullElementIndex(*core_it) != full_i) {
1307 std::cerr << absl::StrCat(
1308 "Subset ", core_j, " in sub-model has mapped element ", *core_it,
1309 " but it is not the same as the full model.\n");
1310 return false;
1311 }
1312 if (++core_it == core_column.end()) {
1313 break;
1314 }
1315 }
1316 if (core_it != core_column.end()) {
1317 std::cerr << absl::StrCat("Subset ", core_j,
1318 " in sub-model has no mapped element ",
1319 *core_it, " in full model view.\n");
1320 return false;
1321 }
1322 }
1323
1324 for (ElementIndex core_i : sub_model.ElementRange()) {
1325 const auto& core_row = sub_model.rows()[core_i];
1326 if (core_row.begin() == core_row.end()) {
1327 std::cerr << absl::StrCat("Core row ", core_i, " empty.\n");
1328 return false;
1329 }
1330
1331 FullElementIndex full_i = sub_model.MapCoreToFullElementIndex(core_i);
1332 if (!is_focus_row_[static_cast<ElementIndex>(full_i)]) {
1333 std::cerr << absl::StrCat("Element ", core_i,
1334 " in sub-model but its mapped element ", full_i,
1335 " not found in full model view.\n");
1336 return false;
1337 }
1338 }
1339
1340 for (FullElementIndex full_i : typed_full_model.ElementRange()) {
1341 if (!IsFocusRow(full_i)) {
1342 continue;
1343 }
1344 ElementIndex core_i = sub_model.MapFullToCoreElementIndex(full_i);
1345 if (core_i < ElementIndex() ||
1346 ElementIndex(sub_model.num_elements()) < core_i) {
1347 std::cerr << absl::StrCat(
1348 "Element ", full_i,
1349 " in full model view but has no mapped element in sub-model.\n");
1350 return false;
1351 }
1352 }
1353 return true;
1354}
1355
1356} // namespace operations_research::scp
double Get() const
Definition timer.h:44
auto SubsetRange() const -> util_intops::StrongIntRange< FullSubsetIndex >
auto ElementRange() const -> util_intops::StrongIntRange< FullElementIndex >
auto columns() const -> util_intops::TransformView< SparseColumn, FullSubsetIndex, SparseColTransform >
void ComputeMultipliersDelta(const SubgradientContext &context, ElementCostVector &delta_mults) override
bool ExitCondition(const SubgradientContext &context) override
bool UpdateCoreModel(SubgradientContext context, CoreModel &core_model, bool force=false) override
void SetFocus(const std::vector< FullSubsetIndex > &columns_focus)
const std::vector< FullSubsetIndex > & fixed_columns() const
List of fixed columns.
FullSubsetIndex MapCoreToFullSubsetIndex(SubsetIndex core_j) const
Cost fixed_cost() const
Current fixed cost: sum of the cost of the fixed columns.
void DualUpdate(const SubModelT &model, Op multiplier_operator)
const SubsetCostVector & reduced_costs() const
const ElementCostVector & multipliers() const
void ComputeAndSetFocus(Cost best_lower_bound, const Solution &best_solution)
bool UpdateCore(Cost best_lower_bound, const ElementCostVector &best_multipliers, const Solution &best_solution, bool force) override
FilterModelView FixingFullModelView() const
Access the full model filtered by the current columns fixed.
void UpdatePricingPeriod(const DualState &full_dual_state, Cost core_lower_bound, Cost core_upper_bound)
Cost FixMoreColumns(const std::vector< SubsetIndex > &columns_to_fix) override
decltype(auto) IsFocusCol(FullSubsetIndex j)
decltype(auto) IsFocusRow(FullElementIndex i)
void AddSubset(FullSubsetIndex subset, Cost cost)
const std::vector< FullSubsetIndex > & subsets() const
void assign(size_type n, const value_type &val)
void STLEraseAllFromSequenceIf(T *v, P pred)
Remove each element e in v satisfying pred(e).
Definition stl_util.h:104
PrimalDualState RunThreePhase(SubModel &model, const Solution &init_solution)
PrimalDualState RunCftHeuristic(SubModel &model, const Solution &init_solution)
Cost CoverGreedly(const SubModel &model, const DualState &dual_state, Cost cost_cutoff, BaseInt stop_size, std::vector< SubsetIndex > &sol_subsets)
Cost DivideIfGE0(Cost numerator, Cost denominator)
Solution RunMultiplierBasedGreedy(const SubModel &model, const DualState &dual_state, Cost cost_cutoff)
bool ValidateSubModel(const SubModelT &model)
void SubgradientOptimization(SubModel &model, SubgradientCBs &cbs, PrimalDualState &best_state)
util_intops::StrongVector< SubsetIndex, Cost > SubsetCostVector
Definition base_types.h:58
Select next search node to expand Select next item_i to add this new search node to the search Generate a new search node where item_i is not in the knapsack Check validity of this new partial solution(using propagators) - If valid
util_intops::StrongVector< FullElementIndex, Cost > FullElementCostVector
util_intops::StrongVector< FullSubsetIndex, bool > FullSubsetBoolVector
util_intops::StrongVector< SubsetIndex, BaseInt > SubsetToIntVector
Definition base_types.h:65
util_intops::StrongIntRange< SubsetIndex > SubsetRange
Definition base_types.h:54
util_intops::StrongVector< ElementIndex, Cost > ElementCostVector
Definition base_types.h:59
double Cost
Basic non-strict type for cost. The speed penalty for using double is ~2%.
Definition base_types.h:32
STL namespace.
false true
Definition numbers.cc:223
#define CFT_BOUND_EPSILON
#define CFT_MEASURE_SCOPE_DURATION(Timer)
#define CFT_MAX_MULTIPLIER
const Cost & best_lower_bound
Avoid copying unused reduced cost during subgradient.