C ++ (heurístico): 2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086
Esto está ligeramente por detrás del resultado de Peter Taylor, siendo 1 a 3 más bajo para n=7
, 9
y 10
. La ventaja es que es mucho más rápido, por lo que puedo ejecutarlo para valores más altos de n
. Y se puede entender sin ninguna matemática elegante. ;)
El código actual está dimensionado para ejecutarse hasta n=15
. Los tiempos de ejecución aumentan aproximadamente en un factor de 4 por cada aumento en n
. Por ejemplo, fue 0.008 segundos n=7
, 0.07 segundos n=9
, 1.34 segundos n=11
, 27 segundos n=13
y 9 minutos n=15
.
Hay dos observaciones clave que utilicé:
- En lugar de operar en los valores mismos, la heurística opera en matrices de conteo. Para hacer esto, primero se genera una lista de todos los arreglos de conteo únicos.
- Usar matrices de conteo con valores pequeños es más beneficioso, ya que eliminan menos espacio de la solución. Esto se basa en cada recuento,
c
excluyendo el rango de c / 2
a 2 * c
de otros valores. Para valores más pequeños de c
, este rango es más pequeño, lo que significa que se excluyen menos valores al usarlo.
Generar matrices de recuento únicas
Fui fuerza bruta en este, iterando a través de todos los valores, generando la matriz de conteo para cada uno de ellos y uniquificando la lista resultante. Ciertamente, esto podría hacerse de manera más eficiente, pero es lo suficientemente bueno para los tipos de valores con los que estamos operando.
Esto es extremadamente rápido para los valores pequeños. Para los valores más grandes, la sobrecarga se vuelve sustancial. Por ejemplo, para n=15
, utiliza aproximadamente el 75% de todo el tiempo de ejecución. Definitivamente, este sería un área a tener en cuenta cuando se intenta ir mucho más alto que n=15
. Incluso el uso de memoria para construir una lista de las matrices de conteo para todos los valores comenzaría a ser problemático.
El número de matrices de conteo únicas es aproximadamente el 6% del número de valores para n=15
. Este recuento relativo se vuelve más pequeño a medida que se n
hace más grande.
Selección codiciosa de valores de matriz de conteo
La parte principal del algoritmo selecciona los valores de la matriz de conteo de la lista generada utilizando un enfoque codicioso simple.
Según la teoría de que el uso de matrices de conteo con recuentos pequeños es beneficioso, las matrices de conteo se ordenan por la suma de sus recuentos.
Luego se verifican en orden y se selecciona un valor si es compatible con todos los valores utilizados anteriormente. Por lo tanto, esto implica un único paso lineal a través de las matrices de conteo únicas, donde cada candidato se compara con los valores que se seleccionaron previamente.
Tengo algunas ideas sobre cómo podría mejorarse la heurística. Pero esto parecía un punto de partida razonable, y los resultados parecían bastante buenos.
Código
Esto no está muy optimizado. Tenía una estructura de datos más elaborada en algún momento, pero habría necesitado más trabajo para generalizarla más allá n=8
, y la diferencia en el rendimiento no parecía muy sustancial.
#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]
.