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