Soma da função sumatória dos divisores com a peneira de Eratóstenes


7

Encontrei o seguinte problema em um banco de problemas on-line: existem até 105 consultas que cada uma pede para calcular a soma onde é a soma dos divisores de . É dado que .

k=euRσ(k)
σ(k)k1 1euR5106

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.0,9

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 .σ(k)kkA[k]σ(k)A[k]i=1kσ(i)O(1)A[R]A[L1]

Como posso torná-lo mais rápido? Conheço duas fórmulas:

(a)     σ(p1a1psas)=i=1spiai+11pi1
(b)     k=1nσ(k)=k=1nknk

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 .O(n2)

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.)


Se eu entendi corretamente, você cria uma grande LookUp Table e responde a consultas, tudo em tempo de execução, e o gargalo está calculando a LookUp? Há duas coisas: você poderia reorganizar seus loops e dividir o trabalho de maneira diferente? Se houver limites na memória e no tempo, mas não no tamanho do programa, você poderá tornar parte da tabela offline?
mal

Você entende corretamente. Não sei como reorganizar os loops, mas agora acho que a peneira linear e o cálculo com a fórmula (a) podem ser mais rápidos.
Igor

este é um problema do "mundo real" ou (aparentemente) "artificial" para, por exemplo, um concurso de programação ou matemática? existe uma pergunta real aqui sobre o cálculo mais eficiente da tabela, mas mesmo uma implementação bastante simples pode calcular todo o tamanho "moderado"106tabela em meros segundos ou menos e, em seguida (aparentemente pelo descr), todas as consultas subseqüentes são apenas O (1) pesquisas de tabela. então qual é o problema com isso? de qualquer maneira, se for artificial, aparentemente, é o caso, não gosto de alguns dos problemas iniciais que tentam fazê-lo parecer um problema do mundo real. sua teoria dos números basicamente aplicado ...
vzn

para o problema artificial, sugira que não forneça limites finitos na mesa e, em vez disso, seja reformulado / focado para perguntar sobre a eficiência de O (f (n)) de diferentes abordagens, ou seja, "a abordagem direta leva tempo O (f1 (n)), pode isso pode ser melhorado para o tempo O (f2 (n))? ". de qualquer maneira tentar Computer Science bate-papo para análise posterior
vzn

@vzn Obrigado pela atenção à minha pergunta. A origem do problema é mencionada em questão e não é "mundo real". Não se trata de computação científica ultrarrápida, mas de algoritmos simples e moderadamente eficientes.
Igor

Respostas:


5

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 0, sk+1 1=sk+dk+1 1. E então f (L, R) =sR-seu-1 1.

A primeira tabela é o problema. Você lida com isso emO(nregistron). 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.

A [1] = 1
for (j = 2; j ≤ M; j += 1)
    A [j] = 1 + j

for (k = 2; k*k ≤ M; k += 1)
    j = k*k
    A [j] += k
    j += k
    s = k + (k + 1)
    while j ≤ M
        A [j] += s
        j += k
        s += 1 // s equals k + j / k

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.


3

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.
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 de1 1 como um divisor).

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.


-1

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


11
O problema é que a criação dos resultados pré-calculados leva muito tempo.
gnasher729

Por que essa seria uma boa abordagem?
Raphael
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.