C ++ (heuristisch): 2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086
Dies ist etwas hinter Peter Taylors Ergebnis zurück, das für und 1 bis 3 niedriger n=7
ist . Der Vorteil ist, dass es viel schneller ist, so dass ich es für höhere Werte von ausführen kann . Und es kann ohne ausgefallene Mathematik verstanden werden. ;)9
10
n
Der aktuelle Code ist so dimensioniert, dass er bis zu ausgeführt werden kann n=15
. Die Laufzeiten erhöhen sich bei jeder Erhöhung von ungefähr um den Faktor 4 n
. Zum Beispiel war sie 0,008 Sekunden lang n=7
, 0,07 Sekunden lang n=9
, 1,34 Sekunden lang n=11
, 27 Sekunden lang n=13
und 9 Minuten langn=15
.
Ich habe zwei wichtige Beobachtungen gemacht:
- Anstatt die Werte selbst zu verarbeiten, werden in der Heuristik Arrays gezählt. Dazu wird zunächst eine Liste aller eindeutigen Zählarrays erstellt.
- Die Verwendung von Zählarrays mit kleinen Werten ist vorteilhafter, da sie weniger Platz in der Lösung beanspruchen. Dies basiert auf jeder Zählung
c
ohne den Bereich von c / 2
bis 2 * c
von anderen Werten. Für kleinere Werte von c
ist dieser Bereich kleiner, was bedeutet, dass durch die Verwendung weniger Werte ausgeschlossen werden.
Generieren Sie eindeutige Zählarrays
Ich habe mich mit aller Kraft an die Arbeit gemacht, alle Werte durchlaufen, das Zählarray für jeden von ihnen generiert und die resultierende Liste eindeutig festgelegt. Dies könnte sicherlich effizienter erfolgen, ist aber für die Werte, mit denen wir arbeiten, gut genug.
Dies geht bei den kleinen Werten extrem schnell. Für die größeren Werte wird der Overhead erheblich. Zum Beispiel verbraucht n=15
es 75% der gesamten Laufzeit. Dies wäre definitiv ein Bereich, den man sich ansehen sollte, wenn man versucht, viel höher zu steigen als n=15
. Selbst die Speicherbelegung für die Erstellung einer Liste der Zählarrays für alle Werte würde problematisch.
Die Anzahl der eindeutigen Zählarrays beträgt ungefähr 6% der Anzahl der Werte für n=15
. Diese relative Anzahl wird kleiner, wenn sie n
größer wird.
Gierige Auswahl von Zählarraywerten
Der Hauptteil des Algorithmus wählt das Zählen von Array-Werten aus der generierten Liste unter Verwendung eines einfachen gierigen Ansatzes aus.
Basierend auf der Theorie, dass die Verwendung von Zählarrays mit kleinen Zählwerten vorteilhaft ist, werden die Zählarrays nach der Summe ihrer Zählwerte sortiert.
Sie werden dann der Reihe nach überprüft und ein Wert wird ausgewählt, wenn er mit allen zuvor verwendeten Werten kompatibel ist. Dies umfasst also einen einzelnen linearen Durchlauf durch die eindeutigen Zählarrays, bei dem jeder Kandidat mit den zuvor ausgewählten Werten verglichen wird.
Ich habe einige Ideen, wie die Heuristik möglicherweise verbessert werden könnte. Dies schien jedoch ein vernünftiger Ausgangspunkt zu sein, und die Ergebnisse sahen recht gut aus.
Code
Dies ist nicht sehr optimiert. Irgendwann hatte ich eine ausgefeiltere Datenstruktur, aber es hätte mehr Arbeit gebraucht, um sie darüber hinaus zu verallgemeinern n=8
, und der Leistungsunterschied schien nicht sehr erheblich zu sein.
#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]
.