Buttare via le persone più grasse da un aereo sovraccarico.


200

Supponiamo che tu abbia un aereo ed è a corto di carburante. A meno che l'aereo non cada 3000 libbre di peso passeggeri, non sarà in grado di raggiungere il prossimo aeroporto. Per salvare il numero massimo di vite, vorremmo prima buttare le persone più pesanti dall'aereo.

E oh sì, ci sono milioni di persone sull'aereo e vorremmo un algoritmo ottimale per trovare i passeggeri più pesanti, senza necessariamente ordinare l'intero elenco.

Questo è un problema proxy per qualcosa che sto cercando di codificare in C ++. Vorrei fare un "partial_sort" sul manifest del passeggero in base al peso, ma non so di quanti elementi avrò bisogno. Potrei implementare il mio algoritmo "partial_sort" ("partial_sort_accumulate_until"), ma mi chiedo se ci sia un modo più semplice per farlo usando lo standard STL.


5
Se l'analogia con l'essere umano regge, potresti iniziare buttando via persone che pesano più di X, ad esempio 120 kg, poiché è molto probabile che siano tra le persone più grasse.
RedX,

132
Tutti i passeggeri collaborerebbero con qualsiasi passaggio dell'algoritmo?
Lior Kogan,

34
argomenti come questo sono il motivo per cui adoro l'IT.
Markus,

14
Posso chiedere per quale compagnia aerea si tratta? Voglio essere sicuro di volare con loro solo prima delle festività natalizie, non dopo essermi lasciato andare.
jp2code,

24
La cooperazione dei passeggeri non è richiesta con l'attrezzatura adeguata (come i sedili di espulsione con bilance integrate).
Jim Fred,

Risposte:


102

Un modo sarebbe usare un heap min ( std::priority_queuein C ++). Ecco come lo faresti, supponendo che tu abbia avuto una MinHeaplezione. (Sì, il mio esempio è in C #. Penso che tu abbia avuto l'idea.)

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.

Secondo i riferimenti standard, il tempo di esecuzione dovrebbe essere proporzionale a n log k, dove si ntrova il numero di passeggeri ed kè il numero massimo di elementi nell'heap. Se supponiamo che il peso dei passeggeri sia in genere di 100 libbre o più, è improbabile che l'heap contenga più di 30 articoli in qualsiasi momento.

Il caso peggiore sarebbe se i passeggeri fossero presentati in ordine dal peso più basso al più alto. Ciò richiederebbe che ogni passeggero venga aggiunto all'heap e che ogni passeggero venga rimosso dall'heap. Tuttavia, con un milione di passeggeri e supponendo che il più leggero pesa 100 libbre, si risolve n log kin un numero ragionevolmente piccolo.

Se ottieni i pesi dei passeggeri in modo casuale, le prestazioni sono molto migliori. Uso qualcosa del genere per un motore di raccomandazione (seleziono i primi 200 articoli da un elenco di diversi milioni). Di solito finisco con solo 50.000 o 70.000 articoli effettivamente aggiunti all'heap.

Ho il sospetto che vedrai qualcosa di abbastanza simile: la maggior parte dei tuoi candidati verrà rifiutata perché sono più leggeri della persona più leggera già sul mucchio. Ed Peekè O(1)un'operazione.

Per ulteriori informazioni sulle prestazioni della selezione heap e della selezione rapida, vedere Quando la teoria incontra la pratica . Versione breve: se selezioni meno dell'1% del numero totale di elementi, la selezione heap è un chiaro vincitore rispetto alla selezione rapida. Oltre l'1%, quindi utilizza la selezione rapida o una variante come Introselect .


1
SoapBox ha pubblicato la risposta più veloce.
Mooing Duck,

7
Secondo la mia lettura, la risposta di SoapBox è l'equivalente morale della risposta di Jim Mischel. SoapBox ha scritto il suo codice in C ++ e quindi usa uno std :: set, che ha lo stesso log (N) di aggiunta del MinHeap.
IvyMike,

1
C'è una soluzione temporale lineare. Lo aggiungerò.
Neil G,

2
C'è una classe STL per un min-heap:std::priority_queue
bdonlan,

3
@MooingDuck: Forse hai frainteso. Il mio codice crea un heap vuoto, proprio come il codice di SoapBox crea un set vuoto. La differenza principale, come la vedo io, è che il suo codice taglia l'insieme del peso in eccesso man mano che vengono aggiunti oggetti di peso maggiore, mentre il mio mantiene l'eccesso e lo ritaglia alla fine. Il set diminuirà potenzialmente man mano che si sposta nell'elenco per trovare persone più pesanti. Il mio heap rimane della stessa dimensione dopo aver raggiunto la soglia di peso e lo taglio dopo aver controllato l'ultimo elemento dell'elenco.
Jim Mischel,

119

Questo non aiuterà per il tuo problema proxy, tuttavia:

Per 1.000.000 di passeggeri a perdere 3000 libbre di peso, ogni passeggero deve perdere (3000/1000000) = 0,003 libbre per persona. Ciò potrebbe essere ottenuto eliminando ogni camicia, o scarpe, o probabilmente anche ritagli di unghie, salvando tutti. Ciò presuppone una raccolta e un lancio efficienti prima che la perdita di peso necessaria aumentasse quando l'aereo consumava più carburante.

In realtà, non consentono più di tagliare le unghie a bordo, quindi è tutto.


14
Adoro la capacità di esaminare il problema e trovare un modo davvero migliore.
fncomp

19
Sei un genio. :)
Jonathan,

