Noktalar saat yönünde sıralansın mı?


156

Bir x, y noktası dizisi verildiğinde, bu dizinin noktalarını saat yönünde (genel ortalama merkez noktalarının çevresinde) nasıl sıralayabilirim? Amacım, çizgileri bir çizgi oluşturma işlevine geçirerek oldukça "katı" görünen bir şeyle sonuçlanmak ve mümkün olduğunca dışbükey çizgiler olmadan kesmektir.

Değer için, Lua kullanıyorum, ancak herhangi bir sahte kod takdir edilecektir.

Güncelleme: Referans için, bu Ciamej'in mükemmel cevabına dayanan Lua kodu (benim "uygulama" önekimi yoksay)

function appSortPointsClockwise(points)
    local centerPoint = appGetCenterPointOfPoints(points)
    app.pointsCenterPoint = centerPoint
    table.sort(points, appGetIsLess)
    return points
end

function appGetIsLess(a, b)
    local center = app.pointsCenterPoint

    if a.x >= 0 and b.x < 0 then return true
    elseif a.x == 0 and b.x == 0 then return a.y > b.y
    end

    local det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y)
    if det < 0 then return true
    elseif det > 0 then return false
    end

    local d1 = (a.x - center.x) * (a.x - center.x) + (a.y - center.y) * (a.y - center.y)
    local d2 = (b.x - center.x) * (b.x - center.x) + (b.y - center.y) * (b.y - center.y)
    return d1 > d2
end

function appGetCenterPointOfPoints(points)
    local pointsSum = {x = 0, y = 0}
    for i = 1, #points do pointsSum.x = pointsSum.x + points[i].x; pointsSum.y = pointsSum.y + points[i].y end
    return {x = pointsSum.x / #points, y = pointsSum.y / #points}
end


1
Radyal çizginin açısını bu noktadan hesaplamayı düşünün. Sonra açıya göre sıralayın.
Başkan James K. Polk

Bilmiyorsanız, lua, tbl'nin ipairs(tbl)indeksleri ve değerleri üzerinde 1'den #tbl'e kadar yinelenen yerleşik bir işleve sahiptir . Toplam hesaplama için, çoğu insanın daha temiz göründüğü bunu yapabilirsiniz:for _, p in ipairs(points) do pointsSum.x = pointsSum.x + p.x; pointsSum.y = pointsSum.y + p.y end
Ponkadoodle

2
@Wallacoloo Bu oldukça tartışmalı. Ayrıca, vanilyada Lua ipairsdöngü için sayısaldan önemli ölçüde yavaştır.
Alexander Gladysh

Davam için işe yaraması için bazı küçük değişiklikler yapmak zorunda kaldım (sadece bir merkezi göre iki noktayı karşılaştırarak). gist.github.com/personalnadir/6624172 Koddaki 0 ile yapılan tüm bu karşılaştırmalar, noktaların rastgele bir nokta yerine orijin etrafında dağıtıldığını varsayar. Ayrıca birinci koşulun merkez noktasının altındaki noktaları yanlış sıralayacağını düşünüyorum. Kod için teşekkürler, gerçekten yararlı oldu!
personalnadir

Yanıtlar:


192

İlk olarak, orta noktayı hesaplayın. Ardından, istediğiniz sıralama algoritmasını kullanarak noktaları sıralayın, ancak bir noktanın diğerinden daha az olup olmadığını belirlemek için özel karşılaştırma rutini kullanın.

Bu basit hesaplama ile bir noktanın (a) merkezle ilişkili olarak diğerinin (b) solunda mı yoksa sağında mı olduğunu kontrol edebilirsiniz:

det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y)

eğer sonuç sıfırsa, o zaman merkezden aynı çizgidedirler, eğer pozitif veya negatifse, o zaman bir tarafta veya diğerinde, yani bir nokta diğerinden önce gelir. Bunu kullanarak noktaları karşılaştırmak ve sıralı dizide hangi sırayla görünmeleri gerektiğini belirlemek için küçük bir ilişki kurabilirsiniz. Ancak bu düzenin başlangıcı nerede olduğunu tanımlamanız gerekir, yani başlangıç ​​açısının hangi açı olacağını (örneğin, x ekseninin pozitif yarısı).

Karşılaştırma işlevinin kodu şöyle görünebilir:

