Area di un poligono che si interseca da solo


32

Considera un poligono potenzialmente autointersecante, definito da un elenco di vertici nello spazio 2D. Per esempio

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

Esistono diversi modi per definire l'area di tale poligono, ma la più interessante è la regola pari-dispari. Prendendo qualsiasi punto nel piano, traccia una linea dal punto verso l'infinito (in qualsiasi direzione). Se quella linea attraversa il poligono un numero dispari di volte, il punto fa parte dell'area del poligono, se attraversa il poligono un numero pari di volte, il punto non fa parte del poligono. Per il poligono di esempio sopra, ecco sia il suo contorno che la sua area pari:

ContornoLa zona

Il poligono non sarà generalmente ortogonale. Ho scelto solo un esempio così semplice per rendere più semplice il conteggio dell'area.

L'area di questo esempio è 17(no 24o 33come potrebbero dare altre definizioni o area).

Si noti che in base a questa definizione l'area del poligono è indipendente dal suo ordine di avvolgimento.

La sfida

Dato un elenco di vertici con coordinate intere che definiscono un poligono, determinane l'area sotto la regola pari-dispari.

È possibile scrivere una funzione o un programma, prendendo l'input tramite STDIN o l'alternativa più vicina, l'argomento della riga di comando o l'argomento della funzione e restituire il risultato o stamparlo su STDOUT o l'alternativa più vicina.

È possibile accettare input in qualsiasi elenco o formato stringa, purché non sia preelaborato.

Il risultato dovrebbe essere un numero in virgola mobile, accurato su 6 cifre (decimali) significative, oppure un risultato razionale la cui rappresentazione in virgola mobile è accurata su 6 cifre significative. (Se produci risultati razionali, saranno probabilmente esatti, ma non posso richiederlo, poiché non ho risultati esatti per riferimento.)

Devi essere in grado di risolvere ciascuno dei casi di test di seguito entro 10 secondi su una macchina desktop ragionevole. (C'è qualche margine di manovra in questa regola, quindi usa il tuo miglior giudizio. Se ci vorranno 20 secondi sul mio laptop, ti darò il beneficio del dubbio, se ci vorrà un minuto, non lo farò.) Penso che questo limite dovrebbe essere molto generoso, ma si suppone che escluda approcci in cui si discretizza il poligono su una griglia sufficientemente sottile e si conta, oppure si utilizzano approcci probabilistici come Monte Carlo. Sii un buon sportivo e non cercare di ottimizzare questi approcci in modo tale da poter comunque rispettare il limite di tempo. ;)

Non è necessario utilizzare alcuna funzione esistente direttamente correlata ai poligoni.

Questo è il golf del codice, quindi vince l'invio più breve (in byte).

ipotesi

  • Tutte le coordinate sono numeri interi nella gamma 0 ≤ x ≤ 100, 0 ≤ y ≤ 100.
  • Ci saranno almeno 3e al massimo 50vertici.
  • Non ci saranno vertici ripetuti. Né i vertici si trovano su un altro bordo. (Tuttavia, potrebbero esserci punti collineari nell'elenco).

Casi test

{{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

1
In particolare, vorrei sostituire i delimitatori in modo da rendere l'elenco un percorso utente PostScript valido, in modo da poter analizzare il tutto con un solo upathoperatore. (In realtà è una conversione 1: 1 estremamente semplice tra i separatori. }, {Diventa solo lineto, e la virgola tra xey viene rimossa e le parentesi graffe di apertura e chiusura vengono sostituite con un'intestazione e un piè di pagina statici ...)
AJMansfield

1
@AJMansfield Di solito non mi dispiace usare rappresentazioni di elenchi nativi convenienti, ma usando upathe linetosembra che tu stia effettivamente preelaborando l'input. Cioè non stai prendendo un elenco di coordinate ma un vero poligono.
Martin Ender,

1
@MattNoonan Oh, questo è un buon punto. Sì, puoi supporre che.
Martin Ender,

2
@Ray Sebbene la direzione possa influire sul numero di incroci, aumenterà o diminuirà sempre solo di 2, preservando la parità. Proverò a trovare un riferimento. Per cominciare, SVG usa la stessa definizione.
Martin Ender,

1
Mathematica 12.0 ha una nuova funzione built-in per questo: CrossingPolygon.
alephalpha

Risposte:


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&

Per prima cosa aggiungi i punti di intersezione al poligono, quindi inverti alcuni dei bordi, quindi la sua area può essere calcolata come un semplice poligono.

inserisci qui la descrizione dell'immagine

Esempio:

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

Sfortunatamente non sono sicuro che questa logica funzionerà per tutte le situazioni. Puoi provare {1,2},{4,4},{4,2},{2,4},{2,1},{5,3}? Dovresti uscire con 3.433333333333309. Ho guardato usando una logica simile.
MickyT

@MickyT Sì, funziona. È tornato 103/30e il valore numerico è 3.43333.
alephalpha,

Mi dispiace per quello. Buona soluzione
MickyT

44

Python 2, 323 319 byte

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"})

Prende un elenco di vertici tramite STDIN come numeri complessi, nella forma seguente

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

e scrive il risultato su STDOUT.

Stesso codice dopo la sostituzione della stringa e alcuni spazi:

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]
)

Spiegazione

Per ogni punto di intersezione di due lati del poligono di input (compresi i vertici), passare una linea verticale attraverso quel punto.

Figura 1

(In effetti, a causa del golf, il programma passa alcune altre righe; non importa, basta che passiamo almeno queste righe.) Il corpo del poligono tra due righe consecutive è composto da trapezi verticali e triangoli e segmenti di linea, come casi speciali di quelli). Deve essere così, poiché se una di queste forme avesse un vertice aggiuntivo tra le due basi, ci sarebbe un'altra linea verticale attraverso quel punto, tra le due linee in questione. La somma delle aree di tutti questi trapezi è l'area del poligono.

