En şişman insanları aşırı yüklenmiş bir uçaktan atmak.


200

Diyelim ki bir uçağınız var ve yakıt seviyesi düşük. Uçak 3000 kilo yolcu ağırlığını düşürmedikçe, bir sonraki havalimanına ulaşamayacak. Maksimum yaşam sayısını kurtarmak için, önce en ağır insanları uçaktan atmak istiyoruz.

Ve evet, uçakta milyonlarca insan var ve listenin tamamını sıralamak zorunda kalmadan en ağır yolcuları bulmak için en uygun algoritmayı istiyoruz.

Bu C ++ kodlamak çalışıyorum bir şey için bir proxy sorunudur. Yolcu tezahüründe ağırlık olarak bir "partial_sort" yapmak istiyorum, ama kaç elemente ihtiyacım olacağını bilmiyorum. Kendi "partial_sort" algoritmamı ("partial_sort_accumulate_until") uygulayabilirim, ancak standart STL kullanarak bunu yapmanın daha kolay bir yolu olup olmadığını merak ediyorum.


5
İnsan ambarlarına benzetme, en ağır insanlar arasında olma olasılığı yüksek olduğundan, X'ten, örneğin 120 kg'dan daha ağır olan insanları atmakla başlayabilirsiniz.
RedX

132
Tüm yolcular algoritmanın herhangi bir adımı ile işbirliği yapar mı?
Lior Kogan

34
bunun gibi konuları neden IT'yi seviyorum.
Markus

14
Bunun hangi havayolu şirketi için olduğunu sorabilir miyim? Eminim sadece onlarla uçmak yapmak istiyorum önce ben üzerinde kendimi indulged ettik sonra değil - tatil sezonu.
jp2code

24
Uygun ekipmanlarla (yerleşik terazili ejektör koltukları gibi) yolcu işbirliği gerekmez.
Jim Fred

Yanıtlar:


102

