Existem algoritmos do mundo real que superam muito a classe abaixo? [fechadas]

39

Ontem à noite eu estava discutindo com outro programador que, embora algo possa ser O (1), uma operação que é O (n) pode superá-la se houver uma constante grande no algoritmo O (1). Ele discordou, então eu trouxe aqui.

Existem exemplos de algoritmos que superam muito os da classe abaixo? Por exemplo, O (n) sendo mais rápido que O (1) ou O (n 2 ) sendo mais rápido que O (n).

Matematicamente, isso pode ser demonstrado para uma função com limites superiores assintóticos, quando você desconsidera fatores constantes, mas esses algoritmos existem na natureza? E onde eu encontraria exemplos deles? Para que tipos de situações elas são usadas?

KyleWpppd
fonte
15
Mesmo para algoritmos "grandes", menores não são necessariamente melhores. Por exemplo, a eliminação gaussiana é O (n ^ 3), mas existem algoritmos que podem fazê-lo em O (n ^ 2), mas o coeficiente para o tempo quadrático de algo é tão grande que as pessoas simplesmente seguem o O (n ^ 3) um.
BlackJack
11
Você precisa adicionar "... para problemas do mundo real" ou algo parecido para tornar essa uma pergunta sensata. Caso contrário, você só precisará aumentar o nsuficiente para compensar a constante (que é o ponto da notação O-grande).
Starblue #
8
Não tome notação grande-O para velocidade.
Codism 7/10/11
16
O objetivo da notação big-O não é dizer com que rapidez um algoritmo é executado, mas quão bem ele é dimensionado.
BlueRaja - Danny Pflughoeft
4
Estou surpreso que ninguém tenha mencionado o algoritmo Simplex para resolver LP. Ele tem um pior caso exponencial com um tempo de execução linear esperado. Na prática, é bastante rápido. É trivial construir um problema que também exiba o pior tempo de execução. Além disso, é muito usado.
Ccoakley

Respostas:

45

Pesquisas em tabelas de dados fixas muito pequenas. Uma tabela de hash otimizada pode ser O (1) e ainda mais lenta que uma pesquisa binária ou mesmo uma pesquisa linear devido ao custo do cálculo de hash.

Loren Pechtel
fonte
14
Mais precisamente, a pesquisa de hashtable é O (m), onde m é o tamanho da chave. Você só pode chamar O (1) se o tamanho da chave for constante. Além disso, geralmente isso é amortizado - caso contrário, a tabela não pode crescer / encolher. As árvores ternárias geralmente podem bater tabelas de hash para pesquisas de strings em contextos em que as strings frequentemente não são encontradas - a pesquisa de árvores ternárias geralmente descobre que a chave não está presente enquanto ainda verifica o primeiro ou os dois caracteres da string, onde A versão hashtable ainda não calculou o hash.
precisa saber é o seguinte
2
Adoro a resposta de Loren Pechtel e o primeiro comentário de Steve314. Eu realmente vi isso acontecer. Se você criar uma classe Java que possui um método hashcode () que leva muito tempo para retornar o valor do hash (e não pode / não pode armazená-lo em cache), use instâncias dessa classe em uma coleção do tipo hash (como HashSet) tornará essa coleção MUITO mais lenta que uma coleção do tipo array (como ArrayList).
Shivan Dragon
1
@ Steve314: por que você assume que as funções de hash são O (m), onde m é o tamanho da chave? As funções de hash podem ser O (1), mesmo se você estiver lidando com strings (ou outro tipo complexo). Não há muito valor para colocá-lo na definição formal do que simplesmente perceber que a função hash pode alterar significativamente a complexidade se uma estrutura de dados incorreta (tabela de hash) for escolhida para sua entrada (o tamanho da chave é imprevisível).
Codism
1
@ Steve314: Note que eu disse tabelas de dados fixas. Eles não crescem. Além disso, você só obtém desempenho O (1) de uma tabela de hash se puder otimizar a chave para garantir que não haja colisões.
Loren Pechtel 7/10
1
@ Loren - estritamente, se a tabela tiver um tamanho fixo, há uma quantidade máxima constante de tempo que você pode gastar procurando um espaço livre. Ou seja, no máximo, pode ser necessário verificar n-1 slots já preenchidos, em que n é o tamanho constante da tabela. Portanto, uma tabela de hash de tamanho fixo realmente é O (1), sem a necessidade de análise amortizada. Isso não significa que você não se importa com os acessos ficando mais lentos à medida que a tabela se enche - apenas que não é o que O grande expressa.
Steve314
25

