Aqui está um simples problema de programação do SPOJ: http://www.spoj.com/problems/PROBTRES/ .
Basicamente, você é solicitado a produzir o maior ciclo de Collatz para números entre iej. (O ciclo de Collatz de um número $ n $ é o número de etapas para, eventualmente, passar de $ n $ a 1.)
Eu estava procurando uma maneira Haskell de resolver o problema com desempenho comparativo do que o Java ou C ++ (para se encaixar no limite de tempo de execução permitido). Embora uma solução Java simples que memorize a duração do ciclo de qualquer ciclo já calculado funcione, não tive êxito em aplicar a ideia de obter uma solução Haskell.
Eu tentei o Data.Function.Memoize, bem como a técnica de memorização de tempo de registro feita em casa, usando a ideia desta publicação: /programming/3208258/memoization-in-haskell . Infelizmente, a memorização realmente torna o cálculo do ciclo (n) ainda mais lento. Eu acredito que a desaceleração vem do alto do caminho Haskell. (Tentei executar com o código binário compilado, em vez de interpretar.)
Eu também suspeito que simplesmente iterar números de i a j pode ser caro ($ i, j \ le10 ^ 6 $). Então, tentei pré-calcular tudo para a consulta de intervalo, usando a idéia de http://blog.openendings.net/2013/10/range-trees-and-profiling-in-haskell.html . No entanto, isso ainda dá o erro "Tempo limite excedido".
Você pode ajudar a informar um bom programa Haskell competitivo para isso?
fonte
Respostas:
Responderei em Scala, porque meu Haskell não é tão recente e as pessoas acreditam que essa é uma questão geral de algoritmo de programação funcional. Vou me ater às estruturas e conceitos de dados que são prontamente transferíveis.
Podemos começar com uma função que gera uma sequência de collatz, que é relativamente direta, exceto pela necessidade de passar o resultado como argumento para torná-lo recursivo:
Na verdade, isso coloca a sequência na ordem inversa, mas é perfeita para o próximo passo, que é armazenar os comprimentos em um mapa:
Você poderia chamar isso com a resposta da primeira etapa, o comprimento inicial e um mapa vazio, como
calculateLengths(collatz(22), 1, Map.empty))
. É assim que você memoriza o resultado. Agora precisamos modificarcollatz
para poder usar isso:Eliminamos a
n == 1
verificação porque podemos apenas inicializar o mapa com1 -> 1
, mas precisamos adicionar1
os comprimentos que inserimos no mapacalculateLengths
. Agora, ele também retorna o comprimento memorizado onde parou de se repetir, que podemos usar para inicializarcalculateLengths
, como:Agora que temos implementações relativamente eficientes das peças, precisamos encontrar uma maneira de alimentar os resultados do cálculo anterior na entrada do próximo cálculo. Isso é chamado de
fold
e se parece com:Agora, para encontrar a resposta real, basta filtrar as chaves no mapa entre o intervalo especificado e encontrar o valor máximo, fornecendo um resultado final de:
No meu REPL para intervalos de tamanho 1000 ou mais, como na entrada de exemplo, a resposta retorna praticamente instantaneamente.
fonte
Karl Bielefeld já respondeu bem à pergunta, vou apenas adicionar uma versão Haskell.
Primeiro, uma versão simples e sem memorização do algoritmo básico para mostrar a recursão eficiente:
Isso deve ser quase autoexplicativo.
Eu também vou usar um simples
Map
para armazenar os resultados.Sempre podemos procurar nossos resultados finais na loja, portanto, para um único valor, a assinatura é
Vamos começar com o caso final
Sim, podemos adicionar isso de antemão, mas eu não me importo. Próximo caso simples, por favor.
Se o valor estiver lá, está. Ainda não fazendo nada.
Se o valor não estiver lá, temos que fazer alguma coisa . Vamos colocar em uma função local. Observe como essa parte parece muito próxima da solução "simples", apenas a recursão é um pouco mais complexa.
Agora finalmente fazemos algo. Se encontrarmos o valor calculado em
store''
(nota de rodapé: existem dois marcadores de sintaxe haskell, mas um é feio, o outro fica confuso com o símbolo primo. Essa é a única razão para o duplo primo.), Basta adicionar o novo valor. Mas agora fica interessante. Se não encontrarmos o valor, precisamos calculá-lo e fazer a atualização. Mas já temos funções para ambos! entãoE agora podemos calcular um único valor com eficiência. Se quisermos calcular vários, apenas passamos a loja através de uma dobra.
(É aqui que você pode inicializar o caso 1/1.)
Agora tudo o que precisamos fazer é extrair o máximo. Por enquanto, não pode haver um valor na loja que seja maior que um no intervalo, portanto basta dizer
Obviamente, se você quiser calcular vários intervalos e compartilhar a loja entre esses cálculos (as dobras são suas amigas), seria necessário um filtro, mas esse não é o foco principal aqui.
fonte
Data.IntMap.Strict
deve ser usado.