Lanzar a las personas más gordas de un avión sobrecargado.

200

Digamos que tienes un avión y tiene poco combustible. A menos que el avión caiga 3000 libras de peso de pasajeros, no podrá llegar al próximo aeropuerto. Para salvar el máximo número de vidas, nos gustaría lanzar primero a las personas más pesadas del avión.

Y sí, hay millones de personas en el avión, y nos gustaría un algoritmo óptimo para encontrar a los pasajeros más pesados, sin necesariamente ordenar la lista completa.

Este es un problema de proxy para algo que estoy tratando de codificar en C ++. Me gustaría hacer una "clasificación parcial" en el manifiesto del pasajero por peso, pero no sé cuántos elementos voy a necesitar. Podría implementar mi propio algoritmo "partial_sort" ("partial_sort_accumulate_until"), pero me pregunto si hay alguna manera más fácil de hacerlo utilizando STL estándar.

IvyMike
fuente
55
Si la analogía con los humanos se mantiene, podría comenzar rechazando a las personas que pesan más de X, por ejemplo 120 kg, ya que es muy probable que se encuentren entre las personas más gordas.
RedX
132
¿Cooperarían todos los pasajeros con algún paso del algoritmo?
Lior Kogan
34
temas como este son por qué me encanta.
Markus
14
¿Puedo preguntar para qué aerolínea es esta? Quiero asegurarme de que solo vuelo con ellos antes de la temporada navideña, no después de que me haya entregado.
jp2code
24
No se requiere la cooperación del pasajero con el equipo adecuado (como asientos eyectores con escalas integradas).
Jim Fred

Respuestas:

102

Una forma sería usar un montón mínimo ( std::priority_queueen C ++). Así es como lo harías, suponiendo que tuvieras una MinHeapclase. (Sí, mi ejemplo está en C #. Creo que tienes la 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.

De acuerdo con las referencias estándar, el tiempo de funcionamiento debe ser proporcional a n log k, donde nes el número de pasajeros yk es el número máximo de artículos en el montón. Si suponemos que el peso de los pasajeros generalmente será de 100 libras o más, entonces es poco probable que el montón contenga más de 30 artículos en cualquier momento.

El peor de los casos sería si los pasajeros se presentan en orden de menor a mayor peso. Eso requeriría que cada pasajero se agregue al montón, y cada pasajero se elimine del montón. Aún así, con un millón de pasajeros y suponiendo que el más ligero pese 100 libras, eln log k resultado es un número razonablemente pequeño.

Si obtiene los pesos de los pasajeros al azar, el rendimiento es mucho mejor. Utilizo algo como esto para un motor de recomendación (selecciono los 200 elementos principales de una lista de varios millones). Por lo general, termino con solo 50,000 o 70,000 artículos realmente agregados al montón.

Sospecho que verás algo bastante similar: la mayoría de tus candidatos serán rechazados porque son más ligeros que la persona más ligera que ya está en el montón. Y Peekes unO(1) operación.

Para obtener más información sobre el rendimiento de la selección de montón y la selección rápida, consulte Cuando la teoría se encuentra con la práctica . Versión corta: si selecciona menos del 1% del número total de elementos, entonces la selección de montón es un claro ganador sobre la selección rápida. Más del 1%, luego use la selección rápida o una variante como Introselect .

Jim Mischel
fuente
1
SoapBox publicó la respuesta más rápida.
Mooing Duck
77
Para mi lectura, la respuesta de SoapBox es el equivalente moral de la respuesta de Jim Mischel. SoapBox escribió su código en C ++ y, por lo tanto, usa un std :: set, que tiene el mismo tiempo de adición de registro (N) que MinHeap.
IvyMike
1
Hay una solución de tiempo lineal. Lo agregaré
Neil G
2
Hay una clase STL para un montón mínimo:std::priority_queue
bdonlan
3
@MooingDuck: Quizás entendiste mal. Mi código crea un montón vacío, al igual que el código de SoapBox crea un conjunto vacío. La principal diferencia, según lo veo, es que su código recorta el conjunto de exceso de peso a medida que se agregan elementos de mayor peso, mientras que el mío mantiene el exceso y lo recorta al final. Su conjunto potencialmente disminuirá de tamaño a medida que avanza por la lista para encontrar personas más pesadas. Mi montón permanece del mismo tamaño después de que alcanza el umbral de peso, y lo recorto después de verificar el último elemento de la lista.
Jim Mischel
119

Sin embargo, esto no ayudará con su problema de proxy:

Para que 1,000,000 de pasajeros bajen 3000 libras de peso, cada pasajero debe perder (3000/1000000) = 0.003 lbs por persona. Eso podría lograrse al deshacerse de la camisa o los zapatos de todos, o probablemente incluso de recortes de uñas, salvando a todos. Esto supone una recolección y eliminación eficiente antes de que la pérdida de peso necesaria aumente a medida que el avión usa más combustible.

En realidad, ya no permiten el cortaúñas a bordo, así que eso está fuera.

aportr
fuente
14
Me encanta la capacidad de analizar el problema y encontrar una mejor manera.
fncomp
19
Eres un genio. :)
Jonathan
3
Creo que solo los zapatos cubrirían esto
Mooing Duck
0.003 lbs es 0.048 oz, que es un poco menos de 1/20 de onza. Entonces, si solo una de cada sesenta personas en el avión se aprovechara de la regla del champú de tres onzas, podría salvar el día simplemente tirando todo ese champú.
Ryan Lundy
43