Multiplicação da matriz. O algoritmo ingênuo O (n ^ 3) é frequentemente usado na prática tão rápido quanto o O de Strassen (n ^ 2.8) para matrizes de pequenas dimensões; e Strassen é usado em vez do algoritmo O (n ^ 2.3) Coppersmith – Winograd para matrizes maiores.

Peter Taylor
fonte
2
Coppersmith-Winograd NUNCA é usado. Implementá-lo seria uma tarefa horrível em si mesma e a constante é tão ruim que seria inviável mesmo para os problemas da matriz científica moderna.
tskuzzy
24

Um exemplo simples é a diferença entre vários algoritmos de classificação. Mergesort, Heapsort e alguns outros são O (n log n) . Quicksort é O (n ^ 2) no pior caso. Mas muitas vezes o Quicksort é mais rápido e, na verdade, ele executa em média como O (n log n) . Mais informações .

Outro exemplo é a geração de um único número de Fibonacci. O algoritmo iterativo é O (n) , enquanto o algoritmo baseado em matriz é O (log n) . Ainda assim, para o primeiro par de milhares de números de Fibonacci, o algoritmo iterativo é provavelmente mais rápido. Isso também depende da implementação, é claro!

Algoritmos com melhor desempenho assintótico podem conter operações caras que não são necessárias com um algoritmo com desempenho pior, mas operações mais simples. No final, a anotação O diz apenas algo sobre desempenho quando o argumento em que ele opera aumenta dramaticamente (se aproxima do infinito).

molf
fonte
Esta é uma ótima explicação do Big-O, mas falha em abordar a questão principal, que é para casos específicos em que um algoritmo O (n) será mais rápido que um O (1).
precisa saber é o seguinte
O número um de Fibonacci está um pouco desligado. O tamanho da saída é exponencial no tamanho da entrada, portanto é uma diferença entre O (lg n * e ^ n) vs O (lg lg n * e ^ n).
Peter Taylor
Adendo: na melhor das hipóteses. O algoritmo baseado em matriz faz multiplicação com números da ordem de 1,5 ^ n, então O (lg lg n * ne ^ n) pode ser o melhor limite possível.
Peter Taylor
1
O Quicksort é normalmente descrito como O (n log n) desempenho esperado de qualquer maneira - o pior caso é bastante improvável para entradas aleatórias, e construir alguma aleatoriedade em um prepass ou na seleção de pivô significa que o pior caso geral é muito improvável para tamanhos de entrada significativos. O pior caso é menos relevante do que o quicksort (1) muito simples e (2) muito amigável ao cache, os quais levam a fatores constantes significativamente melhores do que em muitos outros algoritmos de classificação.
precisa saber é o seguinte
(2) é precisamente o tipo de consideração externa que deve ser levada em consideração quando se olha para o desempenho do grande O. Algoritmicamente, o Mergesort deve sempre superar o Quicksort, mas o uso de recursos e a localidade do cache geralmente invertem suas posições de desempenho no mundo real.
18712 Dan Lyons
18

Nota: Por favor, leia os comentários de @ back2dos abaixo e de outros gurus, pois eles são de fato mais úteis do que eu escrevi - Obrigado por todos os colaboradores.

Penso que no gráfico abaixo (extraído de: Big O notation , procure "The Pessimistic Nature of Algorithms:"), você pode ver que O (log n) nem sempre é melhor do que dizer, O (n). Então, acho que seu argumento é válido.

Pic-1

