Aire d'un polygone auto-intersecté

32

Considérons un polygone potentiellement auto-intersecté, défini par une liste de sommets dans un espace 2D. Par exemple

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

Il existe plusieurs façons de définir l'aire d'un tel polygone, mais la plus intéressante est la règle pair-impair. En prenant n'importe quel point dans l'avion, tracez une ligne du point vers l'infini (dans n'importe quelle direction). Si cette ligne traverse le polygone un nombre impair de fois, le point fait partie de la zone du polygone, s'il traverse le polygone un nombre pair de fois, le point ne fait pas partie du polygone. Pour l'exemple de polygone ci-dessus, voici à la fois son contour ainsi que sa zone paire-impaire:

ContourRégion

Le polygone ne sera en général pas orthogonal. Je n'ai choisi qu'un exemple aussi simple pour faciliter le comptage de la zone.

La zone de cet exemple est 17(pas 24ou 33comme d'autres définitions ou zones pourraient donner).

Notez que sous cette définition, l'aire du polygone est indépendante de son ordre d'enroulement.

Le défi

Étant donné une liste de sommets avec des coordonnées entières définissant un polygone, déterminez son aire sous la règle pair-impair.

Vous pouvez écrire une fonction ou un programme, en saisissant des données via STDIN ou l'alternative la plus proche, l'argument de ligne de commande ou l'argument de fonction et renvoyer le résultat ou l'imprimer dans STDOUT ou l'alternative la plus proche.

Vous pouvez prendre des entrées dans n'importe quel format de liste ou de chaîne pratique, tant qu'il n'est pas prétraité.

Le résultat doit être soit un nombre à virgule flottante, précis à 6 chiffres significatifs (décimaux), soit un résultat rationnel dont la représentation à virgule flottante est précise à 6 chiffres significatifs. (Si vous produisez des résultats rationnels, ils seront probablement exacts, mais je ne peux pas l'exiger, car je n'ai pas de résultats exacts pour référence.)

Vous devez être en mesure de résoudre chacun des cas de test ci-dessous dans les 10 secondes sur une machine de bureau raisonnable. (Il y a une certaine marge de manœuvre dans cette règle, alors utilisez votre meilleur jugement. Si cela prend 20 secondes sur mon ordinateur portable, je vous donnerai le bénéfice du doute, si cela prend une minute, je ne le ferai pas.) Je pense que cette limite devrait être très généreux, mais il est censé exclure les approches où vous discrétisez simplement le polygone sur une grille et un décompte suffisamment fins, ou utilisez des approches probabilistes comme Monte Carlo. Soyez un bon sportif et n'essayez pas d'optimiser ces approches de manière à pouvoir respecter le délai de toute façon. ;)

Vous ne devez utiliser aucune fonction existante directement liée aux polygones.

Il s'agit du code golf, donc la soumission la plus courte (en octets) l'emporte.

Hypothèses

  • Toutes les coordonnées sont des nombres entiers dans la gamme 0 ≤ x ≤ 100, 0 ≤ y ≤ 100.
  • Il y aura au moins 3et au plus des 50sommets.
  • Il n'y aura pas de sommets répétés. Aucun sommet ne se trouvera non plus sur un autre bord. (Il peut cependant y avoir des points colinéaires dans la liste.)

Cas de 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
Martin Ender
la source
1
Plus précisément, je voudrais remplacer les délimiteurs d'une manière qui fait de la liste un chemin utilisateur PostScript valide, afin que je puisse analyser le tout avec un seul upathopérateur. (Il s'agit en fait d'une conversion 1: 1 extrêmement simple entre les séparateurs. Devient }, {juste lineto, et la virgule entre les x et y est supprimée, et les accolades d'ouverture et de fermeture sont remplacées par un en-tête et un pied de page statiques ...)
AJMansfield
1
@AJMansfield Cela ne me dérange généralement pas d'utiliser des représentations de listes natives pratiques, mais l'utilisation upathet les linetosons comme si vous prétraitez réellement l'entrée. C'est-à-dire que vous ne prenez pas une liste de coordonnées mais un véritable polygone.
Martin Ender
1
@MattNoonan Oh, c'est un bon point. Oui, vous pouvez supposer cela.
Martin Ender
2
@Ray Bien que la direction puisse affecter le nombre de passages, elle ne fera qu'augmenter ou diminuer de 2, tout en préservant la parité. Je vais essayer de trouver une référence. Pour commencer, SVG utilise la même définition.
Martin Ender
1
Mathematica 12.0 a une nouvelle fonction intégrée pour cela: CrossingPolygon.
alephalpha

Réponses:

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&

Ajoutez d'abord les points d'intersection au polygone, puis inversez certaines des arêtes, puis sa zone peut être calculée comme un simple polygone.

entrez la description de l'image ici

