C ++ (euristico): 2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086
Questo è leggermente dietro il risultato di Peter Taylor, essendo 1 a 3 inferiore per n=7
, 9
e 10
. Il vantaggio è che è molto più veloce, quindi posso eseguirlo per valori più alti di n
. E può essere compreso senza alcuna fantasia matematica. ;)
Il codice corrente è dimensionato per l'esecuzione n=15
. I tempi di esecuzione aumentano approssimativamente di un fattore 4 per ogni aumento di n
. Ad esempio, era 0,008 secondi per n=7
, 0,07 secondi per n=9
, 1,34 secondi per n=11
, 27 secondi per n=13
e 9 minuti per n=15
.
Ci sono due osservazioni chiave che ho usato:
- Invece di operare sui valori stessi, l'euristica opera sul conteggio delle matrici. Per fare ciò, viene generato prima un elenco di tutti gli array di conteggio unici.
- L'uso di array di conteggio con valori piccoli è più vantaggioso, poiché eliminano meno spazio della soluzione. Questo si basa su ogni conteggio
c
escludendo la gamma di c / 2
a 2 * c
da altri valori. Per valori più piccoli di c
, questo intervallo è più piccolo, il che significa che vengono utilizzati meno valori.
Genera array di conteggio unici
Sono andato su questa forza bruta, ripetendo tutti i valori, generando l'array di conteggio per ciascuno di essi e unificando l'elenco risultante. Questo potrebbe certamente essere fatto in modo più efficiente, ma è abbastanza buono per i tipi di valori con cui operiamo.
Questo è estremamente veloce per i piccoli valori. Per i valori più grandi, il sovraccarico diventa sostanziale. Ad esempio, per n=15
, utilizza circa il 75% dell'intero tempo di esecuzione. Questa sarebbe sicuramente un'area da guardare quando si cerca di andare molto più in alto n=15
. Anche solo l'uso della memoria per la creazione di un elenco degli array di conteggio per tutti i valori inizierebbe a diventare problematico.
Il numero di matrici di conteggio univoche è circa il 6% del numero di valori per n=15
. Questo conteggio relativo diventa più piccolo man mano che n
aumenta.
Selezione avida di valori di array di conteggio
La parte principale dell'algoritmo seleziona il conteggio dei valori di array dall'elenco generato usando un semplice approccio avido.
Sulla base della teoria secondo cui è utile utilizzare gli array di conteggio con piccoli conteggi, gli array di conteggio sono ordinati in base alla somma dei loro conteggi.
Vengono quindi controllati in ordine e viene selezionato un valore se è compatibile con tutti i valori utilizzati in precedenza. Quindi questo comporta un singolo passaggio lineare attraverso le matrici di conteggio univoche, in cui ciascun candidato viene confrontato con i valori precedentemente selezionati.
Ho alcune idee su come l'euristica potrebbe essere potenzialmente migliorata. Ma questo sembrava un ragionevole punto di partenza e i risultati sembravano abbastanza buoni.
Codice
Questo non è altamente ottimizzato. Avevo una struttura di dati più elaborata ad un certo punto, ma avrebbe avuto bisogno di più lavoro per generalizzarla oltre n=8
, e la differenza nelle prestazioni non sembrava molto sostanziale.
#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]
.