Bunun bir yolu, min yığın ( std::priority_queueC ++) kullanmaktır. Bir MinHeapsınıfınız olduğunu varsayarak, bunu nasıl yapacağınız aşağıda açıklanmıştır . (Evet, benim örneğim C # 'da. Sanırım fikri anladınız.)

int targetTotal = 3000;
int totalWeight = 0;
// this creates an empty heap!
var myHeap = new MinHeap<Passenger>(/* need comparer here to order by weight */);
foreach (var pass in passengers)
{
    if (totalWeight < targetTotal)
    {
        // unconditionally add this passenger
        myHeap.Add(pass);
        totalWeight += pass.Weight;
    }
    else if (pass.Weight > myHeap.Peek().Weight)
    {
        // If this passenger is heavier than the lightest
        // passenger already on the heap,
        // then remove the lightest passenger and add this one
        var oldPass = myHeap.RemoveFirst();
        totalWeight -= oldPass.Weight;
        myHeap.Add(pass);
        totalWeight += pass.Weight;
    }
}

// At this point, the heaviest people are on the heap,
// but there might be too many of them.
// Remove the lighter people until we have the minimum necessary
while ((totalWeight - myHeap.Peek().Weight) > targetTotal)
{
    var oldPass = myHeap.RemoveFirst();
    totalWeight -= oldPass.Weight; 
}
// The heap now contains the passengers who will be thrown overboard.

Standart referanslara göre, çalışma süresi ile orantılı olmalıdır n log k, nerede nyolcu sayısıdır ve köbek üzerinde öğelerin sayısıdır. Yolcu ağırlıklarının genellikle 100 lbs veya daha fazla olacağını varsayarsak, yığının herhangi bir zamanda 30'dan fazla öğe içermesi olası değildir.

En kötü durum, yolcuların en düşük ağırlıktan en yükseğe sıralanması durumunda olacaktır. Bu, her yolcunun öbeğe eklenmesini ve her yolcunun öbekten çıkarılmasını gerektirir. Yine de, bir milyon yolcu ile ve en hafif 100 lbs ağırlığında olduğu varsayılarak, n log koldukça küçük bir sayıya kadar çalışıyor.

Yolcuların ağırlıklarını rastgele alırsanız, performans çok daha iyidir. Bir tavsiye motoru için buna benzer bir şey kullanıyorum (birkaç milyon listeden ilk 200 öğeyi seçiyorum). Genellikle yığınlara gerçekten eklenen 50.000 veya 70.000 öğe ile sonuçlanırım.

Oldukça benzer bir şey göreceğinizden şüpheleniyorum: adaylarınızın çoğunluğu, yığın üzerinde bulunan en hafif kişiden daha hafif oldukları için reddedilecek. Ve Peekbir O(1)ameliyat.

Öbek seçiminin ve hızlı seçimin performansı hakkında daha fazla bilgi için, bkz . Teori pratikle buluştuğunda . Kısa sürüm: toplam öğe sayısının% 1'inden daha azını seçerseniz, yığın seçimi hızlı seçim yerine açık bir kazanır. % 1'den fazla, sonra hızlı seçim veya Introselect gibi bir varyant kullanın .


1
SoapBox daha hızlı cevap gönderdi.
Mooing Duck

7
Benim okumaya göre, SoapBox'ın cevabı Jim Mischel'in cevabının ahlaki karşılığıdır. SoapBox kodunu C ++ ile yazdı ve böylece MinHeap ile aynı günlük (N) ekleme süresine sahip bir std :: set kullanıyor.
IvyMike

1
Doğrusal bir zaman çözümü var. Ben ekleyeceğim.
Neil G

2
Min-yığın için bir STL sınıfı vardır: std::priority_queue
bdonlan

3
@MooingDuck: Belki yanlış anladın. Tıpkı SoapBox'ın kodu boş bir küme oluşturduğunda kodum boş bir yığın oluşturur. Gördüğüm gibi en büyük fark, kodunun daha fazla ağırlıktaki öğeler eklendikçe aşırı kilo kümesini keserken, benimki fazlalığı koruyor ve sonunda kesiyor. Daha ağır insanları bulmak için listede ilerledikçe setinin büyüklüğü azalır. Yığım, ağırlık eşiğine ulaştıktan sonra aynı boyutta kalıyor ve listedeki son öğeyi kontrol ettikten sonra kırpıyorum.
Jim Mischel

119

Ancak bu, proxy sorununuz için yardımcı olmaz:

1.000.000 yolcunun 3000 kilo vermesi için her yolcunun kişi başına (3000/1000000) = 0.003 lbs kaybetmesi gerekir. Bu, herkesi kurtarmak için her gömlek veya ayakkabıyı veya muhtemelen tırnak kupürlerini birleştirerek elde edilebilir. Bu, uçak daha fazla yakıt kullandıkça ihtiyaç duyulan kilo kaybının artmasından önce etkili toplama ve atma olduğunu varsayar.

Aslında, artık tırnak makaslarına izin vermiyorlar, bu yüzden dışarıda.


14
Sorunu gözden geçirme ve gerçekten daha iyi bir yol bulma yeteneğini seviyorum.
fncomp

19
Sen bir dahisin. :)
Jonathan

3
Sanırım ayakkabı tek başına bunu kapsıyor
Mooing Duck 23:12

0.003 lbs, onsun 1 / 20'sinin biraz altında olan 0.048 oz. Yani, uçaktaki altmış kişiden sadece biri üç onsluk şampuan kuralından yararlanıyorsa, tüm bu şampuanı atarak günü kurtarabilirsiniz.
Ryan Lundy

43

Aşağıda, basit çözümün oldukça basit bir uygulaması bulunmaktadır. % 100 doğru daha hızlı bir yol olduğunu düşünmüyorum.

size_t total = 0;
std::set<passenger> dead;
for ( auto p : passengers ) {
    if (dead.empty()) {
       dead.insert(p);
       total += p.weight;
       continue;
    }
    if (total < threshold || p.weight > dead.begin()->weight)
    {
        dead.insert(p);
        total += p.weight;
        while (total > threshold)
        {
            if (total - dead.begin()->weight < threshold)
                break;
            total -= dead.begin()->weight;
            dead.erase(dead.begin());
        }
    }
 }

