diff --git a/Makefile.in b/Makefile.in index aa182f5cde..75ab6033ca 100755 --- a/Makefile.in +++ b/Makefile.in @@ -323,7 +323,9 @@ INTSRC0 = \ int-set.cpp var-imp/int.cpp var-imp/bool.cpp var/int.cpp \ var/bool.cpp array.cpp bool.cpp bool/eqv.cpp \ extensional/dfa.cpp extensional/tuple-set.cpp \ - extensional-regular.cpp extensional-tuple-set.cpp \ + extensional-regular.cpp \ + extensional-tuple-set.cpp \ + open-var-list.cpp \ dom.cpp rel.cpp precede.cpp element.cpp count.cpp \ arithmetic.cpp exec.cpp \ exec/when.cpp element/pair.cpp \ @@ -348,6 +350,10 @@ INTSRC0 = \ exception.cpp INTHDR0 = \ + open-var-list.hpp open-propagator.hpp \ + distinct/open.hpp rel/open.hpp sequence/open.hpp \ + arithmetic/open.hpp precede/open.hpp extensional/open.hpp branch/open.hpp \ + growable-int-set.hh growable-int-set.hpp \ limits.hpp support-values.hh support-values.hpp \ idx-view.hh idx-view.hpp div.hh div.hpp \ exec.hh exec/when.hpp \ @@ -906,10 +912,11 @@ INTEXAMPLESRC0 = \ word-square crossword open-shop car-sequencing sat \ bin-packing knights tsp perfect-square schurs-lemma \ dominating-queens colored-matrix multi-bin-packing \ - qcp job-shop + qcp job-shop open-route common-pattern fifteen-puzzle -INTEXAMPLEHDR = $(INTEXAMPLEHDR0:%=examples/%.hpp) +INTEXAMPLEHDR = $(INTEXAMPLEHDR0:%=examples/%.hpp) \ + examples/common-pattern.hh INTEXAMPLESRC = $(INTEXAMPLESRC0:%=examples/%.cpp) ifeq "@enable_examples@" "yes" INSTALLEXAMPLEHDR = $(INTEXAMPLEHDR) @@ -1119,7 +1126,7 @@ EXETARGETS = $(FLATZINCEXE) $(FLATZINCLAUNCHER) INTTESTSRC00 = \ channel arithmetic basic bool count dom distinct element extensional \ - rel linear gcc sorted unshare exec sequence \ + rel linear gcc sorted unshare exec sequence open-var-list \ mm-arithmetic mm-bool mm-lin mm-count mm-rel \ bin-packing order unary cumulative cumulatives circuit \ no-overlap precede inter-distance nvalues member min-distance diff --git a/changelog.in b/changelog.in index b4bfd353b1..e14f52adfd 100755 --- a/changelog.in +++ b/changelog.in @@ -116,6 +116,27 @@ Rank: minor Honor propagation annotations in cumulative and unary scheduling bindings, including the unary specializations of cumulative constraints. +[ENTRY] +Module: int +What: new +Rank: major +[DESCRIPTION] +Add OpenIntVarList and OpenBoolVarList for lists whose length is decided +during propagation and search. Variables are created as needed, and constraints +and branching follow list growth. This can reduce memory use by avoiding +variables and constraints for an unused suffix of a fixed-size array. +[MORE] +OpenVarList supports other variable types through a factory. +Regular-language filtering excludes prefixes whose shortest accepting +continuation exceeds the maximum list length. +Branching uses standard variable and value selectors, with either incremental +list construction or a length decision before assigning values. +Factories and transitions run inside Space::NoGPI scopes by default; +callbacks can opt into global information with Space::UseGPI. +The open-route example introduces list construction and branching. +Common-pattern demonstrates constrained frequent subsequence mining, +and fifteen-puzzle compares open lists with fixed arrays. + [ENTRY] Module: kernel What: new diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index e762f19034..be1a613b08 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -84,6 +84,7 @@ set(GECODE_INT_SOURCES gecode/int/exec/when.cpp gecode/int/extensional-regular.cpp gecode/int/extensional-tuple-set.cpp + gecode/int/open-var-list.cpp gecode/int/extensional/dfa.cpp gecode/int/extensional/tuple-set.cpp gecode/int/gcc.cpp @@ -429,6 +430,7 @@ set(GECODE_TEST_SOURCES test/int/no-overlap.cpp test/int/nvalues.cpp test/int/order.cpp + test/int/open-var-list.cpp test/int/precede.cpp test/int/rel.cpp test/int/sequence.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a223ae844a..6282beaeb8 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -13,6 +13,7 @@ set(GECODE_EXAMPLE_SOURCES car-sequencing.cpp cartesian-heart.cpp colored-matrix.cpp + common-pattern.cpp crew.cpp crossword.cpp crowded-chess.cpp @@ -22,6 +23,7 @@ set(GECODE_EXAMPLE_SOURCES donald.cpp efpa.cpp eq20.cpp + fifteen-puzzle.cpp golden-spiral.cpp golf.cpp golomb-ruler.cpp @@ -40,6 +42,7 @@ set(GECODE_EXAMPLE_SOURCES money.cpp multi-bin-packing.cpp nonogram.cpp + open-route.cpp open-shop.cpp ortho-latin.cpp partition.cpp diff --git a/examples/common-pattern.cpp b/examples/common-pattern.cpp new file mode 100644 index 0000000000..059e1bae33 --- /dev/null +++ b/examples/common-pattern.cpp @@ -0,0 +1,268 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include + +#include "common-pattern.hh" + +#include +#include + +using namespace Gecode; + +/// Sequence database and restrictions on the patterns to mine +class CommonPatternOptions : public Options { +protected: + Driver::StringValueOption _sequences; + Driver::StringValueOption _file; + Driver::StringValueOption _support; + Driver::StringValueOption _required; + Driver::StringOption _format; + Driver::StringOption _language; + Driver::UnsignedIntOption _min_length; + Driver::BoolOption _motif; + std::unique_ptr input; + std::vector required_items; + int threshold; +public: + CommonPatternOptions(void) + : Options("CommonPattern"), + _sequences("sequences","comma-separated sequences over A..Z","ABCBC,BABC,ABDC"), + _file("file","sequence database; one sequence per line",""), + _support("support","minimum supporting sequences, as a percentage","100"), + _required("required","items that must occur, comma-separated letters or IDs",""), + _format("format","database format", + static_cast(PatternMining::Format::LETTERS)), + _language("language","pattern language", + static_cast(PatternMining::Language::DEFAULT)), + _min_length("min-length","minimum pattern length",1), + _motif("motif","default language starts with A and ends with C",true), + threshold(0) { + _format.add(static_cast(PatternMining::Format::LETTERS),"letters"); + _format.add(static_cast(PatternMining::Format::INTEGERS),"integers"); + _format.add(static_cast(PatternMining::Format::SPMF),"spmf"); + _language.add(static_cast(PatternMining::Language::DEFAULT), + "default","use -motif"); + _language.add(static_cast(PatternMining::Language::NONE),"none"); + _language.add(static_cast(PatternMining::Language::MOTIF),"motif"); + _language.add(static_cast(PatternMining::Language::RE10),"re10"); + _language.add(static_cast(PatternMining::Language::RE14),"re14"); + _language.add(static_cast(PatternMining::Language::RE2),"re2"); + add(_sequences); + add(_file); + add(_format); + add(_support); + add(_min_length); + add(_required); + add(_language); + add(_motif); + } + + /// Validate external input before constructing a space + void read(void) { + if (_min_length.value() < 1 || _min_length.value() > Int::Limits::max) + throw std::invalid_argument("Minimum length must be a positive Gecode integer"); + PatternMining::Sequences sequences; + const std::string filename(_file.value()); + if (filename.empty()) { + if (format() != PatternMining::Format::LETTERS) + throw std::invalid_argument("Integer and SPMF formats require -file"); + const std::string text(_sequences.value()); + if (text.empty() || text.back() == ',') + throw std::invalid_argument("Expected comma-separated nonempty A..Z sequences"); + std::istringstream stream(text); + std::string sequence; + while (std::getline(stream,sequence,',')) + sequences.push_back(PatternMining::read_sequence( + sequence,PatternMining::Format::LETTERS)); + } else { + std::ifstream stream(filename); + sequences = PatternMining::read_database(stream,format()); + } + input.reset(new PatternMining::Database(sequences)); + threshold = PatternMining::minimum_support(_support.value(),input->ends.size()); + const std::string items(_required.value()); + if (!items.empty()) { + if (items.back() == ',') + throw std::invalid_argument("Empty required item"); + std::istringstream stream(items); + std::string item; + while (std::getline(stream,item,',')) { + const std::vector parsed = PatternMining::read_sequence(item, + letters() ? PatternMining::Format::LETTERS + : PatternMining::Format::INTEGERS); + if (parsed.size() != 1) + throw std::invalid_argument("Required items must be single letters or IDs"); + required_items.push_back(parsed[0]); + } + } + } + + const PatternMining::Database& database(void) const { return *input; } + const std::vector& required(void) const { return required_items; } + int support(void) const { return threshold; } + int min_length(void) const { return static_cast(_min_length.value()); } + PatternMining::Format format(void) const { + return static_cast(_format.value()); + } + bool letters(void) const { return format() == PatternMining::Format::LETTERS; } + PatternMining::Language language(void) const { + const auto selected = static_cast(_language.value()); + if (selected == PatternMining::Language::DEFAULT) + return _motif.value() ? PatternMining::Language::MOTIF + : PatternMining::Language::NONE; + return selected; + } +}; + +/** + * \brief %Example: constrained frequent subsequences of unknown length + * + * A pattern must occur in at least -support percent of the input sequences. + * Matching preserves order and may skip items; repeated items match distinct + * positions. Each supporting record counts once, including duplicate records. + * By default the inputs ABCBC, BABC, ABDC must all support the pattern, which + * must start with A and end with C. AC and ABC are feasible; ABC is longest. + * + * Ordinary length and regular constraints restrict the pattern, not the + * matching positions in the input records. By default, DFS enumerates every + * qualifying pattern; -search bab finds a longest one. + * + * An OpenIntVarList constructs pattern positions as needed, without an end + * marker or unused suffix variables. Pattern languages therefore describe + * just the pattern, without padding transitions. The private frequency + * constraint maintains supporting suffixes after the assigned prefix and + * removes items with insufficient support. The method follows Amina Kemmar, + * Samir Loudni, Yahia Lebbah, Patrice Boizumault, and Thierry Charnois, + * "PREFIX-PROJECTION Global Constraint for Sequential Pattern Mining", + * CP 2015, pp. 226-243, https://doi.org/10.1007/978-3-319-23219-5_17. + * The supporting suffix lengths also bound how many items can follow the + * assigned prefix. Projection can be combined with the pattern's length, + * item, and language restrictions. OpenRoute is the simpler introduction + * to open lists. + * + * The paper's Table 1 can be mined with: + * -sequences ABCBC,BABC,AB,BCD -support 50 -language none. + * -min-length and -required constrain length and item membership. -language + * also accepts motif (A, any items, C), re10, re14, re2 (expressions + * from the paper). Their letters mean item IDs A=1 through Z=26; integer data + * must use the corresponding mapping for these named expressions. + * + * -file reads one sequence per line in -format letters, integers, or spmf. + * Integer IDs are positive and preserved in output. SPMF records must have + * singleton itemsets: "1 -1 2 -1 3 -1 -2" is the sequence ABC. Multi-item + * itemsets are rejected because they define a different mining problem. + * No gap or time restrictions are supported. + * + * \ingroup Example + */ +class CommonPattern : public IntMaximizeScript { +protected: + OpenIntVarList pattern; + IntVar frequency; + PatternMining::Database database; + bool letters; +public: + enum { SEARCH_BAB, SEARCH_DFS }; + + CommonPattern(const CommonPatternOptions& opt) + : IntMaximizeScript(opt), database(opt.database()), letters(opt.letters()) { + pattern = OpenIntVarList(*this,database.domain(),database.max_length()); + rel(*this,pattern.length(),IRT_GQ,opt.min_length()); + frequency = PatternMining::frequent(*this,pattern,database,opt.support()); + if (opt.language() != PatternMining::Language::NONE) + extensional(*this,pattern,PatternMining::language(opt.language(),database)); + if (!opt.required().empty()) { + REG alphabet(IntArgs(database.alphabet)); + for (int item : opt.required()) + extensional(*this,pattern,*alphabet + REG(item) + *alphabet); + } + branch(*this,pattern,INT_VAR_NONE(),INT_VAL_MIN()); + } + + IntVar cost(void) const override { return pattern.length(); } + + CommonPattern(CommonPattern& s) + : IntMaximizeScript(s), database(s.database), letters(s.letters) { + pattern.update(*this,s.pattern); + frequency.update(*this,s.frequency); + } + + Space* copy(void) override { return new CommonPattern(*this); } + + void print(std::ostream& os) const override { + os << "\t"; + for (int i=0; i('A'+item-1); + else + os << item; + } else { + os << pattern[i]; + } + } + os << " (length " << pattern.length() + << ", support " << frequency << '/' << database.ends.size() + << ")" << std::endl; + } +}; + +/** \brief Main-function + * \relates CommonPattern + */ +int +main(int argc, char* argv[]) { + CommonPatternOptions opt; + opt.solutions(0); + opt.search(CommonPattern::SEARCH_DFS); + opt.search(CommonPattern::SEARCH_BAB,"bab","find a longest pattern"); + opt.search(CommonPattern::SEARCH_DFS,"dfs","enumerate frequent patterns"); + opt.parse(argc,argv); + try { + opt.read(); + } catch (const std::invalid_argument& error) { + std::cerr << error.what() << std::endl; + return 1; + } + if (opt.search() == CommonPattern::SEARCH_DFS) + IntMaximizeScript::run(opt); + else + IntMaximizeScript::run(opt); + return 0; +} + +// STATISTICS: example-any diff --git a/examples/common-pattern.hh b/examples/common-pattern.hh new file mode 100644 index 0000000000..1e7167ce07 --- /dev/null +++ b/examples/common-pattern.hh @@ -0,0 +1,472 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Private modelling support for the common-pattern example. + * Copyright Mikael Zayenz Lagerkvist, 2026. + * Distributed under the MIT license; see LICENSE. + */ + +#ifndef GECODE_EXAMPLES_COMMON_PATTERN_HH +#define GECODE_EXAMPLES_COMMON_PATTERN_HH + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace PatternMining { + + using namespace Gecode; + typedef std::vector> Sequences; + + enum class Format { LETTERS, INTEGERS, SPMF }; + enum class Language { DEFAULT, NONE, MOTIF, RE10, RE14, RE2 }; + + /// Read positive integer items or A..Z (represented by item IDs 1..26) + inline std::vector + read_sequence(const std::string& line, Format format) { + std::vector sequence; + if (format == Format::LETTERS) { + for (char a : line) { + if ((a < 'A') || (a > 'Z')) + throw std::invalid_argument("Letter sequences must contain A..Z only"); + sequence.push_back(a-'A'+1); + } + } else { + std::istringstream stream(line); + std::string token; + bool open_itemset = false; + bool ended = false; + while (stream >> token) { + int item; + const auto parsed = std::from_chars(token.data(),token.data()+token.size(),item); + if ((parsed.ec != std::errc()) || (parsed.ptr != token.data()+token.size())) + throw std::invalid_argument("Invalid integer item"); + if (ended) + throw std::invalid_argument("Items after SPMF sequence end"); + if ((format == Format::SPMF) && (item == -1)) { + if (!open_itemset) + throw std::invalid_argument("Empty SPMF itemset"); + open_itemset = false; + } else if ((format == Format::SPMF) && (item == -2)) { + if (open_itemset) + throw std::invalid_argument("Missing SPMF itemset end"); + ended = true; + } else { + if ((item <= 0) || (item > Int::Limits::max) || + ((format == Format::SPMF) && open_itemset)) + throw std::invalid_argument("Expected positive items; SPMF itemsets must be singletons"); + sequence.push_back(item); + open_itemset = true; + } + } + if (!stream.eof() || ((format == Format::SPMF) && !ended)) + throw std::invalid_argument("Invalid sequence or missing SPMF sequence end"); + } + if (sequence.empty()) + throw std::invalid_argument("Empty sequence"); + return sequence; + } + + /// Read one sequence per line, counting duplicate rows as distinct records + inline Sequences + read_database(std::istream& stream, Format format) { + Sequences input; + std::string line; + while (std::getline(stream,line)) { + if (!line.empty() && (line.back() == '\r')) + line.pop_back(); + if (line.empty() || ((format == Format::SPMF) && + ((line[0] == '#') || (line[0] == '%') || (line[0] == '@')))) + continue; + input.push_back(read_sequence(line,format)); + } + if (!stream.eof() || input.empty()) + throw std::invalid_argument("Cannot read a nonempty sequence database"); + return input; + } + + /// Round the support percentage (at most six decimals) up to a record count + inline int + minimum_support(const std::string& percent, int sequences) { + unsigned long long numerator = 0; + unsigned long long scale = 1; + bool decimal = false; + for (char c : percent) { + if ((c == '.') && !decimal) { + decimal = true; + } else if ((c >= '0') && (c <= '9')) { + numerator = numerator*10 + (c-'0'); + if (decimal) + scale *= 10; + if ((numerator > 100000000ULL) || (scale > 1000000ULL)) + throw std::invalid_argument("Support needs at most six decimals"); + } else { + throw std::invalid_argument("Support must be a percentage in (0,100]"); + } + } + if ((numerator == 0) || (numerator > 100*scale)) + throw std::invalid_argument("Support must be a percentage in (0,100]"); + const unsigned long long denominator = 100*scale; + return static_cast((sequences*numerator+denominator-1)/denominator); + } + + /// Immutable input, with dense ranks internally and original IDs externally + class Database { + public: + /// Concatenated item ranks + IntSharedArray items; + /// Exclusive record ends in items + IntSharedArray ends; + /// Rank-to-ID mapping + IntSharedArray alphabet; + /// Each item's last position per record, in decreasing position order + IntSharedArray last_positions; + /// Exclusive record ends in last_positions + IntSharedArray last_ends; + + explicit Database(const Sequences& input) { + if (input.empty()) + throw std::invalid_argument("Empty database"); + std::vector symbols; + for (const std::vector& sequence : input) { + if (sequence.empty() || + (sequence.size() >= static_cast(Int::Limits::max)-symbols.size())) + throw std::invalid_argument("Empty sequence or database too large"); + for (int item : sequence) { + if ((item <= 0) || (item > Int::Limits::max)) + throw std::invalid_argument("Item IDs must be positive Gecode integers"); + symbols.push_back(item); + } + } + items.init(static_cast(symbols.size())); + std::sort(symbols.begin(),symbols.end()); + symbols.erase(std::unique(symbols.begin(),symbols.end()),symbols.end()); + alphabet.init(static_cast(symbols.size())); + std::copy(symbols.begin(),symbols.end(),alphabet.begin()); + ends.init(static_cast(input.size())); + last_ends.init(static_cast(input.size())); + Region region; + int* seen = region.alloc(alphabet.size()); + std::fill(seen,seen+alphabet.size(),-1); + std::vector last; + int position = 0; + int record = 0; + for (const std::vector& sequence : input) { + const int start = position; + for (int item : sequence) + items[position++] = static_cast(std::lower_bound( + symbols.begin(),symbols.end(),item)-symbols.begin()); + ends[record] = position; + for (int j=position; j-- > start; ) + if (seen[items[j]] != record) { + seen[items[j]] = record; + last.push_back(j); + } + last_ends[record++] = static_cast(last.size()); + } + last_positions.init(static_cast(last.size())); + std::copy(last.begin(),last.end(),last_positions.begin()); + } + + IntSet domain(void) const { return IntSet(alphabet.begin(),alphabet.size()); } + + int max_length(void) const { + int bound = 0; + int start = 0; + for (int end : ends) { + bound = std::max(bound,end-start); + start = end; + } + return bound; + } + + /// Count supporting records, rather than embeddings within a record + int support(const std::vector& pattern) const { + int count = 0; + int start = 0; + for (int end : ends) { + size_t matched = 0; + for (int i=start; (i(database.ends.size())), + size(database.ends.size()), data(database), threshold(support) { + for (int i=0; i(p.size)), size(p.size), + data(p.data), threshold(p.threshold) { + std::copy(p.suffixes,p.suffixes+size,suffixes); + } + + /// Extend the prefix by one item and report whether enough records support it + bool advance(int item) { + const int rank = static_cast(std::lower_bound( + data.alphabet.begin(),data.alphabet.end(),item)-data.alphabet.begin()); + if ((rank == data.alphabet.size()) || (data.alphabet[rank] != item)) + return false; + int n = 0; + for (int i=0; i= threshold; + } + + int support(void) const { return size; } + + /** + * \brief Items occurring in at least threshold supporting suffixes + * + * Last occurrences count each item at most once per record. They are + * scanned in decreasing position order, so the scan stops at the suffix + * boundary. + */ + IntSet frequent(Region& region) const { + const int d = data.alphabet.size(); + int* count = region.alloc(d); + std::fill(count,count+d,0); + for (int i=0; i(d); + int n = 0; + for (int a=0; a= threshold) + values[n++] = data.alphabet[a]; + return IntSet(values,n); + } + + /// The threshold-th largest suffix length bounds every frequent extension + int remaining(Region& region) const { + int* sizes = region.alloc(size); + for (int i=0; i()); + return sizes[threshold-1]; + } + }; + + /** + * \brief Private prefix projection for a growing list + * + * Adapts ProjectSDB (Algorithm 1) and Filter-Prefix-Projection (Algorithm 2) + * from "PREFIX-PROJECTION Global Constraint for Sequential Pattern Mining", + * Amina Kemmar, Samir Loudni, Yahia Lebbah, Patrice Boizumault, and + * Thierry Charnois, CP 2015, https://doi.org/10.1007/978-3-319-23219-5_17 + * + * The open list replaces the termination marker. Suffix lengths bound how + * many more items can follow the assigned prefix. The projection advances + * only through the leading assigned positions. Items frequent in the + * supporting suffixes filter every materialized position after that prefix. + * Adding positions reuses the cached items and suffix-length bound until + * the assigned prefix grows. + * + * The support variable is bounded by the prefix's supporting-record count + * and becomes exact once the list is closed and fully assigned. + */ + class OpenProjection : public Propagator { + protected: + OpenViewList list; + Int::IntView length; + Int::IntView frequency; + Projection projection; + IntSet frequent_items; + int consumed; + int subscribed; + /// Prefix length at which the item filter and length bound were computed + int cached_prefix; + + OpenProjection(Home home, OpenViewList pattern, + const Database& database, int support, IntVar count) + : Propagator(home), list(pattern), length(pattern.length()), frequency(count), + projection(home,database,support), consumed(0), + subscribed(pattern.known_length()), cached_prefix(-1) { + home.notice(*this,AP_DISPOSE); + list.subscribe(home,*this); + length.subscribe(home,*this,Int::PC_INT_BND); + for (int i=0; i pattern, + const Database& database, int support, IntVar count) { + (void) new (home) OpenProjection(home,pattern,database,support,count); + } + }; + + /// Enforce minimum support and return its exact count when the pattern is complete + inline IntVar + frequent(Home home, OpenIntVarList pattern, + const Database& database, int support) { + if (home.failed()) + return IntVar(); + if ((support < 1) || (support > database.ends.size())) { + home.fail(); + return IntVar(); + } + IntVar count(home,support,database.ends.size()); + OpenProjection::post(home,OpenViewList(pattern), + database,support,count); + return count; + } + + /// Built-in motif and benchmark languages described in CommonPattern + inline REG + language(Language name, const Database& database) { + if (name == Language::RE10) + return *REG(1) + REG(2) + (REG(2) | REG(3)) + *REG(4) + + REG(5) + *REG(6) + (REG(7) | REG(8)) + *REG(9); + if (name == Language::RE14) + return *REG(1) + (REG(17) | (REG(2) + *REG(19) + + (REG(2) | REG(3)))) + *REG(4) + REG(5) + *(REG(9) | REG(19)) + + (REG(6) | REG(8)) + *REG(7) + REG(18); + REG alphabet(IntArgs(database.alphabet.size(),database.alphabet.begin())); + if (name == Language::MOTIF) + return REG(1) + *alphabet + REG(3); + return (REG(19) | REG(20)) + alphabet + (REG(18) | REG(11)); + } + +} + +#endif diff --git a/examples/fifteen-puzzle.cpp b/examples/fifteen-puzzle.cpp new file mode 100644 index 0000000000..af5edcdef5 --- /dev/null +++ b/examples/fifteen-puzzle.cpp @@ -0,0 +1,398 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace Gecode; +using Board = std::array; + +/// Options for the initial scramble and bounded solution path. +class PuzzleOptions : public Options { + Driver::UnsignedIntOption _scramble; + Driver::UnsignedIntOption _limit; +public: + enum { MODEL_OPEN, MODEL_FIXED }; + enum { BRANCH_MIN, BRANCH_MANHATTAN }; + PuzzleOptions(void) + : Options("FifteenPuzzle"), + _scramble("scramble","number of random slides from the goal",12), + _limit("limit","maximum number of solution moves",12) { + add(_scramble); + add(_limit); + model(MODEL_OPEN); + model(MODEL_OPEN,"open","construct states as the path grows"); + model(MODEL_FIXED,"fixed","construct all bounded states initially"); + branching(BRANCH_MIN); + branching(BRANCH_MIN,"min","try the lowest blank position first"); + branching(BRANCH_MANHATTAN,"manhattan", + "try the slide with least Manhattan distance first"); + solutions(0); + seed(1); + } + unsigned int + scramble(void) const { + return _scramble.value(); + } + int + limit(void) const { + Int::Limits::check(16*(static_cast(_limit.value())+1), + "FifteenPuzzle::limit"); + return static_cast(_limit.value()); + } +}; + +static Board +goal(void) { + return {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0}; +} + +static IntArgs +neighbors(int blank) { + IntArgs result; + if (blank >= 4) + result << blank-4; + if (blank % 4 != 0) + result << blank-1; + if (blank % 4 != 3) + result << blank+1; + if (blank < 12) + result << blank+4; + return result; +} + +static Board +initial_board(const PuzzleOptions& opt) { + Board board = goal(); + Rnd random(opt.seed()); + int blank = 15; + int previous = -1; + for (unsigned int i=0; i(choices.size()))]; + std::swap(board[blank],board[next]); + previous = blank; + blank = next; + } + return board; +} + +static int +manhattan(const Board& board) { + int distance = 0; + for (int i=0; i<16; i++) { + if (board[i] != 0) { + const int target = board[i]-1; + distance += std::abs(i/4-target/4) + std::abs(i%4-target%4); + } + } + return distance; +} + +static Board +values(const IntVarArgs& tiles) { + Board result; + for (int i=0; i<16; i++) + result[i] = tiles[i].val(); + return result; +} + +// The assigned preceding board changes only at the blank and the moved tile. +static int +manhattan_value(const IntVarArgs& before, int blank, IntVar next) { + int best = next.min(); + int best_change = std::numeric_limits::max(); + for (IntVarValues candidates(next); candidates(); ++candidates) { + const int position = candidates.val(); + const int target = before[position].val()-1; + const int change = + std::abs(blank/4-target/4) + std::abs(blank%4-target%4) - + std::abs(position/4-target/4) - std::abs(position%4-target%4); + if (change < best_change) { + best = position; + best_change = change; + } + } + return best; +} + +static void +print_board(std::ostream& os, const Board& board) { + for (int i=0; i<16; i++) { + os << std::setw(3) << board[i]; + if (i % 4 == 3) + os << '\n'; + } +} + +static void +post_start(Home home, const IntVarArgs& tiles, IntVar blank, + const Board& initial) { + for (int i=0; i<16; i++) + rel(home,tiles[i] == initial[i]); + const int zero = static_cast( + std::find(initial.begin(),initial.end(),0)-initial.begin()); + rel(home,blank == zero); +} + +// Swap the blank with an adjacent tile and preserve every other position. +static void +post_slide(Home home, + const IntVarArgs& before, IntVar old_blank, + const IntVarArgs& after, IntVar new_blank) { + static const TupleSet adjacent = [] { + TupleSet table(2); + for (int i=0; i<16; i++) { + for (int j : neighbors(i)) + table.add(IntArgs({i,j})); + } + table.finalize(); + return table; + }(); + extensional(home,IntVarArgs({old_blank,new_blank}),adjacent); + rel(home,element(after,old_blank) == element(before,new_blank)); + rel(home,element(after,new_blank) == 0); + for (int i=0; i<16; i++) { + rel(home,((old_blank != i) && (new_blank != i)) >> + (before[i] == after[i])); + } +} + +// The last board must be solved; reaching the goal forbids a longer path. +static void +post_goal(Home home, const IntVarArgs& tiles, IntVar length, + int states) { + const Board target = goal(); + BoolExpr matches(tiles[0] == target[0]); + for (int i=1; i<16; i++) + matches = matches && (tiles[i] == target[i]); + BoolVar solved = expr(home,matches); + rel(home,(length == states) >> solved); + rel(home,solved >> (length <= states)); +} + +/** + * \brief %Example: solve the 15-puzzle with an open list of states + * + * Start from a solvable board obtained by randomly sliding from the goal. + * Each path variable records the blank position in one board. The tile list + * holds sixteen variables per board. The transition on the path constructs and + * constrains the next board when that position is materialized. + * Each new board uses the same slide and goal constraints as the fixed + * representation. Propagation determines how much of the path is constructed; + * the initial Manhattan distance bounds the number of moves from below. + * + * The goal closes the path. Length counts states including the start, so + * branch and bound minimizes moves by minimizing length minus one. + * The independent move limit bounds search and need not equal the scramble. + * Defaults use twelve slides, seed one, and a twelve-move limit. Select + * -model fixed to compare with an array representation; -scramble, -seed, + * and -limit control the instance and search bound independently. + * -branching min (default) tries blank positions in numerical order; + * -branching manhattan tries the slide with the lowest resulting Manhattan + * distance, breaking ties by the lowest position. + * + * \ingroup Example + */ +class OpenPuzzle : public Script { + OpenIntVarList tiles; + OpenIntVarList path; +public: + OpenPuzzle(const PuzzleOptions& opt) + : Script(opt), + tiles(*this,IntSet(0,15),16*(opt.limit()+1)), + path(*this,IntSet(0,15), + [](Space& home, OpenIntVarList x, int i) { + static_cast(home).transition(x,i); + },opt.limit()+1) { + const Board initial = initial_board(opt); + rel(*this,tiles.length() == 16*path.length()); + IntVar start = path.get(*this,0); + post_start(*this,state(0),start,initial); + rel(*this,path.length() >= manhattan(initial)+1); + IntValBranch value_branch = INT_VAL_MIN(); + if (opt.branching() == PuzzleOptions::BRANCH_MANHATTAN) + value_branch = INT_VAL([](const Space& home, IntVar next, int i) { + const OpenPuzzle& s = static_cast(home); + return manhattan_value(s.state(i-1),s.path[i-1].val(),next); + }); + branch(*this,path,INT_VAR_NONE(),value_branch); + } + + void + transition(OpenIntVarList x, int i) { + tiles.materialize(*this,16*(i+1)); + if (failed()) + return; + if (i > 0) + post_slide(*this,state(i-1),x[i-1],state(i),x[i]); + post_goal(*this,state(i),path.length(),i+1); + } + + OpenPuzzle(OpenPuzzle& s) : Script(s) { + tiles.update(*this,s.tiles); + path.update(*this,s.path); + } + Space* + copy(void) override { + return new OpenPuzzle(*this); + } + IntVarArgs + state(int i) const { + IntVarArgs result(16); + for (int j=0; j<16; j++) + result[j] = tiles[16*i+j]; + return result; + } + void + constrain(const Space& solution) override { + const OpenPuzzle& best = static_cast(solution); + rel(*this,path.length() < best.path.length().val()); + } + void + print(std::ostream& os) const override { + os << "Moves: " << path.length().val()-1 << "\nBlank positions:"; + for (int i=0; i= manhattan(initial)+1); + IntValBranch value_branch = INT_VAL_MIN(); + if (opt.branching() == PuzzleOptions::BRANCH_MANHATTAN) + value_branch = INT_VAL([](const Space& home, IntVar next, int i) { + const FixedPuzzle& s = static_cast(home); + return manhattan_value(s.state(i-1),s.path[i-1].val(),next); + }); + branch(*this,path,INT_VAR_NONE(),value_branch, + [](const Space& home, IntVar, int i) { + const FixedPuzzle& s = static_cast(home); + const int depth = s.depth(); + return values(s.state(depth)) != goal() && i == depth+1; + }); + } + + FixedPuzzle(FixedPuzzle& s) : Script(s) { + tiles.update(*this,s.tiles); + path.update(*this,s.path); + length.update(*this,s.length); + } + Space* + copy(void) override { + return new FixedPuzzle(*this); + } + IntVarArgs + state(int i) const { + IntVarArgs result(16); + for (int j=0; j<16; j++) + result[j] = tiles[16*i+j]; + return result; + } + int + depth(void) const { + int i = 0; + while (i+1 < path.size() && path[i+1].assigned()) + i++; + return i; + } + void + constrain(const Space& solution) override { + const FixedPuzzle& best = static_cast(solution); + rel(*this,length < best.length.val()); + } + void + print(std::ostream& os) const override { + os << "Moves: " << length.val()-1 << "\nBlank positions:"; + for (int i=0; i(opt); + else + Script::run(opt); + return 0; +} + +// STATISTICS: example-any diff --git a/examples/open-route.cpp b/examples/open-route.cpp new file mode 100644 index 0000000000..b7e6ee143c --- /dev/null +++ b/examples/open-route.cpp @@ -0,0 +1,129 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include +#include + +using namespace Gecode; + +/** + * \brief %Example: simple routes of unknown length + * + * Find all simple routes from node 0 to node 4 in this directed graph: + * 0 -> 1, 2, 4 + * 1 -> 2, 4 + * 2 -> 1, 3 + * 3 -> 2, 4 + * + * The graph contains cycles, but distinct node variables exclude revisits. + * A simple route uses at most five nodes, which bounds the list length + * without excluding any routes. + * + * \ingroup Example + */ +class OpenRoute : public Script { +protected: + /// Number of graph nodes and maximal route length + static const int node_count = 5; + /// Node IDs in route order, including source and destination + OpenIntVarList route; +public: + /// Branching to use for the route + enum { + BRANCH_INCREMENTAL, ///< Assign the current prefix before extending it + BRANCH_LENGTH ///< Choose the complete route length first + }; + + /// Construct the model + OpenRoute(const Options& opt) + : Script(opt), route(*this,IntSet(0,node_count-1),node_count) { + // State 5 consumes the source node. Transitions along graph arcs consume + // their destination node; only node 4 is an accepting state. + DFA paths(5,{ + {5,0,0}, + {0,1,1}, {0,2,2}, {0,4,4}, + {1,2,2}, {1,4,4}, + {2,1,1}, {2,3,3}, + {3,2,2}, {3,4,4} + },{4}); + extensional(*this,route,paths); + distinct(*this,route); + + // Posting the length brancher first selects a length before assigning nodes. + if (opt.branching() == BRANCH_LENGTH) + branch(*this,route.length(),INT_VAL_MIN()); + branch(*this,route,INT_VAR_NONE(),INT_VAL_MIN()); + } + + /// Constructor for cloning \a s, including the space-local open list + OpenRoute(OpenRoute& s) : Script(s) { + route.update(*this,s.route); + } + + /// Perform copying during cloning + Space* + copy(void) override { + return new OpenRoute(*this); + } + + /// Print the materialized route (complete in a solution) + void + print(std::ostream& os) const override { + os << "\t"; + for (int i=0; i 0) + os << " -> "; + os << route[i]; + } + os << " (length " << route.length() << ")" << std::endl; + } +}; + +/** \brief Main-function + * \relates OpenRoute + */ +int +main(int argc, char* argv[]) { + Options opt("OpenRoute"); + opt.solutions(0); + opt.branching(OpenRoute::BRANCH_INCREMENTAL); + opt.branching(OpenRoute::BRANCH_INCREMENTAL,"incremental", + "Assign the current prefix before extending the route"); + opt.branching(OpenRoute::BRANCH_LENGTH,"length", + "Choose the complete route length first"); + opt.parse(argc,argv); + Script::run(opt); + return 0; +} + +// STATISTICS: example-any diff --git a/gecode/int.hh b/gecode/int.hh index 04bfb99d70..b30217ccbb 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -56,6 +56,7 @@ #include #include #include +#include #include #include #include @@ -867,12 +868,277 @@ namespace Gecode { BoolVarArray& operator =(const BoolVarArray&) = default; }; + namespace Int { + template class OpenMaterialize; + } + + template class OpenViewList; + + /** + * \brief Space-local list of variables with an open length + * + * Append variables until the list is closed. The length() variable gives + * the eventual length; propagation creates the positions required by its + * minimum. known_length() counts the created positions, and operator[] + * accesses them. get() creates positions through a requested + * index and raises the minimum length to include it. + * + * Copies of a handle share the list. update() preserves this sharing + * within each clone, with independent mutable state. Lists and appended + * variables must be initialized and belong to the supplied space. + * + * OpenIntVarList and OpenBoolVarList supply variable factories. For other + * types, pass a factory to OpenVarList. Constraint and branching + * overloads depend on the variable type. Var must support default and copy + * construction, assignment, assigned(), varimp(), and update(). + * + * A list can be used in an ordinary Space: + * \code + * class Model : public Space { + * public: + * OpenIntVarList x; + * Model(void) : x(*this,IntSet(0,2),3) { + * rel(*this,x.length(),IRT_GQ,1); + * distinct(*this,x); + * branch(*this,x); + * } + * Model(Model& s) : Space(s) { + * x.update(*this,s.x); + * } + * virtual Space* copy(void) { + * return new Model(*this); + * } + * }; + * \endcode + * + * The \ref OpenRoute "open-route example" introduces construction, + * search, and printing. The \ref CommonPattern "common-pattern example" + * uses an open list for a pattern of unknown length. It avoids allocating + * variables for the longest possible pattern and marking an unused suffix + * with a termination symbol. + * + * \ingroup TaskModelInt + */ + template + class OpenVarList : public LocalHandle { + template friend class OpenViewList; + friend class Int::OpenMaterialize; + private: + /// Space-local list implementation + class Object; + /// Construct a model handle for a source callback + explicit OpenVarList(Object* source); + public: + /** + * \brief Function creating the variable at position \a i in \a home + * + * The function object is shared between clones. It must be deterministic + * and safe for concurrent calls. Captured data must be owned or shared + * and must not be space-local. Returned variables must belong to \a home + * and be initialized. + * + * Calls run inside Space::NoGPI. Posted propagators have space-local + * information and zero AFC, so they do not affect AFC-based selection. + * Identifiers and groups are available; group changes are local to the + * space and copied on cloning. Use Space::UseGPI inside the callback for + * postings that need global information and ordinary AFC. Nested + * materialization enters its own NoGPI scope. Scopes restore the previous + * setting on exit, including exceptions. + */ + typedef std::function Factory; + /** + * \brief Function called after materializing position \a i + * + * Must be deterministic and safe for concurrent calls. Sharing, captured + * data, and NoGPI scopes follow the Factory contract. + * The arguments \a home and \a x refer to the current space. Both + * materialize() and direct append() call the transition after the new + * position is available through indexed access. + */ + typedef std::function x, int i)> + Transition; + /// Construct an uninitialized list + OpenVarList(void); + /// Construct a list using \a create for each position + OpenVarList(Space& home, Factory create, + int max=Int::Limits::max); + /// Construct a list using \a create and transition function \a t + OpenVarList(Space& home, Factory create, Transition t, + int max=Int::Limits::max); + /// Update during cloning + void update(Space& home, OpenVarList& s); + /// Return the number of constructed positions (their values may be unassigned) + int known_length(void) const; + /// Return variable at position \a i, where 0 <= \a i < known_length() + Var operator [](int i) const; + /// Return the eventual-length variable + IntVar length(void) const; + /** + * \brief Append variable \a x + * + * Raises the minimum length to include \a x, then calls the transition + * if supplied (see Transition). Exceeding the maximum length fails the + * space. Throws Int::ArgumentSame if \a x is an unassigned variable + * already in the list. + */ + void append(Space& home, Var x); + /** + * \brief Materialize the first \a n variables + * + * Raises the minimum length to at least \a n and creates the required + * positions, including any further increase imposed by the transition. + * Callbacks use the NoGPI scopes described in Factory. + */ + void materialize(Space& home, int n); + /** + * \brief Require and return the variable at position \a i + * + * Calls materialize() with \a i+1. + * + * Returns an uninitialized variable if \a home is failed or + * materialization fails. Throws Int::OutOfLimits for a negative index, + * an index outside Int::Limits, or (in a nonfailed space) an index at + * least length().max(). Check for failure before using the result, + * for example with an integer list: + * \code + * IntVar y = x.get(home,i); + * if (home.failed()) + * return; + * rel(home,y,IRT_NQ,0); + * \endcode + */ + Var get(Space& home, int i); + /** + * \brief Close the list at its materialized length + * + * Calls materialize() for the minimum length, then assigns length() + * to known_length(). The contained variables need not be assigned. + */ + void close(Space& home); + /** + * \brief Subscribe \a p to the addition of positions + * + * This reports construction of positions. Subscribe separately to the + * length variable or contained variables for length or value changes. + */ + void subscribe(Space& home, Propagator& p); + /// Cancel the subscription of \a p to the addition of positions + void cancel(Space& home, Propagator& p); + /// Reschedule \a p for the addition of positions + void reschedule(Space& home, Propagator& p); + }; + + + /** + * \brief View interface to a space-local list with an open length + * + * Shares the model list and its later additions. Indexed access returns + * views by value, which are valid across growth. Cloning preserves sharing + * within the clone with independent mutable state. + * + * View must be constructible from its VarType. Int::MinusView is supported + * through Int::IntView. The length view is always Int::IntView. + * \ingroup TaskActor + */ + template + class OpenViewList : public LocalHandle { + private: + typedef typename View::VarType Var; + typedef typename OpenVarList::Object Object; + public: + /// Construct an uninitialized list + OpenViewList(void); + /// Share the source of the model list \a list + explicit OpenViewList(const OpenVarList& list); + /// Update during cloning + void update(Space& home, OpenViewList& s); + /// Return the number of constructed positions + int known_length(void) const; + /// Return the view at constructed position \a i + View operator [](int i) const; + /// Return the eventual-length view + Int::IntView length(void) const; + /// Require and construct at least the first \a n positions + void materialize(Space& home, int n); + /// Subscribe \a p to construction of positions + void subscribe(Space& home, Propagator& p); + /// Cancel the subscription of \a p to construction of positions + void cancel(Space& home, Propagator& p); + /// Reschedule \a p for construction of positions + void reschedule(Space& home, Propagator& p); + }; + + + /** + * \brief Open list of integer variables + * \ingroup TaskModelInt + */ + class OpenIntVarList : public OpenVarList { + public: + /** + * \brief Function returning the domain for position \a i + * + * Must be deterministic and safe for concurrent calls. Sharing and + * captured data follow the Factory contract. + */ + typedef std::function Domain; + using OpenVarList::OpenVarList; + /// Construct an uninitialized list + GECODE_INT_EXPORT OpenIntVarList(void); + /// Share an integer list constructed through a variable factory + OpenIntVarList(const OpenVarList& x); + /// Construct a list with maximal length \a max and full domains + GECODE_INT_EXPORT + OpenIntVarList(Space& home, int max=Int::Limits::max); + /// Construct a list using domain \a d for every position + GECODE_INT_EXPORT + OpenIntVarList(Space& home, const IntSet& d, + int max=Int::Limits::max); + /// Construct a list using domain \a d and transition function \a t + GECODE_INT_EXPORT + OpenIntVarList(Space& home, const IntSet& d, Transition t, + int max=Int::Limits::max); + /// Construct a list using \a d to create each position + GECODE_INT_EXPORT + OpenIntVarList(Space& home, Domain d, + int max=Int::Limits::max); + /// Construct a list using \a d and transition function \a t + GECODE_INT_EXPORT + OpenIntVarList(Space& home, Domain d, Transition t, + int max=Int::Limits::max); + private: + /// Convert a position-dependent domain to a variable factory + static Factory factory(Domain d); + }; + + /** + * \brief Open list of Boolean variables + * \ingroup TaskModelInt + */ + class OpenBoolVarList : public OpenVarList { + public: + using OpenVarList::OpenVarList; + /// Construct an uninitialized list + GECODE_INT_EXPORT OpenBoolVarList(void); + /// Share a Boolean list constructed through a variable factory + OpenBoolVarList(const OpenVarList& x); + /// Construct a list with maximal length \a max and domains 0..1 + GECODE_INT_EXPORT + OpenBoolVarList(Space& home, int max=Int::Limits::max); + /// Construct a list with domains 0..1 and transition function \a t + GECODE_INT_EXPORT + OpenBoolVarList(Space& home, Transition t, + int max=Int::Limits::max); + }; + } #include #include +#include + namespace Gecode { /** @@ -1194,6 +1460,17 @@ namespace Gecode { GECODE_INT_EXPORT void rel(Home home, const IntVarArgs& x, IntRelType irt, IntPropLevel ipl=IPL_DEF); + + /** + * \brief Constrain consecutive materialized variables in \a x by \a irt + * + * The constraint applies to variables added after posting. + * + * \ingroup TaskModelIntRelInt + */ + GECODE_INT_EXPORT void + rel(Home home, OpenIntVarList x, IntRelType irt, + IntPropLevel ipl=IPL_DEF); /** \brief Post propagator for relation between \a x and \a y. * * Note that for the inequality relations this corresponds to @@ -1466,6 +1743,19 @@ namespace Gecode { GECODE_INT_EXPORT void precede(Home home, const IntVarArgs& x, int s, int t, IntPropLevel=IPL_DEF); + + /** + * \brief Constrain \a s to precede \a t in the eventual sequence \a x + * + * Every occurrence of \a t requires an earlier occurrence of \a s; + * neither value is required to occur. The constraint applies to variables + * added after posting. The propagation level \a ipl is ignored. + * + * \ingroup TaskModelIntPrecede + */ + GECODE_INT_EXPORT void + precede(Home home, OpenIntVarList x, int s, int t, + IntPropLevel ipl=IPL_DEF); /** \brief Post propagator that successive values in \a c precede each other in \a x * * This constraint enforces that \f$x_0\neq c_k\f$ for \f$0 vars, IntValBranch vals, + IntBranchFilter bf=nullptr, IntVarValPrint vvp=nullptr); + /** + * \brief Branch over an open Boolean list with standard selectors + * + * The incremental branching, filter, and Action/CHB restrictions are as + * for integers. + * \ingroup TaskModelIntBranch + */ + GECODE_INT_EXPORT void + branch(Home home, OpenBoolVarList x, + BoolVarBranch vars, BoolValBranch vals, + BoolBranchFilter bf=nullptr, BoolVarValPrint vvp=nullptr); + /// Branch over an open Boolean list with tie-breaking selectors + GECODE_INT_EXPORT void + branch(Home home, OpenBoolVarList x, + TieBreak vars, BoolValBranch vals, + BoolBranchFilter bf=nullptr, BoolVarValPrint vvp=nullptr); /** * \brief Branch over \a x with variable selection \a vars and value selection \a vals * diff --git a/gecode/int/arithmetic.cpp b/gecode/int/arithmetic.cpp index 743f9dd255..07c9b431a9 100755 --- a/gecode/int/arithmetic.cpp +++ b/gecode/int/arithmetic.cpp @@ -584,6 +584,23 @@ namespace Gecode { } } + void + min(Home home, OpenIntVarList list, + IntVar result, IntPropLevel ipl) { + GECODE_POST; + Int::Arithmetic::OpenMax::post + (home,OpenViewList(list), + Int::MinusView(Int::IntView(result)),ipl); + } + + void + max(Home home, OpenIntVarList list, + IntVar result, IntPropLevel ipl) { + GECODE_POST; + Int::Arithmetic::OpenMax::post + (home,OpenViewList(list),Int::IntView(result),ipl); + } + } // STATISTICS: int-post diff --git a/gecode/int/arithmetic.hh b/gecode/int/arithmetic.hh index 5b65689ce2..8f5d2519dc 100644 --- a/gecode/int/arithmetic.hh +++ b/gecode/int/arithmetic.hh @@ -1172,6 +1172,8 @@ namespace Gecode { namespace Int { namespace Arithmetic { #include +#include + #endif // STATISTICS: int-prop diff --git a/gecode/int/arithmetic/open.hpp b/gecode/int/arithmetic/open.hpp new file mode 100644 index 0000000000..fc4c8351b4 --- /dev/null +++ b/gecode/int/arithmetic/open.hpp @@ -0,0 +1,187 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INT_ARITHMETIC_OPEN_HPP +#define GECODE_INT_ARITHMETIC_OPEN_HPP + +#include +#include + +namespace Gecode { namespace Int { namespace Arithmetic { + + /// Track maxima; negated views implement min(x) = -max(-x) + template + class OpenMax : public OpenListPropagator { + protected: + using OpenListPropagator::list; + using OpenListPropagator::length; + using OpenListPropagator::processed; + using OpenListPropagator::closed; + + class Index : public Advisor { + public: + int i; + + Index(Space& home, Propagator& p, Council& c, int i0) + : Advisor(home,p,c), i(i0) {} + + Index(Space& home, Index& a) + : Advisor(home,a), i(a.i) {} + }; + + View result; + IntPropLevel ipl; + Council c; + /// Maximum lower bound seen so far; bounds only increase + int lower; + /// Result upper bound last applied to all incorporated positions + int upper; + /// The result occurs in the list and supplies its own equality witness + bool witness; + + OpenMax(Home home, OpenViewList list, + View result0, IntPropLevel ipl0) + : OpenListPropagator(home,list), + result(result0), ipl(ipl0), c(home), + lower(Limits::min), upper(result0.max()), witness(false) { + result.subscribe(home,*this,PC_INT_BND); + } + + OpenMax(Space& home, OpenMax& p) + : OpenListPropagator(home,p), ipl(p.ipl), c(home), + lower(p.lower), upper(p.upper), witness(p.witness) { + result.update(home,p.result); + c.update(home,p.c); + } + + public: + virtual Actor* + copy(Space& home) { + return new (home) OpenMax(home,*this); + } + + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + const int size = list.known_length(); + return PropCost::linear(PropCost::LO, + ((closed() && !witness) || (result.max() < upper)) ? + size : size-processed); + } + + virtual ExecStatus + advise(Space& home, Advisor& advisor, const Delta& d) { + if (View::modevent(d) == ME_INT_DOM) + return ES_FIX; + Index& a = static_cast(advisor); + View x = list[a.i]; + lower = std::max(lower,x.min()); + if (x.assigned()) + a.dispose(home,c); + return ((lower > result.min()) || + (witness && closed() && c.empty())) ? ES_NOFIX : ES_FIX; + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + const int size = list.known_length(); + if (result.max() < upper) { + for (int i=0; i variables(home,size); + for (int i=0; i + ::post(home(*this),variables,y))); + GECODE_REWRITE(*this,(Arithmetic::NaryMaxBnd + ::post(home(*this),variables,y))); + } + + virtual void + reschedule(Space& home) { + OpenListPropagator::reschedule(home); + result.reschedule(home,*this,PC_INT_BND); + } + + virtual size_t + dispose(Space& home) { + if (!home.failed()) + for (Advisors as(c); as(); ++as) { + View x = list[as.advisor().i]; + x.cancel(home,as.advisor()); + } + c.dispose(home); + result.cancel(home,*this,PC_INT_BND); + OpenListPropagator::dispose_base(home); + return sizeof(*this); + } + + static void + post(Home home, OpenViewList list, + View result, IntPropLevel ipl) { + (void) new (home) OpenMax(home,list,result,ipl); + } + }; + +}}} + +#endif + +// STATISTICS: int-prop diff --git a/gecode/int/branch.cpp b/gecode/int/branch.cpp index 05dc03bfe2..333a1c0625 100755 --- a/gecode/int/branch.cpp +++ b/gecode/int/branch.cpp @@ -350,6 +350,48 @@ namespace Gecode { assign(home, xv, BOOL_VAR_NONE(), ba, nullptr, vvp); } + void + branch(Home home, OpenIntVarList list) { + branch(home,list,INT_VAR_NONE(),INT_VAL_MIN()); + } + + void + branch(Home home, OpenBoolVarList list) { + branch(home,list,BOOL_VAR_NONE(),BOOL_VAL_MIN()); + } + + void + branch(Home home, OpenIntVarList list, + IntVarBranch vars, IntValBranch vals, + IntBranchFilter bf, IntVarValPrint vvp) { + branch(home,list,TieBreak(vars),vals,bf,vvp); + } + + void + branch(Home home, OpenIntVarList list, + TieBreak vars, IntValBranch vals, + IntBranchFilter bf, IntVarValPrint vvp) { + GECODE_POST; + Int::Branch::post_open_brancher + (home,OpenViewList(list),vars,vals,bf,vvp); + } + + void + branch(Home home, OpenBoolVarList list, + BoolVarBranch vars, BoolValBranch vals, + BoolBranchFilter bf, BoolVarValPrint vvp) { + branch(home,list,TieBreak(vars),vals,bf,vvp); + } + + void + branch(Home home, OpenBoolVarList list, + TieBreak vars, BoolValBranch vals, + BoolBranchFilter bf, BoolVarValPrint vvp) { + GECODE_POST; + Int::Branch::post_open_brancher + (home,OpenViewList(list),vars,vals,bf,vvp); + } + #ifdef GECODE_HAS_CBS void diff --git a/gecode/int/branch.hh b/gecode/int/branch.hh index 1192ee1282..3faab43c37 100755 --- a/gecode/int/branch.hh +++ b/gecode/int/branch.hh @@ -792,6 +792,8 @@ namespace Gecode { namespace Int { namespace Branch { #endif +#include + #endif // STATISTICS: int-branch diff --git a/gecode/int/branch/open.hpp b/gecode/int/branch/open.hpp new file mode 100644 index 0000000000..8153a5a390 --- /dev/null +++ b/gecode/int/branch/open.hpp @@ -0,0 +1,375 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INT_BRANCH_OPEN_HPP +#define GECODE_INT_BRANCH_OPEN_HPP + +#include + +namespace Gecode { namespace Int { namespace Branch { + + /// Standard view/value selectors over the materialized prefix + template + class OpenListBrancher : public Brancher { + protected: + typedef typename View::VarType Var; + OpenViewList list; + ViewSel* vs[n]; + /// Binary value selector, or null for one alternative per domain value + ValSelCommitBase* vsc; + Filter filter; + SharedData> printer; + /// Whether enumerated domain values are tried in increasing order + bool values_min; + bool select_first; + mutable int start; + + OpenListBrancher(Home home, OpenViewList list0, + ViewSel* vs0[n], + ValSelCommitBase* vsc0, + bool values_min0, + bool select_first0, + BranchFilter bf, VarValPrint vvp) + : Brancher(home), list(list0), vsc(vsc0), + filter(bf), printer(vvp), values_min(values_min0), + select_first(select_first0), start(0) { + for (int i=0; icopy(home); + for (; copiedcopy(home); + } catch (...) { + for (int i=0; idispose(home); + if (vsc) + vsc->dispose(home); + throw; + } + } + + /// Select from a temporary array, keeping positions stable across growth + int + position(Space& home) { + if (select_first) + return start; + Region r; + ViewArray x(r,list.known_length()); + for (int i=0; iselect(home,x,start,filter) + : vs[0]->select(home,x,start); + int* ties = r.alloc(x.size()-start); + int n_ties; + if (filter) + vs[0]->ties(home,x,start,ties,n_ties,filter); + else + vs[0]->ties(home,x,start,ties,n_ties); + for (int i=1; (i1); i++) + vs[i]->brk(home,x,ties,n_ties); + return (n_ties > 1) ? vs[n-1]->select(home,x,ties,n_ties) : ties[0]; + } + + static const Choice* + value_choice(const Brancher& b, int i, IntView x) { + return new PosValuesChoice(b,Pos(i),x); + } + + static const Choice* + value_choice(const Brancher&, int, BoolView) { + GECODE_NEVER; + return nullptr; + } + + int + value(const Choice& c, unsigned int a) const { + const PosValuesChoice& pvc = + static_cast(c); + return pvc.val(values_min ? a : pvc.alternatives()-1-a); + } + + public: + virtual bool + status(const Space& home) const { + for (int i=start; i list.known_length(); + } + + virtual const Choice* + choice(Space& home) { + for (int i=start; i(*this,2,Pos(p),vsc->val(home,x,p)) + : value_choice(*this,p,x); + } + // Position -1 marks a length choice: close here or require another item. + return new PosValChoice(*this,2,Pos(-1),list.known_length()); + } + + virtual const Choice* + choice(const Space&, Archive& e) { + int p; + e >> p; + if ((p < 0) || vsc) { + int v; + e >> v; + return new PosValChoice(*this,2,Pos(p),v); + } + unsigned int a; + e >> a; + return new PosValuesChoice(*this,a,Pos(p),e); + } + + virtual ExecStatus + commit(Space& home, const Choice& c, unsigned int a) { + const int p = static_cast(c).pos().pos; + if (p >= 0) { + // Recomputation can replay choices before propagation grows the prefix. + if (p >= list.known_length()) { + list.materialize(home,p+1); + if (home.failed()) + return ES_FAILED; + } + View x(list[p]); + const ModEvent me = vsc + ? vsc->commit(home,a,x,p,static_cast&>(c).val()) + : x.eq(home,value(c,a)); + return me_failed(me) ? ES_FAILED : ES_OK; + } + const int size = static_cast&>(c).val(); + if (a == 0) + return me_failed(list.length().eq(home,size)) + ? ES_FAILED : ES_OK; + list.materialize(home,size+1); + return home.failed() ? ES_FAILED : ES_OK; + } + + virtual NGL* + ngl(Space& home, const Choice& c, unsigned int a) const { + const int p = static_cast(c).pos().pos; + if (p < 0) + return (a == 0) ? new (home) EqNGL + (home,list.length(), + static_cast&>(c).val()) : nullptr; + // No-good extraction can run in an ancestor with a shorter prefix. + if (p >= list.known_length()) + return nullptr; + View x(list[p]); + return vsc + ? vsc->ngl(home,a,x,static_cast&>(c).val()) + : new (home) EqNGL(home,x,value(c,a)); + } + + virtual void + print(const Space& home, const Choice& c, unsigned int a, + std::ostream& o) const { + const int p = static_cast(c).pos().pos; + if (p < 0) { + o << "list length " << ((a == 0) ? "=" : ">") << " " + << static_cast&>(c).val(); + } else { + const int v = vsc ? static_cast&>(c).val() + : value(c,a); + // Tracing can print replayed choices before commit grows the prefix. + if (p >= list.known_length()) { + o << "list[" << p << "] alternative " << a << " (value " << v << ")"; + return; + } + View x(list[p]); + if (printer()) + printer()(home,*this,a,Var(x),p,v,o); + else if (vsc) + vsc->print(home,a,x,p,v,o); + else + o << "list[" << p << "] = " << v; + } + } + + virtual Actor* + copy(Space& home) { + return new (home) OpenListBrancher(home,*this); + } + + virtual size_t + dispose(Space& home) { + home.ignore(*this,AP_DISPOSE,true); + for (int i=0; idispose(home); + if (vsc) + vsc->dispose(home); + filter.dispose(home); + printer.~SharedData>(); + list.~OpenViewList(); + (void) Brancher::dispose(home); + return sizeof(*this); + } + + template + static void + post(Home home, OpenViewList list, VarBranch vars[n], + ValSelCommitBase* vsc, + bool values_min, + BranchFilter bf, VarValPrint vvp) { + ViewSel* vs[n]; + for (int i=0; i* + open_valselcommit(Home home, IntValBranch vals, bool& values_min) { + values_min = vals.select() != IntValBranch::SEL_VALUES_MAX; + return ((vals.select() == IntValBranch::SEL_VALUES_MIN) || + (vals.select() == IntValBranch::SEL_VALUES_MAX)) + ? nullptr : valselcommit(home,vals); + } + + /// Boolean value branching always uses a binary selector + inline ValSelCommitBase* + open_valselcommit(Home home, BoolValBranch vals, bool& values_min) { + values_min = true; + return valselcommit(home,vals); + } + + /// Choose the native filtered or unfiltered brancher policy + template + void + post_open_brancher(Home home, OpenViewList list, + VarBranch vars[n], ValSelCommitBase* vsc, + bool values_min, + BranchFilter bf, + VarValPrint vvp) { + if (bf) + OpenListBrancher,n>::post + (home,list,vars,vsc,values_min,bf,vvp); + else + OpenListBrancher,n>::post + (home,list,vars,vsc,values_min,bf,vvp); + } + + template + void + post_open_brancher(Home home, OpenViewList list, + TieBreak vars, ValBranch vals, + BranchFilter bf, + VarValPrint vvp) { + VarBranch selectors[4] = {vars.a,vars.b,vars.c,vars.d}; + int n = 1; + while ((n < 4) && (selectors[n-1].select() != VarBranch::SEL_NONE) && + (selectors[n-1].select() != VarBranch::SEL_RND) && + (selectors[n].select() != VarBranch::SEL_NONE)) + n++; + typedef typename View::VarType Var; + typename ArrayTraits>::ArgsType x(list.known_length()); + for (int i=0; i list.known_length()); + selectors[i].expand(home,x); + } + bool values_min; + ValSelCommitBase* vsc = open_valselcommit(home,vals,values_min); + switch (n) { + case 1: + post_open_brancher + (home,list,selectors,vsc,values_min,bf,vvp); + break; + case 2: + post_open_brancher + (home,list,selectors,vsc,values_min,bf,vvp); + break; + case 3: + post_open_brancher + (home,list,selectors,vsc,values_min,bf,vvp); + break; + case 4: + post_open_brancher + (home,list,selectors,vsc,values_min,bf,vvp); + break; + } + } + +}}} + +#endif + +// STATISTICS: int-branch diff --git a/gecode/int/distinct.cpp b/gecode/int/distinct.cpp index 646afc045f..a0dd82b885 100755 --- a/gecode/int/distinct.cpp +++ b/gecode/int/distinct.cpp @@ -205,6 +205,12 @@ namespace Gecode { } } + void + distinct(Home home, OpenIntVarList list, IntPropLevel) { + GECODE_POST; + Int::Distinct::Open::post(home,OpenViewList(list)); + } + } // STATISTICS: int-post diff --git a/gecode/int/distinct.hh b/gecode/int/distinct.hh index 41154a6217..ae668790e9 100644 --- a/gecode/int/distinct.hh +++ b/gecode/int/distinct.hh @@ -381,6 +381,8 @@ namespace Gecode { namespace Int { namespace Distinct { #include #include +#include + #endif // STATISTICS: int-prop diff --git a/gecode/int/distinct/open.hpp b/gecode/int/distinct/open.hpp new file mode 100644 index 0000000000..1d7dfef3b3 --- /dev/null +++ b/gecode/int/distinct/open.hpp @@ -0,0 +1,207 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INT_DISTINCT_OPEN_HPP +#define GECODE_INT_DISTINCT_OPEN_HPP + +#include +#include +#include +#include + +namespace Gecode { namespace Int { namespace Distinct { + + template + class Open : public OpenListPropagator { + protected: + using OpenListPropagator::list; + using OpenListPropagator::length; + using OpenListPropagator::processed; + using OpenListPropagator::closed; + + /// Variables whose assigned values have not yet been propagated + View* active; + int active_count; + int active_capacity; + /// Assigned values to exclude from appended variables + GrowableIntSet assigned; + + Open(Home home, OpenViewList list) + : OpenListPropagator(home,list), + active(nullptr), active_count(0), active_capacity(0) { + home.notice(*this,AP_DISPOSE); + } + + Open(Space& home, Open& p) + : OpenListPropagator(home,p), + active((p.active_count == 0) ? nullptr : + home.alloc(p.active_count)), + active_count(p.active_count), active_capacity(p.active_count) { + for (int i=0; i(std::min( + static_cast(std::numeric_limits::max()), + static_cast(std::numeric_limits::max()) / + sizeof(View))); + if (required > limit) + throw MemoryExhausted(); + const int extra = std::min(active_capacity/2+1,limit-active_capacity); + const int capacity = std::max(required,active_capacity+extra); + View* views = home.alloc(capacity); + for (int i=0; i(active,active_capacity); + active = views; + active_capacity = capacity; + } + + /// Exclude a batch from the selected suffix and collect all assignments + template + ExecStatus + exclude_values(Space& home, I excluded, int first, + int* values, int& count) { + count = 0; + for (int i=active_count; i--; ) { + if ((i >= first) && !active[i].assigned()) { + I ranges(excluded); + GECODE_ME_CHECK(active[i].minus_r(home,ranges,false)); + } + if (active[i].assigned()) { + values[count++] = active[i].val(); + active[i] = active[--active_count]; + } + } + return ES_OK; + } + + public: + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::LO, + active_count+(list.known_length()-processed)); + } + + virtual Actor* + copy(Space& home) { + return new (home) Open(home,*this); + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + const int size = list.known_length(); + reserve_active(home,active_count+(size-processed)); + const int first_new = active_count; + for (int i=processed; i(capacity); + int* next = region.alloc(capacity); + int count; + GrowableIntSetRanges history(assigned); + GECODE_ES_CHECK(exclude_values(home,history,first_new,current,count)); + + typedef Iter::Values::ToRanges BatchRanges; + while (count > 0) { + Support::quicksort(current,count); + for (int i=1; i x(home,active_count); + for (int i=0; i::post(home(*this),x)); + } + return ES_FIX; + } + + virtual void + reschedule(Space& home) { + OpenListPropagator::reschedule(home); + for (int i=0; i(active,active_capacity); + assigned.dispose(home); + OpenListPropagator::dispose_base(home); + return sizeof(*this); + } + + static void + post(Home home, OpenViewList list) { + (void) new (home) Open(home,list); + } + }; + +}}} + +#endif + +// STATISTICS: int-prop diff --git a/gecode/int/extensional-regular.cpp b/gecode/int/extensional-regular.cpp index 37383df272..6547cf1a78 100755 --- a/gecode/int/extensional-regular.cpp +++ b/gecode/int/extensional-regular.cpp @@ -55,6 +55,23 @@ namespace Gecode { GECODE_ES_FAIL(Extensional::post_lgp(home,x,dfa)); } + void + extensional(Home home, OpenIntVarList list, DFA dfa, + IntPropLevel) { + GECODE_POST; + GECODE_ES_FAIL((Int::Extensional::OpenRegular + ::post(home,OpenViewList(list),dfa))); + } + + void + extensional(Home home, OpenBoolVarList list, DFA dfa, + IntPropLevel) { + GECODE_POST; + GECODE_ES_FAIL((Int::Extensional::OpenRegular + ::post(home,OpenViewList(list),dfa))); + } + } + // STATISTICS: int-post diff --git a/gecode/int/extensional.hh b/gecode/int/extensional.hh index e1de6da1c5..d532061f71 100755 --- a/gecode/int/extensional.hh +++ b/gecode/int/extensional.hh @@ -801,6 +801,8 @@ namespace Gecode { namespace Int { namespace Extensional { #include +#include + #endif // STATISTICS: int-prop diff --git a/gecode/int/extensional/open.hpp b/gecode/int/extensional/open.hpp new file mode 100644 index 0000000000..0e253d7ff1 --- /dev/null +++ b/gecode/int/extensional/open.hpp @@ -0,0 +1,324 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INT_EXTENSIONAL_OPEN_HPP +#define GECODE_INT_EXTENSIONAL_OPEN_HPP + +#include +#include +#include + +namespace Gecode { namespace Int { namespace Extensional { + + /// Shortest accepting suffix length from each state, or -1 if none exists + inline SharedArray + accepting_distances(const DFA& dfa) { + const int k = dfa.n_states(); + SharedArray distance(k); + Region r; + int* first = r.alloc(k); + int* source = r.alloc(dfa.n_transitions()); + int* next = r.alloc(dfa.n_transitions()); + int* queue = r.alloc(k); + for (int s=0; s + class OpenRegular : public Propagator { + protected: + OpenViewList list; + IntView length; + DFA dfa; + SharedArray distance; + unsigned char* start; + int offset; + int subscribed; + + OpenRegular(Home home, + OpenViewList list0, DFA dfa0) + : Propagator(home), list(list0), + length(list0.length()), + dfa(dfa0), distance(accepting_distances(dfa0)), + start(static_cast(home).alloc(dfa.n_states())), + offset(0), subscribed(list0.known_length()) { + home.notice(*this,AP_DISPOSE); + for (int i=0; i(p.dfa.n_states())), + offset(p.offset), subscribed(p.subscribed) { + list.update(home,p.list); + length.update(home,p.length); + for (int i=0; i= 0) && (distance[s] <= remaining)) + return true; + return false; + } + + private: + /// Mark states supporting the domains from offset through the last layer + const unsigned char* + compute_backward_support(Region& r, int remaining) const { + const int n = list.known_length()-offset; + const int k = dfa.n_states(); + const unsigned long long cells = + static_cast(n+1) * + static_cast(k); + if (cells > + static_cast + ((std::numeric_limits::max)())) + throw OutOfLimits("Int::extensional"); + + unsigned char* backward = + r.alloc(static_cast(cells)); + for (size_t i=0; i(cells); i++) + backward[i] = 0; + + unsigned char* last = backward + static_cast(n)*k; + for (int s=0; s= 0) && (distance[s] <= remaining)) + last[s] = 1; + + for (int i=n; i--; ) { + unsigned char* in = backward + static_cast(i)*k; + unsigned char* out = in+k; + View x(list[offset+i]); + for (ViewValues v(x); v(); ++v) + for (DFA::Transitions t(dfa,v.val()); t(); ++t) + if (out[t.o_state()] != 0) + in[t.i_state()] = 1; + } + return backward; + } + + public: + virtual Actor* + copy(Space& home) { + return new (home) OpenRegular(home,*this); + } + + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::HI,subscribed-offset); + } + + virtual void + reschedule(Space& home) { + length.reschedule(home,*this,PC_INT_BND); + list.reschedule(home,*this); + for (int i=offset; i(k); + unsigned char* next = r.alloc(k); + int* supported = r.alloc(dfa.n_symbols()); + for (int s=0; s(i+1)*k; + for (int s=0; s v(x); v(); ++v) { + bool support = false; + for (DFA::Transitions t(dfa,v.val()); t(); ++t) + if ((reachable[t.i_state()] != 0) && + (out[t.o_state()] != 0)) { + support = true; + next[t.o_state()] = 1; + } + if (support) + supported[n_supported++] = v.val(); + } + if (n_supported == 0) + return ES_FAILED; + Iter::Values::Array v(supported,n_supported); + GECODE_ME_CHECK(x.narrow_v(home,v,false)); + std::swap(reachable,next); + } + + // Pruning an item that aliases length can reduce the suffix budget. + // Recompute support before consuming the prefix. + if (length.max()-size < remaining) + return ES_NOFIX; + + int consumed = 0; + while ((consumed < n) && list[offset+consumed].assigned()) { + const unsigned char* live = + backward + static_cast(consumed+1)*k; + for (int s=0; s list, DFA dfa) { + for (int i=0; i + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INT_GROWABLE_INT_SET_HH +#define GECODE_INT_GROWABLE_INT_SET_HH + +#include +#include + +namespace Gecode { namespace Int { + + class GrowableIntSetRanges; + + /// A Space-owned set of integer values with growable canonical range storage + class GrowableIntSet { + friend class GrowableIntSetRanges; + private: + struct Range { + int min; + int max; + Range(void) {} + }; + Range* _ranges; + unsigned int _count; + unsigned int _capacity; + + /// Largest capacity supported by both counts and allocation sizes + static unsigned int max_capacity(void); + /// Ensure capacity, copying only initialized ranges + void reserve(Space& home, unsigned int required); + /// First range whose maximum is at least value + unsigned int first_max_ge(int value) const; + /// Add one range, allocating only when a new range is inserted + void add(Space& home, int min, int max); + + GrowableIntSet(const GrowableIntSet&) = delete; + GrowableIntSet& operator =(const GrowableIntSet&) = delete; + public: + /// Initialize an empty set + GrowableIntSet(void); + /// Test membership + bool in(int value) const; + /** \brief Add the ranges from a copyable, independent canonical iterator + * + * Endpoints must be within Int::Limits. Mutation invalidates borrowed + * range iterators. The input must not refer to this set's storage. + */ + template void add_r(Space& home, I added); + /// Copy exactly the live ranges into independent storage in home + void update(Space& home, const GrowableIntSet& source); + /// Return allocated storage to home + void dispose(Space& home); + }; + + /// Borrowed forward iterator over a growable set's ranges + class GrowableIntSetRanges { + private: + const GrowableIntSet::Range* current; + const GrowableIntSet::Range* end; + public: + GrowableIntSetRanges(const GrowableIntSet& set); + bool operator ()(void) const; + void operator ++(void); + int min(void) const; + int max(void) const; + unsigned int width(void) const; + }; + +}} + +#include + +#endif + +// STATISTICS: int-other diff --git a/gecode/int/growable-int-set.hpp b/gecode/int/growable-int-set.hpp new file mode 100644 index 0000000000..33856caea2 --- /dev/null +++ b/gecode/int/growable-int-set.hpp @@ -0,0 +1,215 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +namespace Gecode { namespace Int { + + forceinline + GrowableIntSet::GrowableIntSet(void) + : _ranges(nullptr), _count(0), _capacity(0) {} + + forceinline unsigned int + GrowableIntSet::max_capacity(void) { + return static_cast(std::min( + static_cast(std::numeric_limits::max()), + static_cast(std::numeric_limits::max()) / + sizeof(Range))); + } + + inline void + GrowableIntSet::reserve(Space& home, unsigned int required) { + if (required <= _capacity) + return; + const unsigned int limit = max_capacity(); + if (required > limit) + throw MemoryExhausted(); + const unsigned int extra = _capacity / 2U + _capacity % 2U; + const unsigned int grown = (_capacity == 0) ? 2U : + ((_capacity > limit-extra) ? limit : _capacity+extra); + const unsigned int capacity = std::max(required,grown); + Range* ranges = home.alloc(capacity); + for (unsigned int i=0; i<_count; i++) + ranges[i] = _ranges[i]; + if (_ranges != nullptr) + home.free(_ranges,_capacity); + _ranges = ranges; + _capacity = capacity; + } + + forceinline unsigned int + GrowableIntSet::first_max_ge(int value) const { + unsigned int first = 0; + unsigned int after = _count; + while (first < after) { + const unsigned int middle = first + (after-first)/2U; + if (_ranges[middle].max < value) + first = middle+1U; + else + after = middle; + } + return first; + } + + forceinline bool + GrowableIntSet::in(int value) const { + const unsigned int i = first_max_ge(value); + return (i < _count) && (_ranges[i].min <= value); + } + + inline void + GrowableIntSet::add(Space& home, int min, int max) { + const unsigned int first = first_max_ge(min-1); + unsigned int after = first; + while ((after < _count) && (_ranges[after].min <= max+1)) + ++after; + if (first < after) { + _ranges[first].min = std::min(min,_ranges[first].min); + _ranges[first].max = std::max(max,_ranges[after-1U].max); + if (after > first+1U) { + unsigned int out = first+1U; + for (unsigned int i=after; i<_count; i++) + _ranges[out++] = _ranges[i]; + _count = out; + } + return; + } + if (_count == max_capacity()) + throw MemoryExhausted(); + reserve(home,_count+1U); + for (unsigned int i=_count; i>first; --i) + _ranges[i] = _ranges[i-1U]; + _ranges[first].min = min; + _ranges[first].max = max; + ++_count; + } + + template + inline void + GrowableIntSet::add_r(Space& home, I added) { + if (!added()) + return; + I remaining(added); + ++remaining; + if (!remaining()) { + add(home,added.min(),added.max()); + return; + } + const unsigned int limit = max_capacity()-_count; + unsigned int n = 1; + for (; remaining(); ++remaining) { + if (n >= limit) + throw MemoryExhausted(); + ++n; + } + const unsigned int first = first_max_ge(added.min()-1); + reserve(home,_count+n); + // The hole protects unread old ranges while the union is written forward. + for (unsigned int i=_count; i>first; --i) + _ranges[i+n-1U] = _ranges[i-1U]; + unsigned int read = first+n; + const unsigned int end = _count+n; + unsigned int out = first; + while (added() || (read < end)) { + Range next; + if (added() && ((read == end) || + (added.min() < _ranges[read].min))) { + next.min = added.min(); + next.max = added.max(); + ++added; + } else { + next = _ranges[read++]; + } + if ((out > first) && (next.min <= _ranges[out-1U].max+1)) { + _ranges[out-1U].max = std::max(_ranges[out-1U].max,next.max); + } else { + _ranges[out++] = next; + } + } + _count = out; + } + + inline void + GrowableIntSet::update(Space& home, const GrowableIntSet& source) { + const unsigned int count = source._count; + Range* ranges = (count == 0) ? nullptr : home.alloc(count); + for (unsigned int i=0; i(_ranges,_capacity); + _ranges = ranges; + _count = count; + _capacity = count; + } + + forceinline void + GrowableIntSet::dispose(Space& home) { + if (_ranges != nullptr) + home.free(_ranges,_capacity); + _ranges = nullptr; + _count = 0; + _capacity = 0; + } + + forceinline + GrowableIntSetRanges::GrowableIntSetRanges(const GrowableIntSet& set) + : current(set._ranges), + end((set._count == 0) ? set._ranges : set._ranges+set._count) {} + + forceinline bool + GrowableIntSetRanges::operator ()(void) const { + return current != end; + } + + forceinline void + GrowableIntSetRanges::operator ++(void) { + ++current; + } + + forceinline int + GrowableIntSetRanges::min(void) const { + return current->min; + } + + forceinline int + GrowableIntSetRanges::max(void) const { + return current->max; + } + + forceinline unsigned int + GrowableIntSetRanges::width(void) const { + return static_cast(current->max) - + static_cast(current->min) + 1U; + } + +}} + +// STATISTICS: int-other diff --git a/gecode/int/open-propagator.hpp b/gecode/int/open-propagator.hpp new file mode 100644 index 0000000000..f2d6e3e6c3 --- /dev/null +++ b/gecode/int/open-propagator.hpp @@ -0,0 +1,93 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INT_OPEN_PROPAGATOR_HPP +#define GECODE_INT_OPEN_PROPAGATOR_HPP + +#include + +namespace Gecode { namespace Int { + + /// Follow list growth and remember which positions have been incorporated + template + class OpenListPropagator : public Propagator { + protected: + OpenViewList list; + IntView length; + int processed; + + OpenListPropagator(Home home, OpenViewList list0) + : Propagator(home), list(list0), + length(list0.length()), + processed(0) { + length.subscribe(home,*this,PC_INT_BND); + list.subscribe(home,*this); + } + + OpenListPropagator(Space& home, OpenListPropagator& p) + : Propagator(home,p), processed(p.processed) { + list.update(home,p.list); + length.update(home,p.length); + } + + bool + closed(void) const { + return length.max() == list.known_length(); + } + + void + dispose_base(Space& home) { + length.cancel(home,*this,PC_INT_BND); + list.cancel(home,*this); + list.~OpenViewList(); + (void) Propagator::dispose(home); + } + + public: + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::LO,list.known_length()-processed); + } + + virtual void + reschedule(Space& home) { + length.reschedule(home,*this,PC_INT_BND); + list.reschedule(home,*this); + } + }; + +}} + +#endif + +// STATISTICS: int-prop diff --git a/gecode/int/open-var-list.cpp b/gecode/int/open-var-list.cpp new file mode 100644 index 0000000000..91b5ac4fa7 --- /dev/null +++ b/gecode/int/open-var-list.cpp @@ -0,0 +1,79 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include + +namespace Gecode { + + OpenIntVarList::OpenIntVarList(void) {} + + OpenIntVarList::Factory + OpenIntVarList::factory(Domain d) { + if (!d) + throw InvalidFunction("OpenIntVarList"); + return [d](Space& home, int i) { return IntVar(home,d(i)); }; + } + + OpenIntVarList::OpenIntVarList(Space& home, int max) + : OpenIntVarList(home, + IntSet(Int::Limits::min,Int::Limits::max),max) {} + + OpenIntVarList::OpenIntVarList(Space& home, const IntSet& d, + int max) + : OpenIntVarList(home,[d](int) { return d; },max) {} + + OpenIntVarList::OpenIntVarList(Space& home, const IntSet& d, + Transition t, int max) + : OpenIntVarList(home,[d](int) { return d; },t,max) {} + + OpenIntVarList::OpenIntVarList(Space& home, Domain d, int max) + : OpenVarList(home,factory(d),max) {} + + OpenIntVarList::OpenIntVarList(Space& home, Domain d, + Transition t, int max) + : OpenVarList(home,factory(d),t,max) {} + + OpenBoolVarList::OpenBoolVarList(void) {} + + OpenBoolVarList::OpenBoolVarList(Space& home, int max) + : OpenVarList(home, + [](Space& home, int) { return BoolVar(home,0,1); },max) {} + + OpenBoolVarList::OpenBoolVarList(Space& home, Transition t, + int max) + : OpenVarList(home, + [](Space& home, int) { return BoolVar(home,0,1); },t,max) {} + +} + +// STATISTICS: int-other diff --git a/gecode/int/open-var-list.hpp b/gecode/int/open-var-list.hpp new file mode 100644 index 0000000000..4982664115 --- /dev/null +++ b/gecode/int/open-var-list.hpp @@ -0,0 +1,482 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +namespace Gecode { + + template + class OpenVarList::Object : public LocalObject { + public: + class Functions { + public: + Factory create; + Transition transition; + + Functions(Factory create0, Transition transition0) + : create(create0), transition(transition0) {} + }; + + IntVar length; + // A separate event source reports appended positions even when length.min() + // already requires them and therefore does not change during construction. + IntVar materialized; + Var* x; + int n; + int capacity; + typedef std::unordered_set VarImpSet; + VarImpSet identities; + // Only committed positions are indexed. Assigned variables need no alias + // check, so their entries need not be removed. + int indexed; + SharedData functions; + + static int + checked_maximum(int max) { + Int::Limits::check(max,"OpenVarList"); + if (max < 0) + throw Int::VariableEmptyDomain("OpenVarList"); + return max; + } + + static Factory + checked(Factory create) { + if (!create) + throw InvalidFunction("OpenVarList"); + return create; + } + + static Transition + checked(Transition t) { + if (!t) + throw InvalidFunction("OpenVarList"); + return t; + } + + Object(Home home, Factory create, int max) + : LocalObject(home), + length(home,0,checked_maximum(max)), + materialized(home,0,max), + x(nullptr), n(0), capacity(0), indexed(0), + functions(Functions(checked(create),Transition())) { + home.notice(*this,AP_DISPOSE); + } + + Object(Home home, Factory create, Transition t, int max) + : LocalObject(home), + length(home,0,checked_maximum(max)), + materialized(home,0,max), + x(nullptr), n(0), capacity(0), indexed(0), + functions(Functions(checked(create),checked(t))) { + home.notice(*this,AP_DISPOSE); + } + + Object(Space& home, Object& s) + : LocalObject(home,s), x(nullptr), n(s.n), capacity(s.n), indexed(0), + functions(s.functions) { + length.update(home,s.length); + materialized.update(home,s.materialized); + if (capacity > 0) { + x = heap.alloc(capacity); + try { + for (int i=0; i(x,capacity); + throw; + } + } + } + + virtual Actor* + copy(Space& home) { + return new (home) Object(home,*this); + } + + virtual size_t + dispose(Space& home) { + home.ignore(*this,AP_DISPOSE); + if (capacity > 0) + heap.free(x,capacity); + identities.~VarImpSet(); + functions.~SharedData(); + return sizeof(*this); + } + + void + check_alias(Var y) { + if (y.assigned()) + return; + // Each clone builds its own index on the first alias check. Indexing a + // position on the next append puts allocation failures before changes + // to length or materialized. + for (; indexed(x,capacity,next); + capacity = next; + } + } + + void + append(Space& home, Var y) { + if (home.failed()) + return; + if (n == Int::Limits::max) { + home.fail(); + return; + } + check_alias(y); + reserve(); + const int next = n + 1; + Int::IntView l(length); + Int::IntView m(materialized); + if (me_failed(l.gq(home,next)) || me_failed(m.gq(home,next))) { + home.fail(); + return; + } + x[n++] = y; + const Transition& transition = functions().transition; + if (transition) { + Space::NoGPI no_gpi(home); + transition(home,OpenVarList(this),next-1); + } + } + + void + materialize(Space& home, int size) { + Space::NoGPI no_gpi(home); + Int::Limits::nonnegative(size,"OpenVarList::materialize"); + Int::IntView l(length); + if (me_failed(l.gq(home,size))) { + home.fail(); + return; + } + while (n < l.min()) { + const int i = n; + GECODE_VALID_FUNCTION(functions().create); + Var y = functions().create(home,i); + append(home,y); + if (home.failed()) + return; + } + } + + void + subscribe(Space& home, Propagator& p) { + Int::IntView(materialized).subscribe(home,p,Int::PC_INT_BND); + } + + void + cancel(Space& home, Propagator& p) { + Int::IntView(materialized).cancel(home,p,Int::PC_INT_BND); + } + + void + reschedule(Space& home, Propagator& p) { + Int::IntView(materialized).reschedule(home,p,Int::PC_INT_BND); + } + }; + +} + +namespace Gecode { namespace Int { + + /// Materialize positions required by the minimum eventual length + template + class OpenMaterialize : public Propagator, private LocalHandle { + protected: + typedef typename OpenVarList::Object Object; + IntView length; + + OpenMaterialize(Home home, Object* source) + : Propagator(home), LocalHandle(source), length(source->length) { + length.subscribe(home,*this,PC_INT_BND); + } + + OpenMaterialize(Space& home, OpenMaterialize& p) + : Propagator(home,p) { + LocalHandle::update(home,p); + length.update(home,p.length); + } + + public: + virtual Actor* + copy(Space& home) { + return new (home) OpenMaterialize(home,*this); + } + + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + const int n = length.min()-static_cast(object())->n; + return (n <= 1) ? PropCost::unary(PropCost::LO) : + PropCost::linear(PropCost::LO,n); + } + + virtual void + reschedule(Space& home) { + length.reschedule(home,*this,PC_INT_BND); + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + static_cast(object())->materialize(home,length.min()); + if (home.failed()) + return ES_FAILED; + if (length.assigned()) + return home.ES_SUBSUMED(*this); + return ES_FIX; + } + + virtual size_t + dispose(Space& home) { + length.cancel(home,*this,PC_INT_BND); + LocalHandle::~LocalHandle(); + (void) Propagator::dispose(home); + return sizeof(*this); + } + + static void + post(Home home, Object* source) { + if (!source->length.assigned()) + (void) new (home) OpenMaterialize(home,source); + } + }; + +}} + +namespace Gecode { + + template + OpenVarList::OpenVarList(void) {} + + template + OpenVarList::OpenVarList(Object* source) + : LocalHandle(source) {} + + template + OpenVarList::OpenVarList(Space& home, Factory create, int max) + : LocalHandle(new (home) Object(home,create,max)) { + Int::OpenMaterialize::post(home,static_cast(object())); + } + + template + OpenVarList::OpenVarList(Space& home, Factory create, + Transition t, int max) + : LocalHandle(new (home) Object(home,create,t,max)) { + Int::OpenMaterialize::post(home,static_cast(object())); + } + + forceinline + OpenIntVarList::OpenIntVarList(const OpenVarList& x) + : OpenVarList(x) {} + + forceinline + OpenBoolVarList::OpenBoolVarList(const OpenVarList& x) + : OpenVarList(x) {} + + template + void + OpenVarList::update(Space& home, OpenVarList& s) { + LocalHandle::update(home,s); + } + + template + int + OpenVarList::known_length(void) const { + return static_cast(object())->n; + } + + template + Var + OpenVarList::operator [](int i) const { + Object* s = static_cast(object()); + assert((i >= 0) && (i < s->n)); + return s->x[i]; + } + + template + IntVar + OpenVarList::length(void) const { + return static_cast(object())->length; + } + + template + void + OpenVarList::append(Space& home, Var y) { + if (!home.failed()) + static_cast(object())->append(home,y); + } + + template + void + OpenVarList::materialize(Space& home, int n) { + if (!home.failed()) + static_cast(object())->materialize(home,n); + } + + template + Var + OpenVarList::get(Space& home, int i) { + Int::Limits::nonnegative(i,"OpenVarList::get"); + if (home.failed()) + return Var(); + Object* s = static_cast(object()); + if (i >= s->length.max()) + throw Int::OutOfLimits("OpenVarList::get"); + materialize(home,i+1); + if (home.failed()) + return Var(); + return static_cast(object())->x[i]; + } + + template + void + OpenVarList::close(Space& home) { + if (home.failed()) + return; + materialize(home,length().min()); + if (home.failed()) + return; + Object* s = static_cast(object()); + if (me_failed(Int::IntView(s->length).eq(home,s->n))) + home.fail(); + } + + template + void + OpenVarList::subscribe(Space& home, Propagator& p) { + static_cast(object())->subscribe(home,p); + } + + template + void + OpenVarList::cancel(Space& home, Propagator& p) { + static_cast(object())->cancel(home,p); + } + + template + void + OpenVarList::reschedule(Space& home, Propagator& p) { + static_cast(object())->reschedule(home,p); + } + +} + +namespace Gecode { namespace Int { + + /// Construct an element view without changing the shared source list + template + forceinline View + open_list_view(typename View::VarType x) { + return View(x); + } + + template<> + forceinline MinusView + open_list_view(IntVar x) { + return MinusView(IntView(x)); + } + +}} + +namespace Gecode { + + template + OpenViewList::OpenViewList(void) {} + + template + OpenViewList::OpenViewList(const OpenVarList& list) + : LocalHandle(list) {} + + template + void + OpenViewList::update(Space& home, OpenViewList& s) { + LocalHandle::update(home,s); + } + + template + int + OpenViewList::known_length(void) const { + return static_cast(object())->n; + } + + template + View + OpenViewList::operator [](int i) const { + Object* s = static_cast(object()); + assert((i >= 0) && (i < s->n)); + return Int::open_list_view(s->x[i]); + } + + template + Int::IntView + OpenViewList::length(void) const { + return Int::IntView(static_cast(object())->length); + } + + template + void + OpenViewList::materialize(Space& home, int n) { + if (!home.failed()) + static_cast(object())->materialize(home,n); + } + + template + void + OpenViewList::subscribe(Space& home, Propagator& p) { + static_cast(object())->subscribe(home,p); + } + + template + void + OpenViewList::cancel(Space& home, Propagator& p) { + static_cast(object())->cancel(home,p); + } + + template + void + OpenViewList::reschedule(Space& home, Propagator& p) { + static_cast(object())->reschedule(home,p); + } + +} + +// STATISTICS: int-var diff --git a/gecode/int/precede.cpp b/gecode/int/precede.cpp index 65ddbaac34..76dbe262cb 100644 --- a/gecode/int/precede.cpp +++ b/gecode/int/precede.cpp @@ -65,6 +65,30 @@ namespace Gecode { } } + void + precede(Home home, OpenIntVarList list, + int before, int after, IntPropLevel) { + Int::Limits::check(before,"Int::precede"); + Int::Limits::check(after,"Int::precede"); + GECODE_POST; + Int::Precede::Open::post + (home,OpenViewList(list),before,after); + } + + void + precede(Home home, OpenIntVarList list, + const IntArgs& values, IntPropLevel ipl) { + if (values.size() < 2) + return; + for (int i=values.size(); i--; ) + Int::Limits::check(values[i],"Int::precede"); + GECODE_POST; + for (int i=values.size()-1; i--; ) + Int::Precede::Open::post + (home,OpenViewList(list),values[i],values[i+1]); + (void) ipl; + } + } // STATISTICS: int-post diff --git a/gecode/int/precede.hh b/gecode/int/precede.hh index 9dccfe3a96..bc8a808614 100644 --- a/gecode/int/precede.hh +++ b/gecode/int/precede.hh @@ -106,6 +106,8 @@ namespace Gecode { namespace Int { namespace Precede { #include +#include + #endif // STATISTICS: int-prop diff --git a/gecode/int/precede/open.hpp b/gecode/int/precede/open.hpp new file mode 100644 index 0000000000..a29b4e60a1 --- /dev/null +++ b/gecode/int/precede/open.hpp @@ -0,0 +1,190 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INT_PRECEDE_OPEN_HPP +#define GECODE_INT_PRECEDE_OPEN_HPP + +#include +#include + +namespace Gecode { namespace Int { namespace Precede { + + template + class Open : public OpenListPropagator { + protected: + using OpenListPropagator::list; + using OpenListPropagator::length; + using OpenListPropagator::processed; + using OpenListPropagator::closed; + + /// Keep the view as well as its position for independent disposal + class Index : public Advisor { + public: + View x; + int i; + + Index(Space& home, Propagator& p, Council& c, + View x0, int i0) + : Advisor(home,p,c), x(x0), i(i0) {} + + Index(Space& home, Index& a) + : Advisor(home,a), i(a.i) { + x.update(home,a.x); + } + }; + + Council council; + int before; + int after; + /// First and second positions that can still take before + int alpha; + int beta; + /// First position assigned after, or infinity while none is known + int gamma; + + Open(Home home, OpenViewList list, + int before0, int after0) + : OpenListPropagator(home,list), council(home), + before(before0), after(after0), alpha(0), beta(0), + gamma(Limits::max) {} + + Open(Space& home, Open& p) + : OpenListPropagator(home,p), council(home), + before(p.before), after(p.after), + alpha(p.alpha), beta(p.beta), gamma(p.gamma) { + council.update(home,p.council); + } + + public: + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::LO,list.known_length()-alpha); + } + + virtual Actor* + copy(Space& home) { + return new (home) Open(home,*this); + } + + virtual ExecStatus + advise(Space& home, Advisor& a0, const Delta&) { + Index& a = static_cast(a0); + const int i = a.i; + bool changed = false; + if (a.x.assigned()) { + if ((a.x.val() == after) && (i < gamma)) { + gamma = i; + changed = true; + } + changed |= (i == alpha) || (i == beta); + a.dispose(home,council); + } else if ((i < alpha) || (i > gamma)) { + a.x.cancel(home,a); + a.dispose(home,council); + } else { + changed = ((i == alpha) || (i == beta)) && !a.x.in(before); + } + return changed ? ES_NOFIX : ES_FIX; + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + const int size = list.known_length(); + if (before == after) { + for (int i=processed; i gamma) { + GECODE_ME_CHECK(first.eq(home,before)); + return home.ES_SUBSUMED(*this); + } + return ES_FIX; + } + + virtual void + reschedule(Space& home) { + OpenListPropagator::reschedule(home); + View::schedule(home,*this,ME_INT_DOM); + } + + virtual size_t + dispose(Space& home) { + for (Advisors as(council); as(); ++as) + as.advisor().x.cancel(home,as.advisor()); + council.dispose(home); + OpenListPropagator::dispose_base(home); + return sizeof(*this); + } + + static void + post(Home home, OpenViewList list, int before, int after) { + (void) new (home) Open(home,list,before,after); + } + }; + +}}} + +#endif + +// STATISTICS: int-prop diff --git a/gecode/int/rel.cpp b/gecode/int/rel.cpp index 45f9f475d0..00fb2878db 100755 --- a/gecode/int/rel.cpp +++ b/gecode/int/rel.cpp @@ -623,6 +623,37 @@ namespace Gecode { rel(home,y,irt,x,ipl); } + void + rel(Home home, OpenIntVarList list, + IntRelType irt, IntPropLevel ipl) { + if ((irt < IRT_EQ) || (irt > IRT_GR)) + throw Int::UnknownRelation("Int::rel"); + GECODE_POST; + OpenViewList x(list); + switch (irt) { + case IRT_EQ: + Int::Rel::Open::post(home,x,ipl); + break; + case IRT_NQ: + Int::Rel::Open::post(home,x,ipl); + break; + case IRT_LQ: + Int::Rel::Open::post(home,x,ipl); + break; + case IRT_LE: + Int::Rel::Open::post(home,x,ipl); + break; + case IRT_GQ: + Int::Rel::Open::post(home,x,ipl); + break; + case IRT_GR: + Int::Rel::Open::post(home,x,ipl); + break; + default: + GECODE_NEVER; + } + } + } // STATISTICS: int-post diff --git a/gecode/int/rel.hh b/gecode/int/rel.hh index 5818fb33a6..e846ab0574 100755 --- a/gecode/int/rel.hh +++ b/gecode/int/rel.hh @@ -697,6 +697,8 @@ namespace Gecode { namespace Int { namespace Rel { #include #include +#include + #endif diff --git a/gecode/int/rel/open.hpp b/gecode/int/rel/open.hpp new file mode 100644 index 0000000000..f3c366ff3a --- /dev/null +++ b/gecode/int/rel/open.hpp @@ -0,0 +1,268 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INT_REL_OPEN_HPP +#define GECODE_INT_REL_OPEN_HPP + +#include +#include + +namespace Gecode { namespace Int { namespace Rel { + + /// Propagate a relation over an open chain in one actor + template + class Open : public OpenListPropagator { + protected: + using OpenListPropagator::list; + using OpenListPropagator::processed; + using OpenListPropagator::closed; + + /// Identify the position whose domain changed + class Position : public Advisor { + public: + int index; + + Position(Space& home, Propagator& p, Council& council, + int index0) + : Advisor(home,p,council), index(index0) {} + + Position(Space& home, Position& a) + : Advisor(home,a), index(a.index) {} + + void + dispose(Space& home, Council& council) { + Advisor::dispose(home,council); + } + }; + + Council council; + /// Inclusive range of positions needing propagation + int first; + int last; + /// Whether equality propagates holes as well as bounds + bool domain; + + Open(Home home, OpenViewList list, IntPropLevel ipl) + : OpenListPropagator(home,list), council(home), + first(Int::Limits::max), last(-1), + domain((vbd(ipl) == IPL_DOM) || (vbd(ipl) == IPL_DEF)) {} + + Open(Space& home, Open& p) + : OpenListPropagator(home,p), council(home), + first(p.first), last(p.last), domain(p.domain) { + council.update(home,p.council); + } + + void + changed(int index) { + first = std::min(first,index); + last = std::max(last,index); + } + + /// Read decreasing relations in increasing order + View + ordered(int index, int size) const { + return list[((irt == IRT_GQ) || (irt == IRT_GR)) + ? size-1-index : index]; + } + + /// Accumulate the domain intersection in the first position + ExecStatus + propagate_equal(Space& home, int begin, int end, int size) { + View common(list[0]); + const unsigned int old_size = common.size(); + for (int i=begin; i<=end; i++) { + View x(list[i]); + if (x == common) + continue; + if (domain) { + ViewRanges ranges(x); + GECODE_ME_CHECK(common.inter_r(home,ranges,false)); + } else { + GECODE_ME_CHECK(common.gq(home,x.min())); + GECODE_ME_CHECK(common.lq(home,x.max())); + } + } + // If the common domain is unchanged, update only the changed positions. + if ((begin == 0) || (common.size() != old_size)) { + begin = 1; + end = size-1; + } + for (int i=begin; i<=end; i++) { + View x(list[i]); + if (x == common) + continue; + if (domain) { + ViewRanges ranges(common); + GECODE_ME_CHECK(x.inter_r(home,ranges,false)); + } else { + GECODE_ME_CHECK(x.gq(home,common.min())); + GECODE_ME_CHECK(x.lq(home,common.max())); + } + } + return ES_OK; + } + + /// Propagate assignments to their immediate neighbors + ExecStatus + propagate_different(Space& home, int begin, int end, int size) { + for (int i=begin; i<=end; i++) { + View x(list[i]); + if (!x.assigned()) + continue; + if (i > 0) + GECODE_ME_CHECK(list[i-1].nq(home,x.val())); + if (i+1 < size) + GECODE_ME_CHECK(list[i+1].nq(home,x.val())); + } + return ES_OK; + } + + /// Sweep lower bounds forward and upper bounds backward from changed positions + ExecStatus + propagate_order(Space& home, int begin, int end, int size) { + const int offset = ((irt == IRT_LE) || (irt == IRT_GR)) ? 1 : 0; + if constexpr ((irt == IRT_GQ) || (irt == IRT_GR)) { + const int old_begin = begin; + begin = size-1-end; + end = size-1-old_begin; + } + for (int i=std::max(1,begin); i end) && !me_modified(me)) + break; + } + for (int i=std::min(size-2,end); i>=0; i--) { + ModEvent me = ordered(i,size).lq(home,ordered(i+1,size).max()-offset); + if (me_failed(me)) + return ES_FAILED; + if ((i < begin) && !me_modified(me)) + break; + } + return ES_OK; + } + + public: + virtual Actor* + copy(Space& home) { + return new (home) Open(home,*this); + } + + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::LO,list.known_length()); + } + + virtual ExecStatus + advise(Space& home, Advisor& advisor, const Delta& delta) { + Position& a = static_cast(advisor); + View x(list[a.index]); + if constexpr (irt == IRT_NQ) { + if (!x.assigned()) + return ES_FIX; + } else if (((irt != IRT_EQ) || !domain) && + (View::modevent(delta) == ME_INT_DOM)) { + return ES_FIX; + } + changed(a.index); + if (x.assigned()) + return home.ES_NOFIX_DISPOSE(council,a); + return ES_NOFIX; + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + const int size = list.known_length(); + if (processed < size) { + changed(std::max(0,processed-1)); + changed(size-1); + } + for (int i=processed; i 0) && (x == list[i-1])) + return ES_FAILED; + if (!x.assigned()) + x.subscribe(home,*new (home) Position(home,*this,council,i)); + } + processed = size; + + // Advisors record further changes caused by sparse bounds or aliases. + while (first <= last) { + const int begin = first; + const int end = last; + first = Int::Limits::max; + last = -1; + if constexpr (irt == IRT_EQ) { + GECODE_ES_CHECK(propagate_equal(home,begin,end,size)); + } else if constexpr (irt == IRT_NQ) { + GECODE_ES_CHECK(propagate_different(home,begin,end,size)); + } else { + GECODE_ES_CHECK(propagate_order(home,begin,end,size)); + } + } + return (closed() && ((size < 2) || council.empty())) + ? home.ES_SUBSUMED(*this) : ES_FIX; + } + + virtual void + reschedule(Space& home) { + OpenListPropagator::reschedule(home); + first = 0; + last = list.known_length()-1; + View::schedule(home,*this,ME_INT_DOM); + } + + virtual size_t + dispose(Space& home) { + if (!home.failed()) + for (Advisors a(council); a(); ++a) + list[a.advisor().index].cancel(home,a.advisor()); + council.dispose(home); + OpenListPropagator::dispose_base(home); + return sizeof(*this); + } + + static void + post(Home home, OpenViewList list, IntPropLevel ipl) { + (void) new (home) Open(home,list,ipl); + } + }; + +}}} + +#endif + +// STATISTICS: int-prop diff --git a/gecode/int/sequence.cpp b/gecode/int/sequence.cpp index f2e669ee36..008351e590 100755 --- a/gecode/int/sequence.cpp +++ b/gecode/int/sequence.cpp @@ -186,6 +186,51 @@ namespace Gecode { (home,xv,s.min(),q,l,u))); } + void + sequence(Home home, OpenIntVarList x, const IntSet& values, + int width, int lower, int upper, IntPropLevel ipl) { + Int::Limits::check(values.min(),"Int::sequence"); + Int::Limits::check(values.max(),"Int::sequence"); + Int::Limits::check(width,"Int::sequence"); + Int::Limits::check(lower,"Int::sequence"); + Int::Limits::check(upper,"Int::sequence"); + if (width < 1) + throw Int::OutOfLimits("Int::sequence"); + GECODE_POST; + lower = std::max(0,lower); + upper = std::min(width,upper); + if (upper < lower) { + rel(home,x.length(),IRT_LE,width); + return; + } + if ((lower == 0) && (upper == width)) + return; + Int::Sequence::Open::post + (home,OpenViewList(x),values,width,lower,upper,ipl); + } + + void + sliding_sum(Home home, OpenIntVarList list, + int width, int lower, int upper, IntPropLevel ipl) { + Int::Limits::check(width,"Int::sliding_sum"); + Int::Limits::check(lower,"Int::sliding_sum"); + Int::Limits::check(upper,"Int::sliding_sum"); + if (width < 1) + throw Int::OutOfLimits("Int::sliding_sum"); + GECODE_POST; + OpenViewList views(list); + if (upper < lower) { + GECODE_ME_FAIL(views.length().lq(home,width-1)); + return; + } + if (lower == upper) + Int::Sequence::OpenSlidingSumEq::post + (home,views,width,lower,ipl); + else + Int::Sequence::OpenSlidingSumBnd::post + (home,views,width,lower,upper); + } + } // STATISTICS: int-post diff --git a/gecode/int/sequence.hh b/gecode/int/sequence.hh index 8bb247aa9a..d98646e818 100644 --- a/gecode/int/sequence.hh +++ b/gecode/int/sequence.hh @@ -149,6 +149,8 @@ namespace Gecode { namespace Int { namespace Sequence { #include #include +#include + #endif // STATISTICS: int-prop diff --git a/gecode/int/sequence/open.hpp b/gecode/int/sequence/open.hpp new file mode 100644 index 0000000000..55c5271396 --- /dev/null +++ b/gecode/int/sequence/open.hpp @@ -0,0 +1,532 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_INT_SEQUENCE_OPEN_HPP +#define GECODE_INT_SEQUENCE_OPEN_HPP + +#include +#include +#include +#include + +#include + +namespace Gecode { namespace Int { namespace Sequence { + + /// Identify the windows affected by a position's domain change + class OpenWindowAdvisor : public Advisor { + public: + int index; + + OpenWindowAdvisor(Space& home, Propagator& p, + Council& council, int index0) + : Advisor(home,p,council), index(index0) {} + + OpenWindowAdvisor(Space& home, OpenWindowAdvisor& a) + : Advisor(home,a), index(a.index) {} + + void + dispose(Space& home, Council& council) { + Advisor::dispose(home,council); + } + }; + + /// Subscribe once per position and revisit only affected complete windows + template + class OpenWindowPropagator : public OpenListPropagator { + protected: + using OpenListPropagator::list; + using OpenListPropagator::processed; + + int width; + /// Inclusive range of window starts needing propagation + int first; + int last; + Council council; + + OpenWindowPropagator(Home home, OpenViewList list, int width0) + : OpenListPropagator(home,list), width(width0), + first(Int::Limits::max), last(-1), council(home) {} + + OpenWindowPropagator(Space& home, OpenWindowPropagator& p) + : OpenListPropagator(home,p), width(p.width), + first(p.first), last(p.last), council(home) { + council.update(home,p.council); + } + + void + changed(int index) { + first = std::min(first,std::max(0,index-width+1)); + last = std::max(last,std::min(index,list.known_length()-width)); + } + + void + appended(int size) { + if ((size > processed) && (size >= width)) { + first = std::min(first,std::max(0,processed-width+1)); + last = size-width; + } + processed = size; + } + + void + subscribe(Space& home, int index) { + View x(list[index]); + x.subscribe(home,*new (home) + OpenWindowAdvisor(home,*this,council,index)); + } + + void + dispose_windows(Space& home) { + if (!home.failed()) + for (Advisors a(council); a(); ++a) { + View x(list[a.advisor().index]); + if (!x.assigned()) + x.cancel(home,a.advisor()); + } + council.dispose(home); + OpenListPropagator::dispose_base(home); + } + + public: + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::HI,list.known_length()); + } + + virtual void + reschedule(Space& home) { + OpenListPropagator::reschedule(home); + first = 0; + last = list.known_length()-width; + View::schedule(home,*this,ME_INT_DOM); + } + }; + + + /// Membership counts over complete windows of an open list + template + class Open : public OpenWindowPropagator { + protected: + using OpenWindowPropagator::list; + using OpenWindowPropagator::processed; + using OpenWindowPropagator::closed; + using OpenWindowPropagator::width; + using OpenWindowPropagator::first; + using OpenWindowPropagator::last; + using OpenWindowPropagator::council; + using OpenWindowPropagator::appended; + using OpenWindowPropagator::changed; + using OpenWindowPropagator::subscribe; + using OpenWindowPropagator::dispose_windows; + + IntSet values; + int lower; + int upper; + + Open(Home home, OpenViewList list, + const IntSet& values0, int width0, + int lower0, int upper0) + : OpenWindowPropagator(home,list,width0), values(values0), + lower(lower0), upper(upper0) { + home.notice(*this,AP_DISPOSE); + } + + Open(Space& home, Open& p) + : OpenWindowPropagator(home,p), values(p.values), + lower(p.lower), upper(p.upper) {} + + public: + virtual Actor* + copy(Space& home) { + return new (home) Open(home,*this); + } + + virtual ExecStatus + advise(Space& home, Advisor& advisor, const Delta&) { + OpenWindowAdvisor& a = static_cast(advisor); + View x(list[a.index]); + if (undecided(x,values)) + return ES_FIX; + changed(a.index); + if (!x.assigned()) + x.cancel(home,a); + return home.ES_NOFIX_DISPOSE(council,a); + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + const int size = list.known_length(); + for (int i=processed; i(n); + for (int i=0; i upper) || (may < lower)) + return ES_FAILED; + if (must == upper) + exclude_end = i+width; + if (may == lower) + include_end = i+width; + if (i < end-begin) { + must -= status[i] == TS_YES; + may -= status[i] != TS_NO; + must += status[i+width] == TS_YES; + may += status[i+width] != TS_NO; + } + } + if (status[i] == TS_MAYBE) { + if ((i < include_end) && (i < exclude_end)) + return ES_FAILED; + View x(list[begin+i]); + if (i < exclude_end) + GECODE_ME_CHECK(exclude(home,x,values)); + else if (i < include_end) + GECODE_ME_CHECK(include(home,x,values)); + } + } + } + if (first <= last) + return ES_NOFIX; + return closed() && ((size < width) || council.empty()) ? + home.ES_SUBSUMED(*this) : ES_FIX; + } + + virtual size_t + dispose(Space& home) { + home.ignore(*this,AP_DISPOSE); + values.~IntSet(); + dispose_windows(home); + return sizeof(*this); + } + + static void + post(Home home, OpenViewList list, const IntSet& values, + int width, int lower, int upper, IntPropLevel) { + (void) new (home) + Open(home,list,values,width,lower,upper); + } + }; + + + /// Equal sums via the first complete window and periodic equalities + template + class OpenSlidingSumEq : public OpenListPropagator { + protected: + using OpenListPropagator::list; + using OpenListPropagator::processed; + using OpenListPropagator::closed; + + int width; + int sum; + IntPropLevel ipl; + + OpenSlidingSumEq(Home home, OpenViewList list, + int width0, int sum0, IntPropLevel ipl0) + : OpenListPropagator(home,list), + width(width0), sum(sum0), ipl(ipl0) { + // Choose the default from the full window, before linear posting + // eliminates assigned terms. Delayed posting then has the same + // propagation strength as posting before those assignments. + if (vbd(ipl) == IPL_DEF) + ipl = static_cast + (ipl | ((width <= 2) ? IPL_DOM : IPL_BND)); + } + + OpenSlidingSumEq(Space& home, OpenSlidingSumEq& p) + : OpenListPropagator(home,p), + width(p.width), sum(p.sum), ipl(p.ipl) {} + + static void + linear_term(Linear::Term& term, IntView x) { + term.a = 1; + term.x = x; + } + + static void + linear_term(Linear::Term& term, MinusView x) { + term.a = -1; + term.x = x.base(); + } + + static ExecStatus + binary_sum(Home home, IntView x, IntView y, int sum) { + return Rel::EqDom::post + (home,MinusView(x),OffsetView(y,-sum)); + } + + static ExecStatus + binary_sum(Home home, MinusView x, MinusView y, int sum) { + return binary_sum(home,x.base(),y.base(),-sum); + } + + public: + virtual Actor* + copy(Space& home) { + return new (home) OpenSlidingSumEq(home,*this); + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + const int size = list.known_length(); + if ((processed < width) && (size >= width)) { + if (width == 1) { + GECODE_ME_CHECK(list[0].eq(home,sum)); + } else if ((width == 2) && (vbd(ipl) == IPL_DOM)) { + View x(list[0]); + View y(list[1]); + // Clip with wide residuals before constructing y-sum: its + // range must fit integer limits. Binary domain filtering then + // uses range intersections, even for very large domains. + const long long int target = sum; + GECODE_ME_CHECK(x.gq(home,target-y.max())); + GECODE_ME_CHECK(x.lq(home,target-y.min())); + GECODE_ME_CHECK(y.gq(home,target-x.max())); + GECODE_ME_CHECK(y.lq(home,target-x.min())); + GECODE_ES_CHECK(binary_sum(home(*this),x,y,sum)); + } else { + Region region; + Linear::Term* terms = + region.alloc>(width); + for (int i=0; i::post + (home(*this),x,representative))); + } else { + GECODE_ES_CHECK((Rel::EqBnd::post + (home(*this),x,representative))); + } + } + processed = size; + return closed() ? home.ES_SUBSUMED(*this) : ES_FIX; + } + + virtual size_t + dispose(Space& home) { + OpenListPropagator::dispose_base(home); + return sizeof(*this); + } + + static void + post(Home home, OpenViewList list, + int width, int sum, IntPropLevel ipl) { + (void) new (home) OpenSlidingSumEq(home,list,width,sum,ipl); + } + }; + + + /// Bounds filtering for ranged sums over complete windows + template + class OpenSlidingSumBnd : public OpenWindowPropagator { + protected: + using OpenWindowPropagator::list; + using OpenWindowPropagator::processed; + using OpenWindowPropagator::closed; + using OpenWindowPropagator::width; + using OpenWindowPropagator::first; + using OpenWindowPropagator::last; + using OpenWindowPropagator::council; + using OpenWindowPropagator::changed; + using OpenWindowPropagator::appended; + using OpenWindowPropagator::subscribe; + using OpenWindowPropagator::dispose_windows; + + int lower; + int upper; + + OpenSlidingSumBnd(Home home, OpenViewList list, + int width0, int lower0, int upper0) + : OpenWindowPropagator(home,list,width0), + lower(lower0), upper(upper0) {} + + OpenSlidingSumBnd(Space& home, OpenSlidingSumBnd& p) + : OpenWindowPropagator(home,p), + lower(p.lower), upper(p.upper) {} + + public: + virtual Actor* + copy(Space& home) { + return new (home) OpenSlidingSumBnd(home,*this); + } + + virtual ExecStatus + advise(Space& home, Advisor& advisor, const Delta& delta) { + if (View::modevent(delta) == ME_INT_DOM) + return ES_FIX; + OpenWindowAdvisor& a = static_cast(advisor); + changed(a.index); + if (list[a.index].assigned()) + return home.ES_NOFIX_DISPOSE(council,a); + return ES_NOFIX; + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + const int size = list.known_length(); + for (int i=processed; i(n); + int* maximum = region.alloc(n); + for (int i=0; i(windows); + for (int start=0; start upper) || (max_sum < lower)) + return ES_FAILED; + residual[start].lower = static_cast(lower)-max_sum; + residual[start].upper = static_cast(upper)-min_sum; + if (start+1 < windows) { + min_sum -= minimum[start]; + max_sum -= maximum[start]; + min_sum += minimum[start+width]; + max_sum += maximum[start+width]; + } + } + // Monotone queues track the strongest residuals among windows + // covering each position. Every window enters and leaves once. + int* lower_queue = region.alloc(windows); + int* upper_queue = region.alloc(windows); + int lower_head = 0; + int lower_tail = 0; + int upper_head = 0; + int upper_tail = 0; + for (int i=0; i= + residual[i].upper)) + upper_tail--; + upper_queue[upper_tail++] = i; + } + View x(list[begin+i]); + GECODE_ME_CHECK(x.gq(home, + residual[lower_queue[lower_head]].lower+maximum[i])); + GECODE_ME_CHECK(x.lq(home, + residual[upper_queue[upper_head]].upper+minimum[i])); + } + } + if (first <= last) + return ES_NOFIX; + return closed() && ((size < width) || council.empty()) ? + home.ES_SUBSUMED(*this) : ES_FIX; + } + + virtual size_t + dispose(Space& home) { + dispose_windows(home); + return sizeof(*this); + } + + static void + post(Home home, OpenViewList list, + int width, int lower, int upper) { + (void) new (home) + OpenSlidingSumBnd(home,list,width,lower,upper); + } + }; + + +}}} + +#endif + +// STATISTICS: int-prop diff --git a/test/int.cpp b/test/int.cpp index 949652866d..96ee7e6042 100755 --- a/test/int.cpp +++ b/test/int.cpp @@ -36,6 +36,7 @@ #include "test/int.hh" #include +#include namespace Test { namespace Int { @@ -1155,6 +1156,179 @@ do { \ return false; } + OpenTest::OpenTest(const std::string& name, int items0, int extra, + const Gecode::IntSet& domain, Gecode::IntPropLevel level) + : Test(name,items0+extra,domain,false,level), items(items0) {} + + void + OpenTest::post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::OpenIntVarList list(home,dom,items); + post(home,list,x); + for (int i=0; i(*this, + [](Gecode::Space& home, int i) { + OpenTestSpace& s = static_cast(home); + return (i < s.items) ? s.x[i] : Gecode::IntVar(home,s.d); + },items+1)) {} + + OpenTestSpace(OpenTestSpace& s) + : TestSpace(s), items(s.items) { + list.update(*this,s.list); + } + + Gecode::Space* copy(void) override { + return new OpenTestSpace(*this); + } + + void post_open(void) { + if (opt.log) + olog << ind(3) << "Posting with " << list.known_length() + << " open-list items" << std::endl; + static_cast(test)->post(*this,list,x); + } + + /// Whether the target assignment and length are possible after propagation + bool retains(const Assignment& a) { + if (failed() || !list.length().in(items)) + return false; + for (int i=0; i assignments(assignment()); + Assignment& a = *assignments; + for (; a.has_more() && ((solutions < 16) || (nonsolutions < 16)); + a.next(_rand)) { + const bool sol = solution(a); + int& checked = sol ? solutions : nonsolutions; + if (checked == 16) + continue; + checked++; + if (opt.log) { + olog.str(""); + olog << ind(1) << "Open-list assignment: " << a + << (sol ? " (solution)" : " (no solution)") << std::endl; + } + + const auto complete = [&](OpenTestSpace& s) { + s.list.materialize(s,items); + if (_rand(2) == 0) + s.list.close(s); + s.assign(a,false,_rand); + s.list.close(s); + Gecode::rel(s,s.list.length(),IRT_EQ,items); + return s.failed() == !sol; + }; + + const auto history = [&]() { + OpenTestSpace s(arity,dom,this,items); + const int post_at = (items == 0) ? 0 : + static_cast(_rand(static_cast(items))); + const int clone_at = (items == 0) ? 0 : post_at + + static_cast(_rand(static_cast(items-post_at))); + for (int n=0; n<=items; n++) { + if (n == post_at) + s.post_open(); + if (!s.retains(a)) + return complete(s); + + // Restrict an old item or an auxiliary variable, keeping the target. + const int known = std::min(items,s.list.known_length()); + const int available = known+arity-items; + if (available > 0) { + int i = static_cast + (_rand(static_cast(available))); + if (i >= known) + i += items-known; + if (!s.x[i].assigned()) { + if (_rand(4) == 0) { + s.rel(i,IRT_EQ,a[i]); + } else { + IntRelType relation; + int value; + s.rndrel(a,i,relation,value,_rand); + if (relation != IRT_EQ) + s.rel(i,relation,value); + } + } + } + if (!s.retains(a)) + return complete(s); + + if (n == clone_at) { + if (opt.log) + olog << ind(3) << "Cloning open list" << std::endl; + const int before = s.list.known_length(); + const int length_min = s.list.length().min(); + const int length_max = s.list.length().max(); + IntArgs sizes(arity); + for (int i=0; i(s.x[i].size()); + std::unique_ptr c + (static_cast(s.clone())); + if (!complete(*c)) + return false; + if ((s.list.known_length() != before) || + (s.list.length().min() != length_min) || + (s.list.length().max() != length_max)) + return false; + for (int i=0; i(sizes[i])) + return false; + } + + if ((n < items) && (s.list.known_length() <= n)) { + const int end = std::min(items,n+1+static_cast(_rand(2))); + if (_rand(2) == 0) { + if (opt.log) + olog << ind(3) << "Appending through item " << end-1 + << std::endl; + for (int i=s.list.known_length(); i static bool cmp(T x, Gecode::IntRelType r, T y); //@} }; + + /** + * \brief Integer constraint tests with incremental open-list histories + * + * The first items assignment entries belong to the list; the extra entries + * are auxiliary variables. Override solution() for the complete relation + * and the open-list post() overload. All variables use + * the supplied domain, including auxiliary variables; restrictions posted + * by the test must also be represented in solution(). + * + * Runs the ordinary closed-list tests and bounded randomized histories + * that post, grow, prune, clone, and close the list. During these histories + * the list can grow one position beyond the target until it is closed. + */ + class GECODE_TESTINT_EXPORT OpenTest : public Test { + protected: + /// Number of items in the completed list + int items; + public: + /// Initialize item and auxiliary variables with the common domain + OpenTest(const std::string& name, int items, int extra, + const Gecode::IntSet& domain, + Gecode::IntPropLevel ipl=Gecode::IPL_DEF); + /// Post the tested relation on an open list and its variable array + virtual void post(Gecode::Space& home, Gecode::OpenIntVarList list, + Gecode::IntVarArray& x) = 0; + /// Adapt the relation to the ordinary closed-list tests + void post(Gecode::Space& home, Gecode::IntVarArray& x) override; + /// Run closed-list tests and incremental histories + bool run(void) override; + }; //@} /// Iterator for simple integer propagation levels diff --git a/test/int/extensional.cpp b/test/int/extensional.cpp index 7422bf4896..d9311e7d70 100755 --- a/test/int/extensional.cpp +++ b/test/int/extensional.cpp @@ -855,6 +855,298 @@ namespace Test { namespace Int { } }; + /// Space used by open regular tests + class OpenRegularSpace : public Gecode::Space { + public: + Gecode::IntVarArray x; + Gecode::OpenIntVarList list; + + OpenRegularSpace(Gecode::DFA d, int max=8) + : x(*this,3,0,2), list(*this,max) { + Gecode::extensional(*this,list,d); + } + + OpenRegularSpace(OpenRegularSpace& s) + : Gecode::Space(s) { + x.update(*this,s.x); + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new OpenRegularSpace(*this); + } + }; + + /// Test appending and closing an open regular list + class OpenRegularIncremental : public ::Test::Base { + public: + OpenRegularIncremental(void) + : ::Test::Base("Int::Extensional::OpenReg::Incremental", + TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0,{{0,0,1},{1,1,2}},{2},false); + OpenRegularSpace s(d); + + if (s.status() == SS_FAILED) + return false; + s.list.append(s,s.x[0]); + if ((s.status() == SS_FAILED) || + !s.x[0].assigned() || (s.x[0].val() != 0)) + return false; + s.list.append(s,s.x[1]); + if ((s.status() == SS_FAILED) || + !s.x[1].assigned() || (s.x[1].val() != 1)) + return false; + s.list.close(s); + if ((s.status() == SS_FAILED) || + !s.list.length().assigned() || + (s.list.length().val() != 2)) + return false; + + // Pruning the first item to 1 also closes its aliased length. + DFA alias_dfa(0,{{0,1,1},{1,2,2}},{2},false); + OpenRegularSpace alias(alias_dfa,2); + alias.list.append(alias,alias.list.length()); + return alias.status() == SS_FAILED; + } + }; + + /// Materializing required positions wakes regular filtering + class OpenRegularLengthWake : public ::Test::Base { + public: + OpenRegularLengthWake(void) + : ::Test::Base("Int::Extensional::OpenReg::LengthWake") {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0,{{0,0,1},{1,1,2}},{2},false); + OpenRegularSpace s(d,3); + rel(s,s.list.length(),IRT_GQ,2); + if (s.status() == SS_FAILED) + return false; + return (s.list.known_length() == 2) && + s.list[0].assigned() && + (s.list[0].val() == 0) && + s.list[1].assigned() && + (s.list[1].val() == 1); + } + }; + + /// Filter prefixes whose accepting continuations exceed the length bound + class OpenRegularMaxLength : public ::Test::Base { + public: + OpenRegularMaxLength(void) + : ::Test::Base("Int::Extensional::OpenReg::MaxLength", + TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + // Accept 00 or 1000: only the first word fits a maximum of three. + DFA d(0,{{0,0,1},{1,0,5},{0,1,2},{2,0,3},{3,0,4},{4,0,5}}, + {5},false); + OpenRegularSpace short_list(d,1); + if (short_list.status() != SS_FAILED) + return false; + OpenRegularSpace initial(d,3); + initial.list.append(initial,initial.x[0]); + if ((initial.status() == SS_FAILED) || + !initial.x[0].assigned() || (initial.x[0].val() != 0)) + return false; + + OpenRegularSpace source(d,4); + source.list.append(source,source.x[0]); + if ((source.status() == SS_FAILED) || (source.x[0].size() != 2)) + return false; + OpenRegularSpace* clone = + static_cast(source.clone()); + rel(*clone,clone->list.length(),IRT_LQ,3); + const bool clone_ok = (clone->status() != SS_FAILED) && + clone->x[0].assigned() && (clone->x[0].val() == 0); + delete clone; + if (!clone_ok || (source.x[0].size() != 2)) + return false; + + // A bound reduction also checks an already consumed prefix. + rel(source,source.x[0],IRT_EQ,1); + if (source.status() == SS_FAILED) + return false; + rel(source,source.list.length(),IRT_LQ,3); + if (source.status() != SS_FAILED) + return false; + + // Keep the shorter nondeterministic path, even with a cycle. + DFA nfa = DFA::nfa(0, + {{0,0,1},{0,0,2},{1,0,3},{3,0,4},{4,0,5},{2,1,2},{2,0,5}}, + {5}); + OpenRegularSpace nondeterministic(nfa,2); + nondeterministic.list.append(nondeterministic,nondeterministic.x[0]); + if (nondeterministic.status() == SS_FAILED) + return false; + nondeterministic.list.append(nondeterministic,nondeterministic.x[1]); + if ((nondeterministic.status() == SS_FAILED) || + !nondeterministic.x[1].assigned() || + (nondeterministic.x[1].val() != 0)) + return false; + + // Pruning an item aliased with length lowers the maximum while open. + DFA alias_dfa(0, + {{0,2,1},{1,0,2},{2,0,3},{3,0,4},{0,3,5},{5,0,6},{6,0,4}}, + {4},false); + OpenRegularSpace alias(alias_dfa,5); + alias.list.append(alias,alias.list.length()); + alias.list.append(alias,alias.x[0]); + return (alias.status() != SS_FAILED) && + alias.list.length().assigned() && + (alias.list.length().val() == 3); + } + }; + + /// Test backward pruning after appending a restricted variable + class OpenRegularBackward : public ::Test::Base { + public: + OpenRegularBackward(void) + : ::Test::Base("Int::Extensional::OpenReg::Backward") {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0, + { + {0,0,1}, {0,1,2}, + {1,0,3}, {2,1,4}, + {3,2,5}, {4,2,5} + }, + {5},false); + OpenRegularSpace s(d); + rel(s,s.x[1],IRT_EQ,0); + s.list.append(s,s.x[0]); + if ((s.status() == SS_FAILED) || + (s.x[0].min() != 0) || (s.x[0].max() != 1)) + return false; + s.list.append(s,s.x[1]); + return (s.status() != SS_FAILED) && + s.x[0].assigned() && (s.x[0].val() == 0); + } + }; + + /// Test domain notification on an appended variable + class OpenRegularDomainWake : public ::Test::Base { + public: + OpenRegularDomainWake(void) + : ::Test::Base("Int::Extensional::OpenReg::DomainWake") {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0, + { + {0,0,1}, {0,1,2}, + {1,0,3}, {2,1,4}, + {3,2,5}, {4,2,5} + }, + {5},false); + OpenRegularSpace s(d); + s.list.append(s,s.x[0]); + s.list.append(s,s.x[1]); + if ((s.status() == SS_FAILED) || s.x[0].assigned()) + return false; + rel(s,s.x[1],IRT_EQ,0); + return (s.status() != SS_FAILED) && + s.x[0].assigned() && (s.x[0].val() == 0); + } + }; + + /// Test independent extension after cloning + class OpenRegularClone : public ::Test::Base { + public: + OpenRegularClone(void) + : ::Test::Base("Int::Extensional::OpenReg::Clone",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0,{{0,0,1},{1,1,2}},{2},false); + OpenRegularSpace source(d); + source.list.append(source,source.x[0]); + if (source.status() == SS_FAILED) + return false; + + OpenRegularSpace* clone = + static_cast(source.clone()); + clone->list.append(*clone,clone->x[1]); + clone->list.close(*clone); + const bool clone_ok = + (clone->status() != SS_FAILED) && + (clone->list.known_length() == 2); + delete clone; + + source.list.close(source); + const bool source_failed = source.status() == SS_FAILED; + return clone_ok && source_failed && + (source.list.known_length() == 1); + } + }; + + /// Test empty words and automata without accepting states + class OpenRegularEmpty : public Test { + public: + enum class Kind { ACCEPT, REJECT, NO_FINALS }; + protected: + Kind kind; + public: + OpenRegularEmpty(Kind k) + : Test("Extensional::OpenReg::Empty::"+str(static_cast(k)),1,0,0), + kind(k) {} + virtual bool solution(const Assignment&) const { + return kind == Kind::ACCEPT; + } + virtual void post(Gecode::Space& home, Gecode::IntVarArray&) { + using namespace Gecode; + OpenIntVarList list(home,8); + if (kind == Kind::ACCEPT) { + extensional(home,list,DFA()); + } else if (kind == Kind::REJECT) { + extensional(home,list,DFA(0,{{0,0,1}},{1},false)); + } else { + extensional(home,list, + DFA(0,{{0,0,0}},std::initializer_list{},false)); + } + if (kind != Kind::NO_FINALS) + list.close(home); + } + }; + + /// Check domain consistency of the regular constraint on a closed open list + class OpenRegularClosed : public Test { + public: + OpenRegularClosed(void) + : Test("Extensional::OpenReg::Closed",3,0,2,false,Gecode::IPL_DOM) {} + virtual bool solution(const Assignment& x) const { + int parity = 0; + for (int i=0; i + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include "test/int.hh" +#include "examples/common-pattern.hh" + +#include +#include + +#ifdef GECODE_HAS_SET_VARS +#include +#endif +#ifdef GECODE_HAS_FLOAT_VARS +#include +#endif + +#include +#include + +namespace Test { namespace Int { + + namespace OpenList { + + /// Space used by open list materialization tests + class OpenListSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + OpenListSpace(void) + : list(*this, + [](int i) { + return Gecode::IntSet(i,i); + },6) {} + + OpenListSpace(OpenListSpace& s) + : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new OpenListSpace(*this); + } + }; + + /// Test automatic and explicit list materialization + class OpenListMaterialize : public ::Test::Base { + public: + OpenListMaterialize(void) + : ::Test::Base("Int::OpenList::Materialize",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + OpenListSpace source; + rel(source,source.list.length(),IRT_GQ,3); + if ((source.status() == SS_FAILED) || + (source.list.known_length() != 3)) + return false; + for (int i=0; i<3; i++) + if (!source.list[i].assigned() || + (source.list[i].val() != i)) + return false; + + OpenListSpace* clone = + static_cast(source.clone()); + rel(*clone,clone->list.length(),IRT_GQ,5); + const bool clone_ok = + (clone->status() != SS_FAILED) && + (clone->list.known_length() == 5) && + clone->list[4].assigned() && + (clone->list[4].val() == 4); + delete clone; + if (!clone_ok || (source.list.known_length() != 3)) + return false; + + OpenListSpace direct; + IntVar x = direct.list.get(direct,3); + return (direct.list.known_length() == 4) && + (direct.list.length().min() == 4) && + x.assigned() && (x.val() == 3) && + (direct.status() != SS_FAILED); + } + }; + + OpenListMaterialize open_list_materialize; + + class BranchOrderSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + enum class Order { INCREMENTAL, LENGTH_MIN, LENGTH_MAX }; + + BranchOrderSpace(Order order) + : list(*this,Gecode::IntSet(0,1),3) { + using namespace Gecode; + DFA accepts_one(0,{ + {0,0,0}, {0,1,1}, + {1,0,1}, {1,1,1} + },{1}); + extensional(*this,list,accepts_one); + if (order == Order::LENGTH_MIN) + branch(*this,list.length(),INT_VAL_MIN()); + else if (order == Order::LENGTH_MAX) + branch(*this,list.length(),INT_VAL_MAX()); + branch(*this,list); + } + + BranchOrderSpace(BranchOrderSpace& s) + : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new BranchOrderSpace(*this); + } + }; + + class BranchOrder : public ::Test::Base { + protected: + static bool + check(BranchOrderSpace::Order order, int first_length) { + using namespace Gecode; + Search::Options options; + options.threads = 1; + options.c_d = 1000; + options.a_d = 1000; + BranchOrderSpace* root = new BranchOrderSpace(order); + DFS engine(root,options); + delete root; + + int solutions = 0; + int first = -1; + while (BranchOrderSpace* solution = engine.next()) { + if (solutions == 0) + first = solution->list.known_length(); + solutions++; + delete solution; + } + return (first == first_length) && (solutions == 11); + } + + public: + BranchOrder(void) + : ::Test::Base("Int::OpenList::BranchOrder") {} + + virtual bool + run(void) { + using namespace Gecode; + return check(BranchOrderSpace::Order::INCREMENTAL,3) && + check(BranchOrderSpace::Order::LENGTH_MIN,1) && + check(BranchOrderSpace::Order::LENGTH_MAX,3); + } + }; + + class SelectorSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + SelectorSpace(Gecode::IntValBranch vals, bool length_first=false) + : list(*this,[](int i) { return Gecode::IntSet(i,i+1); },3) { + using namespace Gecode; + if (length_first) + branch(*this,list.length(),INT_VAL_MIN()); + branch(*this,list,tiebreak(INT_VAR_SIZE_MIN(),INT_VAR_MAX_MAX()),vals); + } + + SelectorSpace(SelectorSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new SelectorSpace(*this); + } + }; + + class Selectors : public ::Test::Base { + public: + Selectors(void) : ::Test::Base("Int::OpenList::Selectors") {} + + virtual bool + run(void) { + using namespace Gecode; + const IntValBranch values[] = { + INT_VAL_MIN(), INT_VAL_MAX(), INT_VAL_MED(), INT_VAL_RND(Rnd(1)), + INT_VAL_SPLIT_MIN(), INT_VAL_SPLIT_MAX(), + INT_VAL_RANGE_MIN(), INT_VAL_RANGE_MAX(), + INT_VALUES_MIN(), INT_VALUES_MAX(), + INT_VAL([](const Space&, IntVar x, int) { return x.max(); }) + }; + for (const IntValBranch& vals : values) + for (bool length_first : {false,true}) { + SelectorSpace root(vals,length_first); + Search::Options options; + options.threads = 1; + options.c_d = 1000; + options.a_d = 1000; + DFS engine(&root,options); + int solutions = 0; + while (SelectorSpace* solution = engine.next()) { + bool valid = solution->list.length().assigned() && + (solution->list.length().val() == solution->list.known_length()); + for (int i=0; ilist.known_length(); i++) + valid &= solution->list[i].assigned() && + (solution->list[i].val() >= i) && + (solution->list[i].val() <= i+1); + delete solution; + if (!valid) + return false; + solutions++; + } + if (solutions != 15) + return false; + } + return true; + } + }; + + class NoGoods : public ::Test::Base { + public: + NoGoods(void) : ::Test::Base("Int::OpenList::NoGoods") {} + + virtual bool + run(void) { + using namespace Gecode; + SelectorSpace root(INT_VAL_MIN()); + root.list.materialize(root,1); + Search::NodeStop stop(6); + Search::Options options; + options.stop = &stop; + options.threads = 1; + options.nogoods_limit = 100; + std::unique_ptr engine + (Search::dfsengine(&root,options)); + bool found[16] = {}; + const auto record = [&found](SelectorSpace* solution) { + int code = 1; + for (int i=0; ilist.known_length(); i++) + code = 2*code + solution->list[i].val()-i; + found[code] = true; + delete solution; + }; + while (Space* solution = engine->next()) + record(static_cast(solution)); + if (!engine->stopped()) + return false; + // Deeper choices refer to positions absent from the restart root. + engine->nogoods().post(root); + DFS restarted(&root); + while (SelectorSpace* solution = restarted.next()) + record(solution); + // Extracted nogoods must allow every unvisited word. + for (int code=2; code<16; code++) + if (!found[code]) + return false; + return true; + } + }; + + class SelectorChoiceSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + SelectorChoiceSpace(Gecode::IntValBranch vals) + : list(*this,Gecode::IntSet({0,2,5}),3) { + using namespace Gecode; + list.materialize(*this,3); + branch(*this,list, + tiebreak(INT_VAR_SIZE_MIN(), + INT_VAR_MERIT_MAX([](const Space&, IntVar, int i) { + return i; + })),vals, + [](const Space&, IntVar, int i) { return i != 1; }, + [](const Space&, const Brancher&, unsigned int, IntVar, + int i, const int& value, std::ostream& out) { + out << i << ":" << value; + }); + } + + SelectorChoiceSpace(SelectorChoiceSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new SelectorChoiceSpace(*this); + } + }; + + class SelectorChoice : public ::Test::Base { + public: + SelectorChoice(void) : ::Test::Base("Int::OpenList::SelectorChoice") {} + + virtual bool + run(void) { + using namespace Gecode; + for (int multi=0; multi<2; multi++) { + SelectorChoiceSpace source(multi ? INT_VALUES_MAX() : INT_VAL_MAX()); + if (source.status() != SS_BRANCH) + return false; + const Choice* original = source.choice(); + Archive archive; + original->archive(archive); + const Choice* restored = source.choice(archive); + bool valid = restored->alternatives() == (multi ? 3U : 2U); + for (unsigned int a=0; aalternatives(); a++) { + SelectorChoiceSpace* clone = + static_cast(source.clone()); + clone->commit(*restored,a); + const int expected[] = {5,2,0}; + valid &= !clone->failed() && !clone->list[0].assigned() && + !clone->list[1].assigned(); + if (multi || (a == 0)) + valid &= clone->list[2].assigned() && + (clone->list[2].val() == expected[a]); + else + valid &= !clone->list[2].in(5); + delete clone; + } + SelectorChoiceSpace* excluded = + static_cast(source.clone()); + NGL* literal = excluded->ngl(*restored,0); + valid &= (literal != nullptr) && (literal->status(*excluded) == NGL::NONE); + if (literal) + valid &= (literal->prune(*excluded) != ES_FAILED) && + !excluded->list[2].in(5); + delete excluded; + std::ostringstream printed; + source.print(*restored,0,printed); + valid &= printed.str() == "2:5"; + delete restored; + delete original; + if (!valid) + return false; + } + return true; + } + }; + + class BoolSpace : public Gecode::Space { + public: + Gecode::OpenBoolVarList list; + + BoolSpace(bool length_first) + : list(*this,3) { + using namespace Gecode; + DFA accepts_one(0,{{0,0,0},{0,1,1},{1,0,1},{1,1,1}},{1}); + extensional(*this,list,accepts_one); + if (length_first) + branch(*this,list.length(),INT_VAL_MIN()); + branch(*this,list, + BOOL_VAR_MERIT_MAX([](const Space&, BoolVar, int i) { return i; }), + BOOL_VAL_MAX()); + } + + BoolSpace(BoolSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new BoolSpace(*this); + } + }; + + class BoolList : public ::Test::Base { + public: + BoolList(void) : ::Test::Base("Int::OpenList::Bool",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + for (bool length_first : {false,true}) + for (int recompute=0; recompute<2; recompute++) { + BoolSpace root(length_first); + Search::Options options; + options.threads = 1; + options.c_d = recompute ? 1000 : 1; + options.a_d = options.c_d; + DFS engine(&root,options); + int solutions = 0; + while (BoolSpace* solution = engine.next()) { + bool valid = solution->list.length().assigned() && + (solution->list.length().val() == solution->list.known_length()); + int ones = 0; + for (int i=0; ilist.known_length(); i++) { + valid &= solution->list[i].assigned(); + ones += solution->list[i].val(); + } + delete solution; + if (!valid || (ones == 0)) + return false; + solutions++; + } + if (solutions != 11) + return false; + } + return true; + } + }; + + template + class AliasSpace : public Gecode::Space { + public: + Gecode::OpenVarList list; + + AliasSpace(void) + : list(*this,[](Gecode::Space& home, int) { + return Var(home,0,1); + },16) {} + + AliasSpace(AliasSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new AliasSpace(*this); + } + + bool + rejects(int i) { + const int n = list.known_length(); + try { + list.append(*this,list[i]); + } catch (const Gecode::Int::ArgumentSame&) { + return list.known_length() == n; + } + return false; + } + }; + + class Aliases : public ::Test::Base { + protected: + template + static bool + check(void) { + using namespace Gecode; + AliasSpace source; + Var assigned(source,0,0); + source.list.append(source,assigned); + source.list.append(source,assigned); + Var first(source,0,1); + source.list.append(source,first); + source.list.append(source,Var(source,0,1)); + if (!source.rejects(2)) + return false; + rel(source,first,IRT_EQ,0); + source.list.append(source,first); + if ((source.list.known_length() != 5) || + (source.status() == SS_FAILED)) + return false; + std::unique_ptr> clone + (static_cast*>(source.clone())); + if (!clone->rejects(3)) + return false; + clone->list.append(*clone,Var(*clone,0,1)); + rel(*clone,clone->list[3],IRT_EQ,0); + clone->list.append(*clone,clone->list[3]); + return (clone->list.known_length() == 7) && + (clone->status() != SS_FAILED) && + (source.list.known_length() == 5) && + !source.list[3].assigned() && source.rejects(3); + } + + public: + Aliases(void) : ::Test::Base("Int::OpenList::Aliases",TestTags::check()) {} + + virtual bool + run(void) { + return check() && check(); + } + }; + + Aliases aliases; + + class FactorySpace : public Gecode::Space { + public: +#ifdef GECODE_HAS_SET_VARS + Gecode::OpenVarList sets; +#endif +#ifdef GECODE_HAS_FLOAT_VARS + Gecode::OpenVarList floats; +#endif + + FactorySpace(void) { + using namespace Gecode; +#ifdef GECODE_HAS_SET_VARS + sets = OpenVarList(*this, + [](Space& home, int i) { + return SetVar(home,IntSet(i,i),IntSet(i,i)); + },3); + sets.materialize(*this,1); +#endif +#ifdef GECODE_HAS_FLOAT_VARS + floats = OpenVarList(*this, + [](Space& home, int i) { return FloatVar(home,i,i); },3); + floats.materialize(*this,1); +#endif + } + + FactorySpace(FactorySpace& s) : Gecode::Space(s) { +#ifdef GECODE_HAS_SET_VARS + sets.update(*this,s.sets); +#endif +#ifdef GECODE_HAS_FLOAT_VARS + floats.update(*this,s.floats); +#endif + } + + virtual Gecode::Space* + copy(void) { + return new FactorySpace(*this); + } + }; + + class Factory : public ::Test::Base { + public: + Factory(void) : ::Test::Base("Int::OpenList::Factory",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + FactorySpace source; + if (source.status() == SS_FAILED) + return false; + FactorySpace* clone = static_cast(source.clone()); +#ifdef GECODE_HAS_SET_VARS + clone->sets.materialize(*clone,3); + clone->sets.close(*clone); + source.sets.close(source); +#endif +#ifdef GECODE_HAS_FLOAT_VARS + clone->floats.materialize(*clone,3); + clone->floats.close(*clone); + source.floats.close(source); +#endif + bool valid = (clone->status() != SS_FAILED) && + (source.status() != SS_FAILED); +#ifdef GECODE_HAS_SET_VARS + valid &= (source.sets.known_length() == 1) && + (clone->sets.known_length() == 3) && + (clone->sets.length().val() == 3) && + clone->sets[2].assigned() && (clone->sets[2].glbMin() == 2); +#endif +#ifdef GECODE_HAS_FLOAT_VARS + valid &= (source.floats.known_length() == 1) && + (clone->floats.known_length() == 3) && + (clone->floats.length().val() == 3) && + clone->floats[2].assigned() && (clone->floats[2].min() == 2); +#endif + delete clone; + return valid; + } + }; + + class FixedStatistics : public ::Test::Base { + public: + FixedStatistics(void) : ::Test::Base("Int::OpenList::FixedStatistics") {} + + virtual bool + run(void) { + using namespace Gecode; + for (int chb=0; chb<2; chb++) { + SelectorSpace open(INT_VAL_MIN()); + bool rejected = false; + try { + branch(open,open.list, + chb ? INT_VAR_CHB_MIN() : INT_VAR_ACTION_MIN(1.0), + INT_VAL_MIN()); + } catch (const Gecode::Int::UnknownBranching&) { + rejected = true; + } + if (!rejected) + return false; + SelectorChoiceSpace fixed(INT_VAL_MIN()); + branch(fixed,fixed.list, + chb ? INT_VAR_CHB_MIN() : INT_VAR_ACTION_MIN(1.0), + INT_VAL_MIN()); + DFS engine(&fixed); + int solutions = 0; + while (SelectorChoiceSpace* solution = engine.next()) { + solutions++; + delete solution; + } + if (solutions != 27) + return false; + } + return true; + } + }; + + Factory factory; + FixedStatistics fixed_statistics; + Selectors selectors; + NoGoods no_goods; + SelectorChoice selector_choice; + BoolList bool_list; + + class TransitionSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + TransitionSpace(void) + : list(*this,Gecode::IntSet(0,4), + [](Gecode::Space& home, Gecode::OpenIntVarList x, int i) { + using namespace Gecode; + if (i == 0) { + rel(home,x[i],IRT_EQ,0); + } else { + linear(home,IntArgs({-1,1}), + IntVarArgs({x[i-1],x[i]}),IRT_EQ,1); + } + },4) { + Gecode::branch(*this,list); + } + + TransitionSpace(TransitionSpace& s) + : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new TransitionSpace(*this); + } + }; + + class Transition : public ::Test::Base { + public: + Transition(void) + : ::Test::Base("Int::OpenList::Transition") {} + + virtual bool + run(void) { + using namespace Gecode; + Search::Options options; + options.threads = 1; + options.c_d = 1000; + options.a_d = 1000; + TransitionSpace* root = new TransitionSpace; + DFS engine(root,options); + delete root; + + int solutions = 0; + while (TransitionSpace* solution = engine.next()) { + bool valid = + solution->list.length().assigned() && + (solution->list.length().val() == + solution->list.known_length()); + for (int i=0; ilist.known_length(); i++) + valid &= solution->list[i].assigned() && + (solution->list[i].val() == i); + delete solution; + if (!valid) + return false; + solutions++; + } + return solutions == 5; + } + }; + + + /// View handles share growth while cloned spaces keep independent domains + class Views : public ::Test::Base { + class ViewSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + Gecode::OpenViewList direct; + Gecode::OpenViewList negative; + + ViewSpace(void) + : list(*this,Gecode::IntSet(0,10),5), + direct(list), negative(list) {} + + ViewSpace(ViewSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + direct.update(*this,s.direct); + negative.update(*this,s.negative); + } + + Gecode::Space* copy(void) override { + return new ViewSpace(*this); + } + }; + public: + Views(void) + : ::Test::Base("Int::OpenList::Views",TestTags::check()) {} + + bool run(void) override { + using namespace Gecode; + ViewSpace source; + Gecode::Int::IntView length = source.negative.length(); + if (me_failed(length.gq(source,2)) || + (source.status() == SS_FAILED) || + (source.direct.known_length() != 2) || + (source.negative.known_length() != 2)) + return false; + if (me_failed(source.negative[0].gq(source,-7)) || + me_failed(source.direct[0].gq(source,2)) || + (source.status() == SS_FAILED) || + (source.list[0].min() != 2) || (source.list[0].max() != 7) || + (source.negative[0].min() != -7) || + (source.negative[0].max() != -2)) + return false; + + std::unique_ptr clone + (static_cast(source.clone())); + clone->negative.materialize(*clone,3); + if (me_failed(clone->negative[0].eq(*clone,-5)) || + me_failed(clone->direct.length().eq(*clone,3)) || + (clone->status() == SS_FAILED) || + (clone->list.known_length() != 3) || + (clone->direct.known_length() != 3) || + (clone->direct[0].val() != 5) || + (clone->negative[2].min() != -10)) + return false; + source.direct.materialize(source,4); + return (source.status() != SS_FAILED) && + (source.list.known_length() == 4) && + (source.negative.known_length() == 4) && + (source.negative[3].max() == 0) && + (source.list[0].min() == 2) && (source.list[0].max() == 7) && + (clone->negative.known_length() == 3) && + (clone->list.length().val() == 3); + } + }; + + + /// Test constraint semantics both while growing and after closure + class Constraint : public OpenTest { + public: + enum class Kind { + DISTINCT, REL, SLIDING, SLIDING_SUM, SLIDING_RANGE, MIN_MAX, + PRECEDE, PRECEDE_SAME + }; + protected: + Kind kind; + int width; + Gecode::IntRelType relation; + public: + Constraint(const std::string& name, Kind k, int size, int width0=2, + Gecode::IntPropLevel level=Gecode::IPL_DEF) + : OpenTest("OpenList::"+name,size,0,Gecode::IntSet(0,2),level), + kind(k), width(width0), relation(Gecode::IRT_LE) {} + + Constraint(Gecode::IntRelType relation0, Gecode::IntPropLevel level) + : OpenTest("OpenList::Rel::"+str(relation0)+"::"+str(level), + 3,0,Gecode::IntSet({0,2,5}),level), + kind(Kind::REL), width(0), relation(relation0) {} + + virtual bool + solution(const Assignment& x) const { + switch (kind) { + case Kind::DISTINCT: + for (int i=0; i 3)) + return false; + } + return true; + case Kind::MIN_MAX: { + int lower = x[0]; + int upper = x[0]; + for (int i=1; i clone + (static_cast(source.clone())); + clone->list.materialize(*clone,3); + if ((clone->status() == SS_FAILED) || + (clone->list[2].min() != 5) || (clone->list[2].max() != 7) || + (source.list.known_length() != 2)) + return false; + rel(*clone,clone->list[2],IRT_EQ,5); + if ((clone->status() == SS_FAILED) || (clone->minimum.max() != 5) || + (source.minimum.max() != 7)) + return false; + clone->list.close(*clone); + rel(*clone,clone->list[0],IRT_EQ,7); + if ((clone->status() == SS_FAILED) || + !clone->minimum.assigned() || (clone->minimum.val() != 5) || + !clone->maximum.assigned() || (clone->maximum.val() != 7)) + return false; + rel(source,source.maximum,IRT_LQ,5); + return source.status() == SS_FAILED; + } + }; + + /// An empty list has no minimum + class EmptyMinimum : public Test { + public: + EmptyMinimum(void) : Test("OpenList::EmptyMinimum",1,0,1) {} + virtual bool solution(const Assignment&) const { return false; } + virtual void post(Gecode::Space& home, Gecode::IntVarArray& x) { + Gecode::OpenIntVarList list(home,0); + Gecode::min(home,list,x[0]); + list.close(home); + } + }; + + class IncrementalConstraints : public ::Test::Base { + public: + IncrementalConstraints(void) + : ::Test::Base("Int::OpenList::IncrementalConstraints", + TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + class IncrementalSpace : public Space { + public: + OpenIntVarList list; + IntVar minimum; + IntVar maximum; + + IncrementalSpace(void) + : list(*this,IntSet(0,2),2), + minimum(*this,0,2), maximum(*this,0,2) { + distinct(*this,list); + rel(*this,list,IRT_LE); + Gecode::sequence(*this,list,IntSet(2,2),2,1,1); + sliding_sum(*this,list,2,2,2); + min(*this,list,minimum); + max(*this,list,maximum); + precede(*this,list,0,2); + } + + IncrementalSpace(IncrementalSpace& s) + : Space(s) { + list.update(*this,s.list); + minimum.update(*this,s.minimum); + maximum.update(*this,s.maximum); + } + + virtual Space* + copy(void) { + return new IncrementalSpace(*this); + } + } source; + + source.list.materialize(source,1); + rel(source,source.list[0],IRT_EQ,0); + if ((source.status() == SS_FAILED) || + !source.list[0].assigned()) + return false; + + IncrementalSpace* clone = + static_cast(source.clone()); + clone->list.materialize(*clone,2); + clone->list.close(*clone); + const bool valid = + (clone->status() != SS_FAILED) && + clone->list[1].assigned() && + (clone->list[1].val() == 2) && + clone->minimum.assigned() && (clone->minimum.val() == 0) && + clone->maximum.assigned() && (clone->maximum.val() == 2); + delete clone; + return valid && (source.list.known_length() == 1); + } + }; + + class CallbackSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + CallbackSpace(Gecode::OpenIntVarList::Domain domain, + Gecode::OpenIntVarList::Transition transition) + : list(*this,domain,transition,8) {} + + CallbackSpace(CallbackSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new CallbackSpace(*this); + } + }; + + /// Range unions and later clone growth preserve canonical sets + class GrowableIntSet : public ::Test::Base { + class SetSpace : public Gecode::Space { + public: + Gecode::Int::GrowableIntSet values; + + SetSpace(void) {} + + SetSpace(SetSpace& s) : Gecode::Space(s) { + values.update(*this,s.values); + } + + ~SetSpace(void) { + values.dispose(*this); + } + + Gecode::Space* copy(void) override { + return new SetSpace(*this); + } + }; + + static bool + check(const SetSpace& home, const Gecode::IntSet& expected) { + Gecode::Int::GrowableIntSetRanges actual(home.values); + Gecode::IntSetRanges ranges(expected); + for (; actual() && ranges(); ++actual, ++ranges) { + if ((actual.min() != ranges.min()) || + (actual.max() != ranges.max()) || + (actual.width() != ranges.width())) + return false; + for (int value : {ranges.min()-1,ranges.min(), + ranges.max(),ranges.max()+1}) + if (home.values.in(value) != expected.in(value)) + return false; + } + if (actual() || ranges()) + return false; + for (int value=-40; value<=40; value++) + if (home.values.in(value) != expected.in(value)) + return false; + return true; + } + + static bool + add(SetSpace& home, std::set& expected, + const Gecode::IntSet& batch) { + Gecode::IntSetRanges ranges(batch); + home.values.add_r(home,ranges); + for (Gecode::IntSetValues values(batch); values(); ++values) + expected.insert(values.val()); + const std::vector values(expected.begin(),expected.end()); + return check(home,Gecode::IntSet(values.data(), + static_cast(values.size()))); + } + + public: + GrowableIntSet(void) + : ::Test::Base("Int::OpenList::GrowableIntSet",TestTags::check()) {} + + bool run(void) override { + using namespace Gecode; + SetSpace source; + std::set expected; + const IntSet batches[] = { + IntSet::empty, IntSet({10,12}), IntSet(10,12), + IntSet(-10,-8), IntSet(20,22), IntSet(0,2), IntSet(3,9), + IntSet(-9,-7), IntSet(18,20), IntSet(11,11), + IntSet({{-12,-9},{-6,1},{15,16},{19,24}}), + IntSet({Gecode::Int::Limits::min,Gecode::Int::Limits::max}) + }; + for (const IntSet& batch : batches) + if (!add(source,expected,batch)) + return false; + if (source.status() == SS_FAILED) + return false; + std::unique_ptr clone + (static_cast(source.clone())); + std::unique_ptr sibling + (static_cast(source.clone())); + std::set clone_expected(expected); + const IntSet growth({{-30,-29},{-20,-19},{14,14},{18,18}, + {26,26},{28,28},{30,30}}); + if (!add(*clone,clone_expected,IntSet(26,26)) || + !add(*clone,clone_expected,growth) || + !add(source,expected,IntSet::empty) || + !add(*sibling,expected,IntSet::empty)) + return false; + // A single wide range exercises unsigned width and removes all holes. + const IntSet full(Gecode::Int::Limits::min,Gecode::Int::Limits::max); + IntSetRanges ranges(full); + source.values.add_r(source,ranges); + return check(source,full) && + add(*clone,clone_expected,IntSet::empty) && + add(*sibling,expected,IntSet::empty); + } + }; + + /// Values assigned before list growth can trigger several pruning waves + class DistinctWaves : public ::Test::Base { + public: + DistinctWaves(void) + : ::Test::Base("Int::OpenList::DistinctWaves",TestTags::check()) {} + + bool run(void) override { + using namespace Gecode; + CallbackSpace source([](int) { return IntSet(1,4); }, + [](Space&, OpenIntVarList, int) {}); + distinct(source,source.list); + source.list.append(source,IntVar(source,1,1)); + if (source.status() == SS_FAILED) + return false; + std::unique_ptr duplicate + (static_cast(source.clone())); + // Reverse the chain so each assignment needs another pruning wave. + for (int lower=3; lower>=1; lower--) + source.list.append(source,IntVar(source,lower,lower+1)); + if (source.status() == SS_FAILED) + return false; + for (int i=1; i<4; i++) + if (!source.list[i].assigned() || (source.list[i].val() != 5-i)) + return false; + source.list.close(source); + if ((source.status() == SS_FAILED) || + !source.list.length().assigned() || + (source.list.length().val() != 4)) + return false; + duplicate->list.append(*duplicate,IntVar(*duplicate,1,2)); + duplicate->list.append(*duplicate,IntVar(*duplicate,1,2)); + return duplicate->status() == SS_FAILED; + } + }; + + GrowableIntSet growable_int_set; + DistinctWaves distinct_waves; + + class GroupSpace : public Gecode::Space { + public: + enum class Kind { REL, SUM, PRECEDE }; + Gecode::OpenIntVarList list; + Gecode::PropagatorGroup group; + + GroupSpace(Kind kind, bool close) + : list(*this,Gecode::IntSet(0,2),3) { + using namespace Gecode; + switch (kind) { + case Kind::REL: + rel((*this)(group),list,IRT_NQ); + break; + case Kind::SUM: + sliding_sum((*this)(group),list,2,2,2); + break; + case Kind::PRECEDE: + precede((*this)(group),list,0,1); + break; + } + list.materialize(*this,2); + if (close) + list.close(*this); + } + + GroupSpace(GroupSpace& s) : Gecode::Space(s), group(s.group) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new GroupSpace(*this); + } + }; + + /// Test group control after an open constraint has posted its children + class Group : public ::Test::Base { + protected: + GroupSpace::Kind kind; + bool kill; + public: + Group(const std::string& name, GroupSpace::Kind kind0, bool kill0) + : ::Test::Base("Int::OpenList::Group::"+name+ + (kill0 ? "::Kill" : "::Disable")), + kind(kind0), kill(kill0) {} + + virtual bool + run(void) { + using namespace Gecode; + for (int close=0; close<2; close++) { + GroupSpace source(kind,close != 0); + if (source.status() == SS_FAILED) + return false; + if (kill) + source.group.kill(source); + else + source.group.disable(source); + // These values are in the domains but violate each constraint. + rel(source,source.list[0],IRT_EQ, + kind == GroupSpace::Kind::PRECEDE ? 2 : 0); + rel(source,source.list[1],IRT_EQ, + kind == GroupSpace::Kind::PRECEDE ? 1 : 0); + if (source.status() == SS_FAILED) + return false; + if (!kill) { + source.group.enable(source); + if (source.status() != SS_FAILED) + return false; + } + } + return true; + } + }; + + Group group_rel_kill("Rel",GroupSpace::Kind::REL,true); + Group group_rel_disable("Rel",GroupSpace::Kind::REL,false); + Group group_sum_kill("Sum",GroupSpace::Kind::SUM,true); + Group group_sum_disable("Sum",GroupSpace::Kind::SUM,false); + Group group_precede_kill("Precede",GroupSpace::Kind::PRECEDE,true); + Group group_precede_disable("Precede",GroupSpace::Kind::PRECEDE,false); + + class Dispose : public ::Test::Base { + public: + Dispose(void) + : ::Test::Base("Int::OpenList::Dispose",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + std::shared_ptr domain(new int(0)); + std::shared_ptr transition(new int(0)); + CallbackSpace* source = new CallbackSpace( + [domain](int) { return IntSet(0,1); }, + [transition](Space&, OpenIntVarList, int) {}); + source->list.materialize(*source,2); + DFA dfa(0,{{0,0,0},{0,1,0}},{0}); + extensional(*source,source->list,dfa); + if (source->status() == SS_FAILED) { + delete source; + return false; + } + CallbackSpace* clone = static_cast(source->clone()); + delete source; + const bool retained = (domain.use_count() > 1) && + (transition.use_count() > 1); + delete clone; + return retained && (domain.use_count() == 1) && + (transition.use_count() == 1); + } + }; + + class CallbackLength : public ::Test::Base { + public: + CallbackLength(void) + : ::Test::Base("Int::OpenList::CallbackLength",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + for (int assign=0; assign<2; assign++) + for (int close=0; close<2; close++) { + CallbackSpace source([](int) { return IntSet(0,1); }, + [assign](Space& home, OpenIntVarList list, int i) { + if (i == 0) + rel(home,list.length(),assign ? IRT_EQ : IRT_GQ,3); + }); + rel(source,source.list.length(),IRT_GQ,1); + if (close) + source.list.close(source); + if ((source.status() == SS_FAILED) || + (source.list.known_length() != 3)) + return false; + } + return true; + } + }; + + /// Callbacks suppress AFC unless opted in; nested growth restores each scope + class NoGPI : public ::Test::Base { + protected: + static void + post(Gecode::Space& home, Gecode::PropagatorGroup group) { + Gecode::IntVar x(home,0,10); + Gecode::IntVar y(home,0,10); + Gecode::rel(home(group),x,Gecode::IRT_LE,y); + } + + static int + count(const Gecode::Space& home, Gecode::PropagatorGroup group, + double afc) { + int n = 0; + for (Gecode::Propagators ps(home,group); ps(); ++ps) { + if (ps.propagator().afc() != afc) + return -1; + n++; + } + return n; + } + + public: + NoGPI(void) + : ::Test::Base("Int::OpenList::NoGPI",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + CallbackSpace home([](int) { return IntSet(0,1); }, + [](Space&, OpenIntVarList, int) {}); + PropagatorGroup factory; + PropagatorGroup transition; + PropagatorGroup selected; + PropagatorGroup ordinary; + PropagatorGroup suppressed; + OpenVarList list(home, + [factory,selected](Space& home, int) { + post(home,factory); + { + Space::UseGPI use_gpi(home); + post(home,selected); + } + return IntVar(home,0,10); + }, + [transition,selected](Space& home, OpenVarList list, int i) { + post(home,transition); + if (i == 3) + throw 1; + { + Space::UseGPI use_gpi(home); + post(home,selected); + if (i == 0) + list.materialize(home,2); + post(home,selected); + } + post(home,transition); + },4); + + list.materialize(home,1); + post(home,ordinary); + if ((list.known_length() != 2) || + (count(home,factory,0.0) != 2) || + (count(home,transition,0.0) != 4) || + (count(home,selected,1.0) != 6)) + return false; + { + Space::NoGPI no_gpi(home); + { + Space::UseGPI use_gpi(home); + list.append(home,IntVar(home,0,10)); + post(home,ordinary); + } + post(home,suppressed); + } + bool threw = false; + try { + list.append(home,IntVar(home,0,10)); + } catch (int) { + threw = true; + } + post(home,ordinary); + return threw && (count(home,factory,0.0) == 2) && + (count(home,transition,0.0) == 7) && + (count(home,selected,1.0) == 8) && + (count(home,ordinary,1.0) == 3) && + (count(home,suppressed,0.0) == 1) && + (home.status() != SS_FAILED); + } + }; + + class FailedGet : public ::Test::Base { + public: + FailedGet(void) + : ::Test::Base("Int::OpenList::FailedGet") {} + + virtual bool + run(void) { + using namespace Gecode; + CallbackSpace source([](int) { return IntSet(0,1); }, + [](Space& home, OpenIntVarList, int) { home.fail(); }); + IntVar x = source.list.get(source,4); + return source.failed() && (x.varimp() == nullptr); + } + }; + + /// A value commit must not construct positions required by pending propagation + class ValueCommit : public ::Test::Base { + public: + ValueCommit(void) + : ::Test::Base("Int::OpenList::ValueCommit") {} + + virtual bool + run(void) { + using namespace Gecode; + CallbackSpace source([](int) { return IntSet(0,1); }, + [](Space& home, OpenIntVarList, int i) { + if (i == 1) + home.fail(); + }); + source.list.materialize(source,1); + branch(source,source.list); + if (source.status() != SS_BRANCH) + return false; + const Choice* choice = source.choice(); + bool valid = true; + for (unsigned int alternative=0; alternative<2; alternative++) { + CallbackSpace* clone = static_cast(source.clone()); + rel(*clone,clone->list.length(),IRT_GQ,2); + clone->commit(*choice,alternative); + valid &= !clone->failed() && (clone->list.known_length() == 1) && + clone->list[0].assigned() && + (clone->list[0].val() == static_cast(alternative)); + valid &= clone->status() == SS_FAILED; + delete clone; + } + delete choice; + return valid; + } + }; + + /// Replay value choices after propagation has extended the original list + class ValueReplay : public ::Test::Base { + public: + ValueReplay(void) + : ::Test::Base("Int::OpenList::ValueReplay",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + CallbackSpace source([](int) { return IntSet(0,1); }, + [](Space& home, OpenIntVarList list, int i) { + if (i == 0) { + BoolVar zero(home,0,1); + rel(home,list[0],IRT_EQ,0,Reify(zero)); + rel(home,list.length(),IRT_GQ,2,Reify(zero,RM_IMP)); + } + }); + source.list.materialize(source,1); + branch(source,source.list); + if (source.status() != SS_BRANCH) + return false; + std::unique_ptr first(source.choice()); + std::unique_ptr replay + (static_cast(source.clone())); + source.commit(*first,0); + if ((source.status() != SS_BRANCH) || + (source.list.known_length() != 2)) + return false; + std::unique_ptr second(source.choice()); + // Recomputation commits the path without propagating between choices. + replay->commit(*first,0); + for (unsigned int a=0; aalternatives(); a++) { + std::ostringstream printed; + replay->print(*second,a,printed); + if (printed.str().empty() || (replay->list.known_length() != 1)) + return false; + } + replay->commit(*second,0); + return (replay->status() != SS_FAILED) && + (replay->list.known_length() == 2) && + replay->list[0].assigned() && (replay->list[0].val() == 0) && + replay->list[1].assigned() && (replay->list[1].val() == 0); + } + }; + + /// Impossible window bounds still permit lists too short to contain a window + class ImpossibleWindow : public ::Test::Base { + public: + ImpossibleWindow(void) + : ::Test::Base("Int::OpenList::ImpossibleWindow") {} + + virtual bool + run(void) { + using namespace Gecode; + for (int sum=0; sum<2; sum++) + for (int size=0; size<3; size++) { + CallbackSpace source([](int) { return IntSet(0,1); }, + [](Space&, OpenIntVarList, int) {}); + if (sum) + sliding_sum(source,source.list,2,2,1); + else + Gecode::sequence(source,source.list,IntSet(0,1),2,3,3); + rel(source,source.list.length(),IRT_EQ,size); + const bool failed = source.status() == SS_FAILED; + if (failed != (size == 2)) + return false; + if (!failed && (source.list.known_length() != size)) + return false; + } + return true; + } + }; + + /// Search model for the private common-pattern projection + class PatternSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + Gecode::IntVar frequency; + + PatternSpace(const PatternMining::Database& data, int support) + : list(*this,data.domain(),data.max_length()) { + Gecode::rel(*this,list.length(),Gecode::IRT_GQ,1); + frequency = PatternMining::frequent(*this,list,data,support); + Gecode::branch(*this,list); + } + + PatternSpace(PatternSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + frequency.update(*this,s.frequency); + } + + virtual Gecode::Space* copy(void) { + return new PatternSpace(*this); + } + }; + + /// Compare complete solution sets with independently enumerated subsequences + class PatternEnumeration : public ::Test::Base { + public: + PatternEnumeration(void) + : ::Test::Base("Int::OpenList::Pattern::Enumeration", + TestTags::check()) {} + + virtual bool run(void) { + using namespace Gecode; + const PatternMining::Sequences inputs[] = { + {{17,1000000,17},{17,1000000,17},{1000000,17,42},{17,42}}, + {{1,1},{1,2},{2,1}} + }; + for (const PatternMining::Sequences& input : inputs) { + std::set> candidates; + for (const std::vector& row : input) + for (unsigned int mask=1; mask<(1U< pattern; + for (unsigned int i=0; i(input.size())}; + for (int threshold : thresholds) { + std::set> expected; + std::set> found; + for (const std::vector& pattern : candidates) { + int support = 0; + for (const std::vector& row : input) { + auto next = row.begin(); + bool matches = true; + for (int item : pattern) { + next = std::find(next,row.end(),item); + if (next == row.end()) { + matches = false; + break; + } + ++next; + } + support += matches; + } + if (support >= threshold) + expected.insert(pattern); + } + PatternSpace root(data,threshold); + Search::Options options; + options.threads = 1; + options.c_d = 1000; + options.a_d = 1000; + DFS search(&root,options); + while (PatternSpace* solution = search.next()) { + std::unique_ptr owned(solution); + if (!solution->list.length().assigned() || + (solution->list.length().val() != + solution->list.known_length())) + return false; + std::vector pattern; + for (int i=0; ilist.known_length(); i++) { + if (!solution->list[i].assigned()) + return false; + pattern.push_back(solution->list[i].val()); + } + if (!solution->frequency.assigned() || + (solution->frequency.val() != data.support(pattern)) || + !found.insert(pattern).second) + return false; + } + if (found != expected) + return false; + } + } + return true; + } + }; + + /// Clones keep separate projections; later assignments wait for their prefix + class PatternGrowth : public ::Test::Base { + public: + PatternGrowth(void) + : ::Test::Base("Int::OpenList::Pattern::Growth",TestTags::check()) {} + + virtual bool run(void) { + using namespace Gecode; + const PatternMining::Database data({{1,2,1},{1,2,2},{1,1,2}}); + PatternSpace source(data,2); + source.list.materialize(source,1); + rel(source,source.list[0],IRT_EQ,1); + if (source.status() == SS_FAILED) + return false; + std::unique_ptr clone + (static_cast(source.clone())); + clone->list.materialize(*clone,2); + rel(*clone,clone->list[1],IRT_EQ,1); + if ((clone->status() == SS_FAILED) || + !clone->list.length().assigned() || + (clone->list.length().val() != 2) || + (source.list.known_length() != 1) || + (source.list.length().min() != 1)) + return false; + source.list.materialize(source,2); + rel(source,source.list[1],IRT_EQ,2); + if ((source.status() == SS_FAILED) || + !source.list.length().assigned() || + (source.list.length().val() != 2) || + (clone->list[1].val() != 1)) + return false; + + // Assigning a later position must not advance the projected prefix. + PatternSpace later(data,2); + later.list.materialize(later,2); + rel(later,later.list[1],IRT_EQ,2); + if ((later.status() == SS_FAILED) || later.list[0].assigned()) + return false; + std::unique_ptr invalid + (static_cast(later.clone())); + rel(*invalid,invalid->list[0],IRT_EQ,2); + if (invalid->status() != SS_FAILED) + return false; + rel(later,later.list[0],IRT_EQ,1); + if (later.status() == SS_FAILED) + return false; + + // Both singleton prefixes are frequent, but no extension is frequent. + const PatternMining::Database terminal({{1,1},{1,2},{2,1}}); + for (int item=1; item<=2; item++) { + PatternSpace leaf(terminal,2); + leaf.list.materialize(leaf,1); + rel(leaf,leaf.list[0],IRT_EQ,item); + if ((leaf.status() == SS_FAILED) || + !leaf.list.length().assigned() || + (leaf.list.length().val() != 1)) + return false; + } + return true; + } + }; + + /// Input validation and exact rounding of percentage support + class PatternInput : public ::Test::Base { + public: + PatternInput(void) + : ::Test::Base("Int::OpenList::Pattern::Input",TestTags::check()) {} + + virtual bool run(void) { + using namespace PatternMining; + std::istringstream rows("# comment\n17 -1 1000000 -1 -2\r\n" + "17 -1 1000000 -1 -2\n"); + if (read_database(rows,Format::SPMF) != + Sequences({{17,1000000},{17,1000000}}) || + read_sequence("ABA",Format::LETTERS) != std::vector({1,2,1}) || + read_sequence("17 1000000",Format::INTEGERS) != + std::vector({17,1000000}) || + (minimum_support("33.333333",3) != 1) || + (minimum_support("33.333334",3) != 2) || + (minimum_support("100",2000000000) != 2000000000) || + (minimum_support("0.000001",2000000000) != 20)) + return false; + for (const char* text : {"1 2 -1 -2", "1 -2", "1 -1", + "1 -1 -2 2", "2147483648 -1 -2"}) { + bool rejected = false; + try { + (void) read_sequence(text,Format::SPMF); + } catch (const std::invalid_argument&) { + rejected = true; + } + if (!rejected) + return false; + } + for (const char* text : {"0", "101", "0.0000001", + "184467440737095516160"}) { + bool rejected = false; + try { + (void) minimum_support(text,3); + } catch (const std::invalid_argument&) { + rejected = true; + } + if (!rejected) + return false; + } + return true; + } + }; + + PatternEnumeration pattern_enumeration; + PatternGrowth pattern_growth; + PatternInput pattern_input; + + ValueCommit value_commit; + ValueReplay value_replay; + ImpossibleWindow impossible_window; + + Dispose disposal; + CallbackLength callback_length; + NoGPI no_gpi; + FailedGet failed_get; + + BranchOrder branch_order; + Transition transition; + Views views; + Constraint distinct("Distinct",Constraint::Kind::DISTINCT,3); + Constraint rel_eq(Gecode::IRT_EQ,Gecode::IPL_BND); + Constraint rel_eq_dom(Gecode::IRT_EQ,Gecode::IPL_DOM); + Constraint rel_nq(Gecode::IRT_NQ,Gecode::IPL_DEF); + Constraint rel_lq(Gecode::IRT_LQ,Gecode::IPL_DEF); + Constraint rel_le(Gecode::IRT_LE,Gecode::IPL_DEF); + Constraint rel_gq(Gecode::IRT_GQ,Gecode::IPL_DEF); + Constraint rel_gr(Gecode::IRT_GR,Gecode::IPL_DEF); + Constraint sliding("Sliding",Constraint::Kind::SLIDING,4); + Constraint sliding_one("Sliding1",Constraint::Kind::SLIDING,3,1); + Constraint sliding_sum("SlidingSum",Constraint::Kind::SLIDING_SUM,4); + Constraint sliding_range("SlidingSumRange",Constraint::Kind::SLIDING_RANGE,4); + Constraint range_one("SlidingSumRange1",Constraint::Kind::SLIDING_RANGE,3,1); + Constraint min_max("MinMax",Constraint::Kind::MIN_MAX,3); + Constraint precedence("Precede",Constraint::Kind::PRECEDE,3); + Constraint precedence_same("PrecedeSame",Constraint::Kind::PRECEDE_SAME,3); + Constraint sliding_three("Sliding3",Constraint::Kind::SLIDING,5,3); + Constraint sum_three("SlidingSum3",Constraint::Kind::SLIDING_SUM,5,3); + Constraint sum_three_dom("SlidingSum3Dom",Constraint::Kind::SLIDING_SUM, + 5,3,Gecode::IPL_DOM); + Constraint sum_two_bnd("SlidingSum2Bnd",Constraint::Kind::SLIDING_SUM, + 4,2,Gecode::IPL_BND); + Constraint range_three("SlidingSumRange3",Constraint::Kind::SLIDING_RANGE,5,3); + Extremum min_result(true,false,Gecode::IPL_BND); + Extremum max_result(false,false,Gecode::IPL_BND); + Extremum min_domain(true,false,Gecode::IPL_DOM); + Extremum max_domain(false,false,Gecode::IPL_DOM); + Extremum min_alias(true,true,Gecode::IPL_DOM); + Extremum max_alias(false,true,Gecode::IPL_DOM); + ExtremumBounds extremum_bounds; + EmptyMinimum empty_minimum; + IncrementalConstraints incremental_constraints; + + } + +}} + +// STATISTICS: test-int