Area Poligon yang Berhubungan Sendiri

32

Pertimbangkan poligon yang berpotongan-sendiri yang berpotensi, yang ditentukan oleh daftar simpul dalam ruang 2D. Misalnya

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

Ada beberapa cara untuk mendefinisikan bidang poligon seperti itu, tetapi yang paling menarik adalah aturan genap. Mengambil titik mana pun di pesawat, menggambar garis dari titik keluar hingga tak terbatas (ke segala arah). Jika garis itu melintasi poligon beberapa kali ganjil, titik tersebut merupakan bagian dari area poligon, jika garis itu melintasi poligon beberapa kali, titik tersebut bukan bagian dari poligon. Untuk contoh poligon di atas, berikut adalah garis besarnya dan juga area genapnya:

Garis besarDaerah

Poligon pada umumnya tidak ortogonal. Saya hanya memilih contoh sederhana untuk memudahkan menghitung area.

Area dari contoh ini adalah 17(tidak 24atau 33sebagai definisi atau area lain mungkin menghasilkan).

Perhatikan bahwa di bawah definisi ini area poligon tidak tergantung pada urutan belitannya.

Tantangan

Diberikan daftar simpul dengan koordinat bilangan bulat yang mendefinisikan poligon, tentukan luasnya di bawah aturan genap.

Anda dapat menulis suatu fungsi atau program, mengambil input melalui STDIN atau alternatif terdekat, argumen baris perintah atau argumen fungsi dan mengembalikan hasilnya atau mencetaknya ke STDOUT atau alternatif terdekat.

Anda dapat mengambil input dalam format string atau daftar yang mudah digunakan, selama itu tidak diproses sebelumnya.

Hasilnya harus berupa angka floating point, akurat hingga 6 digit signifikan (desimal), atau hasil rasional yang representasi floating point-nya akurat hingga 6 digit signifikan. (Jika Anda menghasilkan hasil yang rasional, kemungkinan besar akan tepat, tetapi saya tidak dapat meminta ini, karena saya tidak memiliki hasil yang tepat untuk referensi.)

Anda harus dapat menyelesaikan setiap kasus uji di bawah ini dalam waktu 10 detik pada mesin desktop yang masuk akal. (Ada beberapa kelonggaran dalam aturan ini, jadi gunakan penilaian terbaik Anda. Jika butuh 20 detik pada laptop saya, saya akan memberi Anda manfaat dari keraguan, jika butuh satu menit, saya tidak akan.) Saya pikir batas ini harus sangat murah hati tetapi seharusnya mengesampingkan pendekatan di mana Anda hanya mendiskreditkan poligon pada kisi dan jumlah yang cukup baik, atau menggunakan pendekatan probabilistik seperti Monte Carlo. Jadilah olahragawan yang baik dan jangan mencoba mengoptimalkan pendekatan ini sehingga Anda tetap dapat memenuhi batas waktu. ;)

Anda tidak boleh menggunakan fungsi apa pun yang ada yang terkait langsung dengan poligon.

Ini adalah kode golf, jadi pengiriman terpendek (dalam byte) menang.

Asumsi

  • Semua koordinat bilangan bulat dalam kisaran 0 ≤ x ≤ 100, 0 ≤ y ≤ 100.
  • Setidaknya akan ada 3dan paling banyak 50simpul.
  • Tidak akan ada simpul berulang. Tidak ada simpul terletak di tepi yang lain. (Namun, mungkin ada titik collinear dalam daftar.)

Uji Kasus