Emmad Kareem
fonte
6
A questão queria exemplos específicos do mundo real de algoritmos. Isso não tem nenhum como está.
Megan Walker #
19
Você não vê nada no gráfico que responda à pergunta. Isso é enganador. Este gráfico apenas plota as funções y = 1, y = log xetc., e a interseção de y = 1e y = xé realmente o ponto (1,1). Se isso realmente estivesse correto, do que diria, os algoritmos de maior complexidade podem ser mais rápidos para 0 a 2 entradas, algo que as pessoas dificilmente se importariam. O que o gráfico deixa completamente de levar em conta (e de que vem a diferença de desempenho perceptível em questão) são fatores constantes.
Oct2
@ Samuel Walker, obrigado pelo comentário. O link fornecido (Link-1) possui alguns exemplos de algoritmos por categoria.
NoChance
5
@ back2dos: O gráfico por si só não responde à pergunta, mas pode ser usado para respondê-la. A forma de cada função exibida é a mesma para qualquer escala e fator constante. Com isso, o gráfico mostra que, dada a combinação de funções, há uma gama de entradas para as quais uma é menor e uma gama de entradas para as quais a outra é.
Jan Hudec
2
@dan_waterworth, você está certo, eu admito esse ponto e removo esse comentário. No entanto, a resposta é incorreta ou enganosa em dois aspectos: 1) O ponto principal do Big-O é que ele fornece um limite superior à complexidade; só é significativo para n grande porque descartamos explicitamente termos menores que são sobrecarregados pelo maior termo à medida que n cresce. 2) O objetivo da pergunta é encontrar exemplos de dois algoritmos em que aquele com o limite maior de Big-O supera o de um limite inferior. Esta resposta falha porque não fornece nenhum exemplo.
Caleb
11

Para valores práticos de n, sim. Isso aparece muito na teoria do CS. Freqüentemente, existe um algoritmo complicado que possui tecnicamente um desempenho grande de Oh, mas os fatores constantes são tão grandes que o tornam impraticável.

Certa vez, meu professor de geometria computacional descreve um algoritmo para triangular um polígono em tempo linear, mas ele terminou com "muito complicado. Acho que ninguém realmente o implementou" (!!).

Além disso, as pilhas de fibonacci têm melhores características do que as pilhas normais, mas não são muito populares, porque na prática não se saem tão bem quanto as pilhas regulares. Isso pode cascatear com outros algoritmos que usam pilhas - por exemplo, os caminhos mais curtos de Dijkstra são matematicamente mais rápidos com uma pilha de fibonacci, mas geralmente não na prática.

Gabe Moothart
fonte
É mais rápido para grandes gráficos da ordem de 100.000 vértices.
tskuzzy
Montes de Fibonacci foram o meu primeiro (na verdade, o segundo) pensamento também.
Konrad Rudolph
10

Compare a inserção em uma lista vinculada e a inserção em uma matriz redimensionável.

A quantidade de dados deve ser bastante grande para que a inserção da lista vinculada O (1) valha a pena.

Uma lista vinculada possui uma sobrecarga extra para os próximos ponteiros e desreferências. Uma matriz redimensionável precisa copiar dados. Essa cópia é O (n), mas na prática é muito rápida.

Winston Ewert
fonte
1
Uma matriz redimensionável é dobrada em tamanho cada vez que é preenchida; portanto, o custo médio de redimensionamento por inserção é O (1).
Kevin cline #
2
@kevincline, sim, mas o O (n) vem da necessidade de mover todos os elementos após o ponto de inserção para a frente. A alocação é amortizada O (1) tempo. Meu argumento é que esse movimento ainda é muito rápido, portanto, na prática, geralmente é melhor do que listas vinculadas.
Winston Ewert
A razão pela qual matrizes contíguas são tão rápidas em comparação com listas vinculadas é devido ao cache do processador. Atravessar uma lista vinculada causará uma falta de cache para cada elemento. Para obter o melhor dos dois mundos, use uma lista vinculada não rolada .
dan_waterworth
Matrizes redimensionáveis ​​nem sempre copiam. Depende do que está sendo executado e se há algo no caminho. O mesmo para o tamanho da duplicação, específico da implementação. O roll over roll over coisa é um problema. As listas vinculadas geralmente são melhores para filas de tamanho desconhecido, embora os buffers rotativos permitam que as filas executem seu dinheiro. Em outros casos, as listas vinculadas são úteis porque a alocação ou expansão simplesmente não permite que você tenha coisas contíguas o tempo todo, portanto, você precisará de um ponteiro de qualquer maneira.
jgmjgm 9/01
@jgmjgm, se você inserir no meio de uma matriz redimensionável, absolutamente copiará os elementos depois disso.
Winston Ewert
8

A notação Big-Oh é usada para descrever a taxa de crescimento de uma função, portanto, é possível que um algoritmo O (1) seja mais rápido, mas apenas até um determinado ponto (o fator constante).