A continuación se muestra una implementación bastante simple de la solución directa. No creo que haya una forma más rápida que sea 100% correcta.

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

Esto funciona al llenar el conjunto de "personas muertas" hasta que alcanza el umbral. Una vez que se alcanza el umbral, seguimos revisando la lista de pasajeros que intentan encontrar alguno que sea más pesado que la persona muerta más ligera. Cuando encontramos uno, los agregamos a la lista y luego comenzamos a "Guardar" a las personas más ligeras de la lista hasta que no podamos guardar más.

En el peor de los casos, esto funcionará casi igual que una especie de la lista completa. Pero en el mejor de los casos (la "lista muerta" se llena correctamente con las primeras X personas) funcionará O(n).

Plataforma improvisada
fuente
1
Creo que debe actualizar totaljunto a continue; Aparte de eso, esta es la respuesta que iba a publicar. Solución súper rápida
Mooing Duck
2
Esta es la respuesta correcta, esta es la respuesta más rápida, esta es también la respuesta con la menor complejidad.
Xander Tulip
Probablemente podría exprimir un poco más almacenando en caché dead.begin () y reorganizando un poco las cosas para minimizar la ramificación, que en los procesadores modernos es bastante lento
Wug
dead.begin () es muy probablemente trival y casi con certeza estaría en línea con solo un acceso a datos. Pero sí, moverse por algunos de los ifs generaría un poco más de rendimiento al reducir las ramas ... pero probablemente a un gran costo para la legibilidad.
SoapBox
1
Esto es lógicamente elegante y aborda TODOS los requisitos del OP, incluido no conocer el número de pasajeros por adelantado. Sin embargo, después de haber pasado gran parte de los últimos 5 meses trabajando con STL Maps & Sets, estoy seguro de que el uso extensivo de los iteradores utilizados paralizaría el rendimiento. Simplemente complete el conjunto y luego repita de derecha a izquierda hasta que la suma de las personas más pesadas sea mayor a 3,000. Un conjunto de 1 millón de elementos, presentado en orden aleatorio, se cargará a ~ 30 millones / seg en núcleos i5 || i7 3.4Ghz. Iteración al menos 100 veces más lenta. KISS ganará aquí.
user2548100
32

Asumiendo que todos los pasajeros cooperarán: use una red de clasificación paralela . (ver también esto )

Aquí hay una demostración en vivo

Actualización: video alternativo (saltar a 1:00)

Pidiendo pares de personas para comparar-intercambiar: no puede ser más rápido que esto.