{{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
sumber
1
Secara khusus, saya ingin mengganti pembatas dengan cara yang membuat daftar Path Pengguna PostScript yang valid, jadi saya bisa menguraikan semuanya dengan satu upathoperator. (Ini sebenarnya adalah konversi 1: 1 yang sangat sederhana antara pemisah. }, {Hanya menjadi lineto, dan koma antara x dan y dihapus, dan kawat gigi pembuka dan penutup diganti dengan header dan footer statis ...)
AJMansfield
1
@ AJMansfield Saya biasanya tidak keberatan menggunakan representasi daftar asli yang nyaman, tetapi menggunakan upathdan linetoterdengar seperti Anda benar-benar memproses input. Yaitu Anda tidak mengambil daftar koordinat tetapi poligon yang sebenarnya.
Martin Ender
1
@ MattNoonan Oh, itu poin yang bagus. Ya, Anda mungkin menganggap itu.
Martin Ender
2
@ Ray Sementara arah dapat mempengaruhi jumlah penyeberangan, itu hanya akan meningkat atau berkurang sebesar 2, menjaga paritas. Saya akan mencoba mencari referensi. Sebagai permulaan, SVG menggunakan definisi yang sama.
Martin Ender
1
Mathematica 12,0 memiliki baru built-in fungsi untuk ini: CrossingPolygon.
alephalpha

Jawaban:

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&

Pertama-tama tambahkan titik persimpangan ke poligon, lalu balikkan beberapa tepinya, kemudian luasnya dapat dihitung seperti poligon sederhana.

masukkan deskripsi gambar di sini

Contoh:

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
sumber
Sayangnya saya tidak yakin logika ini akan bekerja untuk semua situasi. Bisakah kamu mencoba {1,2},{4,4},{4,2},{2,4},{2,1},{5,3}? Anda harus keluar dengan 3.433333333333309. Saya melihat menggunakan logika yang sama.
MickyT
@MickyT Ya, itu berhasil. Itu dikembalikan 103/30, dan nilai numeriknya 3.43333.
alephalpha
Maaf soal itu. Solusi bagus
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"})

Mengambil daftar simpul melalui STDIN sebagai bilangan kompleks, dalam bentuk berikut

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

, dan menulis hasilnya ke STDOUT.

Kode yang sama setelah penggantian string dan beberapa spasi:

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

Penjelasan

Untuk setiap titik perpotongan dua sisi poligon input (termasuk simpul), lewati garis vertikal melewati titik itu.

Gambar 1

(Faktanya, karena bermain golf, program melewati beberapa baris lagi; itu tidak terlalu penting, selama kita melewati setidaknya baris-baris ini.) Tubuh poligon antara dua garis berurutan terdiri dari trapesium vertikal ( dan segitiga, dan segmen garis, sebagai kasus khusus dari mereka). Itu harus menjadi kasus, karena jika salah satu dari bentuk-bentuk ini memiliki simpul tambahan antara dua pangkalan, akan ada garis vertikal lain melalui titik itu, antara dua garis tersebut. Jumlah area dari semua trapesium tersebut adalah area poligon.

Inilah cara kami menemukan trapesium ini: Untuk setiap pasang garis vertikal berturut-turut, kami menemukan segmen dari setiap sisi poligon yang (dengan benar) terletak di antara kedua garis ini (yang mungkin tidak ada untuk beberapa sisi). Dalam ilustrasi di atas, ini adalah enam segmen merah, ketika mempertimbangkan dua garis vertikal merah. Perhatikan bahwa segmen ini tidak saling berpotongan dengan benar (yaitu, mereka hanya dapat bertemu pada titik akhir, benar-benar bertepatan atau tidak berpotongan sama sekali, karena, sekali lagi, jika berpotongan dengan benar akan ada garis vertikal lain di antaranya;) dan jadi masuk akal untuk berbicara tentang memesannya dari atas ke bawah, yang kami lakukan. Menurut aturan genap, begitu kita melewati segmen pertama, kita berada di dalam poligon; begitu kita melewati yang kedua, kita keluar; yang ketiga, lagi; yang keempat, keluar; dan seterusnya...

Secara keseluruhan, ini adalah algoritma O ( n 3 log n ).

Elo
sumber
4
Ini brilian! Aku tahu aku bisa mengandalkanmu untuk yang satu ini. ;) (Anda mungkin ingin menjawab pertanyaan ini di atas di Stack Overflow.)
Martin Ender
@ MartinBüttner Biarkan mereka datang :)
Ell
7
Kerja bagus dan penjelasan yang bagus
MickyT
1
Ini jawaban yang mengesankan. Apakah Anda mengembangkan algoritme sendiri atau apakah ada pekerjaan yang ada untuk masalah ini? Jika ada karya yang ada, saya akan menghargai pointer ke tempat saya dapat menemukannya. Saya tidak tahu bagaimana mengatasi hal ini.
Logic Knight
5
@CarpetPython Saya mengembangkannya sendiri, tapi saya akan sangat terkejut jika belum pernah dilakukan sebelumnya.
Ell
9

Haskell, 549

Sepertinya saya tidak bisa bermain golf yang satu ini cukup jauh, tetapi konsepnya berbeda dari dua jawaban lainnya, jadi saya pikir saya akan membagikannya. Ia melakukan operasi rasional O (N ^ 2) untuk menghitung 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

Contoh:

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

Idenya adalah untuk memasang kembali poligon di setiap persimpangan, menghasilkan penyatuan poligon tanpa tepi yang bersilangan. Kami kemudian dapat menghitung area (yang ditandatangani) dari masing-masing poligon menggunakan rumus tali sepatu Gauss ( http://en.wikipedia.org/wiki/Shoelace_formula ). Aturan genap bahkan menuntut bahwa ketika persimpangan dikonversi, luas poligon baru dihitung secara relatif relatif terhadap poligon lama.

Sebagai contoh, perhatikan poligon dalam pertanyaan awal. Persimpangan di kiri atas dikonversi menjadi dua jalur yang hanya bertemu pada satu titik; kedua jalur keduanya berorientasi searah jarum jam, sehingga area masing-masing akan positif kecuali kita menyatakan bahwa jalur dalam dibobot oleh -1 relatif terhadap jalur luar. Ini setara dengan pembalikan jalur alphaalpha.

Poligon berasal dari contoh asli

Sebagai contoh lain, perhatikan poligon dari komentar MickyT:

Poligon berasal dari komentar MickyT

Di sini, beberapa poligon berorientasi searah jarum jam dan beberapa berlawanan arah jarum jam. Aturan sign-flip-on-crossing memastikan bahwa wilayah yang berorientasi searah jarum jam mengambil faktor tambahan -1, menyebabkan mereka berkontribusi jumlah positif ke area tersebut.

Begini cara program bekerja:

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
sumber