C ++ (heurística): 2, 4, 10, 16, 31, 47, 75, 111, 164, 232, 328, 445, 606, 814, 1086
Isso está um pouco atrás do resultado de Peter Taylor, sendo 1 a 3 menor para n=7
, 9
e 10
. A vantagem é que é muito mais rápido, para que eu possa executá-lo com valores mais altos de n
. E isso pode ser entendido sem nenhuma matemática sofisticada. ;)
O código atual é dimensionado para executar até n=15
. Os tempos de execução aumentam aproximadamente um fator de 4 para cada aumento em n
. Por exemplo, foram 0,008 segundos para n=7
, 0,07 segundos para n=9
, 1,34 segundos para n=11
, 27 segundos para n=13
e 9 minutos para n=15
.
Existem duas observações principais que usei:
- Em vez de operar com os próprios valores, a heurística opera contando matrizes. Para fazer isso, uma lista de todas as matrizes de contagem exclusivas é gerada primeiro.
- Usar matrizes de contagem com valores pequenos é mais benéfico, pois elimina menos espaço na solução. Isso é baseado em cada contagem,
c
excluindo o intervalo de c / 2
até 2 * c
de outros valores. Para valores menores de c
, esse intervalo é menor, o que significa que menos valores são excluídos usando-o.
Gere matrizes de contagem exclusivas
Fiz força bruta neste, iterando todos os valores, gerando a matriz de contagem para cada um deles e unificando a lista resultante. Isso certamente poderia ser feito com mais eficiência, mas é bom o suficiente para os tipos de valores com os quais estamos operando.
Isso é extremamente rápido para os pequenos valores. Para os valores maiores, a sobrecarga se torna substancial. Por exemplo, n=15
ele usa cerca de 75% de todo o tempo de execução. Definitivamente, seria uma área para se olhar ao tentar ir muito mais alto do que n=15
. Mesmo o uso da memória para criar uma lista de matrizes de contagem para todos os valores começaria a se tornar problemático.
O número de matrizes de contagem exclusivas é de cerca de 6% do número de valores para n=15
. Essa contagem relativa se torna menor à medida que n
se torna maior.
Seleção gananciosa dos valores da matriz de contagem
A parte principal do algoritmo seleciona os valores da matriz de contagem da lista gerada usando uma abordagem simples e gananciosa.
Com base na teoria de que o uso de matrizes de contagem com pequenas contagens é benéfico, as matrizes de contagem são classificadas pela soma de suas contagens.
Eles são então verificados em ordem e um valor é selecionado se for compatível com todos os valores usados anteriormente. Portanto, isso envolve uma única passagem linear pelas matrizes de contagem exclusivas, em que cada candidato é comparado aos valores que foram selecionados anteriormente.
Tenho algumas idéias sobre como a heurística poderia ser potencialmente aprimorada. Mas isso parecia um ponto de partida razoável, e os resultados pareciam muito bons.
Código
Isso não é altamente otimizado. Eu tinha uma estrutura de dados mais elaborada em algum momento, mas seria necessário mais trabalho para generalizá-la além n=8
, e a diferença no desempenho não parecia muito substancial.
#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]
.