Bu, eşiğe ulaşana kadar "ölü insanlar" setini doldurarak çalışır. Eşik değere ulaşıldığında, en hafif ölü kişiden daha ağır olanları bulmaya çalışan yolcular listesine devam ediyoruz. Birini bulduğumuzda, bunları listeye ekleriz ve daha fazla kaydedemeyene kadar en hafif kişileri listeden "Kaydetmeye" başlarız.

En kötü durumda, bu tüm listenin bir türüyle aynı performansı gösterecektir. Ama en iyi durumda ("ölü liste" ilk X kişi ile düzgün bir şekilde doldurulur) gerçekleştirecektir O(n).


1
Sanırım, Diğer'in totalyanında güncellemeniz gerekiyor, göndereceğim continue; cevap bu. Süper hızlı çözüm
Mooing Duck

2
Bu doğru cevap, bu en hızlı cevap, bu aynı zamanda en düşük karmaşıklığa sahip cevap.
Xander Tulip

Muhtemelen dead.begin () önbelleğe alarak ve modern işlemcilerde oldukça yavaş olan dallanmayı en aza indirecek şeyleri yeniden düzenleyerek biraz daha
sıkabilirsiniz

dead.begin () büyük olasılıkla üç değerlidir ve neredeyse kesinlikle bir veri erişimine çevrilir. Ama evet, birkaç ifs etrafında hareket şubeleri azaltarak biraz daha fazla performans dışarı olurdu ... ama muhtemelen okunabilirlik için büyük bir maliyetle.
SoapBox

1
Bu mantıklı bir şekilde zariftir ve öndeki yolcuların sayısını bilmemek de dahil olmak üzere OP'nin TÜM gerekliliklerini karşılar. Son 5 ayın çoğunu STL Maps & Sets ile çalışarak geçirdim, eminim kullanılan yineleyicilerin yaygın kullanımı performansı sakatlayacaktır. Sadece seti doldurun ve en ağır insanların toplamı 3.000'den büyük olana kadar sağdan sola yineleyin. Rasgele sırayla sunulan 1 milyon elemandan oluşan bir set, i5 || i7 3.4Ghz çekirdeklerine ~ 30 milyon / sn yükleyecektir. En az 100X kadar yavaş yineleme. KISS burada kazanacak.
user2548100

32

Tüm yolcuların işbirliği yapacağını varsayarsak: Paralel bir sıralama ağı kullanın . (bakınız ayrıca bu )

İşte canlı bir gösteri