bool less(point a, point b)
{
    if (a.x - center.x >= 0 && b.x - center.x < 0)
        return true;
    if (a.x - center.x < 0 && b.x - center.x >= 0)
        return false;
    if (a.x - center.x == 0 && b.x - center.x == 0) {
        if (a.y - center.y >= 0 || b.y - center.y >= 0)
            return a.y > b.y;
        return b.y > a.y;
    }

    // compute the cross product of vectors (center -> a) x (center -> b)
    int det = (a.x - center.x) * (b.y - center.y) - (b.x - center.x) * (a.y - center.y);
    if (det < 0)
        return true;
    if (det > 0)
        return false;

    // points a and b are on the same line from the center
    // check which point is closer to the center
    int d1 = (a.x - center.x) * (a.x - center.x) + (a.y - center.y) * (a.y - center.y);
    int d2 = (b.x - center.x) * (b.x - center.x) + (b.y - center.y) * (b.y - center.y);
    return d1 > d2;
}

Bu, noktaları saat 12'den başlayarak saat yönünde sıralayacaktır. Aynı "saat" üzerindeki puanlar, merkezden daha ileri olanlardan başlanarak sıralanacaktır.

Tamsayı türlerini (Lua'da gerçekten mevcut olmayan) kullanıyorsanız, det, d1 ve d2 değişkenlerinin gerçekleştirilen hesaplamaların sonucunu tutabilecek türde olduğundan emin olmanız gerekir.

Mümkün olduğunca dışbükey görünen sağlam bir şey elde etmek istiyorsanız, sanırım bir Dışbükey Gövde arıyorsunuz . Graham Scan'i kullanarak hesaplayabilirsiniz . Bu algoritmada, noktaları özel bir pivot noktasından başlayarak saat yönünde (veya saat yönünün tersine) sıralamanız gerekir. Ardından, dışbükey gövdeye yeni noktalar ekleyerek sola veya sağa dönüp dönmediğinizi kontrol ederken her seferinde basit döngü adımlarını tekrarlarsınız, bu kontrol yukarıdaki karşılaştırma işlevinde olduğu gibi bir çapraz ürüne dayanır.

Düzenle:

if (a.y - center.y >= 0 || b.y - center.y >=0)X = 0 ve y negatif olan noktaların, merkezden daha ileri noktalardan başlayarak sıralandığından emin olmak için bir if ifadesi daha eklendi . Aynı 'saatte' puanların sırasını umursamıyorsanız, if ifadesini atlayabilir ve her zaman geri dönebilirsiniz a.y > b.y.

Ekleyerek ifadeleri ise ilk düzeltildi -center.xve -center.y.

İkinci if ifadesi eklendi (a.x - center.x < 0 && b.x - center.x >= 0). Eksik olduğu açık bir gözetimdi. Bazı denetimler gereksiz olduğu için if ifadeleri şimdi yeniden düzenlenebilir. Örneğin, ilk if ifadesindeki ilk koşul yanlışsa, ikincisinin ilk koşulu doğru olmalıdır. Ancak, basitlik uğruna olduğu gibi kodu bırakmaya karar verdim. Derleyicinin kodu optimize etmesi ve yine de aynı sonucu üretmesi oldukça olasıdır.


25
+1: Hayır atan(), karekök ve hatta bölünme yok. Bu, bilgisayar grafikleri düşüncesinin iyi bir örneğidir. Tüm kolay vakaları mümkün olan en kısa sürede durdurun ve zor durumlarda bile, gerekli cevabı bilmek için mümkün olduğunca az hesaplayın.
RBerteig

Ancak tüm noktaları diğerleriyle karşılaştırmayı gerektirir. Yeni noktalar eklemek için basit bir yöntem var mı?
Iterator

2
nokta kümesinin a priori olduğu biliniyorsa, sadece O (n * log n) karşılaştırmalarını alır. Bu arada puan eklemek istiyorsanız, bunları dengeli bir ikili arama ağacı gibi sıralı bir kümede tutmanız gerekir. Böyle bir durumda yeni bir nokta eklemek O (log n) karşılaştırmaları gerektirir ve bu, kutupsal koordinatları içeren çözüm için tamamen aynıdır.
ciamej

2
Bu eksik mi: if (ax - center.x <0 && bx - center.x> = 0) false değerini döndürür;
Tom Martin

2
Selam. Oldukça eski, ama: "Bu, puanları saat 12'den başlayarak saat yönünde sıralar." Neden saat 12 ve nasıl 6 olarak değiştirebilirim? Birisi bana söyleyebilir mi?
Ismoh

20

Sorununuza ilginç bir alternatif yaklaşım, Gezgin Satıcı Sorunu (TSP) için yaklaşık minimum değeri bulmak olacaktır. tüm puanlarınızı birbirine bağlayan en kısa yol. Noktalarınız dışbükey bir şekil oluşturuyorsa, doğru çözüm olmalıdır, aksi halde yine de iyi görünmelidir ("katı" bir şekil düşük bir çevre / alan oranına sahip olarak tanımlanabilir, bu da burada optimize ediyoruz) .

Tercih ettiğiniz dilde bir ton bulabileceğinizden emin olduğum TSP için herhangi bir optimize edici uygulamasını kullanabilirsiniz.


Amanın. "İlginç" bir yetersizliktir. :)
Yineleyici