Exemple:

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
Alephalpha
la source
Malheureusement, je ne suis pas sûr que cette logique fonctionnera dans toutes les situations. Pouvez-vous essayer {1,2},{4,4},{4,2},{2,4},{2,1},{5,3}? Vous devriez sortir avec 3.433333333333309. J'ai regardé en utilisant une logique similaire.
MickyT
@MickyT Oui, cela fonctionne. Il est retourné 103/30et la valeur numérique est 3.43333.
alephalpha
Désolé pour ça. Bonne solution
MickyT
44

Python 2, 323 319 octets

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

Prend une liste de sommets via STDIN sous forme de nombres complexes, sous la forme suivante

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

et écrit le résultat dans STDOUT.

Même code après remplacement de la chaîne et un peu d'espacement:

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

Explication

Pour chaque point d'intersection de deux côtés du polygone d'entrée (y compris les sommets), passez une ligne verticale à travers ce point.

Figure 1

(En fait, en raison du golf, le programme passe quelques lignes de plus; cela n'a pas vraiment d'importance, tant que nous passons au moins ces lignes.) Le corps du polygone entre deux lignes consécutives est composé de trapèzes verticaux ( et les triangles et les segments de ligne, comme cas particuliers de ceux-ci). Cela doit être le cas, car si l'une de ces formes avait un sommet supplémentaire entre les deux bases, il y aurait une autre ligne verticale passant par ce point, entre les deux lignes en question. La somme des aires de tous ces trapèzes est l'aire du polygone.

Voici comment nous trouvons ces trapèzes: Pour chaque paire de lignes verticales consécutives, nous trouvons les segments de chaque côté du polygone qui se trouvent (correctement) entre ces deux lignes (qui pourraient ne pas exister pour certains des côtés). Dans l'illustration ci-dessus, ce sont les six segments rouges, si l'on considère les deux lignes verticales rouges. Notez que ces segments ne se croisent pas correctement (c'est-à-dire qu'ils ne peuvent se rencontrer qu'à leurs points d'extrémité, coïncider complètement ou ne pas se croiser du tout, car, encore une fois, s'ils se croisaient correctement, il y aurait une autre ligne verticale entre les deux;) et il est donc logique de parler de les ordonner de haut en bas, ce que nous faisons. Selon la règle pair-impair, une fois que nous traversons le premier segment, nous sommes à l'intérieur du polygone; une fois que nous avons traversé le second, nous sommes sortis; le troisième, encore une fois; le quatrième, sorti; etc...

Dans l'ensemble, il s'agit d'un algorithme O ( n 3 log n ).

Aune
la source
4
C'est génial! Je savais que je pouvais compter sur toi pour celui-ci. ;) (Vous voudrez peut-être répondre à cette question sur Stack Overflow.)
Martin Ender
@ MartinBüttner Continuez à venir :)
Ell
7
Excellent travail et une grande explication
MickyT
1
C'est une réponse impressionnante. Avez-vous développé l'algorithme vous-même ou existe-t-il des travaux sur ce problème? S'il y a du travail existant, j'apprécierais un pointeur vers où je peux le trouver. Je n'avais aucune idée de comment y faire face.
Logic Knight
5
@CarpetPython Je l'ai développé moi-même, mais je serais très surpris si cela n'avait pas été fait auparavant.
Ell
9

Haskell, 549

Il ne semble pas que je puisse jouer assez loin, mais le concept était différent des deux autres réponses, alors j'ai pensé que je le partagerais de toute façon. Il effectue O (N ^ 2) des opérations rationnelles pour calculer l'aire.

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

Exemple:

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

L'idée est de recâbler le polygone à chaque croisement, résultant en une union de polygones sans arêtes entrecroisées. Nous pouvons ensuite calculer l'aire (signée) de chacun des polygones en utilisant la formule de lacet de Gauss ( http://en.wikipedia.org/wiki/Shoelace_formula ). La règle pair-impair exige que lorsqu'un croisement est converti, l'aire du nouveau polygone soit comptée négativement par rapport à l'ancien polygone.

Par exemple, considérez le polygone dans la question d'origine. Le croisement en haut à gauche est converti en deux chemins qui ne se rencontrent qu'en un point; les deux chemins sont tous deux orientés dans le sens des aiguilles d'une montre, de sorte que les zones de chacun seraient positives à moins que nous déclarions que le chemin intérieur est pondéré par -1 par rapport au chemin extérieur. Cela équivaut à l'inversion de chemin d'alphaalpha.

Polygones dérivés de l'exemple d'origine

Comme autre exemple, considérons le polygone du commentaire de MickyT:

Polygones dérivés du commentaire de MickyT

Ici, certains des polygones sont orientés dans le sens horaire et d'autres dans le sens antihoraire. La règle de basculement de signe garantit que les régions orientées dans le sens horaire captent un facteur supplémentaire de -1, ce qui les amène à contribuer positivement à la zone.

Voici comment fonctionne le programme:

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
Matt Noonan
la source