Notações comuns:

O (1) - O número de iterações (às vezes você pode se referir a isso como tempo gasto pelo usuário pela função) não depende do tamanho da entrada e é constante.

O (n) - O número de iterações cresce em uma proporção linear ao tamanho da entrada. Significado - se o algoritmo itera através de qualquer entrada N, 2 * N vezes, ainda é considerado O (n).

O (n ^ 2) (quadrático) - O número de iterações é o tamanho da entrada ao quadrado.

Yam Marcovic
fonte
2
Para adicionar um exemplo a uma resposta excelente: um método O (1) pode levar 37 anos por chamada, enquanto um método O (n) pode levar 16 * n microssegundos por chamada. O que é mais rápido?
Kaz Dragon
16
Não vejo completamente como isso responde à pergunta.
Avakar 7/10/11
7
Eu entendo big-O. Isso não aborda a questão real, que é exemplos específicos de funções em que algoritmos com um grande O menor são superados por aqueles com um grande O maior.
precisa saber é o seguinte
Quando você coloca a pergunta no formato "Existem exemplos ...", alguém inevitavelmente responderá "Sim". sem dar nada.
rakslice
1
@rakslice: Talvez sim. No entanto, este site exige explicação (ou melhor ainda, prova) de quaisquer declarações que você fizer. Agora, a melhor maneira de prova, que existem tais exemplos é dar um;)
back2dos
6

As bibliotecas Regex geralmente são implementadas para fazer backtracking com pior tempo exponencial, em vez de geração do DFA com complexidade de O(nm).

O retrocesso ingênuo pode ter um desempenho melhor quando a entrada permanece no caminho rápido ou falha sem a necessidade de retroceder excessivamente.

(Embora essa decisão não seja apenas baseada no desempenho, é também permitir referências anteriores.)

dan_waterworth
fonte
Eu acho que também é parcialmente histórico - o algoritmo para transformar uma expressão regular em um DFA foi patenteado quando algumas das ferramentas anteriores (sed e grep, eu acho) estavam sendo desenvolvidas. É claro que ouvi isso do meu professor de compiladores, que não tinha muita certeza, então essa é uma conta de terceiros.
Tikhon Jelvis
5

Um O(1)algoritmo:

def constant_time_algorithm
  one_million = 1000 * 1000
  sleep(one_million) # seconds
end

Um O(n)algoritmo:

def linear_time_algorithm(n)
  sleep(n) # seconds
end

Claramente, para qualquer valor de nonde n < one_million, o O(n)algoritmo dado no exemplo será mais rápido que o O(1)algoritmo.

Embora este exemplo seja um pouco complexo, é equivalente em espírito ao exemplo a seguir:

def constant_time_algorithm
  do_a_truckload_of_work_that_takes_forever_and_a_day
end

def linear_time_algorithm(n)
  i = 0
  while i < n
    i += 1
    do_a_minute_amount_of_work_that_takes_nanoseconds
  end
end

Você deve saber as constantes e coeficientes em sua Oexpressão, e você deve saber o intervalo esperado de n, a fim de determinar a priori qual algoritmo vai acabar por ser mais rápido.

Caso contrário, você deve comparar os dois algoritmos com valores nno intervalo esperado para determinar a posteriori qual algoritmo acabou sendo mais rápido.

yfeldblum
fonte
4

Classificação:

A classificação por inserção é O (n ^ 2), mas supera os outros algoritmos de classificação O (n * log (n)) para um pequeno número de elementos.

Essa é a razão pela qual a maioria das implementações de classificação usa uma combinação de dois algoritmos. Por exemplo, use a classificação de mesclagem para dividir grandes matrizes até atingirem um determinado tamanho de matriz, depois use a classificação de inserção para classificar as unidades menores e mesclá-las novamente com a classificação de mesclagem.

Consulte Timsort a implementação padrão atual da classificação Python e Java 7 que usa essa técnica.

OliverS
fonte
3

O Bubblesort na memória pode superar o quicksort quando o programa está sendo trocado para o disco ou é necessário ler todos os itens do disco ao comparar.

Este deve ser um exemplo com o qual ele pode se relacionar.

user1249
fonte
As complexidades citadas no quicksort e bubblesort não assumem acesso aleatório à memória O (1)? Se esse não for mais o caso, a complexidade das classificações rápidas não precisaria ser reexaminada?
Viktor Dahl
@ViktorDahl, o tempo de acesso ao item não faz parte do que tradicionalmente está sendo medido nas complexidades do algoritmo de classificação, de modo que "O (1)" não é a escolha certa de palavras aqui. Use "tempo constante". Há algum tempo, o PHK escreveu um artigo sobre algoritmos de classificação, sabendo que alguns itens são mais caros para serem recuperados do que outros (memória virtual) - queue.acm.org/detail.cfm?id=1814327 - você pode achar interessante.
Eu vejo o meu erro agora. Geralmente, mede-se o número de comparações e, é claro, elas não são afetadas pela velocidade do meio de armazenamento. Além disso, obrigado pelo link.
Viktor Dahl
3

Geralmente, os algoritmos mais avançados assumem uma certa quantidade de configuração (cara). Se você precisar executá-lo apenas uma vez, talvez seja melhor usar o método da força bruta.

Por exemplo: pesquisa binária e pesquisa de tabela de hash são muito mais rápidas por pesquisa do que uma pesquisa linear, mas exigem que você classifique a lista ou construa a tabela de hash, respectivamente.

A classificação custará N log (N) e a tabela de hash custará pelo menos N. Agora, se você estiver fazendo centenas ou milhares de pesquisas, isso ainda é uma economia amortizada. Mas se você precisar fazer apenas uma ou duas pesquisas, pode fazer sentido fazer apenas a pesquisa linear e economizar o custo de inicialização.

Matthew Scouten
fonte
1

A descriptografia é geralmente 0 (1). Por exemplo, o espaço da chave para DES é 2 ^ 56, portanto, a descriptografia de qualquer mensagem é uma operação de tempo constante. É que você tem um fator de 2 ^ 56, então é uma constante muito grande.

Zachary K
fonte
A descriptografia de uma mensagem O ( n ) não é, onde n é proporcional ao tamanho da mensagem? Contanto que você tenha a chave correta, o tamanho da chave nem será levado em consideração; alguns algoritmos têm processos mínimos de configuração / expansão de chaves (DES, RSA - observe que a geração de chaves ainda pode ser uma tarefa complexa, mas que nada tem a ver com a expansão de chaves), enquanto outros são extremamente complexos (Blowfish vem à mente), mas Uma vez feito, o tempo para realizar o trabalho real é proporcional ao tamanho da mensagem, daí O (n).
um CVn
Você provavelmente quer dizer análise criptográfica em vez de descriptografia?
precisa saber é o seguinte
3
Bem, sim, existem inúmeras coisas que você pode considerar constantes e declarar que um algoritmo é O (1). [triagem implicitamente assume os elementos levar uma quantidade constante de tempo para comparar, por exemplo, ou qualquer matemática com números não bignum]
Random832
1

Diferentes implementações de conjuntos vêm à minha mente. Um dos mais ingênuos é implementá-lo sobre um vetor, o que significa que, removeassim como containse, portanto, addtodos recebem O (N).
Uma alternativa é implementá-lo sobre algum hash de uso geral, que mapeia hashes de entrada para valores de entrada. Executa implementação conjunto Tais um com o (1) para add, containse remove.

Se assumirmos que N é cerca de 10 ou mais, então a primeira implementação é provavelmente mais rápida. Tudo o que precisa fazer para encontrar um elemento é comparar 10 valores a um.
A outra implementação terá que iniciar todos os tipos de transformações inteligentes, que podem ser muito mais caras do que fazer 10 comparações. Com toda a sobrecarga, você pode até ter falhas de cache e, em seguida, realmente não importa a rapidez da sua solução em teoria.

Isso não significa que a pior implementação que você pode imaginar terá um desempenho decente, se N for pequeno o suficiente. Simplesmente significa, para N suficientemente pequeno, que uma implementação ingênua, com pouco espaço e sobrecarga, pode realmente exigir menos instruções e causar menos falhas de cache do que uma implementação que coloca a escalabilidade em primeiro lugar e, portanto, será mais rápida.

Você realmente não pode saber o quão rápido algo está em um cenário do mundo real, até que você o coloque em um e simplesmente meça. Muitas vezes, os resultados são surpreendentes (pelo menos para mim).

back2dos
fonte
1