3
Penso che solo le scarpe coprirebbero questo
Mooing Duck

0,003 libbre sono 0,048 once, che è poco meno di 1/20 di oncia. Quindi, se una persona su sessanta persone sull'aereo stava approfittando della regola dello shampoo da tre once, potresti salvare la giornata semplicemente buttando via tutto quello shampoo.
Ryan Lundy,

43

Di seguito è riportata un'implementazione piuttosto semplice della soluzione semplice. Non penso che ci sia un modo più veloce che sia corretto al 100%.

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());
        }
    }
 }

Funziona riempiendo l'insieme di "persone morte" fino a quando non raggiunge la soglia. Una volta raggiunta la soglia, continuiamo a esaminare l'elenco dei passeggeri cercando di trovare quelli più pesanti della persona morta più leggera. Quando ne abbiamo trovato uno, li aggiungiamo all'elenco e quindi iniziamo a "Salvare" le persone più leggere dall'elenco fino a quando non possiamo più salvarle.

Nel peggiore dei casi, questo funzionerà più o meno come una specie di tutto l'elenco. Ma nel migliore dei casi (la "lista morta" è compilata correttamente con le prime X persone) si esibirà O(n).


1
Penso che devi aggiornare totalaccanto a continue; Altro, questa è la risposta che stavo per pubblicare. Soluzione super veloce
Mooing Duck

2
Questa è la risposta corretta, questa è la risposta più veloce, questa è anche la risposta con la più bassa complessità.
Xander Tulip,

Probabilmente potresti spremerlo un po 'di più memorizzando nella cache dead.begin () e riorganizzando un po' le cose per ridurre al minimo la ramificazione, che sui processori moderni è piuttosto lenta
Wug

dead.begin () è molto probabilmente un trival e sarebbe quasi sicuramente in linea con un semplice accesso ai dati. Ma sì, spostarsi tra alcuni degli if darebbe un po 'più di prestazioni riducendo i rami ... ma probabilmente con un grande costo per la leggibilità.
SoapBox

1
Questo è logicamente elegante e risponde a TUTTI i requisiti del PO, compreso non conoscere il numero di passeggeri in anticipo. Avendo trascorso gran parte degli ultimi 5 mesi lavorando con STL Maps & Sets, sono sicuro che l'uso estensivo degli iteratori utilizzati comprometterebbe le prestazioni. Basta popolare il set e quindi scorrere da destra a sinistra fino a quando la somma delle persone più pesanti è maggiore di 3.000. Un set di 1 milione di elementi, presentato in ordine casuale, verrà caricato a ~ 30 milioni / sec su core i5 || i7 3,4 Ghz. Iterazione almeno 100 volte più lenta. KISS vincerà qui.
user2548100,

32

Supponendo che tutti i passeggeri collaboreranno: utilizzare una rete di smistamento parallela . (vedi anche questo )

Ecco una dimostrazione dal vivo

