C ++ (эвристический): 2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086
Это немного отстает от результата Питера Тейлора, будучи на 1–3 меньше n=7
, 9
и 10
. Преимущество в том, что это намного быстрее, поэтому я могу запустить его для более высоких значений n
. И это можно понять без всякой причудливой математики. ;)
Текущий код рассчитан на запуск n=15
. Время выполнения увеличивается примерно в 4 раза для каждого увеличения n
. Например, это было 0,008 секунды n=7
, 0,07 секунды n=9
, 1,34 секунды n=11
, 27 секунд n=13
и 9 минут n=15
.
Я использовал два ключевых наблюдения:
- Вместо того, чтобы воздействовать на сами значения, эвристика работает на подсчете массивов. Для этого сначала создается список всех уникальных счетных массивов.
- Использование подсчета массивов с небольшими значениями более выгодно, так как они уменьшают пространство решения. Это основано на каждом счете за
c
исключением диапазона c / 2
для 2 * c
других значений. Для меньших значений c
этот диапазон меньше, что означает, что меньшее количество значений исключается при его использовании.
Генерация уникальных массивов подсчета
Я использовал грубую силу, перебирая все значения, генерируя массив count для каждого из них и выводя результирующий список. Конечно, это можно сделать более эффективно, но этого достаточно для тех значений, с которыми мы работаем.
Это очень быстро для небольших значений. Для больших значений накладные расходы становятся существенными. Например, для n=15
него используется около 75% всего времени выполнения. Это определенно будет область, на которую стоит обратить внимание при попытке подняться намного выше n=15
. Даже просто использование памяти для построения списка подсчитывающих массивов для всех значений станет проблематичным.
Количество уникальных счетных массивов составляет около 6% от количества значений для n=15
. Этот относительный счет становится меньше, когда n
становится больше.
Жадный выбор подсчета значений массива
Основная часть алгоритма выбирает подсчет значений массива из сгенерированного списка, используя простой жадный подход.
Исходя из теории, что использование счетных массивов с небольшими счетами выгодно, счетные массивы сортируются по сумме их подсчетов.
Затем они проверяются по порядку, и выбирается значение, если оно совместимо со всеми ранее использованными значениями. Таким образом, это включает в себя один линейный проход через уникальные счетные массивы, где каждый кандидат сравнивается с ранее выбранными значениями.
У меня есть некоторые идеи о том, как можно улучшить эвристику. Но это казалось разумной отправной точкой, и результаты выглядели довольно хорошо.
Код
Это не очень оптимизировано. В какой-то момент у меня была более сложная структура данных, но для ее обобщения потребовалось бы больше работы n=8
, и разница в производительности не казалась существенной.
#include <cstdint>
#include <cstdlib>
#include <vector>
#include <algorithm>
#include <sstream>
#include <iostream>
typedef uint32_t Value;
class Counter {
public:
static void setN(int n);
Counter();
Counter(Value val);
bool operator==(const Counter& rhs) const;
bool operator<(const Counter& rhs) const;
bool collides(const Counter& other) const;
private:
static const int FIELD_BITS = 4;
static const uint64_t FIELD_MASK = 0x0f;
static int m_n;
static Value m_valMask;
uint64_t fieldSum() const;
uint64_t m_fields;
};
void Counter::setN(int n) {
m_n = n;
m_valMask = (static_cast<Value>(1) << n) - 1;
}
Counter::Counter()
: m_fields(0) {
}
Counter::Counter(Value val) {
m_fields = 0;
for (int k = 0; k < m_n; ++k) {
m_fields <<= FIELD_BITS;
m_fields |= __builtin_popcount(val & m_valMask);
val >>= 1;
}
}
bool Counter::operator==(const Counter& rhs) const {
return m_fields == rhs.m_fields;
}
bool Counter::operator<(const Counter& rhs) const {
uint64_t lhsSum = fieldSum();
uint64_t rhsSum = rhs.fieldSum();
if (lhsSum < rhsSum) {
return true;
}
if (lhsSum > rhsSum) {
return false;
}
return m_fields < rhs.m_fields;
}
bool Counter::collides(const Counter& other) const {
uint64_t fields1 = m_fields;
uint64_t fields2 = other.m_fields;
for (int k = 0; k < m_n; ++k) {
uint64_t c1 = fields1 & FIELD_MASK;
uint64_t c2 = fields2 & FIELD_MASK;
if (c1 > 2 * c2 || c2 > 2 * c1) {
return false;
}
fields1 >>= FIELD_BITS;
fields2 >>= FIELD_BITS;
}
return true;
}
int Counter::m_n = 0;
Value Counter::m_valMask = 0;
uint64_t Counter::fieldSum() const {
uint64_t fields = m_fields;
uint64_t sum = 0;
for (int k = 0; k < m_n; ++k) {
sum += fields & FIELD_MASK;
fields >>= FIELD_BITS;
}
return sum;
}
typedef std::vector<Counter> Counters;
int main(int argc, char* argv[]) {
int n = 0;
std::istringstream strm(argv[1]);
strm >> n;
Counter::setN(n);
int nBit = 2 * n - 1;
Value maxVal = static_cast<Value>(1) << nBit;
Counters allCounters;
for (Value val = 0; val < maxVal; ++val) {
Counter counter(val);
allCounters.push_back(counter);
}
std::sort(allCounters.begin(), allCounters.end());
Counters::iterator uniqEnd =
std::unique(allCounters.begin(), allCounters.end());
allCounters.resize(std::distance(allCounters.begin(), uniqEnd));
Counters solCounters;
int nSol = 0;
for (Value idx = 0; idx < allCounters.size(); ++idx) {
const Counter& counter = allCounters[idx];
bool valid = true;
for (int iSol = 0; iSol < nSol; ++iSol) {
if (solCounters[iSol].collides(counter)) {
valid = false;
break;
}
}
if (valid) {
solCounters.push_back(counter);
++nSol;
}
}
std::cout << "result: " << nSol << std::endl;
return 0;
}
L1[i]/2 <= L2[i] <= 2*L1[i]
.