Files
ortools-clone/ortools/sat/circuit_test.cc
Mizux Seiha 4f381f6d07 backport from main:
* bump abseil to 20250814
* bump protobuf to v32.0
* cmake: add ccache auto support
* backport flatzinc, math_opt and sat update
2025-09-16 16:25:04 +02:00

338 lines
12 KiB
C++

// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/sat/circuit.h"
#include <algorithm>
#include <cstdint>
#include <functional>
#include <numeric>
#include <utility>
#include <vector>
#include "absl/log/check.h"
#include "absl/types/span.h"
#include "gtest/gtest.h"
#include "ortools/graph/strongly_connected_components.h"
#include "ortools/sat/integer.h"
#include "ortools/sat/integer_search.h"
#include "ortools/sat/model.h"
#include "ortools/sat/sat_base.h"
#include "ortools/sat/sat_solver.h"
namespace operations_research {
namespace sat {
namespace {
std::function<void(Model*)> DenseCircuitConstraint(
int num_nodes, bool allow_subcircuit,
bool allow_multiple_subcircuit_through_zero) {
return [=](Model* model) {
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
for (int tail = 0; tail < num_nodes; ++tail) {
for (int head = 0; head < num_nodes; ++head) {
if (!allow_subcircuit && tail == head) continue;
tails.push_back(tail);
heads.push_back(head);
literals.push_back(Literal(model->Add(NewBooleanVariable()), true));
}
}
LoadSubcircuitConstraint(num_nodes, tails, heads,
/*enforcement_literals=*/{}, literals, model,
allow_multiple_subcircuit_through_zero);
};
}
int CountSolutions(Model* model) {
int num_solutions = 0;
while (true) {
const SatSolver::Status status = SolveIntegerProblemWithLazyEncoding(model);
if (status != SatSolver::Status::FEASIBLE) break;
// Add the solution.
++num_solutions;
// Loop to the next solution.
model->Add(ExcludeCurrentSolutionAndBacktrack());
}
return num_solutions;
}
int Factorial(int n) { return n ? n * Factorial(n - 1) : 1; }
TEST(ReindexArcTest, BasicCase) {
const int num_nodes = 1000;
std::vector<int> tails(num_nodes);
std::vector<int> heads(num_nodes);
for (int i = 0; i < num_nodes; ++i) {
tails[i] = 100 * i;
heads[i] = 100 * i;
}
ReindexArcs(&tails, &heads);
for (int i = 0; i < num_nodes; ++i) {
EXPECT_EQ(i, tails[i]);
EXPECT_EQ(i, heads[i]);
}
}
TEST(ReindexArcTest, NegativeNumbering) {
const int num_nodes = 1000;
std::vector<int> tails(num_nodes);
std::vector<int> heads(num_nodes);
for (int i = 0; i < num_nodes; ++i) {
tails[i] = -100 * i;
heads[i] = -100 * i;
}
ReindexArcs(&tails, &heads);
for (int i = 0; i < num_nodes; ++i) {
EXPECT_EQ(i, tails[num_nodes - 1 - i]);
EXPECT_EQ(i, heads[num_nodes - 1 - i]);
}
}
TEST(CircuitConstraintTest, NodeWithNoArcsIsUnsat) {
static const int kNumNodes = 2;
Model model;
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
tails.push_back(0);
heads.push_back(1);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
LoadSubcircuitConstraint(kNumNodes, tails, heads, /*enforcement_literals=*/{},
literals, &model);
EXPECT_TRUE(model.GetOrCreate<SatSolver>()->ModelIsUnsat());
}
TEST(CircuitConstraintTest, AllCircuits) {
static const int kNumNodes = 4;
Model model;
model.Add(
DenseCircuitConstraint(kNumNodes, /*allow_subcircuit=*/false,
/*allow_multiple_subcircuit_through_zero=*/false));
const int num_solutions = CountSolutions(&model);
EXPECT_EQ(num_solutions, Factorial(kNumNodes - 1));
}
TEST(CircuitConstraintTest, AllSubCircuits) {
static const int kNumNodes = 4;
Model model;
model.Add(
DenseCircuitConstraint(kNumNodes, /*allow_subcircuit=*/true,
/*allow_multiple_subcircuit_through_zero=*/false));
const int num_solutions = CountSolutions(&model);
int expected = 1; // No circuit at all.
for (int circuit_size = 2; circuit_size <= kNumNodes; ++circuit_size) {
// The number of circuit of a given size is:
// - n for the first element
// - times (n-1) for the second
// - ...
// - times (n - (circuit_size - 1)) for the last.
// That is n! / (n - circuit_size)!, and like this we count circuit_size
// times the same circuit, so we have to divide by circuit_size in the end.
expected += Factorial(kNumNodes) /
(circuit_size * Factorial(kNumNodes - circuit_size));
}
EXPECT_EQ(num_solutions, expected);
}
TEST(CircuitConstraintTest, AllVehiculeRoutes) {
static const int kNumNodes = 4;
Model model;
model.Add(
DenseCircuitConstraint(kNumNodes, /*allow_subcircuit=*/false,
/*allow_multiple_subcircuit_through_zero=*/true));
const int num_solutions = CountSolutions(&model);
int expected = 1; // 3 outgoing arcs from zero.
expected += 2 * 3; // 2 outgoing arcs from zero. 3 pairs, 2 direction.
expected += 6; // full circuit.
EXPECT_EQ(num_solutions, expected);
}
TEST(CircuitConstraintTest, AllCircuitCoverings) {
// This test counts the number of circuit coverings of the clique on
// num_nodes with num_distinguished distinguished nodes, i.e. graphs that are
// vertex-disjoint circuits where every circuit must contain exactly one
// distinguished node.
//
// When writing n the number of nodes and k the number of distinguished nodes,
// and the number of such coverings T(n, k), we have:
// T(n,1) = (n-1)!, T(k,k) = 1, T(n,k) = (n-1)!/(k-1)! for n >= k >= 1.
// Indeed, we can enumerate canonical representations, e.g. [1]64[2]35,
// by starting with [1][2]...[k], and place every node in turn at its final
// place w.r.t. existing neighbours. To generate the above example, we go
// though [1][2], [1][2]3, [1]4[2]3, [1]4[2]35, [1]64[2]35.
// At the first iteration, there are k choices, then k+1 ... n-1.
for (int num_nodes = 1; num_nodes <= 6; num_nodes++) {
for (int num_distinguished = 1; num_distinguished <= num_nodes;
num_distinguished++) {
Model model;
std::vector<int> distinguished(num_distinguished);
std::iota(distinguished.begin(), distinguished.end(), 0);
std::vector<std::vector<Literal>> graph(num_nodes);
std::vector<Literal> arcs;
for (int i = 0; i < num_nodes; i++) {
graph[i].resize(num_nodes);
for (int j = 0; j < num_nodes; j++) {
const auto var = model.Add(NewBooleanVariable());
graph[i][j] = Literal(var, true);
arcs.emplace_back(graph[i][j]);
}
if (i >= num_distinguished) {
model.Add(ClauseConstraint({graph[i][i].Negated()}));
}
}
model.Add(ExactlyOnePerRowAndPerColumn(graph));
model.Add(CircuitCovering(graph, distinguished));
const int64_t num_solutions = CountSolutions(&model);
EXPECT_EQ(num_solutions * Factorial(num_distinguished - 1),
Factorial(num_nodes - 1));
}
}
}
TEST(CircuitConstraintTest, InfeasibleBecauseOfMissingArcs) {
Model model;
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
for (const auto arcs :
std::vector<std::pair<int, int>>{{0, 1}, {1, 1}, {0, 2}, {2, 2}}) {
tails.push_back(arcs.first);
heads.push_back(arcs.second);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
LoadSubcircuitConstraint(3, tails, heads, /*enforcement_literals=*/{},
literals, &model, false);
const SatSolver::Status status = SolveIntegerProblemWithLazyEncoding(&model);
EXPECT_EQ(status, SatSolver::Status::INFEASIBLE);
}
// The graph look like this with a self-loop at 2. If 2 is not selected
// (self-loop) then there is one solution (0,1,3,0) and (0,3,5,0). Otherwise,
// there is 2 more solutions with 2 inserted in one of the two routes.
//
// 0 ---> 1 ---> 4 -------------
// | | ^ |
// | -----> 2* --> 5 ---> 0
// | ^ ^
// | | |
// -------------> 3 ------
//
TEST(CircuitConstraintTest, RouteConstraint) {
Model model;
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
for (const auto arcs : std::vector<std::pair<int, int>>{{0, 1},
{0, 3},
{1, 2},
{1, 4},
{2, 2},
{2, 4},
{2, 5},
{3, 2},
{3, 5},
{4, 0},
{5, 0}}) {
tails.push_back(arcs.first);
heads.push_back(arcs.second);
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
LoadSubcircuitConstraint(6, tails, heads, /*enforcement_literals=*/{},
literals, &model, true);
const int64_t num_solutions = CountSolutions(&model);
EXPECT_EQ(num_solutions, 3);
}
TEST(NoCyclePropagatorTest, CountAllSolutions) {
// We create a 2 * 2 grid with diagonal arcs.
Model model;
int num_nodes = 0;
const int num_x = 2;
const int num_y = 2;
const auto get_index = [&num_nodes](int x, int y) {
const int index = x * num_y + y;
num_nodes = std::max(num_nodes, index + 1);
return index;
};
std::vector<int> tails;
std::vector<int> heads;
std::vector<Literal> literals;
for (int x = 0; x < num_x; ++x) {
for (int y = 0; y < num_y; ++y) {
for (const int x_dir : {-1, 0, 1}) {
for (const int y_dir : {-1, 0, 1}) {
const int head_x = x + x_dir;
const int head_y = y + y_dir;
if (x_dir == 0 && y_dir == 0) continue;
if (head_x < 0 || head_x >= num_x) continue;
if (head_y < 0 || head_y >= num_y) continue;
tails.push_back(get_index(x, y));
heads.push_back(get_index(head_x, head_y));
literals.push_back(Literal(model.Add(NewBooleanVariable()), true));
}
}
}
}
model.TakeOwnership(
new NoCyclePropagator(num_nodes, tails, heads, literals, &model));
// Graph is small enough.
CHECK_EQ(num_nodes, 4);
CHECK_EQ(tails.size(), 12);
// Counts solution with brute-force algo.
int num_expected_solutions = 0;
std::vector<std::vector<int>> subgraph(num_nodes);
std::vector<std::vector<int>> components;
const int num_cases = 1 << tails.size();
for (int mask = 0; mask < num_cases; ++mask) {
for (int n = 0; n < num_nodes; ++n) {
subgraph[n].clear();
}
for (int a = 0; a < tails.size(); ++a) {
if ((1 << a) & mask) {
subgraph[tails[a]].push_back(heads[a]);
}
}
components.clear();
FindStronglyConnectedComponents(num_nodes, subgraph, &components);
bool has_cycle = false;
for (const std::vector<int> compo : components) {
if (compo.size() > 1) {
has_cycle = true;
break;
}
}
if (!has_cycle) ++num_expected_solutions;
}
EXPECT_EQ(num_expected_solutions, 543);
// There is 12 arcs.
// So out of 2^12 solution, we have to exclude all the one with cycles.
EXPECT_EQ(CountSolutions(&model), 543);
}
} // namespace
} // namespace sat
} // namespace operations_research