Lior Kogan
fuente
1
Esto sigue siendo una especie y será O (nlogn). Ciertamente puede ser más rápido, como un O (nlogk) donde k << n, se ha proporcionado una solución.
Adam
1
@ Adam: es de forma paralela. La ordenación tiene un límite inferior de O (nlog n) pasos SECUENCIALES. Sin embargo, pueden ser paralelos, por lo que la complejidad del tiempo puede ser mucho menor. ver por ejemplo cs.umd.edu/~gasarch/ramsey/parasort.pdf
Lior Kogan
1
Bueno, el OP dice "Este es un problema de proxy para algo que estoy tratando de codificar en C ++". Entonces, incluso si los pasajeros cooperan, no computarán por usted. Es una buena idea, pero la suposición de ese documento de que obtienes nprocesadores no es válida.
Adam
@LiorKogan: el video de demostración en vivo ya no está disponible en YouTube
Adelin
@Adelin: Gracias, video alternativo agregado
Lior Kogan
21

@Blastfurnace estaba en el camino correcto. Utiliza la selección rápida donde los pivotes son umbrales de peso. Cada partición divide un conjunto de personas en conjuntos y devuelve el peso total de cada conjunto de personas. Continúa rompiendo el cubo apropiado hasta que sus cubos correspondientes a las personas de mayor peso pesen más de 3000 libras, y su cubo más bajo que está en ese conjunto tiene 1 persona (es decir, no se puede dividir más).

Este algoritmo es amortizado en tiempo lineal, pero en el peor de los casos cuadrático. Creo que es el único algoritmo de tiempo lineal .


Aquí hay una solución de Python que ilustra este 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)

Salida:

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]
Neil G
fuente
3
+1. Esta es una idea interesante, aunque no estoy seguro de que sea bastante lineal. A menos que me falte algo, debe iterar sobre los elementos para calcular el peso total del cubo, y debe volver a calcular el cubo alto (al menos parcialmente) cada vez que se divide. Todavía será más rápido que mi enfoque basado en el montón en el caso general, pero creo que estás subestimando la complejidad.
Jim Mischel
2
@ Jim: debe ser la misma complejidad que la selección rápida . Sé que la descripción en wikipedia no es la mejor, pero la razón por la que es tiempo amortizado lineal es que cada vez que haces una partición, trabajas con solo un lado de la partición. Sin rigor, imagine que cada partición divide el conjunto de personas en dos. Luego, el primer paso toma O (n), luego O (n / 2), etc. y, n + n / 2 + n / 4 + ... = 2n.
Neil G
2
@ Jim: De todos modos, su algoritmo tiene el mejor tiempo de peor caso, mientras que el mío tiene el mejor tiempo promedio de caso. Creo que ambas son buenas soluciones.
Neil G
2
@JimMischel, NeilG: codepad.org/FAx6hbtc Verifiqué que todos tienen los mismos resultados y corregí los de Jim. FullSort: 1828 garrapatas. JimMischel: 312 garrapatas. SoapBox 109 garrapatas. NeilG: 641 garrapatas.
Mooing Duck
2
@NeilG: codepad.org/0KmcsvwD Usé std :: partición para acelerar mi implementación de su algoritmo. stdsort: 1812 garrapatas. FullHeap 312 garrapatas. Soapbox / JimMichel: 109 ticks, NeilG: 250 ticks.
Mooing Duck
11

Suponiendo que, al igual que los pesos de las personas, tiene una buena idea de cuáles son los valores máximos y mínimos que se utilizarán en una clasificación de radix para ordenarlos en O (n). Luego, simplemente trabaje desde el extremo más pesado de la lista hacia el más ligero. Tiempo total de ejecución: O (n). Desafortunadamente, no hay una implementación de una clasificación de radix en el STL, pero es bastante sencillo de escribir.

Keith Irwin
fuente
Sin embargo, no usaría una clasificación general de radix, ya que no tiene que ordenar completamente la lista para obtener la respuesta.
Mooing Duck
1
Para aclarar, una clasificación de radix es una buena idea. Solo asegúrese de escribir uno personalizado optimizado.
Mooing Duck
1
@Mooing: Es cierto que no tiene que hacer una clasificación de radix completa, pero en el momento en que publiqué esto, no había algoritmos O (n) publicados y fue fácil de ver. Creo que la respuesta de Neil G es la mejor ahora que lo ha explicado de manera más completa y explícitamente comenzó a usar la mediana como eje de su selección. Pero usar una clasificación de radix estándar es un poco más fácil y es menos probable que tenga errores de implementación sutiles, por lo que dejaré mi respuesta. Hacer una ordenación parcial personalizada de radix definitivamente sería más rápido, pero no asintóticamente.
Keith Irwin
6