Güncelleme: Alternatif video (1: 00'a atla)

Çiftlerden karşılaştırma-değişim yapmalarını istemek - bundan daha hızlı olamazsınız.


1
Bu hala bir çeşittir ve O (nlogn) olacaktır. Kesinlikle daha hızlı olabilirsiniz, k << n çözümünün sağlandığı O (nlogk) olarak.
Adam

1
@Adam: Paralel bir çeşit. Sıralama O (nlog n) SIRALI adımların alt sınırına sahiptir. Bununla birlikte, paralel olabilirler, bu nedenle zaman karmaşıklığı çok daha düşük olabilir. bakınız örneğin cs.umd.edu/~gasarch/ramsey/parasort.pdf
Lior Kogan

1
OP, "Bu, C ++ 'da kodlamaya çalıştığım bir şey için bir proxy sorunu" diyor. Bu yüzden yolcular işbirliği yapacak olsalar bile, sizin için hesaplamazlar. Bu güzel bir fikir, ancak bu kağıdın nişlemcileri aldığınız varsayımı tutmuyor.
Adam

@LiorKogan - canlı demo video artık youtube kullanılabilir
Adelin

@Adelin: Teşekkürler, alternatif video eklendi
Lior Kogan

21

@Blastfurnace doğru yoldaydı. Pivotların ağırlık eşikleri olduğu yerlerde hızlı seçim kullanırsınız. Her bölüm bir grup kişiyi kümelere ayırır ve her bir grup için toplam ağırlığı döndürür. En yüksek ağırlıktaki insanlara karşılık gelen kovalar 3000 kilodan fazla olana ve bu setteki en düşük kova 1 kişiye sahip olana kadar (yani daha fazla bölünemez) uygun kova kırmaya devam edersiniz.

Bu algoritma doğrusal amortismana tabi tutulur, ancak ikinci dereceden en kötü durumdur. Tek lineer zaman algoritması olduğunu düşünüyorum .


İşte bu algoritmayı gösteren bir Python çözümü:

#!/usr/bin/env python
import math
import numpy as np
import random

OVERWEIGHT = 3000.0
in_trouble = [math.floor(x * 10) / 10
              for x in np.random.standard_gamma(16.0, 100) * 8.0]
dead = []
spared = []

dead_weight = 0.0

while in_trouble:
    m = np.median(list(set(random.sample(in_trouble, min(len(in_trouble), 5)))))
    print("Partitioning with pivot:", m)
    lighter_partition = []
    heavier_partition = []
    heavier_partition_weight = 0.0
    in_trouble_is_indivisible = True
    for p in in_trouble:
        if p < m:
            lighter_partition.append(p)
        else:
            heavier_partition.append(p)
            heavier_partition_weight += p
        if p != m:
            in_trouble_is_indivisible = False
    if heavier_partition_weight + dead_weight >= OVERWEIGHT and not in_trouble_is_indivisible:
        spared += lighter_partition
        in_trouble = heavier_partition
    else:
        dead += heavier_partition
        dead_weight += heavier_partition_weight
        in_trouble = lighter_partition

print("weight of dead people: {}; spared people: {}".format(
    dead_weight, sum(spared)))
print("Dead: ", dead)
print("Spared: ", spared)

Çıktı:

Partitioning with pivot: 121.2
Partitioning with pivot: 158.9
Partitioning with pivot: 168.8
Partitioning with pivot: 161.5
Partitioning with pivot: 159.7
Partitioning with pivot: 158.9
weight of dead people: 3051.7; spared people: 9551.7
Dead:  [179.1, 182.5, 179.2, 171.6, 169.9, 179.9, 168.8, 172.2, 169.9, 179.6, 164.4, 164.8, 161.5, 163.1, 165.7, 160.9, 159.7, 158.9]
Spared:  [82.2, 91.9, 94.7, 116.5, 108.2, 78.9, 83.1, 114.6, 87.7, 103.0, 106.0, 102.3, 104.9, 117.0, 96.7, 109.2, 98.0, 108.4, 99.0, 96.8, 90.7, 79.4, 101.7, 119.3, 87.2, 114.7, 90.0, 84.7, 83.5, 84.7, 111.0, 118.1, 112.1, 92.5, 100.9, 114.1, 114.7, 114.1, 113.7, 99.4, 79.3, 100.1, 82.6, 108.9, 103.5, 89.5, 121.8, 156.1, 121.4, 130.3, 157.4, 138.9, 143.0, 145.1, 125.1, 138.5, 143.8, 146.8, 140.1, 136.9, 123.1, 140.2, 153.6, 138.6, 146.5, 143.6, 130.8, 155.7, 128.9, 143.8, 124.0, 134.0, 145.0, 136.0, 121.2, 133.4, 144.0, 126.3, 127.0, 148.3, 144.9, 128.1]

3
+1. Bu ilginç bir fikir, ancak oldukça doğrusal olduğundan emin değilim. Bir şey eksik olmadıkça, kepçenin toplam ağırlığını hesaplamak için öğeleri tekrarlamanız gerekir ve her bölüştüğünüzde yüksek kepçeyi (en azından kısmen) yeniden hesaplamanız gerekir. Genel durumda yığın tabanlı yaklaşımımdan daha hızlı olacak, ancak bence karmaşıklığı küçümsüyorsun.
Jim Mischel

2
@Jim: Hızlı seçim ile aynı karmaşıklık olmalıdır . Vikipedi üzerindeki açıklamanın en iyi olmadığını biliyorum, ancak doğrusal amortisman süresinin nedeni, her bölüm yaptığınızda, bölümün yalnızca bir tarafı ile çalışmanızdır. Titizlikle, her bölümün insan setini ikiye böldüğünü hayal edin. Daha sonra, ilk adım O (n), sonra O (n / 2) vb. Alır ve n + n / 2 + n / 4 + ... = 2n.
Neil G

2
@Jim: Her neyse, algoritmanız en kötü durum süresine sahipken, benimki en iyi ortalama durum süresine sahip. Her ikisinin de iyi çözüm olduğunu düşünüyorum.
Neil G

2
@JimMischel, NeilG: codepad.org/FAx6hbtc Hepsinin aynı sonuçları verdiğini doğruladım ve Jim'in düzeltildi. FullSort: 1828 keneler. JimMischel: 312 keneler. SoapBox 109 keneler. NeilG: 641 keneler.
Mooing Duck

2
@NeilG: codepad.org/0KmcsvwD Algoritmanızı daha hızlı uygulamak için std :: partition kullandım. stdsort: 1812 keneler. FullHeap 312 keneler. Soapbox / JimMichel: 109 keneler, NeilG: 250 keneler.
Mooing Duck

11

İnsanların ağırlıkları gibi, maksimum ve minimum değerlerin O (n) cinsinden sıralamak için bir sayı tabanı sıralaması kullanmasının muhtemel olduğu konusunda iyi bir fikriniz olduğunu varsayarsak. Sonra listenin en ağır ucundan en hafifine doğru çalışın. Toplam çalışma süresi: O (n). Ne yazık ki, STL'de bir sayı tabanı sıralaması uygulaması yoktur, ancak yazmak oldukça kolaydır.


Yanıtı türetmek için listeyi tam olarak sıralamanız gerekmediğinden, genel bir sayı tabanı sıralaması kullanmam.
Mooing Duck

1
Netleştirmek için, bir basamağa göre sıralama ise iyi bir fikir. Sadece özelleştirilmiş bir optimize yazdığınızdan emin olun.
Mooing Ördek

1
@Mooing: Tam bir sayı tabanı sıralaması yapmanız gerekmediği doğrudur, ancak bunu yayınladığımda, O (n) algoritması yayınlanmadı ve bunu görmek kolaydı. Bence Neil G'nin cevabı şimdi daha iyi ve açık bir şekilde medyanı seçimi için pivot olarak kullanmaya başladığı için en iyisi. Ancak standart bir sayı tabanı sıralaması kullanmak biraz daha kolay ve ince uygulama hatalarına sahip olma olasılığı daha düşüktür, bu yüzden cevabımı bırakacağım. Özelleştirilmiş kısmi sayı tabanı sıralaması yapmak kesinlikle daha hızlı olurdu, ancak asimptotik olarak değil.
Keith Irwin

6

Neden "sıralama" dan farklı bir iptal kuralına sahip kısmi bir hızlı sıralama kullanmıyorsunuz. Çalıştırabilir ve daha sonra sadece daha yüksek yarıyı kullanabilir ve bu üst yarının içindeki ağırlık, en azından artık atılacak ağırlığı içermeyene kadar devam edebilir, özyinelemede bir adım geri gidip listeyi sıralayabilirsiniz. Bundan sonra, insanları sıralı listenin üst ucundan atmaya başlayabilirsiniz.


Neil G'nin algoritmasının ardındaki temel kavram bence .
Mooing Ördek

Neil G'nin kullandığı hızlı seçimin özü budur.
Michael Donohue

6

Büyük Paralel Turnuva Sıralaması: -

Ailse'nin her iki tarafında standart üç koltuk bulunduğunu varsayarsak: -

  1. Pencere koltuğundaki yolculardan, pencere koltuğundaki kişiden daha ağırlarsa orta koltuğa geçmelerini isteyin.

  2. Orta koltuktaki yolculardan, daha ağırlarsa koridordaki yolcu ile değiştirmelerini isteyin.

  3. Sol koridordaki yolcunun ağır koridordaki sağ koridordaki kimliğiyle değiştirmesini isteyin.

  4. Kabarcık sağ koridordaki yolcuları sıralayın. (N satır için n adım atar). - sağ koridordaki yolculardan öndeki kişiyle n -1 kez takas etmelerini isteyin.

5 3000 liraya ulaşana kadar kapıdan dışarı atın.

Gerçekten sıska bir yolcu yükünüz varsa 3 adım + n adım artı 30 adım.

İki koridorlu bir düzlem için - talimatlar daha karmaşıktır, ancak performans hemen hemen aynıdır.


Lior Kogan'ın cevabı ile aynı, ama çok daha fazla ayrıntı.
Mooing Duck

7
"Yeterince iyi" bir çözüm "özgür sosis" sunmak ve cepheye ulaşan ilk on beşi atmak olacaktır. Her seferinde en uygun çözümü sunmaz, ancak düz "O" ile çalışır.
James Anderson

Daha ağır olanların muhtemelen daha yavaş olacağı için son 15'i atmak daha iyi olmaz mıydı?
Peter

@Patriker - Amacım minimum insanla 3000 kilo vermek olduğuna inanıyorum. Her ne kadar adım 4'ü "kişi ile n - 29 kez takas etmek" olarak değiştirmek için algoritmayı optimize edebilseniz de, en ağır 30 sırada değil, en cömert olanı ön plana çıkarır.
James Anderson

4

Muhtemelen std::nth_elementen ağır 20 kişiyi doğrusal zamanda bölmek için kullanırdım . Sonra ağırların en ağırını bulmak ve çarpmak için daha karmaşık bir yöntem kullanın.


3

Ortalamayı ve standart sapmayı elde etmek için listeden bir geçiş yapabilir, ardından bunu gitmek zorunda olan insan sayısına yaklaşık olarak ayarlayabilirsiniz. Bu numaraya göre liste oluşturmak için partial_sort komutunu kullanın. Tahmin düşükse, geri kalanında yeni bir tahminle partial_sort'u tekrar kullanın.



2

İşte Python'un yerleşik heapq modülünü kullanan yığın tabanlı bir çözüm. Python'da orijinal soruya cevap vermiyor, ancak diğer yayınlanan Python çözümünden daha temiz (IMHO).

import itertools, heapq

# Test data
from collections import namedtuple

Passenger = namedtuple("Passenger", "name seat weight")

passengers = [Passenger(*p) for p in (
    ("Alpha", "1A", 200),
    ("Bravo", "2B", 800),
    ("Charlie", "3C", 400),
    ("Delta", "4A", 300),
    ("Echo", "5B", 100),
    ("Foxtrot", "6F", 100),
    ("Golf", "7E", 200),
    ("Hotel", "8D", 250),
    ("India", "8D", 250),
    ("Juliet", "9D", 450),
    ("Kilo", "10D", 125),
    ("Lima", "11E", 110),
    )]

# Find the heaviest passengers, so long as their
# total weight does not exceeed 3000

to_toss = []
total_weight = 0.0

for passenger in passengers:
    weight = passenger.weight
    total_weight += weight
    heapq.heappush(to_toss, (weight, passenger))

    while total_weight - to_toss[0][0] >= 3000:
        weight, repreived_passenger = heapq.heappop(to_toss)
        total_weight -= weight


if total_weight < 3000:
    # Not enough people!
    raise Exception("We're all going to die!")

# List the ones to toss. (Order doesn't matter.)

print "We can get rid of", total_weight, "pounds"
for weight, passenger in to_toss:
    print "Toss {p.name!r} in seat {p.seat} (weighs {p.weight} pounds)".format(p=passenger)

K = atılacak yolcu sayısı ve N = yolcu sayısı ise, bu algoritma için en iyi durum O (N) ve bu algoritma için en kötü durum Nlog (N) olur. En kötü durum, k uzun süre N'ye yakınsa ortaya çıkar. En kötü oyunculardan bir örnek:

weights = [2500] + [1/(2**n+0.0) for n in range(100000)] + [3000]

Ancak, bu durumda (insanları uçaktan atmak (bir paraşütle, sanırım)) o zaman k 3000'den az olmalı, bu da << "milyonlarca insan". Bu nedenle ortalama çalışma süresi, insan sayısına doğrusal olan Nlog (k) ile ilgili olmalıdır.

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.