Сбрасывать самых толстых людей с перегруженного самолета.

200

Допустим, у вас есть самолет, и у него мало топлива. Если самолет не сбросит 3000 фунтов веса пассажира, он не сможет добраться до следующего аэропорта. Чтобы сохранить максимальное количество жизней, мы хотели бы сначала сбросить с самолета самых тяжелых людей.

И о, да, в самолете миллионы людей, и мы хотели бы найти оптимальный алгоритм поиска самых тяжелых пассажиров без необходимости сортировки всего списка.

Это проблема прокси для чего-то, что я пытаюсь кодировать на C ++. Я хотел бы сделать «part_sort» для пассажирского манифеста по весу, но я не знаю, сколько элементов мне понадобится. Я мог бы реализовать свой собственный алгоритм "component_sort" ("partal_sort_accumulate_until "), но мне интересно, есть ли какой-нибудь более простой способ сделать это, используя стандартный STL.

IvyMike
источник
5
Если провести аналогию с человеком, вы могли бы начать с того, что отбрасываете людей, которые весят больше X, например, 120 кг, поскольку они, скорее всего, будут одними из самых полных людей.
RedX
132
Будут ли все пассажиры сотрудничать с любым шагом алгоритма?
Лиор Коган
34
такие темы, вот почему я люблю это.
Маркус
14
Могу я спросить, для какой авиакомпании это? Я хочу быть уверенным, что буду летать с ними только перед началом сезона отпусков, а не после того, как перестану баловаться.
jp2code
24
Сотрудничество с пассажирами не требуется при наличии надлежащего оборудования (например, эжекторных сидений со встроенными весами).
Джим Фред

Ответы:

102

Один из способов - использовать минимальную кучу ( std::priority_queueв C ++). Вот как ты это сделаешь, если у тебя есть MinHeapкласс. (Да, мой пример на C #. Я думаю, вы поняли идею.)

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.

Согласно стандартным ссылкам, время выполнения должно быть пропорционально тому n log k, где nнаходится количество пассажиров и kмаксимальное количество элементов в куче. Если предположить, что вес пассажиров обычно составляет 100 фунтов или более, то маловероятно, что куча будет содержать более 30 предметов в любой момент.

В худшем случае пассажиры будут представлены в порядке от самого низкого веса до самого высокого. Для этого потребуется, чтобы каждый пассажир был добавлен в кучу, а каждый пассажир был удален из кучи. Тем не менее, имея миллион пассажиров и предполагая, что самый легкий весит 100 фунтов, n log kполучается достаточно мало.

Если вы получаете веса пассажиров случайным образом, производительность намного лучше. Я использую что-то вроде этого для механизма рекомендаций (я выбираю лучшие 200 пунктов из списка из нескольких миллионов). Я обычно получаю только 50 000 или 70 000 предметов, добавленных в кучу.

Я подозреваю, что вы увидите нечто очень похожее: большинство ваших кандидатов будут отклонены, потому что они легче, чем самый легкий человек, уже находящийся в куче. И Peekэто O(1)операция.

Для получения дополнительной информации о производительности динамического выбора и быстрого выбора см. Раздел «Когда теория встречается с практикой» . Краткая версия: если вы выбираете менее 1% от общего количества предметов, то выбор кучи - явный победитель быстрого выбора. Более 1%, затем используйте быстрый выбор или вариант типа Introselect .

Джим Мишель
источник
1
SoapBox разместил более быстрый ответ.
Mooing Duck
7
На мой взгляд, ответ SoapBox является моральным эквивалентом ответа Джима Мишеля. SoapBox написал свой код на C ++, и поэтому он использует std :: set, который имеет то же время добавления log (N), что и MinHeap.
IvyMike
1
Существует линейное решение по времени. Я добавлю это.
Нил Г
2
Есть класс STL для мини-кучи:std::priority_queue
bdonlan
3
@MooingDuck: Возможно, вы не поняли. Мой код создает пустую кучу, так же как код SoapBox создает пустой набор. Основное отличие, на мой взгляд, состоит в том, что его код обрезает набор избыточного веса при добавлении элементов с более высоким весом, тогда как мой код поддерживает избыточный вес и обрезает его в конце. Его набор потенциально уменьшится в размере, поскольку он перемещается по списку, находя более тяжелых людей. Моя куча остается того же размера после того, как она достигает порогового значения веса, и я урезаю ее после проверки последнего элемента в списке.
Джим Мишель
119

Это не поможет для вашей проблемы с прокси:

Чтобы 1 000 000 пассажиров сбросили 3000 фунтов веса, каждый пассажир должен потерять (3000/1000000) = 0,003 фунта на человека. Этого можно достичь, сбросив каждую рубашку, обувь или, возможно, даже вырезанные ногти, чтобы спасти всех. Это предполагает эффективный сбор и сброс до того, как потеря веса увеличилась, поскольку самолет использовал больше топлива.

На самом деле, они больше не допускают кусачки для ногтей на борту, так что это не так.

aportr
источник
14
Любите способность смотреть через проблему и находить действительно лучший путь.
Fncomp
19
Ты гений. :)
Джонатан
3
Я думаю, что только обувь будет покрывать это
Mooing Duck
0,003 фунта - это 0,048 унции, что чуть меньше 1/20 унции. Поэтому, если один из шестидесяти человек в самолете воспользовался правилом шампуня в три унции, вы могли бы спасти день, просто выбрасывая весь этот шампунь.
Райан Ланди
43

