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
, цей діапазон є меншим, що означає, що менша кількість значень виключається при його використанні.
Створення унікальних масивів підрахунку
Я набрав грубу силу на цьому, повторюючи всі значення, генеруючи масив для кожного з них і уніфікувавши отриманий список. Це, безумовно, можна зробити більш ефективно, але це досить добре для типів цінностей, з якими ми працюємо.
Це надзвичайно швидко для малих значень. Для більших значень накладні витрати стають істотними. Наприклад, для 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]
.