Викидання найпотужніших людей з перевантаженого літака.


200

Скажімо, у вас літак, і він мало пального. Якщо літак не скине 3000 фунтів ваги пасажирів, він не зможе дістатися до наступного аеропорту. Щоб врятувати максимальну кількість життів, ми хотіли б спочатку скинути найважчих людей із літака.

І о так, у літаку мільйони людей, і ми хотіли б оптимального алгоритму пошуку найважчих пасажирів, не обов'язково сортуючи весь список.

Це проблема проксі-сервера для чогось, що я намагаюся кодувати в C ++. Я хотів би зробити "частковий_сорт" на пасажирському маніфесті за вагою, але я не знаю, скільки елементів мені потрібно. Я міг би реалізувати свій власний алгоритм "частковий_ортаж" ("частковий_сорт_акумулювання_утиліти"), але мені цікаво, чи є простіший спосіб зробити це за допомогою стандартної STL.


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, який має той же часопис (N) додавання часу, що і MinHeap.
IvyMike

1
Існує лінійне часове рішення. Я додам його.
Ніл G

2
Існує клас STL для міні-купи:std::priority_queue
bdonlan

3
@MooingDuck: Можливо, ви неправильно зрозуміли. Мій код створює порожню купу так само, як код SoapBox створює порожній набір. Як я бачу, головна відмінність полягає в тому, що його код обробляє набір зайвої ваги в міру додавання предметів з більшою вагою, тоді як мій підтримує надлишки і обробляє його в кінці. Його набір потенційно зменшиться в розмірі, коли він рухатиметься за списком, де знаходяться важчі люди. Моя купа залишається однакового розміру після досягнення межі ваги, і я підрізаю її після перевірки останнього елемента в списку.
Джим Мішель

119

Однак це не допоможе для вашої проблеми з проксі-сервером:

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

Насправді вони більше не дозволяють стрибати нігті на ногах на борту, тож це не вдається.


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 () - це, швидше за все, тривалість і майже напевно буде націлений лише на доступ до даних. Але так, пересування декількох ifs витягнуло б трохи більше продуктивності, зменшивши гілки ... але, мабуть, великі витрати на читабельність.
SoapBox

1
Це логічно елегантно і відповідає ВСІМ вимогам ОП, включаючи невідоме число пасажирів на фронті. Провівши більшу частину останніх 5 місяців, працюючи з STL Maps & Sets, я впевнений, що широке використання ітераторів, які використовуються, призведе до каліцтву. Просто заповніть набір, а потім повторіть справа наліво, поки сума найважчих людей не перевищить 3000. Набір з 1 мільйона елементів, представлених у випадковому порядку, завантажиться на ~ 30 млн / сек на ядрах i5 || i7 3,4 ГГц. Ітерація принаймні 100X така повільна. Тут виграє KISS.
користувач2548100

32

Припускаючи, що всі пасажири будуть співпрацювати: Використовуйте паралельну мережу сортування . (див. також це )

Ось жива демонстрація

Оновлення: альтернативне відео (перейти до 1:00)

Попросити пар людей порівняти-обмінятися - ви не можете отримати швидше за це.


1
Це все ще різновид і буде O (nlogn). Ви, звичайно, можете отримати швидше, як O (nlogk), де k << n, рішення було надано.
Адам

1
@Adam: Це паралельний сорт. Сортування має нижню межу послідовних кроків O (nlog n). Однак вони можуть бути паралельними, тому складність у часі може бути значно нижчою. див., наприклад, cs.umd.edu/~gasarch/ramsey/parasort.pdf
Lior Kogan

1
Добре, що ОП каже: "Це проблема проксі-сервера для чогось, що я намагаюся кодувати в 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: Це має бути така ж складність, як і швидкий вибір . Я знаю, що опис у wikipedia не найкращий, але причина, що це лінійний амортизований час, полягає в тому, що кожен раз, коли ви робите розділ, ви працюєте лише з однією стороною розділу. Не суворо, уявіть, що кожна секція розділяє набір людей надвоє. Тоді перший крок бере O (n), потім O (n / 2) і т. Д. І, n + n / 2 + n / 4 + ... = 2n.
Ніл G

2
@Jim: У будь-якому випадку, ваш алгоритм має найкращий найгірший час, а мій - найкращий середній час. Я думаю, що вони обидва хороші рішення.
Ніл G

2
@JimMischel, NeilG: codepad.org/FAx6hbtc Я перевірив, що всі мають однакові результати, і виправив Джим. FullSort: 1828 кліщів. JimMischel: 312 кліщів. SoapBox 109 кліщів. NeilG: 641 кліща.
Mooing Duck

2
@NeilG: codepad.org/0KmcsvwD Я використовував std :: розділ для того, щоб швидше реалізувати ваш алгоритм. стдсорт: 1812 кліщів. FullHeap 312 кліщів. Мильниця / JimMichel: 109 кліщів, NeilG: 250 кліщів.
Mooing Duck

11

Якщо припустити, що, як і ваги людей, ви маєте гарне уявлення про те, які максимальні та мінімальні значення, ймовірно, використовуватимуть за допомогою радикального сортування для їх сортування в O (n). Тоді просто працюйте від найважчого кінця списку до найлегшого. Загальний час роботи: O (n). На жаль, в STL не існує реалізації свого типу radix, але це досить просто.


Я б не використовував загальний радіусний сорт, оскільки для отримання відповіді вам не доведеться повністю сортувати список.
Mooing Duck

1
Для уточнення хороша ідея - сорт радіації . Просто не забудьте написати індивідуальну оптимізовану.
Mooing Duck

1
@Mooing: Це правда, що вам не доведеться робити повне сортування радіусом, але в той час, коли я публікував це, не було розміщено жодних (N) алгоритмів, і це було легко побачити. Я думаю, що відповідь Ніла Г. - це найкращий зараз, коли він пояснив це більш повно і чітко почав використовувати медіану як опорний елемент для свого вибору. Але використовувати стандартний сортування radix трохи простіше і менше шансів виникнути непомітні помилки в реалізації, тому я збираюся залишити свою відповідь. Зробити індивідуальну часткову радіаційну подію, безумовно, було б швидше, але не асимптотично.
Кіт Ірвін

6

Чому б вам не скористатись частковим кваксортом із іншим правилом переривання, ніж "відсортовано". Ви можете запустити його, а потім використовувати лише верхню половину і продовжувати, поки вага всередині цієї вищої половини не містить ваги, яку потрібно принаймні викинути більше, ніж ви повернетесь на один крок у рекурсії та сортуєте список. Після цього ви можете почати викидати людей з найвищого кінця цього відсортованого списку.


Це основна концепція алгоритму Нілу G, я думаю .
Mooing Duck

у цьому полягає суть швидкого вибору, для чого використовується Ніл G.
Майкл Донохуе

6

Масово-паралельний турнір Сортування: -

Якщо припустити стандартні три місця з боків від ailse: -

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

  2. Попросіть пасажирів на середньому сидінні помінятися з пасажиром на сидінні на проході, якщо вони важчі.

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

  4. Бульбашка сортує пасажирів на правому сидінні проходу. (Робить n кроків для n рядків). - попросіть пасажирів, які перебувають на правильному сидінні проходу, обмінятись з людиною перед n -1 разів.

5 Вибивайте їх із дверей, поки не досягнете 3000 фунтів.

3 кроки + n кроків плюс 30 кроків, якщо у вас дійсно худий пасажирський вантаж.

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


те саме, що відповідь Ліора Когана, але набагато детальніше.
Mooing Duck

7
"Досить хорошим" рішенням було б запропонувати "безкоштовні хот-доги" та викинути перші п’ятнадцять, які досягли фронту. Не забезпечуватиме оптимальне рішення кожного разу, але працює в простому "О".
Джеймс Андерсон

Чи не було б краще викинути останні 15, оскільки важчі будуть, мабуть, повільнішими?
Пітер

@Patriker - Я вважаю, що мета - втратити 3000 фунтів за мінімальної кількості людей. Хоча ви могли б оптимізувати алгоритм, змінивши крок 4, щоб "поміняти особу на п - 29 разів", що дозволило б отримати 30 найвищих попереду, хоча не в строгому ваговому порядку.
Джеймс Андерсон

4

Можливо, я б скористався std::nth_elementподілом 20 найважчих людей за лінійний час. Тоді скористайтеся більш складним методом, щоб знайти і відбити найважчу з важких областей.


3

Ви можете зробити один прохід над списком, щоб отримати середнє та стандартне відхилення, а потім скористатися цим, щоб визначити кількість людей, які повинні пройти. Використовуйте частину_орти для створення списку на основі цього числа. Якщо здогад був низьким, знову використовуйте частину_sort з новою здогадкою.



2

Ось рішення на основі купи за допомогою вбудованого модуля heapq Python. Це в 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), що є лінійним щодо кількості людей.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.