Ecco come troviamo questi trapezi: Per ogni coppia di linee verticali consecutive, troviamo i segmenti di ciascun lato del poligono che (correttamente) si trovano tra queste due linee (che potrebbero non esistere per alcuni lati). Nell'illustrazione sopra, questi sono i sei segmenti rossi, quando si considerano le due linee verticali rosse. Si noti che questi segmenti non si intersecano correttamente tra loro (cioè, possono incontrarsi solo nei loro punti finali, coincidere completamente o non intersecarsi affatto, poiché, ancora una volta, se si intersecano correttamente ci sarebbe un'altra linea verticale in mezzo;) e quindi ha senso parlare di ordinarli dall'alto in basso, cosa che facciamo. Secondo la regola pari-dispari, una volta attraversato il primo segmento, siamo all'interno del poligono; una volta attraversato il secondo, siamo fuori; il terzo, di nuovo; il quarto, fuori; e così via...

Nel complesso, questo è un algoritmo O ( n 3 log n ).


4
È brillante! Sapevo di poter contare su di te per questo. ;) (Potresti voler rispondere a questa domanda sopra Stack Overflow.)
Martin Ender

@ MartinBüttner Continuate a venire :)
Ell

7
Ottimo lavoro e una grande spiegazione
MickyT

1
Questa è una risposta impressionante. Hai sviluppato tu stesso l'algoritmo o esiste un lavoro esistente su questo problema? Se esiste un lavoro esistente, apprezzerei un puntatore a dove posso trovarlo. Non avevo idea di come affrontare questo.
Logic Knight,

5
@CarpetPython L'ho sviluppato da solo, ma sarei molto sorpreso se non fosse mai stato fatto prima.
Ell

9

Haskell, 549

Non mi sembra di poter giocare a golf abbastanza in basso, ma il concetto era diverso dalle altre due risposte, quindi ho pensato di condividerlo comunque. Esegue operazioni razionali O (N ^ 2) per calcolare l'area.

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

Esempio:

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

L'idea è di ricollegare il poligono ad ogni incrocio, risultando in un'unione di poligoni senza bordi intersecanti. Possiamo quindi calcolare l'area (firmata) di ciascuno dei poligoni usando la formula del laccio da scarpe di Gauss ( http://en.wikipedia.org/wiki/Shoelace_formula ). La regola pari-dispari richiede che quando viene convertito un incrocio, l'area del nuovo poligono viene contata negativamente rispetto al vecchio poligono.

Ad esempio, considera il poligono nella domanda originale. L'incrocio in alto a sinistra viene convertito in due percorsi che si incontrano solo in un punto; i due percorsi sono entrambi orientati in senso orario, quindi le aree di ciascuno sarebbero positive a meno che non dichiarassimo che il percorso interno è ponderato per -1 rispetto al percorso esterno. Ciò equivale all'inversione del percorso dell'alfaalfa.

Poligoni derivati ​​dall'esempio originale

Come altro esempio, considera il poligono dal commento di MickyT:

Poligoni derivati ​​dal commento di MickyT

Qui, alcuni dei poligoni sono orientati in senso orario e altri in senso antiorario. La regola del segno di capovolgimento incrociato assicura che le regioni orientate in senso orario raccolgano un fattore aggiuntivo di -1, facendo sì che contribuiscano in modo positivo all'area.

Ecco come funziona il programma:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.