Ниже приведена довольно простая реализация простого решения. Я не думаю, что есть более быстрый способ, который на 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());
        }
    }
 }

Это работает, заполняя набор «мертвых людей», пока не достигнет порога. Как только порог достигнут, мы продолжаем просматривать список пассажиров, пытаясь найти тех, кто тяжелее самого легкого мертвого человека. Найдя его, мы добавляем его в список, а затем начинаем «Сохранять» самых легких людей из списка, пока мы не можем больше сохранять.

В худшем случае это будет работать примерно так же, как и весь список. Но в лучшем случае («мертвый список» правильно заполняется первыми X людьми) он будет работать O(n).

импровизированная трибуна
источник
1
Я думаю, что вы должны обновить totalрядом с « continue; Кроме этого», это ответ, который я собирался опубликовать. Супер быстрое решение
Mooing Duck
2
Это правильный ответ, это самый быстрый ответ, это также ответ с наименьшей сложностью.
Ксандер Тюльпан
Вы могли бы, вероятно, выжать немного больше из этого, кэшируя dead.begin () и немного переставляя вещи, чтобы минимизировать ветвление, которое на современных процессорах довольно медленное
Wug
dead.begin (), скорее всего, тривальный и почти наверняка будет привязан только к доступу к данным. Но да, перемещение нескольких if-ов увеличило бы производительность за счет уменьшения количества веток ... но, вероятно, с большими затратами на удобочитаемость.
SoapBox
1
Это логически элегантно и отвечает ВСЕМ требованиям ОП, включая незнание количества пассажиров заранее. Хотя я потратил большую часть последних 5 месяцев на работу с STL Maps & Sets, я уверен, что широкое использование итераторов снизит производительность. Просто заполните набор, а затем итерацию справа налево, пока сумма самых тяжелых людей не станет больше 3000. Набор из 1 миллиона элементов, представленный в случайном порядке, будет загружаться со скоростью ~ 30 миллионов в секунду на ядрах i5 || i7 3,4 ГГц. Итерация как минимум в 100 раз медленнее. ПОЦЕЛУЙ победит здесь.
user2548100
32

Предполагая, что все пассажиры будут сотрудничать: Используйте сеть параллельной сортировки . (см. также это )

Вот живая демонстрация

Обновление: альтернативное видео (переход к 1:00)

Запрашивать пары людей для сравнения-обмена - вы не можете получить быстрее, чем это.