@Iterator: Fikrimden oldukça memnun kaldım, bunun için aşağı düşmekten oldukça hayal kırıklığına uğradım: - / Sence geçerli mi?
static_rtti

1
Tabii ki NP tam orijinal algoritmadan değil birçok hızlı yaklaşımdan birini kullanmanızı öneriyordum.
static_rtti

6
Ek açıyı takdir ediyorum! Gelecekte birileri bu konu üzerinde beyin fırtınası seçeneklerine bakmak için yanarsa, birkaç farklı, çok farklı cevaplara sahip olmak çok yardımcı olabilir.
Philipp Lenssen

1
Yaklaşımımın muhtemelen daha yavaş olduğunu, ancak karmaşık durumlarda daha doğru olduğunu unutmayın: Örneğin, "8" puanlarının olduğu durumu düşünün. Kutupsal koordinatlar bu durumda size yardımcı olmaz ve elde edeceğiniz sonuç büyük ölçüde seçtiğiniz merkeze bağlı olacaktır. TSP çözümü herhangi bir "sezgisel" parametreden bağımsızdır.
static_rtti

19

İstediğiniz, kutupsal koordinatlar olarak bilinen bir sistemdir . Kartezyen'den kutupsal koordinatlara dönüşüm her dilde kolayca yapılabilir. Formüller bu bölümde bulunabilir .

Kutupsal koordinatlara dönüştürdükten sonra, teta açısına göre sıralayın.


4
Bu işe yarayacaktır, ancak sipariş sorusunu cevaplamak için gerekenden daha fazla hesaplama yapma kusuru da olacaktır. Pratikte, gerçek açıları veya radyal mesafeleri umursamıyorsunuz, sadece göreceli sıralaması. ciamej'in çözümü daha iyidir çünkü bölünmelerden, kare köklerden ve triglerden kaçınır.
RBerteig

1
"Daha iyi" için kriterinizin ne olduğundan emin değilim. Örneğin, tüm noktaları birbiriyle karşılaştırmak bir tür hesaplama israfıdır. Trig, yetişkinleri korkutan bir şey değil, değil mi?
Iterator

3
Bu trig korkutucu değil. Sorun şu ki, trigerin hesaplanması pahalı ve açıların göreceli sırasını belirlemek için gerekli değildi. Benzer şekilde, yarıçapları sıraya koymak için kare kökleri almanıza gerek yoktur. Kartezyen'den kutupsal koordinatlara tam bir dönüşüm hem bir ark tanjantını hem de bir kare kökü yapar. Bu nedenle cevabınız doğrudur, ancak bilgisayar grafikleri veya hesaplama geometrisi bağlamında, bunu yapmanın en iyi yolu olmayacaktır.
RBerteig

Anladım. Ancak, OP comp-geo olarak yayınlamadı, bu başka biri tarafından etiketlenmişti. Yine de, diğer çözümün # numaralı noktada polinom olduğu anlaşılıyor, yoksa yanılıyor muyum? Öyleyse, bu trig'den daha fazla döngü yakar.
Iterator

Comp-geo etiketini gerçekten fark etmemiştim, sadece soru için rasyonel uygulamaların biri ya da diğeri olması gerektiğini varsaydım. Sonuçta, sadece birkaç puan varsa performans sorusu tartışmalı hale gelir ve / veya işlem nadiren yeterli olur. Bu noktada, nasıl yapılacağını bilmek önemli hale geliyor ve bu yüzden cevabınızın doğru olduğunu kabul ediyorum. "Saat yönünde bir emir" kavramının hemen hemen herkese açıklanabilecek şekilde nasıl hesaplanacağını açıklar.
RBerteig

3

