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].