Encontrei o seguinte problema em um banco de problemas on-line: existem atéconsultas que cada uma pede para calcular a soma onde é a soma dos divisores de . É dado que .
Minha solução (descrita abaixo) é baseada na peneira de Eratóstenes. Eu o implementei em C ++ e funciona em cerca de segundos, em média, o que é muito lento. Eu sei que esse problema pode ser resolvido pelo menos duas vezes mais rápido, mas não sei como.
Então, aqui está minha solução (matrizes são baseadas em 0):
M = 5 * 1e6
M = array of zeroes of size M + 1
A[1] = 1
for (k = 2; k <= M; k += 1)
for (j = k; j <= M; j += k)
A[j] += k
Eu pré -calculo através da peneira de Eratóstenes para cada abaixo do valor máximo possível. Quando o loop principal atinge , mantém o valor de . Então, redesigno para . Após esse pré-processamento, todas as consultas podem ser computadas no tempo calculando .
Como posso torná-lo mais rápido? Conheço duas fórmulas:
O problema com (a) é que computá-lo (pelo menos na minha implementação) é mais lento que o indicado acima. O problema com (b) é que não entendo como calcular a soma do prefixo com essa abordagem mais rapidamente do que no tempo .
Existe um algoritmo mais eficiente para esse problema?
(O banco do problema credita a fonte original do problema como 2012 Kharkiv, Escola de Inverno, Dia de Sergey Kopelovich, Problema H.)
fonte
Respostas:
Isso não é realmente ciência da computação ...
Você cria uma tabela d onde armazena a soma dos divisores de k, para k = 1 a M, onde M =5 ⋅106 . Essa é a parte que é de tempo crítico. Então você cria uma tabela s onde armazena a soma dos divisores para todos os 1 ≤ j ≤ k, para k = 1 a M. Isso é fácil,s0 0= 0 , sk + 1=sk+dk + 1 . E então f (L, R) =sR-sL - 1 .
A primeira tabela é o problema. Você lida com isso emO ( n logn ) . E você só precisa de um fator dois, você diz ...
Você terá uma matriz d com 5 milhões de entradas, provavelmente 4 bytes por entrada = 20 megabytes. Em um processador típico que você teria no seu computador doméstico, 20 megabytes não cabem em nenhum cache. E seu código faz muitos acessos a elementos dessa matriz em ordem quase aleatória. Para cada divisor em potencial k, você visita todos os números divisíveis por k e aumenta a soma dos divisores em k.
Vamos fazer isso com menos visitas: quando você visitar j, que é divisível por k, adicione os dois divisores ke j / k. Mas quando você fizer isso, comece comj =k2 , adicionando apenas k (porque k = j / k, e você não deseja contar o divisor duas vezes) e adicione k e j / k para mais j. Você não precisa dividir, porque j / k será igual a k + 1, k + 2, k + 3 etc. Inicializamos a matriz para o caso k = 1, que está configurando A [j] = 1 + j / 1 para j ≥ 2.
Você não salva operações. No entanto, agora você está acessando a matriz A em um padrão muito mais regular, portanto, você economizará tempo porque o acesso aos itens será mais rápido. j será menor, aumentando o número de iterações para cada j, o que fará com que a previsão de ramificação funcione melhor.
Para obter mais melhorias, você descobriria quantos itens da matriz cabem no cache do processador em seu computador e executaria todo o código apenas para subfaixas da matriz (por exemplo, alterando apenas A [0] para A [99999] e alterando A [100000] a A [199999] e assim por diante). Dessa forma, a maioria dos acessos à memória acessará apenas a memória cache, que pode ser substancialmente mais rápida.
Você está fazendo N pesquisas em uma tabela de tamanho M. Se M é substancialmente maior que N, provavelmente deve pensar em abordagens que não constroem essa tabela e que podem ser muito mais lentas por pesquisa, mas mais rápidas em geral devido a o pequeno número de pesquisas. Mesmo no caso em que N ≤ 100.000 e M = 5.000.000, você pode, por exemplo, não contar os divisores 1, 2, 3, 4, j / 1, j / 2, j / 3, j / 4 na tabela (o que torna um pouco mais rápido para compilar) e lidar com isso durante a pesquisa.
Ou você pode adicionar a soma dos divisores apenas para números ímpares e calcular a soma dos divisores para números pares (se a soma dos divisores de um k ímpar é s, então a soma de 2k é 3s, para 4k é 7s , para 8k são 15s etc.), o que economizaria quase um fator 2.
PS. Eu o medi ... tornando o algoritmo para contar todas as somas de divisores mais amigáveis ao cache, adicionando j e k / j dobrou a velocidade. Calcular a soma dos divisores para k ímpares primeiro e depois calcular k mesmo a partir dos valores ímpares, torna-o um total de 7 vezes mais rápido. Obviamente, todos são apenas fatores constantes.
fonte
Então, deixe-me reorganizar um pouco o seu problema: o uso da peneira primária deve ser útil, mas a peneira Erathostenes normal não é boa o suficiente.
O que você precisa é de uma peneira primária trabalhando em tempo linear, atingindo todos os números apenas uma vez.1 1 como um divisor).
Uma descrição da peneira linear de horário nobre mostra como cruzar todos os números apenas uma vez.
O que são benefícios? Bem, se em vez de cruzar números inserirmos a soma dos divisores, teremos um algoritmo rápido para colocar divisores (lembre-se de
Também há uma etapa adicional, os números primos não são calculados, portanto, ao encontrar uma, devemos escrever seu divisor como esse número + 1.
Em seguida, deve haver aprovação cumulativa (passando pela matriz adicionando o último item para torná-lo soma de todos os divisores anteriores).
Dessa forma, todos os números devem ser escritos exatamente uma vez, portanto é certamente melhor do que a tentativa original.
O que mais poderia ter sido feito?
Como existem menos consultas do que números, pensei que talvez possamos omitir o cálculo de toda a matriz?
Isso pode ser feito de pelo menos duas maneiras: a mais óbvia é tornar a matriz parcial (ou mesmo inteira) offline (não durante a medição do tempo), aumentando o programa, mas não havia limite de tamanho.
Outro é calcular toda a matriz de divisores cumulativos e ajustar algumas funções que recuperam resultados de índices.
As funções em si podem ser um pouco complicadas ou, para facilitar o pensamento, podemos dividi-las em intervalos - tornando-os mais curtos e fáceis de encontrar.
A enorme complexidade por trás disso é feita offline e durante o tempo de execução, apenas as consultas são importantes, uma vez que não há peneira.
fonte
Você pode armazenar resultados pré-calculados para intervalos {L = 1, R = k * 10 ^ 4} e força bruta apenas cerca de 2 * 10 ^ 4 números
fonte