Aggiornamento: video alternativo (vai all'1:00)

Chiedere a coppie di persone di confrontare lo scambio - non si può ottenere più velocemente di così.


1
Questo è ancora un tipo e sarà O (nlogn). Sicuramente puoi essere più veloce, come O (nlogk) in cui è stata fornita la soluzione k << n.
Adam,

1
@Adam: è un ordinamento parallelo. L'ordinamento ha un limite inferiore di O (nlog n) passi SEQUENZIALI. Tuttavia possono essere in parallelo, quindi la complessità temporale può essere molto più bassa. vedi ad esempio cs.umd.edu/~gasarch/ramsey/parasort.pdf
Lior Kogan

1
Bene, l'OP dice "Questo è un problema proxy per qualcosa che sto cercando di codificare in C ++". Quindi, anche se i passeggeri collaboreranno, non calcoleranno per te. È un'idea chiara, ma il presupposto di quel documento che si ottengono nprocessori non regge.
Adam,

@LiorKogan - il video dimostrativo dal vivo non è più disponibile su YouTube
Adelin l'

@Adelin: Grazie, video alternativo aggiunto
Lior Kogan,

21

@Blastfurnace era sulla buona strada. Si utilizza la selezione rapida in cui i perni sono soglie di peso. Ogni partizione suddivide un set di persone in set e restituisce il peso totale per ogni set di persone. Continui a rompere il secchio appropriato fino a quando i tuoi secchi corrispondenti alle persone con il peso più alto sono oltre 3000 libbre e il tuo secchio più basso che si trova in quel set ha 1 persona (cioè, non può essere ulteriormente suddiviso).

Questo algoritmo è ammortizzato nel tempo lineare, ma nel caso peggiore quadratico. Penso che sia l'unico algoritmo di tempo lineare .


Ecco una soluzione Python che illustra questo algoritmo:

#!/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)

Produzione:

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. Questa è un'idea interessante, anche se non sono sicuro che sia abbastanza lineare. A meno che non mi manchi qualcosa, devi scorrere gli articoli per calcolare il peso totale del secchio e devi ricalcolare il secchio alto (almeno parzialmente) ogni volta che dividi. Sarà comunque più veloce del mio approccio basato sull'heap nel caso generale, ma penso che tu stia sottovalutando la complessità.
Jim Mischel,

2
@Jim: dovrebbe avere la stessa complessità della selezione rapida . So che la descrizione su Wikipedia non è la migliore, ma la ragione per cui è tempo ammortizzato lineare è che ogni volta che fai una partizione, lavori solo con un lato della partizione. Non rigorosamente, immagina che ogni partizione divida l'insieme di persone in due. Quindi, il primo passo prende O (n), quindi O (n / 2), ecc. E, n + n / 2 + n / 4 + ... = 2n.
Neil G,

2
@Jim: Comunque, il tuo algoritmo ha il miglior tempo nel caso peggiore, mentre il mio ha il miglior tempo medio nel caso. Penso che siano entrambe buone soluzioni.
Neil G,

2
@JimMischel, NeilG: codepad.org/FAx6hbtc Ho verificato che tutti avessero gli stessi risultati e corretto Jim. FullSort: 1828 tick. JimMischel: 312 tick. SoapBox 109 tick. NeilG: 641 tick.
Mooing Duck,

2
@NeilG: codepad.org/0KmcsvwD Ho usato std :: partition per velocizzare l'implementazione del tuo algoritmo. stdsort: 1812 tick. FullHeap 312 tick. Soapbox / JimMichel: 109 tick, NeilG: 250 tick.
Mooing Duck,

11

Supponendo che, come i pesi delle persone, hai una buona idea di quali valori massimi e minimi saranno probabilmente utilizzati da un ordinamento radix per ordinarli in O (n). Quindi lavora semplicemente dall'estremità più pesante dell'elenco verso il più leggero. Tempo di esecuzione totale: O (n). Sfortunatamente, non c'è un'implementazione di un ordinamento radix nell'STL, ma è abbastanza semplice da scrivere.


Non userei un ordinamento radix generale, poiché non è necessario ordinare completamente l'elenco per ricavare la risposta.
Mooing Duck,

1
Per chiarire, una sorta di radix è una buona idea. Assicurati solo di scriverne uno personalizzato ottimizzato.
Mooing Duck,

1
@Mooing: è vero che non è necessario eseguire un ordinamento completo di Radix, ma al momento in cui l'ho pubblicato non c'erano algoritmi O (n) pubblicati e questo era facile da vedere. Penso che la risposta di Neil G sia la migliore ora che l'ha spiegata in modo più completo ed esplicitamente iniziato a usare la mediana come perno per la sua selezione. Ma usare un ordinamento radix standard è leggermente più semplice e ha meno probabilità di avere bug di implementazione sottili, quindi lascerò la mia risposta. Fare un ordinamento parziale parziale personalizzato sarebbe sicuramente più veloce, ma non asintoticamente.
Keith Irwin,

6

Perché non usi un quicksort parziale con una regola di interruzione diversa da "ordinata". È possibile eseguirlo e quindi utilizzare solo la metà superiore e andare avanti fino a quando il peso all'interno di questa metà superiore non contiene più il peso che deve essere almeno eliminato, quindi si torna indietro di un passo nella ricorsione e si ordina l'elenco. Dopodiché puoi iniziare a buttare via le persone dalla parte alta di quell'elenco ordinato.


Questo è il concetto alla base dell'algoritmo di Neil G, penso .
Mooing Duck,

questa è l'essenza di quickselect, che è ciò che Neil G sta usando.
Michael Donohue,

6

Ordinamento del torneo in modo massiccio parallelo: -

Supponendo un tre posti standard su ciascun lato del corridoio: -

  1. Chiedi ai passeggeri nel posto vicino al finestrino di spostarsi nel posto centrale se sono più pesanti della persona nel posto vicino al finestrino.

  2. Chiedere ai passeggeri nel sedile centrale di scambiare con il passeggero nel sedile del corridoio se sono più pesanti.

  3. Chiedere al passeggero nel sedile del corridoio sinistro di scambiare con il passeggero nel sedile del corridoio destro se sono più pesanti.

  4. Bubble ordina i passeggeri nel sedile del corridoio destro. (Prende n passaggi per n righe). - chiedere ai passeggeri nel posto di corridoio destro di scambiare con la persona di fronte n -1 volte.

5 Calciali fuori dalla porta fino a raggiungere i 3000 chili.

3 gradini + n gradini più 30 gradini se hai un carico passeggeri magro.

Per un piano a due navate - le istruzioni sono più complesse ma le prestazioni sono più o meno le stesse.


come la risposta di Lior Kogan, ma molti più dettagli.
Mooing Duck,

7
Una soluzione "abbastanza buona" sarebbe quella di offrire "hot dog gratuiti" e buttare via i primi quindici che hanno raggiunto il fronte. Non fornirà la soluzione ottimale ogni volta, ma verrà eseguito semplicemente "O".
James Anderson,

Non sarebbe meglio buttare via gli ultimi 15 poiché quelli più pesanti saranno probabilmente più lenti?
Peter,

@Patriker - Credo che l'obiettivo sia perdere 3000 chili con il numero minimo di persone. Sebbene sia possibile ottimizzare l'algoritmo modificando il passaggio 4 per "scambiare con la persona da n - 29 volte" che porterebbe i 30 porcini in primo piano, tuttavia, non in ordine di peso rigoroso.
James Anderson,

4

Probabilmente userei std::nth_elementper partizionare le 20 persone più pesanti in tempo lineare. Quindi utilizzare un metodo più complesso per trovare e sbattere fuori il più pesante dei pesanti.


3

È possibile effettuare un passaggio sull'elenco per ottenere la media e la deviazione standard, quindi utilizzarlo per approssimare il numero di persone che devono andare. Utilizzare partial_sort per generare l'elenco in base a quel numero. Se l'ipotesi era bassa, utilizzare di nuovo partial_sort sul resto con una nuova ipotesi.



2

Ecco una soluzione basata su heap che utilizza il modulo heapq integrato di Python. È in Python, quindi non risponde alla domanda originale, ma è più pulito (IMHO) rispetto all'altra soluzione Python pubblicata.

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)

Se k = il numero di passeggeri da lanciare e N = il numero di passeggeri, il caso migliore per questo algoritmo è O (N) e il caso peggiore per questo algoritmo è Nlog (N). Il caso peggiore si verifica se k è vicino a N per lungo tempo. Ecco un esempio del cast peggiore:

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

Tuttavia, in questo caso (buttando via le persone dall'aereo (con un paracadute, presumo)) allora k deve essere inferiore a 3000, che è << "milioni di persone". Il tempo di esecuzione medio dovrebbe quindi riguardare Nlog (k), che è lineare rispetto al numero di persone.

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.