Área de um polígono com auto-interseção


32

Considere um polígono com potencial de auto-interseção, definido por uma lista de vértices no espaço 2D. Por exemplo

{{0, 0}, {5, 0}, {5, 4}, {1, 4}, {1, 2}, {3, 2}, {3, 3}, {2, 3}, {2, 1}, {4, 1}, {4, 5}, {0, 5}}

Existem várias maneiras de definir a área desse polígono, mas a mais interessante é a regra ímpar. Tomando qualquer ponto do plano, desenhe uma linha do ponto ao infinito (em qualquer direção). Se essa linha cruza o polígono um número ímpar de vezes, o ponto faz parte da área do polígono, se cruza o polígono um número par de vezes, o ponto não faz parte do polígono. Para o exemplo de polígono acima, aqui estão o contorno e a área ímpar:

EsboçoÁrea

O polígono geralmente não será ortogonal. Eu escolhi apenas um exemplo tão simples para facilitar a contagem da área.

A área deste exemplo é 17(não 24ou 33como outras definições ou áreas podem render).

Observe que, nessa definição, a área do polígono é independente de sua ordem de enrolamento.

O desafio

Dada uma lista de vértices com coordenadas inteiras que definem um polígono, determine sua área sob a regra de pares ímpares.

Você pode escrever uma função ou programa, recebendo informações via STDIN ou alternativa mais próxima, argumento de linha de comando ou argumento de função e retornar o resultado ou imprimi-lo em STDOUT ou alternativa mais próxima.

Você pode receber entradas em qualquer formato conveniente de lista ou string, desde que não seja pré-processado.

O resultado deve ser um número de ponto flutuante, com precisão de 6 dígitos significativos (decimais) ou um resultado racional cuja representação de ponto flutuante é precisa de 6 dígitos significativos. (Se você produzir resultados racionais, é provável que sejam exatos, mas não posso exigir isso, pois não tenho resultados exatos para referência.)

Você deve ser capaz de resolver cada um dos casos de teste abaixo em 10 segundos em uma máquina de desktop razoável. (Existe alguma margem de manobra nessa regra, portanto, use seu bom senso. Se levar 20 segundos no meu laptop, eu lhe darei o benefício da dúvida; se demorar um minuto, não vou.) Acho que esse limite deve ser muito generoso, mas deve excluir abordagens em que você apenas discretiza o polígono em uma grade e contagem suficientemente finas, ou usa abordagens probabilísticas como Monte Carlo. Seja um bom esportista e não tente otimizar essas abordagens para que você possa cumprir o prazo de qualquer maneira. ;)

Você não deve usar nenhuma função existente relacionada diretamente a polígonos.

Isso é código de golfe, então a submissão mais curta (em bytes) vence.

Suposições

  • Todas as coordenadas são inteiros no intervalo 0 ≤ x ≤ 100, 0 ≤ y ≤ 100.
  • Haverá pelo menos 3e no máximo 50vértices.
  • Não haverá vértices repetidos. Nenhum dos vértices estará em outra extremidade. (Porém, pode haver pontos colineares na lista.)

Casos de teste

{{0, 0}, {5, 0}, {5, 4}, {1, 4}, {1, 2}, {3, 2}, {3, 3}, {2, 3}, {2, 1}, {4, 1}, {4, 5}, {0, 5}}
17.0000

{{22, 87}, {6, 3}, {98, 77}, {20, 56}, {96, 52}, {79, 34}, {46, 78}, {52, 73}, {81, 85}, {90, 43}}
2788.39

{{90, 43}, {81, 85}, {52, 73}, {46, 78}, {79, 34}, {96, 52}, {20, 56}, {98, 77}, {6, 3}, {22, 87}}
2788.39

{{70, 33}, {53, 89}, {76, 35}, {14, 56}, {14, 47}, {59, 49}, {12, 32}, {22, 66}, {85, 2}, {2, 81},
 {61, 39}, {1, 49}, {91, 62}, {67, 7}, {19, 55}, {47, 44}, {8, 24}, {46, 18}, {63, 64}, {23, 30}}
