goll.cc
🇺🇸

Raiz quadrada por restauração

Como o nome já diz, esse algoritmo pode ser usado para calcular a parte inteira da raiz quadrada de um número inteiro. Funciona de forma similar à divisão por restauração, de modo iterativo, onde cada iteração nos deixa mais próximos do resultado final. Mas como e por quê esse algoritmo funciona?

Essa é a pergunta que vamos responder hoje, especificamente para base 2 (binário). De uma forma bem peculiar, talvez.

P.S. A versão em inglês dessa página tem alguns links para páginas com mais explicações.

Descrição

Dado um número 𝑋 representado em binário, podemos adicionar um zero à esquerda se necessário, de modo que esse número tenha 2𝑘 dígitos, para algum inteiro 𝑘. Após fazer isso, podemos considerar 𝑄 e 𝑅 que satisfaçam 𝑋=𝑄2+𝑅, de modo que 𝑅0 e 𝑄 seja maximizado (essa é uma forma de definir a raiz quadrada para inteiros). 𝑄 terá 𝑘 dígitos:

𝑋é no máximo22𝑘1<22𝑘=(2𝑘)2

Assim, 𝑞+(𝑘1) é o bit mais significativo (MSB) na representação de binária de 𝑄, e 𝑞0 é o menos significativo (LSB). O primeiro passo do algoritmo é dividir 𝑋 em pares de bits. Começamos do par mais significativo e subtraimos 1. Se o resultado for maior ou igual a 0, então 𝑞𝑘1=1, caso contrário, 𝑞𝑘1=0 e o valor anterior desse par é restaurado.

Daí, o resultado obtido é concatenado com o próximo par e tentamos subtrair 0𝑞𝑘101 (essa notação foi usada para representar concatenação). O resultado irá determinar 𝑞𝑘2. O próximo passo seria agrupar novamente com o próximo par e subtrair 00𝑞𝑘1𝑞𝑘201, encontrando 𝑞𝑘3 etc. Esse processo é repetido até que não haja mais pares. O resto da última subtração corresponde a 𝑅.

Uma possível solução

Um fato curioso: todos os números quadrados podem ser expressos como uma soma de números ímpares consecutivos, começando do 1.

𝑛=1𝑝(2𝑛1)=𝑝2

Isso permite implementar uma solução bem simples, que consiste em subtrair ímpares de 𝑋 sucessivamente, mantendo uma contagem de quantos ímpares foram subtraídos e do valor de 𝑋. Ao final, quando não for mais possível subtrair, a quantidade de ímpares subtraídos será 𝑄 e o resto será 𝑅. Mas esse processo é bem ineficiente. Dá pra melhorar bastante, e é isso que esse algoritmo faz: subtrai vários números ímpares, em vez de apenas um, por vez.

Uma demonstração

Vamos tentar entender o que está acontecendo em cada iteração. Assumindo que estamos em uma determinada iteração e todos os bits de 𝑄 determinados até então são 0, podemos concluir que o valor subtraído na iteração atual é uma potência de 4. No caso base (𝑘=1), subtraímos 1. Quando 𝑘 é incrementado, o valor sendo subtraído na primeira iteração é deslocado para a esquerda em duas unidades, que é o mesmo que multiplicar por 22=4. E como 4𝑛=22𝑛=(2𝑛)2, o valor sendo subtraído nesse caso é uma soma de números ímpares consecutivos, começando em 1 (com 2𝑛 termos).

Ou seja, isso nos mostra que a quantidade de números ímpares que compõem essa soma é reduzida pela metade a cada iteração. Mas e se algum bit de 𝑄 for diferente de 0? Isso implica que foi possível subtrair uma certa quantidade de números ímpares, logo, devemos avançar na sequência de alguma forma (não começar mais no 1), para que esses números não sejam subtraídos novamente. Podemos mostrar que adicionar os bits de 𝑄 ao subtraendo serve exatamente para resolver esse problema.

Considerando que estamos na 𝑖-ésima iteração (onde a primeira seria 0 e a última 𝑘1), o valor sendo subtraído nessa iteração, que iremos chamar de 𝑠, será dado por:

𝑠=22𝑘2𝑖2+𝑚=0𝑖1𝑞𝑘𝑚1·22𝑘𝑖𝑚1

O primeiro termo corresponde à constante 1, enquanto o segundo corresponde aos bits de 𝑄 de iterações anteriores. A ilustração a seguir mostra a representação binária de 𝑠:

Vamos denotar por 𝐷(𝑛,𝑚) o conjunto ou subsequência de números ímpares começando no enésimo número com 𝑚 entradas. Para cada bit de 𝑄 que for 1, temos que pular metade da quantidade de números ímpares subtraída na iteração correspondente. Podemos deduzir, então, que o índice inicial 𝑛𝑖 (começando em 1, pois usaremos números ímpares da forma 2𝑛1) será:

𝑛𝑖=𝑚=0𝑖1𝑞𝑘𝑚1·2𝑘𝑚1+1

E se assumirmos, por enquanto, que a quantidade de números ímpares subtraídos sempre for reduzida pela metade a cada iteração, independentemente dos bits anteriores de 𝑄, então o valor subtraído na 𝑖-ésima iteração será dado por:

𝑠=𝐷(𝑛𝑖,2𝑘𝑖1)

Podemos mostrar que vale a recíproca, ou seja, partindo de 𝐷, chegaremos a 𝑠, na forma que foi definido anteriormente:

𝐷=𝑡=12𝑘𝑖1(2(𝑛𝑖+𝑡1)1)=2·2𝑘𝑖1·𝑛𝑖+2𝑘𝑖1(2𝑘𝑖1+1)3·2𝑘𝑖1=22𝑘2𝑖2+2𝑘𝑖·𝑛𝑖2𝑘𝑖=22𝑘2𝑖2+2𝑘𝑖(𝑚=0𝑖1𝑞𝑘𝑚1·2𝑘𝑚1+1)2𝑘𝑖=𝑠

Mas por quê o algoritmo tem essa estrutura? Por que usamos binário? Embora outras bases possam ser usadas, essa base deixa o algoritmo mais simples e é muito útil, ao permitir seu uso em computadores. Note que |𝐷| é uma função exponencial de 𝑖: podemos interpretar que esse algoritmo faz uma busca binária no espaço de valores possíveis para 𝑄, que pode assumir 2𝑘 valores distintos, e cada um deles deve ser possível de se alcançar partindo do ponto inicial. Dessa forma, a complexidade desse algoritmo é 𝑂(𝑘).

Curiosidade

Como cada bit na representação binária de 𝑄 é atribuído uma única vez, esse algoritmo pode ser implementado usando apenas circuitos combinacionais (que foi como eu descobri esse algoritmo originalmente)

O diagrama a seguir mostra uma árvore de decisão binária representando todas as escolhas que o algoritmo pode tomar para 𝑘=3, partindo do ponto inicial. Cada nível é uma iteração. Uma linha pontilhada indica que a subtração resultou em um valor negativo e o valor anterior foi restaurado. Cada nó mostra o valor subtraído naquela iteração, como uma soma de ímpares consecutivos. Note que as folhas não são mostradas; seria possível estender esse diagrama. Além disso, cada caminho da raiz até uma folha corresponde a um valor diferente para 𝑄.