Başka bir sürüm (a, b'den önce saat yönünün tersine gelirse true değerini döndürür):

    bool lessCcw(const Vector2D &center, const Vector2D &a, const Vector2D &b) const
    {
        // Computes the quadrant for a and b (0-3):
        //     ^
        //   1 | 0
        //  ---+-->
        //   2 | 3

        const int dax = ((a.x() - center.x()) > 0) ? 1 : 0;
        const int day = ((a.y() - center.y()) > 0) ? 1 : 0;
        const int qa = (1 - dax) + (1 - day) + ((dax & (1 - day)) << 1);

        /* The previous computes the following:

           const int qa =
           (  (a.x() > center.x())
            ? ((a.y() > center.y())
                ? 0 : 3)
            : ((a.y() > center.y())
                ? 1 : 2)); */

        const int dbx = ((b.x() - center.x()) > 0) ? 1 : 0;
        const int dby = ((b.y() - center.y()) > 0) ? 1 : 0;
        const int qb = (1 - dbx) + (1 - dby) + ((dbx & (1 - dby)) << 1);

        if (qa == qb) {
            return (b.x() - center.x()) * (a.y() - center.y()) < (b.y() - center.y()) * (a.x() - center.x());
        } else {
            return qa < qb;
       } 
    }

Bu daha hızlıdır, çünkü derleyici (Visual C ++ 2015'te test edilmiştir) dax, day, dbx, dby'yi hesaplamak için atlama üretmez. İşte derleyiciden çıktı düzeneği:

; 28   :    const int dax = ((a.x() - center.x()) > 0) ? 1 : 0;

    vmovss  xmm2, DWORD PTR [ecx]
    vmovss  xmm0, DWORD PTR [edx]

; 29   :    const int day = ((a.y() - center.y()) > 0) ? 1 : 0;

    vmovss  xmm1, DWORD PTR [ecx+4]
    vsubss  xmm4, xmm0, xmm2
    vmovss  xmm0, DWORD PTR [edx+4]
    push    ebx
    xor ebx, ebx
    vxorps  xmm3, xmm3, xmm3
    vcomiss xmm4, xmm3
    vsubss  xmm5, xmm0, xmm1
    seta    bl
    xor ecx, ecx
    vcomiss xmm5, xmm3
    push    esi
    seta    cl

; 30   :    const int qa = (1 - dax) + (1 - day) + ((dax & (1 - day)) << 1);

    mov esi, 2
    push    edi
    mov edi, esi

; 31   : 
; 32   :    /* The previous computes the following:
; 33   : 
; 34   :    const int qa =
; 35   :        (   (a.x() > center.x())
; 36   :         ? ((a.y() > center.y()) ? 0 : 3)
; 37   :         : ((a.y() > center.y()) ? 1 : 2));
; 38   :    */
; 39   : 
; 40   :    const int dbx = ((b.x() - center.x()) > 0) ? 1 : 0;

    xor edx, edx
    lea eax, DWORD PTR [ecx+ecx]
    sub edi, eax
    lea eax, DWORD PTR [ebx+ebx]
    and edi, eax
    mov eax, DWORD PTR _b$[esp+8]
    sub edi, ecx
    sub edi, ebx
    add edi, esi
    vmovss  xmm0, DWORD PTR [eax]
    vsubss  xmm2, xmm0, xmm2

; 41   :    const int dby = ((b.y() - center.y()) > 0) ? 1 : 0;

    vmovss  xmm0, DWORD PTR [eax+4]
    vcomiss xmm2, xmm3
    vsubss  xmm0, xmm0, xmm1
    seta    dl
    xor ecx, ecx
    vcomiss xmm0, xmm3
    seta    cl

; 42   :    const int qb = (1 - dbx) + (1 - dby) + ((dbx & (1 - dby)) << 1);

    lea eax, DWORD PTR [ecx+ecx]
    sub esi, eax
    lea eax, DWORD PTR [edx+edx]
    and esi, eax
    sub esi, ecx
    sub esi, edx
    add esi, 2

; 43   : 
; 44   :    if (qa == qb) {

    cmp edi, esi
    jne SHORT $LN37@lessCcw

; 45   :        return (b.x() - center.x()) * (a.y() - center.y()) < (b.y() - center.y()) * (a.x() - center.x());

    vmulss  xmm1, xmm2, xmm5
    vmulss  xmm0, xmm0, xmm4
    xor eax, eax
    pop edi
    vcomiss xmm0, xmm1
    pop esi
    seta    al
    pop ebx

; 46   :    } else {
; 47   :        return qa < qb;
; 48   :    }
; 49   : }

    ret 0
$LN37@lessCcw:
    pop edi
    pop esi
    setl    al
    pop ebx
    ret 0
?lessCcw@@YA_NABVVector2D@@00@Z ENDP            ; lessCcw

Zevk almak.


1
Anahtardaki iki dönüş ifadesi matematiksel olarak eşdeğerdir. Geçişin bir nedeni var mı?
unagi

0
  • vektör3 a = yeni vektör3 (1, 0, 0) .............. wrt X_axis
  • vektör3 b = any_point - Merkez;
- y = |a * b|   ,   x =  a . b

- Atan2(y , x)...............................gives angle between -PI  to  + PI  in radians
- (Input % 360  +  360) % 360................to convert it from  0 to 2PI in radians
- sort by adding_points to list_of_polygon_verts by angle  we got 0  to 360

Sonunda Anticlockwize sıralanmış verts olsun

list.Reverse () .................. Clockwise_order

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.