2037.98

{{42, 65}, {14, 59}, {97, 10}, {13, 1}, {2, 8}, {88, 80}, {24, 36}, {95, 94}, {18, 9}, {66, 64},
 {91, 5}, {99, 25}, {6, 66}, {48, 55}, {83, 54}, {15, 65}, {10, 60}, {35, 86}, {44, 19}, {48, 43},
 {47, 86}, {29, 5}, {15, 45}, {75, 41}, {9, 9}, {23, 100}, {22, 82}, {34, 21}, {7, 34}, {54, 83}}
3382.46

{{68, 35}, {43, 63}, {66, 98}, {60, 56}, {57, 44}, {90, 52}, {36, 26}, {23, 55}, {66, 1}, {25, 6},
 {84, 65}, {38, 16}, {47, 31}, {44, 90}, {2, 30}, {87, 40}, {19, 51}, {75, 5}, {31, 94}, {85, 56},
 {95, 81}, {79, 80}, {82, 45}, {95, 10}, {27, 15}, {18, 70}, {24, 6}, {12, 73}, {10, 31}, {4, 29},
 {79, 93}, {45, 85}, {12, 10}, {89, 70}, {46, 5}, {56, 67}, {58, 59}, {92, 19}, {83, 49}, {22,77}}
3337.62

{{15, 22}, {71, 65}, {12, 35}, {30, 92}, {12, 92}, {97, 31}, {4, 32}, {39, 43}, {11, 40}, 
 {20, 15}, {71, 100}, {84, 76}, {51, 98}, {35, 94}, {46, 54}, {89, 49}, {28, 35}, {65, 42}, 
 {31, 41}, {48, 34}, {57, 46}, {14, 20}, {45, 28}, {82, 65}, {88, 78}, {55, 30}, {30, 27}, 
 {26, 47}, {51, 93}, {9, 95}, {56, 82}, {86, 56}, {46, 28}, {62, 70}, {98, 10}, {3, 39}, 
 {11, 34}, {17, 64}, {36, 42}, {52, 100}, {38, 11}, {83, 14}, {5, 17}, {72, 70}, {3, 97}, 
 {8, 94}, {64, 60}, {47, 25}, {99, 26}, {99, 69}}
3514.46

11
Especificamente, gostaria de substituir os delimitadores de forma a tornar a lista um caminho de usuário PostScript válido, para que eu possa analisar tudo com um upathoperador. (Na verdade, é uma conversão 1: 1 extremamente simples entre os separadores. }, {Acaba de se tornar lineto, e a vírgula entre xey é removida, e as chaves de abertura e fechamento são substituídas por um cabeçalho e rodapé estáticos ...)
AJMansfield

11
@AJMansfield Normalmente, não me importo de usar representações de lista nativas convenientes, mas usar upathe linetoparecer que você está realmente pré-processando a entrada. Ou seja, você não está fazendo uma lista de coordenadas, mas um polígono real.
Martin Ender

11
@ MattNoonan Oh, esse é um bom argumento. Sim, você pode assumir isso.
Martin Ender

2
@Ray Embora a direção possa afetar o número de passagens, ela só aumentará ou diminuirá em 2, preservando a paridade. Vou tentar encontrar uma referência. Para começar, o SVG usa a mesma definição.
Martin Ender

11
Mathematica 12.0 tem um novo built-in função para isso: CrossingPolygon.
alephalpha 16/04

Respostas:


14

Mathematica, 247 225 222