Лиор Коган
источник
1
Это все еще своего рода и будет O (nlogn). Вы, конечно, можете получить быстрее, как O (nlogk), где k << n, решение было предоставлено.
Адам
1
@ Адам: это параллельная сортировка. Сортировка имеет нижнюю границу O (nlog n) ПОСЛЕДОВАТЕЛЬНЫХ шагов. Однако они могут быть параллельными, поэтому временная сложность может быть намного ниже. см., например, cs.umd.edu/~gasarch/ramsey/parasort.pdf
Лиор Коган
1
Что ж, OP говорит: «Это проблема с прокси-сервером для того, что я пытаюсь кодировать на C ++». Таким образом, даже если пассажиры будут сотрудничать, они не будут рассчитывать для вас. Это хорошая идея, но предположение о том, что вы получаете nпроцессоры, не соответствует действительности.
Адам
@LiorKogan - демо-видео в реальном времени больше не доступно на YouTube
Adelin
@Adelin: Спасибо, добавлено альтернативное видео
Лиор Коган,
21

@Blastfurnace был на правильном пути. Вы используете быстрый выбор, где стержни являются порогами веса. Каждый раздел разбивает один набор людей на наборы и возвращает общий вес для каждого набора людей. Вы продолжаете разбивать соответствующее ведро до тех пор, пока ваши ведра, соответствующие людям с наибольшим весом, не превысят 3000 фунтов, а ваше самое низкое ведро, которое находится в этом наборе, имеет 1 человека (то есть его нельзя разделить дальше)

Этот алгоритм амортизируется по линейному времени, но наихудший случай - квадратичный. Я думаю, что это единственный линейный алгоритм времени .


Вот решение Python, которое иллюстрирует этот алгоритм:

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

Вывод:

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. Это интересная идея, хотя я не уверен, что она довольно линейная. Если я что-то не упустил, вам придется перебирать элементы, чтобы вычислить общий вес сегмента, и вам придется пересчитывать высокий сегмент (по крайней мере, частично) каждый раз, когда вы разделяете. Это все равно будет быстрее, чем мой подход, основанный на куче, в общем случае, но я думаю, что вы недооцениваете сложность.
Джим Мишель
2
@Jim: он должен быть такой же сложности, как быстрый выбор . Я знаю, что описание в Википедии не самое лучшее, но причина того, что это линейное амортизированное время, состоит в том, что каждый раз, когда вы делаете раздел, вы работаете только с одной стороной раздела. Не строго говоря, представьте, что каждый раздел делит множество людей на две части. Затем на первом этапе выполняется O (n), затем O (n / 2) и т. Д., А также n + n / 2 + n / 4 + ... = 2n.
Нил Г
2
@Jim: Во всяком случае, ваш алгоритм имеет лучшее время наихудшего случая, в то время как мой алгоритм имеет лучшее среднее время. Я думаю, что они оба хорошие решения.
Нил Г
2
@JimMischel, NeilG: codepad.org/FAx6hbtc Я подтвердил, что у всех одинаковые результаты, и исправил Джима. Полная сортировка: 1828 тиков. ДжимМишель: 312 тиков. SoapBox 109 тиков. NeilG: 641 тик.
Mooing Duck
2
@NeilG: codepad.org/0KmcsvwD Я использовал std :: partition, чтобы ускорить реализацию вашего алгоритма. стандартная сортировка: 1812 тиков. FullHeap 312 тиков. Soapbox / JimMichel: 109 тиков, NeilG: 250 тиков.
Mooing Duck
11

Предполагая, что, подобно весам людей, у вас есть хорошее представление о том, какие максимальные и минимальные значения могут использоваться для сортировки по осям, чтобы отсортировать их по O (n). Тогда просто работайте от самого тяжелого конца списка к самому легкому. Общее время работы: O (n). К сожалению, в STL нет реализации радикальной сортировки, но написать ее довольно просто.

Кит Ирвин
источник
Я бы не стал использовать общую сортировку по основанию, поскольку вам не нужно полностью сортировать список, чтобы получить ответ.
Mooing Duck
1
Чтобы уточнить, десятичную рода является хорошей идеей. Только не забудьте написать индивидуальный оптимизированный.
Mooing Duck
1
@Mooing: Это правда, что вам не нужно делать полную сортировку по основанию, но в то время, когда я публиковал это, не было опубликовано ни одного O (n) алгоритма, и это было легко увидеть. Я думаю, что ответ Нила Джи - он лучший сейчас, когда он объяснил это более полно и явно начал использовать медиану в качестве основы для своего выбора. Но использование стандартной радикальной сортировки немного проще и с меньшей вероятностью приведет к незначительным ошибкам реализации, поэтому я оставлю свой ответ. Выполнение индивидуальной частичной сортировки по осям будет быстрее, но не асимптотически.
Кит Ирвин,
6

