将最胖的人从超载的飞机上摔下来。


200

假设您有一架飞机,而且燃油低。除非飞机掉落3000磅的乘客重量,否则它将无法到达下一个机场。为了最大程度地挽救生命,我们希望首先将最重的人员从飞机上赶下。

哦,是的,飞机上有数百万人,我们希望找到一种最佳算法来查找最重的乘客,而不必对整个列表进行排序。

这是我尝试用C ++编写代码的代理问题。我想按重量对旅客舱单进行“ partial_sort”,但我不知道我需要多少元素。我可以实现自己的“ partial_sort”算法(“ partial_sort_accumulate_until”),但是我想知道是否有使用标准STL进行此操作的简便方法。


5
如果用与人类类似的方法,您可以从甩掉重量超过X(例如120公斤)的人开始,因为这些人很可能是最胖的人。
RedX 2011年

132
所有乘客都会配合算法的任何步骤吗?
Lior Kogan

34
这样的话题就是为什么我喜欢IT。
马库斯

14
请问这是哪家航空公司?我想确保只在假期之前与他们一起飞行,而不是在过度沉迷自己之后。
jp2code 2011年

24
使用适当的设备(例如带内置秤的顶出座椅)不需要乘客配合。
吉姆·弗雷德

Answers:


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万人次,并且假设最轻的乘客体重为100磅,那么这个n log k数目就算是很小的了。

如果您随机获得乘客的体重,则性能会更好。我在推荐引擎中使用了类似的内容(我从几百万个列表中选择了前200个项目)。我通常最终只将50,000或70,000个项目实际添加到堆中。

我怀疑您会看到非常相似的东西:您的大多数候选人将被拒绝,因为他们比已有的最轻的人轻。并且Peek是一项O(1)操作。

有关堆选择和快速选择的性能的更多信息,请参阅理论与实践相结合。简短版本:如果您选择的项目少于项目总数的1%,那么堆选择显然是快速选择的赢家。超过1%,然后使用快速选择或类似Introselect的变体。


1
SoapBox发布了更快的答案。
Mooing Duck 2011年

7
就我的阅读而言,SoapBox的答案在道德上等同于Jim Mischel的答案。SoapBox用C ++编写了他的代码,因此他使用了std :: set,它与MinHeap的log(N)添加时间相同。
IvyMike 2011年

1
有一个线性时间解决方案。我将其添加。
尼尔·G

2
有一个用于最小堆的STL类:std::priority_queue
bdonlan

3
@MooingDuck:也许你误会了。我的代码创建一个空堆,就像SoapBox的代码创建一个空集一样。正如我所看到的,主要区别在于他的代码会在添加更高重量的项目时修剪掉多余的重量,而我的代码会保留多余的并在末尾对其进行修剪。当他在列表中移动时,他的布景可能会缩小,从而找到更多的人。达到重量阈值后,我的堆保持不变,在检查列表中的最后一项后,我对其进行修剪。
Jim Mischel '10

119

但是,这对您的代理问题没有帮助:

对于1,000,000名乘客,要减轻3000磅的重量,每位乘客必须每人减重(3000/1000000)= 0.003磅。这可以通过抛弃每件衬衫,鞋子甚至指甲剪裁来节省所有人来实现。假定在有效重量的收集和抛弃之前,随着飞机使用更多的燃料,所需的重量损失会增加。

实际上,他们再也不允许指甲钳在船上了,所以这就是事实。


14
喜欢解决问题并找到真正更好的方法的能力。
fncomp 2011年

19
你是个天才。:)
乔纳森(Jonathan)

3
我觉得鞋独自将涵盖这
鸣叫鸭

0.003磅等于0.048盎司,不到1/20盎司。因此,如果飞机上只有六十分之一的人在利用三盎司的洗发水规则,只需扔掉所有洗发水就可以节省一天的时间。
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 2011年

2
这是正确的答案,这是最快的答案,这也是复杂度最低的答案。
Xander Tulip

您可能可以通过缓存dead.begin()并通过重新布置一些东西以最小化分支来从中挤出更多的东西,这在现代处理器上是非常缓慢的
Wug 2012年

dead.begin()最有可能是琐事,几乎可以肯定地将其内联到仅数据访问中。但是,是的,通过减少分支数可以减少性能,但可能会增加可读性。
SoapBox 2012年

1
从逻辑上讲这是优雅的,并且可以满足OP的所有要求,包括不知道前面的乘客人数。尽管在过去5个月的大部分时间里都在使用STL Maps&Sets,但我确信广泛使用迭代器会削弱性能。只需填充集合,然后从右向左进行迭代,直到最重的人的总和超过3,000。一组以随机顺序显示的100万个元素在i5 || i7 3.4Ghz内核上的加载速度约为3000万/秒。迭代速度至少要慢100倍。吻会在这里赢。
user2548100 2013年

32