p=Partition[#,2,1,1]&;{a_,b_}~r~{c_,d_}=Det/@{{a-c,c-d},{a,c}-b}/Det@{a-b,c-d};f=Abs@Tr@MapIndexed[Det@#(-1)^Tr@#2&,p[Join@@MapThread[{1-#,#}&/@#.#2&,{Sort/@Cases[{s_,t_}/;0<=s<=1&&0<=t<=1:>s]/@Outer[r,#,#,1],#}]&@p@#]]/2&

Primeiro adicione os pontos de interseção ao polígono, depois inverta algumas das arestas e, em seguida, sua área pode ser calculada como um polígono simples.

insira a descrição da imagem aqui

Exemplo:

In[2]:= f[{{15, 22}, {71, 65}, {12, 35}, {30, 92}, {12, 92}, {97, 31}, {4, 32}, {39, 43}, {11, 40}, 
 {20, 15}, {71, 100}, {84, 76}, {51, 98}, {35, 94}, {46, 54}, {89, 49}, {28, 35}, {65, 42}, 
 {31, 41}, {48, 34}, {57, 46}, {14, 20}, {45, 28}, {82, 65}, {88, 78}, {55, 30}, {30, 27}, 
 {26, 47}, {51, 93}, {9, 95}, {56, 82}, {86, 56}, {46, 28}, {62, 70}, {98, 10}, {3, 39}, 
 {11, 34}, {17, 64}, {36, 42}, {52, 100}, {38, 11}, {83, 14}, {5, 17}, {72, 70}, {3, 97}, 
 {8, 94}, {64, 60}, {47, 25}, {99, 26}, {99, 69}}]

Out[2]= 3387239559852305316061173112486233884246606945138074528363622677708164\
 6419838924305735780894917246019722157041758816629529815853144003636562\
 9161985438389053702901286180223793349646170997160308182712593965484705\
 3835036745220226127640955614326918918917441670126958689133216326862597\
 0109115619/\
 9638019709367685232385259132839493819254557312303005906194701440047547\
 1858644412915045826470099500628074171987058850811809594585138874868123\
 9385516082170539979030155851141050766098510400285425157652696115518756\
 3100504682294718279622934291498595327654955812053471272558217892957057\
 556160

In[3]:= N[%] (*The numerical value of the last output*)

Out[3]= 3514.46

Infelizmente, não tenho certeza se essa lógica funcionará para todas as situações. Você pode tentar {1,2},{4,4},{4,2},{2,4},{2,1},{5,3}? Você deve sair com 3.433333333333309. Eu olhei usando uma lógica semelhante.
MickyT 13/03/2015

@MickyT Sim, funciona. Ele retornou 103/30e o valor numérico é 3.43333.
alephalpha

Me desculpe por isso. Boa solução
MickyT 13/03/2015

44

Python 2, 323 319 bytes

exec u"def I(s,a,b=1j):c,d=s;d-=c;c-=a;e=(d*bX;return e*(0<=(b*cX*e<=e*e)and[a+(d*cX*b/e]or[]\nE=lambda p:zip(p,p[1:]+p);S=sorted;P=E(input());print sum((t-b)*(r-l)/2Fl,r@E(S(i.realFa,b@PFe@PFi@I(e,a,b-a)))[:-1]Fb,t@E(S(((i+j)XFe@PFi@I(e,l)Fj@I(e,r)))[::2])".translate({70:u" for ",64:u" in ",88:u".conjugate()).imag"})

Leva uma lista de vértices através de STDIN como números complexos, da seguinte forma

[  X + Yj,  X + Yj,  ...  ]

e grava o resultado em STDOUT.

Mesmo código após a substituição da string e algum espaçamento:

def I(s, a, b = 1j):
    c, d = s; d -= c; c -= a;
    e = (d*b.conjugate()).imag;
    return e * (0 <= (b*c.conjugate()).imag * e <= e*e) and \
           [a + (d*c.conjugate()).imag * b/e] or []

E = lambda p: zip(p, p[1:] + p);
S = sorted;

P = E(input());

print sum(
    (t - b) * (r - l) / 2

    for l, r in E(S(
        i.real for a, b in P for e in P for i in I(e, a, b - a)
    ))[:-1]

    for b, t in E(S(
        ((i + j).conjugate()).imag for e in P for i in I(e, l) for j in I(e, r)
    ))[::2]
)

Explicação

Para cada ponto de interseção de dois lados do polígono de entrada (incluindo os vértices), passe uma linha vertical nesse ponto.

figura 1

(De fato, devido ao golfe, o programa passa mais algumas linhas; isso realmente não importa, contanto que passemos pelo menos por essas linhas.) O corpo do polígono entre duas linhas consecutivas é composto por trapézios verticais ( e triângulos e segmentos de linha, como casos especiais desses). Deve ser o caso, pois se alguma dessas formas tivesse um vértice adicional entre as duas bases, haveria outra linha vertical nesse ponto, entre as duas linhas em questão. A soma das áreas de todos esses trapézios é a área do polígono.

Eis como encontramos esses trapézios: Para cada par de linhas verticais consecutivas, encontramos os segmentos de cada lado do polígono que (adequadamente) se encontram entre essas duas linhas (que podem não existir para alguns dos lados). Na ilustração acima, esses são os seis segmentos vermelhos, considerando as duas linhas verticais vermelhas. Observe que esses segmentos não se cruzam adequadamente (ou seja, eles só podem se encontrar em seus pontos finais, coincidir completamente ou não se cruzam, pois, mais uma vez, se eles se cruzarem adequadamente, haveria outra linha vertical no meio;) e, portanto, faz sentido falar em encomendar de cima para baixo, o que fazemos. De acordo com a regra dos pares pares, quando cruzamos o primeiro segmento, estamos dentro do polígono; uma vez que cruzamos o segundo, estamos fora; o terceiro, novamente; o quarto, fora; e assim por diante...

No geral, este é um algoritmo O ( n 3 log n ).


4
Isto é brilhante! Eu sabia que poderia contar com você para este. ;) (Você pode querer responder a esta pergunta sobre estouro de pilha).
Martin Ender