Sim, para N. adequadamente pequeno. Sempre haverá um N, acima do qual você sempre terá a ordem O (1) <O (lg N) <O (N) <O (N log N) <O (N ^ c ) <O (c ^ N) (onde O (1) <O (lg N) significa que em um algoritmo O (1) haverá menos operações quando N for adequadamente grande ec é uma constante fixa maior que 1 )

Digamos que um algoritmo O (1) específico execute exatamente f (N) = 10 ^ 100 (um googol) e um algoritmo O (N) execute exatamente g (N) = 2 N + 5 operações. O algoritmo O (N) oferecerá maior desempenho até que você N seja aproximadamente um googol (na verdade, quando N> (10 ^ 100 - 5) / 2), portanto, se você esperasse que N estivesse na faixa de 1000 a um bilhão, sofreria uma penalidade maior usando o algoritmo O (1).

Ou, para uma comparação realista, digamos que você esteja multiplicando números de n dígitos. O algoritmo Karatsuba é no máximo 3 n ^ (lg 3) operações (que é aproximadamente O (n ^ 1.585)) enquanto o algoritmo Schönhage – Strassen é O (N log N log log N), que é uma ordem mais rápida , mas para citar wikipedia:

Na prática, o algoritmo de Schönhage-Strassen começa a superar métodos mais antigos, como a multiplicação de Karatsuba e Toom-Cook, para números além de 2 ^ 2 ^ 15 a 2 ^ 2 ^ 17 (10.000 a 40.000 dígitos decimais). [4] [5] [6 ]

Portanto, se você estiver multiplicando números de 500 dígitos, não faz sentido usar o algoritmo "mais rápido" pelos grandes argumentos O.

EDIT: Você pode encontrar determinar f (N) comparado g (N), tomando o limite N-> infinito de f (N) / g (N). Se o limite for 0, então f (N) <g (N), se o limite for infinito, então f (N)> g (N), e se o limite for alguma outra constante, então f (N) ~ g (N) em termos de grande notação O.

dr jimbob
fonte
1

O método simplex para programação linear pode ser exponencial no pior dos casos, enquanto algoritmos de ponto interior relativamente novos podem ser polinomiais.

No entanto, na prática, o pior cenário exponencial para o método simplex não aparece - o método simplex é rápido e confiável, enquanto os algoritmos iniciais de pontos interiores eram muito lentos para serem competitivos. (Agora existem algoritmos de pontos interiores mais modernos que são competitivos - mas o método simplex também é ...)

tempestade
fonte
0

O algoritmo de Ukkonen para criar tentativas de sufixo é O (n log n). Tem a vantagem de estar "on-line" - ou seja, você pode acrescentar mais texto de forma incremental.

Recentemente, outros algoritmos mais complexos alegaram ser mais rápidos na prática, principalmente porque o acesso à memória tem uma localidade mais alta, melhorando assim a utilização do cache do processador e evitando paradas no pipeline da CPU. Veja, por exemplo, esta pesquisa , que afirma que 70-80% do tempo de processamento é gasto aguardando memória, e este artigo descrevendo o algoritmo "wotd".

As tentativas de sufixo são importantes em genética (para correspondência de seqüências genéticas) e, um pouco menos importante, na implementação de dicionários Scrabble.

Ed Staub
fonte
0

Sempre existe o algoritmo mais rápido e mais curto para qualquer problema bem definido . Porém, é apenas puramente teoricamente o algoritmo (assintoticamente) mais rápido.

Dado qualquer descrição de um problema P e uma instância para esse problema que eu , ele enumera todos os algoritmos possíveis A e provas Pr , verificando para cada tal par se Pr é uma prova válida de que A é o algoritmo assintoticamente mais rápido para P . Se ele encontrar uma tal prova, em seguida, executa um em I .

A procura desse par à prova de problemas tem complexidade O (1) (para um problema fixo P ), portanto, você sempre usa o algoritmo assintoticamente mais rápido para o problema. No entanto, como essa constante é tão indescritivelmente enorme em quase todos os casos, esse método é completamente inútil na prática.

Alex ten Brink
fonte
0

Muitas linguagens / estruturas usam correspondência de padrão ingênua para corresponder as strings em vez do KMP . Procuramos por cordas como Tom, Nova York, em vez de ababaabababababaababababababab.

Lukasz Madon
fonte