假设所有乘客都将配合:使用并行的分拣网络。(另见

这是现场表演

更新:备用视频(跳至1:00)

要求成对的人进行比较交换-您无法比这更快。


1
这仍然是一种排序,将为O(nlogn)。作为O(nlogk),您肯定会变得更快,其中提供了k << n,解。
亚当

1
@亚当:这是并行排序。排序的下限为O(nlog n)个SEQUENTIAL步骤。但是它们可以并行,因此时间复杂度可以低得多。参见例如cs.umd.edu/~gasarch/ramsey/parasort.pdf
Lior Kogan

1
好吧,OP说:“这是我要用C ++编写代码的代理问题。” 因此,即使乘客会合作,他们也不会为您计算。这是一个n很好的主意,但是该论文关于您拥有处理器的假设并不成立。
亚当

@LiorKogan -现场演示视频不再可用在YouTube上
Adelin

@Adelin:谢谢您,添加了其他视频
Lior Kogan

21

@高炉在正确的轨道上。您可以在枢轴为权重阈值的地方使用quickselect。每个分区将一组人分成几组,并返回每组人的总权重。您继续打断适当的水桶,直到与体重最重的人对应的水桶超过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。我知道Wikipedia上的描述不是最好的描述,但它是线性摊销时间的原因是,每次创建分区时,您只能使用分区的一侧。非严格地,想象每个分区将一组人分成两部分。然后,第一步取O(n),然后取O(n / 2),依此类推,然后取n + n / 2 + n / 4 + ... = 2n。
尼尔·G

2
@Jim:无论如何,您的算法具有最坏的情况下的时间,而我的算法具有最差的平均情况下的时间。我认为它们都是很好的解决方案。
尼尔·G

2
@ JimMischel,NeilG:codepad.org/FAx6hbtc 我验证了所有结果都相同,并更正了Jim的结果。FullSort:1828个滴答声。JimMischel:312个滴答声。SoapBox 109个滴答。NeilG:641个滴答声。
Mooing Duck

2
@NeilG:codepad.org/0KmcsvwD 我使用std :: partition使我的算法实现速度更快。stdsort:1812滴答声。FullHeap 312个滴答。Soapbox / JimMichel:109滴答,NeilG:250滴答。
Mooing Duck

11

假设像人们的体重一样,您最好知道使用基数排序将其最大值和最小值排序为O(n)的最大值和最小值。然后只需从列表中最重的一端到最轻的一端工作。总运行时间:O(n)。不幸的是,STL中没有基数排序的实现,但是编写起来非常简单。


我不会使用一般的基数排序,因为您不必对列表进行完全排序即可得出答案。
Mooing Duck 2011年

1
澄清一下,基数排序个好主意。只需确保编写一个定制的优化版本即可。
Mooing Duck

1
@Mooing:的确,您不必进行完整的基数排序,但是在我发布此内容时,还没有发布O(n)算法,这很容易理解。我认为Neil G的答案是他最好的答案,因为他已经对其进行了更全面的解释,并明确开始使用中位数作为选择的关键。但是使用标准基数排序稍微容易一些,并且不太可能出现细微的实现错误,因此我将保留答案。进行自定义的部分基数排序肯定会更快,但并非渐近。
基思·欧文

6

为什么不使用部分中止规则而不是“已排序”的中止规则呢?您可以运行它,然后只使用上半部分,然后继续操作,直到该上半部分中的权重不再包含至少已被抛弃的权重为止,这比您在递归中返回一步并对列表进行排序而言。之后,您可以开始将人们从该排序列表的高端排除在外。


认为这就是Neil G算法背后的基本概念。
Mooing Duck 2011年

这就是快速选择的本质,这就是尼尔·G在使用的东西。
Michael Donohue

6

大规模平行锦标赛排序:-

假设一个标准的三个位置在每个侧面:

  1. 要求靠窗座位的乘客比靠窗座位的人重。

  2. 请中间座位的乘客较重,与过道座位的乘客交换。

  3. 要求左过道座位​​上的乘客与右过道座位上的乘客交换他们较重的乘客。

  4. 气泡将乘客放在右过道座位上。(对n行采取n步)。-要求右过道座位上的乘客与前面的人交换n -1次。

5将它们踢出门,直到达到3000磅。

3步+ n步+ 30步(如果您真的很瘦)。

对于两个过道飞机-指令更复杂,但性能大致相同。


与Lior Kogan的答案相同,但细节更多。
Mooing Duck 2011年

7
一个“足够好”的解决方案将是提供“免费的热狗”,并丢弃到达前端的前十五个。不会每次都提供最佳解决方案,而是以纯“ O”运行。
詹姆斯·安德森

丢掉最后15个不是更好,因为较重的可能会更慢?
彼得

@Patriker-我相信目标是使最少的人数减少3000磅。尽管您可以通过将步骤4更改为“与n的人交换n次-29次”来优化算法,但这将使30个最靠前的人排在最前面,但是,按严格的顺序排列并非如此。
詹姆斯·安德森

4

我可能会std::nth_element习惯于在线性时间内划分20个最重的人。然后,使用更复杂的方法来查找和消除最重的重物。


3

您可以遍历列表以获取均值和标准差,然后使用它来估算必须去的人数。使用partial_sort根据该数字生成列表。如果猜测很低,请对其余部分再次使用partial_sort,并进行新的猜测。



2

这是使用Python内置的heapq模块的基于堆的解决方案。它在Python中,因此无法回答原始问题,但比其他发布的Python解决方案更干净(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),它与人数成线性关系。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.