@ MartinBüttner Mantenha-os vir :)
Ell

7
Grande trabalho e uma grande explicação
MickyT

11
Esta é uma resposta impressionante. Você mesmo desenvolveu o algoritmo ou existe trabalho existente sobre esse problema? Se houver trabalho existente, eu apreciaria um ponteiro para onde posso encontrá-lo. Eu não tinha idéia de como lidar com isso.
Logic Knight

5
@CarpetPython Eu mesmo desenvolvi, mas ficaria muito surpreso se isso não tivesse sido feito antes.
Ell

9

Haskell, 549

Não parece que eu possa jogar este aqui longe o suficiente, mas o conceito era diferente das outras duas respostas, então imaginei que o compartilharia de qualquer maneira. Ele executa operações racionais O (N ^ 2) para calcular a área.

import Data.List
_%0=2;x%y=x/y
h=sort
z f w@(x:y)=zipWith f(y++[x])w
a=(%2).sum.z(#);(a,b)#(c,d)=b*c-a*d
(r,p)?(s,q)=[(0,p)|p==q]++[(t,v t p r)|u t,u$f r]where f x=(d q p#x)%(r#s);t=f s;u x=x^2<x
v t(x,y)(a,b)=(x+t*a,y+t*b);d=v(-1)
s x=zip(z d x)x
i y=h.(=<<y).(?)=<<y
[]!x=[x];x!_=x
e n(a@(x,p):y)|x>0=(n!y,a):(e(n!y)$tail$dropWhile((/=p).snd)y)|0<1=(n,a):e n y
c[p]k=w x[]where((_,q):x)=e[]p;w((n,y):z)b|q==y=(k,map snd(q:b)):c n(-k)|0<1=w z(y:b);c[]_=[]
b(s,p)=s*a p
u(_,x)(_,y)=h x==h y
f p=abs$sum$map b$nubBy u$take(length p^2)$c[cycle$i$s p]1

Exemplo:

λ> f test''
33872395598523053160611731124862338842466069451380745283636226777081646419838924305735780894917246019722157041758816629529815853144003636562916198543838905370290128618022379334964617099716030818271259396548470538350367452202261276409556143269189189174416701269586891332163268625970109115619 % 9638019709367685232385259132839493819254557312303005906194701440047547185864441291504582647009950062807417198705885081180959458513887486812393855160821705399790301558511410507660985104002854251576526961155187563100504682294718279622934291498595327654955812053471272558217892957057556160
λ> fromRational (f test'')
3514.4559380388832

A idéia é religar o polígono a cada cruzamento, resultando em uma união de polígonos sem arestas que se cruzam. Podemos então calcular a área (assinada) de cada um dos polígonos usando a fórmula de cadarço de Gauss ( http://en.wikipedia.org/wiki/Shoelace_formula ). A regra par-ímpar exige que, quando um cruzamento é convertido, a área do novo polígono seja contada negativamente em relação ao polígono antigo.

Por exemplo, considere o polígono na pergunta original. A travessia no canto superior esquerdo é convertida em dois caminhos que se encontram apenas em um ponto; os dois caminhos são ambos orientados no sentido horário, de modo que as áreas de cada um seriam positivas, a menos que declarássemos que o caminho interno é ponderado por -1 em relação ao caminho externo. Isso é equivalente à reversão do caminho de alphaalpha.

Polígonos derivados do exemplo original

Como outro exemplo, considere o polígono do comentário de MickyT:

Polígonos derivados do comentário de MickyT

Aqui, alguns dos polígonos são orientados no sentido horário e outros no sentido anti-horário. A regra de assinar flip-on-crossing garante que as regiões orientadas no sentido horário apanham um fator extra de -1, fazendo com que elas contribuam com uma quantidade positiva para a área.

Veja como o programa funciona:

import Data.List  -- for sort and nubBy

-- Rational division, with the unusual convention that x/0 = 2
_%0=2;x%y=x/y

-- Golf
h=sort

-- Define a "cyclic zipWith" operation. Given a list [a,b,c,...z] and a binary
-- operation (@), z (@) [a,b,c,..z] computes the list [b@a, c@b, ..., z@y, a@z]
z f w@(x:y)=zipWith f(y++[x])w

-- The shoelace formula for the signed area of a polygon
a=(%2).sum.z(#)

-- The "cross-product" of two 2d vectors, resulting in a scalar.
(a,b)#(c,d)=b*c-a*d

-- Determine if the line segment from p to p+r intersects the segment from
-- q to q+s.  Evaluates to the singleton list [(t,x)] where p + tr = x is the
-- point of intersection, or the empty list if there is no intersection. 
(r,p)?(s,q)=[(0,p)|p==q]++[(t,v t p r)|u t,u$f r]where f x=(d q p#x)%(r#s);t=f s;u x=x^2<x

-- v computes an affine combination of two vectors; d computes the difference
-- of two vectors.
v t(x,y)(a,b)=(x+t*a,y+t*b);d=v(-1)

-- If x is a list of points describing a polygon, s x will be the list of
-- (displacement, point) pairs describing the edges.
s x=zip(z d x)x

-- Given a list of (displacement, point) pairs describing a polygon's edges,
-- create a new polygon which also has a vertex at every point of intersection.
-- Mercilessly golfed.
i y=h.(=<<y).(?)=<<y


-- Extract a simple polygon; when an intersection point is reached, fast-forward
-- through the polygon until we return to the same point, then continue.  This
-- implements the edge rewiring operation. Also keep track of the first
-- intersection point we saw, so that we can process that polygon next and with
-- opposite sign.
[]!x=[x];x!_=x
e n(a@(x,p):y)|x>0=(n!y,a):(e(n!y)$tail$dropWhile((/=p).snd)y)|0<1=(n,a):e n y

-- Traverse the polygon from some arbitrary starting point, using e to extract
-- simple polygons marked with +/-1 weights.
c[p]k=w x[]where((_,q):x)=e[]p;w((n,y):z)b|q==y=(k,map snd(q:b)):c n(-k)|0<1=w z(y:b);c[]_=[]

-- If the original polygon had N vertices, there could (very conservatively)
-- be up to N^2 points of intersection.  So extract N^2 polygons using c,
-- throwing away duplicates, and add up the weighted areas of each polygon.
b(s,p)=s*a p
u(_,x)(_,y)=h x==h y
f p=abs$sum$map b$nubBy u$take(length p^2)$c[cycle$i$s p]1
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.