¿Por qué no utiliza un ordenamiento rápido parcial con una regla de cancelación diferente a "ordenada"? Puede ejecutarlo y luego usar solo la mitad superior y continuar hasta que el peso dentro de esta mitad superior ya no contenga el peso que al menos debe desecharse, luego retroceda un paso en la recursión y clasifique la lista. Después de eso, puede comenzar a echar a las personas del extremo superior de esa lista ordenada.

Sim
fuente
Ese es el concepto básico detrás del algoritmo de Neil G, creo .
Mooing Duck
esa es la esencia de la selección rápida, que es lo que usa Neil G.
Michael Donohue
6

Clasificación masiva de torneos paralelos: -

Asumiendo un estándar de tres asientos a cada lado del ailse: -

  1. Pida a los pasajeros en el asiento de la ventana que se muevan al asiento del medio si son más pesados ​​que la persona en el asiento de la ventana.

  2. Pida a los pasajeros en el asiento del medio que intercambien con el pasajero en el asiento del pasillo si son más pesados.

  3. Pídale al pasajero en el asiento del pasillo izquierdo que cambie con el pasajero en el asiento del pasillo derecho si son más pesados.

  4. Bubble clasifica a los pasajeros en el asiento del pasillo derecho. (Toma n pasos para n filas). - solicite a los pasajeros en el asiento del pasillo derecho que intercambien con la persona que está enfrente n -1 veces.

5 Sácalos de la puerta hasta que alcances las 3000 libras.

3 pasos + n pasos más 30 pasos si tiene una carga de pasajeros realmente delgada.

Para un avión de dos pasillos: las instrucciones son más complejas, pero el rendimiento es casi el mismo.

James Anderson
fuente
igual que la respuesta de Lior Kogan, pero con mucho más detalle.
Mooing Duck
77
Una solución "suficientemente buena" sería ofrecer "perritos calientes gratis" y tirar los primeros quince que llegaron al frente. No proporcionará la solución óptima cada vez, pero se ejecuta en "O".
James Anderson el
¿No sería mejor tirar los últimos 15 ya que los más pesados ​​probablemente serán más lentos?
Peter
@Patriker: creo que el objetivo es perder 3000 libras con el número mínimo de personas. Aunque podría optimizar el algoritmo cambiando el paso 4 para "intercambiar con la persona desde n - 29 veces", lo que llevaría a los 30 más carnosos al frente, sin embargo, no en un estricto orden de peso.
James Anderson
4

Probablemente usaría std::nth_elementpara dividir a las 20 personas más pesadas en tiempo lineal. Luego use un método más complejo para encontrar y eliminar el más pesado de los pesados.

Alto horno
fuente
3

Puede hacer un pase sobre la lista para obtener la media y la desviación estándar, luego usar eso para aproximar el número de personas que tienen que ir. Use partial_sort para generar la lista basada en ese número. Si la suposición fue baja, use parcial_sort nuevamente en el resto con una nueva suposición.

Mark Ransom
fuente
2

Aquí hay una solución basada en almacenamiento dinámico que utiliza el módulo integrado de almacenamiento dinámico de Python. Está en Python, así que no responde la pregunta original, pero es más limpio (en mi humilde opinión) que la otra solución publicada de Python.

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)

Si k = el número de pasajeros a lanzar y N = el número de pasajeros, entonces el mejor caso para este algoritmo es O (N) y el peor caso para este algoritmo es Nlog (N). El peor de los casos ocurre si k está cerca de N durante mucho tiempo. Aquí hay un ejemplo del peor elenco:

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

Sin embargo, en este caso (arrojar personas del avión (supongo que con un paracaídas)), entonces k debe ser inferior a 3000, que es << "millones de personas". Por lo tanto, el tiempo de ejecución promedio debe ser sobre Nlog (k), que es lineal para el número de personas.

Andrew Dalke
fuente