Почему бы вам не использовать частичную быструю сортировку с правилом прерывания, отличным от «отсортированного». Вы можете запустить его, а затем использовать только верхнюю половину и продолжать до тех пор, пока вес в этой верхней половине не будет содержать вес, который по крайней мере должен быть отброшен больше, чем вы вернетесь на один шаг в рекурсии и отсортируете список. После этого вы можете начать выбрасывать людей из верхней части этого отсортированного списка.

Сим
источник
Это основная концепция алгоритма Нила Дж, я думаю .
Mooing Duck
в этом суть быстрого выбора, который использует Нил Дж.
Майкл Донохью
6

Массивно Параллельный Турнир Сортировать: -

Предполагая три стандартных места на каждой стороне эйлса:

  1. Попросите пассажиров на сиденье у окна переместиться на среднее сиденье, если они тяжелее человека на сиденье у окна.

  2. Попросите пассажиров на среднем сиденье поменяться местами с пассажиром, если они тяжелее.

  3. Попросите пассажира на левом месте прохода поменяться с пассажиром на правом месте, если он тяжелее.

  4. Пузырьки рассортируют пассажиров в правом месте у прохода. (Делает n шагов для n строк). - попросите пассажиров в правом месте у прохода поменяться с лицом вперед n -1 раз.

5 Выбивайте их из двери, пока не наберете 3000 фунтов.

3 шага + n шагов плюс 30 шагов, если у вас действительно тощий пассажирский груз.

Для самолета с двумя проходами - инструкции более сложные, но производительность примерно одинакова.

Джеймс Андерсон
источник
такой же, как ответ Лиора Когана, но гораздо более подробно.
Mooing Duck
7
«Достаточно хорошим» решением было бы предложить «бесплатные хот-доги» и выбросить первые пятнадцать, которые достигли фронта. Не будет предлагать оптимальное решение каждый раз, но работает в простом «О».
Джеймс Андерсон
Разве не было бы лучше выбросить последние 15, так как более тяжелые, вероятно, будут медленнее?
Питер
@Patriker - я считаю, что цель - сбросить 3000 фунтов при минимальном количестве людей. Хотя вы могли бы оптимизировать алгоритм, изменив шаг 4, чтобы «поменяться с человеком из n - 29 раз», что получило бы 30 самых худых результатов, но не в строгом порядке веса.
Джеймс Андерсон
4

Я бы, вероятно, использовал std::nth_elementдля разделения 20 самых тяжелых людей за линейное время. Затем используйте более сложный метод, чтобы найти и отбить самые тяжелые из них.

доменный
источник
3

Вы можете сделать один проход по списку, чтобы получить среднее значение и стандартное отклонение, а затем использовать его для приблизительного числа людей, которые должны идти. Используйте part_sort для создания списка на основе этого числа. Если предположение было низким, снова используйте part_sort для остатка с новым предположением.

Марк Рэнсом
источник
2

Вот решение на основе кучи с использованием встроенного в Python модуля heapq. Он написан на Python, поэтому не отвечает на первоначальный вопрос, но он чище (IMHO), чем другое опубликованное решение 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)

Если k = количество пассажиров, которое нужно подбросить, а N = количество пассажиров, то лучшим вариантом для этого алгоритма является O (N), а наихудшим случаем для этого алгоритма является Nlog (N). Наихудший случай возникает, если k находится вблизи N в течение длительного времени. Вот пример худшего состава:

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

Однако в этом случае (выбрасывая людей из самолета (я полагаю, с парашютом)) тогда k должно быть меньше 3000, что составляет «миллионы людей». Следовательно, среднее время выполнения должно быть около Nlog (k), которое является линейным по отношению к количеству людей.

Эндрю Далке
источник