C ++ (heuristique): 2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086
Ceci est légèrement derrière le résultat de Peter Taylor, étant 1 à 3 plus bas pour n=7
, 9
et10
. L'avantage est que c'est beaucoup plus rapide, donc je peux l'exécuter pour des valeurs plus élevées de n
. Et cela peut être compris sans mathématiques sophistiquées. ;)
Le code actuel est dimensionné pour s'exécuter jusqu'à n=15
. Les temps d'exécution augmentent d'environ un facteur 4 pour chaque augmentation de n
. Par exemple, elle était de 0,008 seconde pour n=7
, 0,07 seconde pour n=9
, 1,34 seconde pour n=11
, 27 secondes pour n=13
et 9 minutes pourn=15
.
Il y a deux observations clés que j'ai utilisées:
- Au lieu d'opérer sur les valeurs elles-mêmes, l'heuristique opère sur les tableaux de comptage. Pour ce faire, une liste de tous les tableaux de comptage uniques est générée en premier.
- L'utilisation de tableaux de comptage avec de petites valeurs est plus avantageuse, car ils éliminent moins d'espace de solution. Ceci est basé sur chaque comptage en
c
excluant la plage de c / 2
à 2 * c
d'autres valeurs. Pour les valeurs plus petites de c
, cette plage est plus petite, ce qui signifie que moins de valeurs sont exclues en l'utilisant.
Générer des tableaux de comptage uniques
J'ai utilisé la force brute sur celui-ci, en parcourant toutes les valeurs, en générant le tableau de comptage pour chacune d'entre elles et en unifiant la liste résultante. Cela pourrait certainement être fait plus efficacement, mais c'est assez bon pour les types de valeurs avec lesquelles nous fonctionnons.
C'est extrêmement rapide pour les petites valeurs. Pour les valeurs plus élevées, les frais généraux deviennent substantiels. Par exemple, pour n=15
, il utilise environ 75% de la durée totale d'exécution. Ce serait certainement un domaine à examiner lorsque vous essayez d'aller beaucoup plus haut que n=15
. Même l'utilisation de la mémoire pour construire une liste des tableaux de comptage pour toutes les valeurs commencerait à devenir problématique.
Le nombre de tableaux de comptage uniques représente environ 6% du nombre de valeurs pour n=15
. Ce nombre relatif devient plus petit à mesure qu'il n
devient plus grand.
Sélection gourmande de valeurs de tableau de comptage
La partie principale de l'algorithme sélectionne les valeurs de tableau de comptage dans la liste générée en utilisant une approche simple et gourmande.
Basé sur la théorie selon laquelle l'utilisation de tableaux de comptage avec de petits comptes est bénéfique, les tableaux de comptage sont triés par la somme de leurs comptes.
Ils sont ensuite vérifiés dans l'ordre et une valeur est sélectionnée si elle est compatible avec toutes les valeurs précédemment utilisées. Cela implique donc un seul passage linéaire à travers les tableaux de comptage uniques, où chaque candidat est comparé aux valeurs précédemment sélectionnées.
J'ai quelques idées sur la façon dont l'heuristique pourrait potentiellement être améliorée. Mais cela semblait être un point de départ raisonnable et les résultats semblaient plutôt bons.
Code
Ce n'est pas très optimisé. J'avais une structure de données plus élaborée à un moment donné, mais il aurait fallu plus de travail pour la généraliser au n=8
- delà , et la différence de performance ne semblait pas très importante.
#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]
.