![]() |
Problema B - Desenhando Quadrados |
Tipo de problema: Matemática/Pesquisa
Autor do Problema: Pedro Ribeiro (DCC/FCUP)
Problema:
Dado um conjunto de N pontos no plano, todos com
coordenadas inteiras e sem pontos repetidos, a tua tarefa é
calcular o número de quadrados que se podem formar usando apenas
esses pontos. Dentro desses quadrados, tens também de calcular
qual é a área do maior, no caso de haver pelo menos um quadrado.
Restrições:
1 ≤ N ≤ 3 000 | Número de pontos a considerar | |
1 ≤ Xi, Yi ≤ 20 000 | Coordenadas dos pontos |
Nota Inicial:
Todas as soluções deste problema passam por enumerar de alguma
maneira todos os quadrados existentes. Sendo assim, para
calcular a área do maior quadrado basta calcular a área de cada
quadrado que se enumera (usando as coordenadas dos pontos do
quadrado, calculando a distância entre dois pontos que formem um
dos lados e tomando o seu quadrado). Sendo assim vamos apenas
descrever a operação de enumerar todos os quadrados e a operação
de calcular a área fica assumida como trivial.
Solução base:
A primeira parte para resolver este problema passa por perceber
como perceber que quatro pontos são um quadrado. A ideia desta
solução é então considerar cada conjunto de quatro pontos (com 4
loops encadeados) e saber se esse conjunto forma ou não um
quadrado. Notem que não basta ver se a figura formada tem os
lados são iguais, logo num dos casos de exemplo temos conjuntos
de 4 pontos que formam um losângulo que tem os 4 lados iguais,
mas que não é um quadrado.
Podemos fazer esta verificação de várias maneiras, todas elas um pouco geométricas. Uma primeira seria considerar o teorema de pitágoras e assim verificar se o quadrado da diagonal é igual a duas vezes o quadrado do lado da figura. Caso seja, temos um quadrado. Outra maneira seria além de verificar que os 4 lados são iguais, verificar também que os 4 ângulos são iguais, podendo fazer isto utilizando vetores e a noção de produto escalar por exemplo (que inclui a noção trignométrica de senos e cosenos). Finalmente, talvez a mais simples seria simplesmente verificar que o comprimento das duas diagonais da figura são iguais. Caso seja, novamente, quadrado!
Temos assim uma maneira de exaustivamente verificar quantos conjuntos de 4 pontos formam quadrados.
Esta seria uma solução O(N^4) que daria aproximadamente 50 pontos.
Solução Quase-Ótima:
Para melhorar a solução anterior temos de pensar qual o mínimo
de informação que precisamos para definir um
quadrado. Claramente se apenas tivermos um ponto não conseguimos
definir um só quadrado, visto que podemos ter imensos quadrados
que incluam aquele ponto (basta variar o comprimento dos
lados). Com dois pontos já só temos dois possíveis quadrados que
incluam esses dois pontos. A imagem seguinte ilustra essa
situação (os dois pontos escolhidos estão a vermelho e a preto
os dois quadrados).
Sendo assim, vamos pegar em cada par de pontos e ver se conseguimos formar um dos quadrados como na imagem acima. Só precisamos de um porque ao considerarmos todos os pares, caso um dos quadrados exista vamos considerar os 4 pares de pontos que formam os seus lados, o que se procurássemos pelos dois quadrados íamos contar cada quadrado 4 vezes (uma por cada um dos 4 pares). Sendo assim, basta-nos para cada par de pontos escolher um dos quadrados, calcular as coordenadas dos outros dois pontos e verificar se ambos estão na nossa lista de N pontos.
Para calcular as coordenadas dos outros dois pontos podíamos usar várias abordagens. A primeira seria usar trigonometria, o que para quem está habituado seria muito fácil, mas caso contrário não seria assim tão fácil. Outra alternativa seria pensar um pouco como manipular as coordenadas de cada ponto. Vejamos a imagem seguinte para perceber melhor isto.
Na imagem anterior, o segmento a vermelho seria o equivalente ao segmento gerado pelo par de pontos que estamos a considerar, sendo esses pontos os pontos marcados como 'A' e 'B'. Cada segmento etiquetado por x e y tem o mesmo comprimento e x = B.x - A.x, e y = A.y - B.y. Agora a ideia é ir "rodando" os pontos usando o esquema da figura. Somando x à coordenada y do ponto B e y à coordenada x obtemos as coordenadas do ponto D. De maneira análoga, subtraindo x à coordenada x do ponto D e somando y à coordenada y obtemos as coordenadas do ponto C. Agora que já temos as coordenadas dos outros dois pontos, basta verificar se estão na nossa lista de pontos. Já agora, também era possível fazer estes cálculos usando vetores, que dariam contas um pouco mais complicadas, mas nada de especial.
Para saber se os pontos estão na nossa lista temos várias alternativas. A primeira e mais simples, passaria por fazer uma pesquisa linear pela lista de pontos, ou seja, comparar as coordenadas cada ponto da lista com as de C e D. Assim, por cada par de pontos fazemos uma pesquisa em O(N) e encontraríamos todos os retângulos.
Esta seria uma solução O(N^3) que daria aproximadamente 60 pontos.
Solução Quase-Quase-Ótima:
Para melhorar a solução temos de descer o fator linear para, por
exemplo logarítmico. A já conhecida pesquisa binária parece ser
um exemplo de algo que funcionaria, mas não nos podemos esquecer
que estamos a trabalhar com pontos, ou seja, elementos em
2D. Existem estruturas de dados que fazem estas operações
eficientemente, mas a maior parte é bastante complexa e ainda
mais complexa para implementar. Porém, neste caso em específico
podíamos reparar que o valor máximo para cada coordenada é de
20,000 logo podemos "concatenar" o valor de cada coordenada para
obter um único inteiro que representa cada ponto. A ideia é
multiplicar a coordenada y por 100,000 e somar-lhe o valor da
coordenada x. Por exemplo se tivéssemos o ponto (90, 120)
representá-lo-iamos por 12,000,090. Notem que o valor máximo
possível, 2,000,020,000 cabe num inteiro (mas não por
muito). Sendo assim já seria possível guardar cada ponto numa
lista ordenada e fazer uma pesquisa binária para verificar se
ele existe.
Uma maneira alternativa de conseguir uma complexidade semelhante seria usar por exemplo um map da STL. Podíamos usá-lo com inteiros como no parágrafo anterior, ou usar por exemplo pairs da STL.
Esta seria uma solução O(N^2 * log(V)) que daria aproximadamente 80 pontos.
Solução Ótima:
Este problema tinha casos de teste e limites apertados mesmo
para poder diferenciar as várias soluções. Num
recente artigo
do Loop menciona-se que é raro existir um problema onde uma
solução O(N) passe mas O(N * log(N)) não. Bom, este é um desses
problemas raros. Vamos então melhorá-lo.
Podemos retirar o fator logaritmo de várias maneiras. Uma muito interessante seria reparar que 2,000,020,000 bits são "apenas" 250 MB (mais coisa menos coisa). Logo, visto que a memória máxima global para este problema era de 256 MB, se explorássemos bem os conjuntos de dados que temos, conseguíamos guardar um bit para cada um dos possíveis 2,000,020,000 indicando se esse ponto existiria ou não (podiamos melhorar isto ainda mais visto que números como o 1,000,090,000 não são possíveis). Uma possível abordagem seria guardar um conjunto de 2,000,020,000 / 32 inteiros (cada inteiro tem 32 bits) e com alguma matemática descobrir onde está cada inteiro e com operações bitwise manipular os vários bits. Uma alternativa mais "preguiçosa" seria usar por exemplo bitsets da STL, que faz precisamente isto que referimos.
Uma outra solução seria usar uma trie, que funciona como uma árvore onde cada nó representa um dígito e uma pesquisa passa por percorrer essa árvore pelos dígitos respetivos (usando a técnica do primeiro parágrafo desta secção) e se for possível percorrer esse caminho e chegarmos a um nó que esteja marcado como final, temos um ponto. Notem que em teoria esta solução tem um fator igual ao número de dígitos adicional à complexidade, que equivale a um logaritmo de base 10, mas como para o valor máximo o seu logaritmo de base 10 é tão pequeno, vamos considerar que não é introduzido nenhum fator.
Esta seria uma solução O(N^2) que daria aproximadamente 100 pontos.
Ligações interessantes: