오버로드 된 비행기에서 가장 뚱뚱한 사람들을 버림.


200

비행기가 있고 연료가 부족하다고 가정 해 봅시다. 비행기가 승객 무게 3000 파운드를 떨어 뜨리지 않으면 다음 공항에 도착할 수 없습니다. 최대한의 생명을 구하기 위해 가장 무거운 사람들을 먼저 비행기에서 내쫓고 싶습니다.

그렇습니다. 비행기에는 수백만 명의 사람들이 있으며, 전체 목록을 정렬하지 않고도 가장 많은 승객을 찾는 최적의 알고리즘을 원합니다.

이것은 C ++로 코딩하려는 것에 대한 프록시 문제입니다. 승객 매니페스트에 대해 무게 기준으로 "partial_sort"를 수행하고 싶지만 필요한 요소 수를 알 수 없습니다. 내 "partial_sort"알고리즘 ( "partial_sort_accumulate_until")을 구현할 수 있지만 표준 STL을 사용하여이 작업을 수행하는 더 쉬운 방법이 있는지 궁금합니다.


5
인간과의 비유라면 X보다 무게가 큰 사람들, 예를 들어 120kg을 버리는 것으로 시작할 수 있습니다.
RedX

132
모든 승객이 알고리즘의 모든 단계에 협력합니까?
Lior Kogan

34
이와 같은 주제는 제가 IT를 좋아하는 이유입니다.
Markus

14
어떤 항공사인지 물어볼 수 있습니까? 나는 휴가 시즌 전에 그들과 함께 날아가고 싶어요 .
jp2code

24
스케일이 내장 된 이젝터 시트와 같은 적절한 장비를 사용하여 승객의 협조가 필요하지 않습니다.
Jim Fred

답변:


102

한 가지 방법은 사용하는 것입니다 분 힙 ( std::priority_queueC ++에서)를. 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의 대답은 Jim Mischel의 대답과 도덕적으로 같습니다. SoapBox는 C ++로 코드를 작성 했으므로 std :: set을 사용하는데, 이는 MinHeap과 동일한 log (N) 추가 시간을 갖습니다.
IvyMike

1
선형 시간 솔루션이 있습니다. 추가하겠습니다.
Neil G

2
최소 힙에 대한 STL 클래스가 있습니다.std::priority_queue
bdonlan

3
@MooingDuck : 아마도 당신은 오해했을 것입니다. SoapBox의 코드가 빈 세트를 만드는 것처럼 내 코드는 빈 힙을 만듭니다. 내가 아는 바와 같이, 가장 큰 차이점은 그의 코드는 더 높은 무게의 아이템이 추가됨에 따라 초과 중량 세트를 잘라내는 반면, 광산은 초과 량을 유지하고 끝에 트림한다는 것입니다. 더 많은 사람들을 찾는 목록을 살펴보면 그의 세트가 잠재적으로 줄어 듭니다. 힙이 무게 임계 값에 도달 한 후에도 같은 크기를 유지하며 목록의 마지막 항목을 확인한 후 다듬습니다.
Jim Mischel

119

그러나 이것은 프록시 문제에 도움이되지 않습니다.

1,000,000 명의 승객이 3000 파운드의 무게를 떨어 뜨리려면 각 승객은 1 인당 (3000/1000000) = 0.003lbs를 잃어야합니다. 모든 셔츠 나 신발을 제압하거나 손톱을 잘라서 모든 사람을 구할 수 있습니다. 이것은 비행기가 더 많은 연료를 사용함에 따라 필요한 중량 손실이 증가하기 전에 효율적인 수집 및 분사를 가정합니다.

사실, 그들은 손톱깎이를 더 이상 탑승 할 수 없기 때문에 끝났습니다.


14
문제를 살펴보고 진정으로 더 나은 방법을 찾는 능력을 좋아하십시오.
fncomp

19
너는 천재 야. :)
Jonathan

3
나는 신발만으로도 이것을 덮을 것이라고 생각합니다.
Mooing Duck

0.003 lbs는 0.048 oz로 1/20 온스에 불과합니다. 비행기에있는 60 명 중 1 명이 3 온스 샴푸 규칙을 이용하고 있다면, 그 샴푸를 모두 버려서 하루를 절약 할 수 있습니다.
Ryan Lundy

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 23.38에

2
정답입니다. 가장 빠른 답입니다. 또한 복잡도가 가장 낮은 답이기도합니다.
Xander Tulip

dead.begin ()을 캐싱하고 분기를 최소화하기 위해 비트를 다시 정렬하여 최신 프로세서에서 속도가 느릴 수 있습니다.
Wug

dead.begin ()은 아마도 부족할 가능성이 높으며 거의 ​​확실하게 데이터 액세스에 인라인됩니다. 그러나 몇 가지 if를 이동하면 분기를 줄임으로써 성능이 약간 향상되지만 가독성이 높아질 것입니다.
SoapBox

1
이는 논리적으로 우아하며, 승객 수를 미리 모르는 등 OP의 모든 요구 사항을 해결합니다. 지난 5 개월 동안 STL Maps & Sets와 함께 일하면서 많은 반복기를 사용하면 성능이 저하 될 것이라고 확신합니다. 세트를 채우고 가장 무거운 사람들의 합이 3,000보다 클 때까지 오른쪽에서 왼쪽으로 반복하십시오. i5 || i7 3.4Ghz 코어에서 임의 순서로 표시되는 1 백만 개의 요소 집합이 초당 ~ 3 천만 속도로로드됩니다. 최소 100 배 느린 반복. KISS가 이길 것입니다.
user2548100

32

모든 승객이 협조한다고 가정하면 : 병렬 정렬 네트워크를 사용하십시오 . (참고 )

라이브 데모입니다

업데이트 : 대체 비디오 (1 시로 이동)

한 쌍의 사람들에게 비교 교환을 요청하면 이보다 더 빠를 수 없습니다.


1
이것은 여전히 ​​정렬이며 O (nlogn)입니다. k << n 인 솔루션이 제공되는 O (nlogk)로 확실히 더 빨라질 수 있습니다.
Adam

1
@ 아담 : 그것은 병렬 정렬입니다. 정렬에는 O (nlog n) SEQUENTIAL 단계의 하한이 있습니다. 그러나 병렬로 처리 할 수 ​​있으므로 시간 복잡성이 훨씬 낮아질 수 있습니다. 예를 참조 cs.umd.edu/~gasarch/ramsey/parasort.pdf
리 오르 년 Kogan에게

1
OP는 "이것은 C ++로 코딩하려는 무언가에 대한 프록시 문제"라고 말합니다. 따라서 승객이 협조하더라도 계산하지 않습니다. 깔끔한 아이디어이지만 n프로세서 를 얻는다 는 가정의 견해 는 없습니다.
Adam

@LiorKogan-라이브 데모 비디오는 더 이상 YouTube에서 사용할 수 없습니다
Adelin

@Adelin : 감사합니다. 대체 비디오가 추가되었습니다
Lior Kogan

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. 나는 그것이 선형 적이라고 확신하지는 않지만 흥미로운 아이디어입니다. 내가 빠진 것이 아니라면 버킷의 총 무게를 계산하기 위해 항목을 반복해야하며 분할 할 때마다 높은 버킷을 (적어도 부분적으로) 다시 계산해야합니다. 일반적인 경우 힙 기반 접근 방식보다 여전히 빠르지 만 복잡성을 과소 평가한다고 생각합니다.
Jim Mischel

2
@Jim : quickselect 와 같은 복잡성 이어야 합니다. 나는 위키 백과에 대한 설명이 최선이 아니라는 것을 알고 있지만 선형 상각 시간은 파티션을 할 때마다 파티션의 한 쪽면에서만 작업하기 때문입니다. 엄밀히 말하면, 각 파티션이 여러 사람들을 둘로 나눈다 고 상상해보십시오. 그런 다음 첫 번째 단계는 O (n), O (n / 2) 등, n + n / 2 + n / 4 + ... = 2n을 취합니다.
Neil G

2
@Jim : 어쨌든, 알고리즘은 최악의 경우 시간이 가장 좋은 반면 광산은 평균 경우가 가장 좋습니다. 둘 다 좋은 솔루션이라고 생각합니다.
Neil G

2
@ JimMischel, NeilG : codepad.org/FAx6hbtc 나는 모두 동일한 결과를 보았고 Jim의 것을 수정했습니다. 전체 정렬 : 1828 개의 틱. JimMischel : 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) 알고리즘이 게시되지 않았으므로 쉽게 볼 수 있습니다. 나는 Neil G의 대답은 그가 최고의 선택이라고 생각합니다. 이제는 자신의 선택의 중심으로 중앙값을 사용하여 더 자세하고 명확하게 설명하기 시작했습니다. 그러나 표준 기수 정렬을 사용하는 것은 약간 쉽고 구현 버그가 미묘 할 가능성이 적으므로 답을 남겨 두겠습니다. 커스터마이즈 된 부분 기수 정렬은 더 빠르지 만, 무증상은 아닙니다.
Keith Irwin

6

"정렬 된"과 다른 중단 규칙이있는 부분 빠른 정렬을 사용하지 않는 이유는 무엇입니까? 당신은 그것을 실행 한 다음 더 높은 반쪽을 사용 하고이 더 높은 반쪽의 무게가 재귀에서 한 단계 뒤로 가서 목록을 정렬하는 것보다 더 이상 버려야 할 무게를 포함하지 않을 때까지 계속 진행할 수 있습니다. 그 후 당신은 그 분류 된 목록의 최고급에서 사람들을 쫓아 내기 시작할 수 있습니다.


그건 내가 닐 G의 알고리즘 뒤에 기본 개념의 생각 .
Mooing Duck

이것이 바로 Neil G가 사용하는 quickselect의 본질입니다.
Michael Donohue

6

대규모 병렬 토너먼트 정렬 :-

ailse의 양쪽에 표준 3 개의 좌석이 있다고 가정하면 :-

  1. 창가 좌석에있는 승객보다 창가 좌석에있는 승객보다 무거운 경우 창가 좌석의 승객에게 중간 좌석으로 이동하도록 요청하십시오.

  2. 중간 좌석에있는 승객이 더 무겁다면 통로 좌석에있는 승객과 교환하도록 요청하십시오.

  3. 왼쪽 통로 좌석에있는 승객에게 오른쪽 통로 좌석 ID에있는 승객과 더 무거운 좌석을 교환하도록 요청하십시오.

  4. 오른쪽 통로 좌석에서 승객을 기포 정렬합니다. (n 개의 행에 대해 n 단계를 수행합니다). -오른쪽 통로 좌석에있는 승객에게 앞쪽에있는 사람과 1 번 교체하도록 요청하십시오.

5 3000 파운드에 도달 할 때까지 문을 걷어차십시오.

정말 마른 승객이있는 경우 3 단계 + n 단계 + 30 단계

2 개의 통로 평면의 경우 명령이 더 복잡하지만 성능은 거의 같습니다.


Lior Kogan의 답변과 동일하지만 훨씬 자세한 내용입니다.
Mooing Duck

7
"충분히 좋은"솔루션은 "무료 핫도그"를 제공하고 전선에 도달 한 처음 15 개를 버리는 것입니다. 매번 최적의 솔루션을 제공하지는 않지만 일반 "O"로 실행됩니다.
James Anderson

더 무거운 것이 아마 느릴 것이므로 마지막 15를 버리는 것이 좋지 않을까요?
피터

@Patriker-목표는 최소 인원으로 3000 파운드를 잃는 것입니다. 4 단계를 "n-29 번에서 사람과 교환"으로 변경하여 알고리즘을 최적화 할 수 있지만, 무게가 엄격하지는 않지만 30 돼지 고기를 가장 앞쪽으로 가져옵니다.
James Anderson

4

아마 std::nth_element20 명의 가장 무거운 사람들을 선형 시간으로 분할 하는 데 사용할 것입니다 . 그런 다음 더 복잡한 방법을 사용하여 가장 무거운 무거운 것을 찾아 내십시오.


3

평균과 표준 편차를 얻기 위해 목록을 한 번 통과 한 다음이를 사용하여 이동해야하는 사람의 수를 근사화 할 수 있습니다. partial_sort를 사용하여 해당 번호를 기반으로 목록을 생성하십시오. 추측이 낮 으면 나머지 추측에서 새로운 추측으로 partial_sort를 다시 사용하십시오.



2

다음은 Python의 내장 heapq 모듈을 사용하는 힙 기반 솔루션입니다. 파이썬으로되어 있으므로 원래 질문에 대답하지 않지만 게시 된 다른 파이썬 솔루션보다 깨끗합니다 (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 = 던질 승객 수와 N = 승객 수인 경우이 알고리즘의 가장 좋은 경우는 O (N)이고이 알고리즘의 가장 나쁜 경우는 Nlog (N)입니다. k가 N 근처에 오랫동안 있으면 최악의 경우가 발생합니다. 최악의 캐스트 예는 다음과 같습니다.

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

그러나이 경우 (비행기에서 낙하산을 던지는 사람들을 던짐) k는 3000보다 작아야합니다. << "수백만의 사람들"입니다. 따라서 평균 런타임은 Nlog (k) 정도 여야하며 이는 사람 수와 선형입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.