超载飞机上把最胖的人扔下去。

202

假设你有一架飞机,但油量很低。为了到达下一个机场,除非卸下 3000 磅的乘客重量,否则该飞机将无法到达。为了拯救尽可能多的生命,我们希望首先把最重的人扔出飞机。

还有,这架飞机上有数百万人,我们需要一个最优算法来找出最重的乘客,而不必对整个名单进行排序。

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


5
如果将类比用于人类,你可以开始淘汰那些体重超过X(例如120公斤)的人,因为这些人很可能是最胖的人之一。 - RedX
132
请问原文所在的上下文是什么?以便我能更好地理解并提供准确的翻译。 - Lior Kogan
34
这类话题是我喜爱信息技术的原因。 - Markus
14
这是哪家航空公司的?我想确保在假期季节之前,只和他们一起飞行,而不是等到我过度放纵之后再飞行。 - user153923
24
如有适当设备(例如带有内置秤的射出座椅),乘客合作并非必需。 - Jim Fred
显示剩余11条评论
12个回答

121

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

为了让100万名乘客减轻3000磅的重量,每个乘客必须失去(3000/1000000)= 0.003磅。这可以通过抛弃每个人的衬衫、鞋子,甚至指甲剪来实现,从而拯救每个人。这假设在飞机使用更多燃料之前有效地收集和抛弃所需减轻的重量。

实际上,他们不再允许携带指甲刀,所以这个方法行不通。


14
喜爱能够透过问题并找到更好的解决方式的能力。 - fncomp
3
我认为仅凭鞋子就足以应对这个。 - Mooing Duck
0.003磅是0.048盎司,仅略小于1/20盎司。因此,如果飞机上只有六十分之一的人在利用三盎司洗发水规定,您只需扔掉所有洗发水就可以拯救这一天。 - Ryan Lundy

103

一种方法是使用 最小堆(在 C++ 中可使用 std::priority_queue)。以下是使用 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的变体。

7
依照我的理解,SoapBox的答案在道德上相当于Jim Mischel的答案。SoapBox用C++编写了他的代码,因此他使用了std::set,这与MinHeap具有相同的log(N)添加时间。 - IvyMike
1
有一个线性时间的解决方案。我会添加它。 - Neil G
2
有一个用于最小堆的STL类:std::priority_queue - bdonlan
3
也许你误解了。我的代码创建了一个空堆,就像SoapBox的代码创建了一个空集合一样。我认为主要的区别在于他的代码在添加更高重量的物品时会修剪掉超额重量的部分,而我的代码在结束后才修剪它。当他通过找到更重的人并移动列表时,他的集合可能会减小。我的堆在达到重量阈值后保持相同大小,而我会在检查列表中最后一个项目后进行修剪。 - Jim Mischel
1
@NeilG:然后这个3000磅的家伙将被放在堆上,其中一个300磅的家伙将被移除。当我看到每个人时,堆中将包含九个300磅的家伙和一个大家伙。接下来会发生什么在代码描述之后。我想我应该用代码来写这个,以减少混淆。 - Jim Mischel
显示剩余11条评论

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
除此之外,我认为你必须在continue;旁边更新total。总之,这就是我要发布的答案。超级快速解决方案。 - Mooing Duck
2
这是正确的答案,也是最快的答案,同时也是复杂度最低的答案。 - Xander Tulip
你可能可以通过缓存dead.begin()并稍微重新排列一下来最小化分支,从而在现代处理器上提高一些性能。 - Wug
dead.begin() 很可能是微不足道的,几乎肯定会被内联为数据访问。但是,移动一些 if 语句可以通过减少分支来挤出更多性能…但这可能会大大降低可读性。 - SoapBox
1
这是逻辑上优雅的,并且满足了所有OP的要求,包括不事先知道乘客数量。虽然我在过去的5个月中大部分时间都在使用STL Maps&Sets,但我确信所使用的迭代器的广泛使用会降低性能。只需填充集合,然后从右到左迭代,直到最重的人的总和大于3,000为止。以随机顺序呈现的1百万个元素的集合,在i5 || i7 3.4Ghz核心上的加载速度约为每秒30百万次。迭代至少慢100倍。在这里,KISS将获胜。 - user2548100

31

1
这仍然是一种排序算法,时间复杂度为O(nlogn)。当然,你可以得到更快的算法,例如提供了一个时间复杂度为O(nlogk)的解决方案,其中k << n。 - Adam
2
@Adam:这是并行排序。排序的下限为O(nlog n) SEQUENTIAL步骤。但是它们可以并行,因此时间复杂度可以更低。例如,请参见http://www.cs.umd.edu/~gasarch/ramsey/parasort.pdf。 - Lior Kogan
1
好的,OP说:“这是我正在尝试用C++编写的某个代理问题。”所以即使乘客愿意合作,他们也不会为您计算。这是一个不错的想法,但该论文假设您获得了“n”个处理器,这一点并不成立。 - Adam

22

@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
这是一个有趣的想法,尽管我不确定它是否完全线性。除非我漏掉了什么,否则您必须迭代每个项目以计算桶的总重量,并且每次分裂时都必须重新计算高桶(至少部分)。在一般情况下,这仍将比基于堆的方法快,但我认为您低估了复杂性。 - 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: http://codepad.org/FAx6hbtc 我验证了所有的都有相同的结果,并纠正了Jim的问题。FullSort:1828个滴答声。JimMischel:312个滴答声。SoapBox 109个滴答声。NeilG:641个滴答声。 - Mooing Duck
2
@NeilG:http://codepad.org/0KmcsvwD 我使用了std::partition来使你的算法实现速度更快。stdsort:1812个滴答声。FullHeap 312个滴答声。Soapbox/JimMichel:109个滴答声,NeilG:250个滴答声。 - Mooing Duck
显示剩余25条评论

11

假设像人的体重一样,你对值的最大和最小值有一个很好的想法,使用基数排序在O(n)的时间内对它们进行排序。然后从列表的最重端向最轻的端逐步处理。总运行时间:O(n)。不幸的是,STL中没有基数排序的实现,但编写它相当简单。


我不会使用通用基数排序,因为您不必完全对列表进行排序才能得出答案。 - Mooing Duck
1
澄清一下,基数排序确实是个好主意。只要确保编写一个定制的优化版本。 - Mooing Duck
1
@Mooing:确实不需要完整的基数排序,但在我发布这篇文章时,还没有发布O(n)算法,而这是一个容易看到的算法。我认为Neil G的答案现在是最好的,因为他更充分地解释了它,并明确地开始使用中位数作为选择的枢轴。但是使用标准基数排序稍微容易一些,而且不太可能有微妙的实现错误,所以我会保留我的答案。进行定制的部分基数排序肯定会更快,但不是渐近的。 - Keith Irwin

6
为什么不使用一个部分快速排序,并采用一个不同于“已排序”方式的中止规则呢? 你可以运行它,然后只使用更高的一半,继续进行,直到该更高的一半内的权重不再包含必须被丢弃的权重,然后您回溯递归中的一步并对列表进行排序。在那个排序过的列表的末尾开始扔掉人员。

这是 Neil G 算法背后的基本概念,我认为。 - Mooing Duck
这就是快速选择算法的精髓,Neil G正在使用它。 - Michael Donohue

6

大规模并行锦标赛排序:

假设过道两侧都有三个座位:

  1. 如果靠窗的乘客比窗户座位上的人重,就请他们移到中间座位。

  2. 如果中间座位上的乘客更重,请让他们和过道座位上的乘客交换位置。

  3. 如果右边过道座位上的乘客更重,请让左边过道座位上的乘客换到右边去。

  4. 对右边过道座位上的乘客进行气泡排序(对于n排需要n步)。 -- 让右侧过道座位上的乘客与前面的乘客交换n-1次。

5 将乘客踢出飞机门,直到总重量达到3000磅。

3个步骤+ n个步骤,如果乘客负载非常少,则需要额外30个步骤。

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


与Lior Kogan的答案相同,但更详细。 - Mooing Duck
7
一个“足够好的”解决方案是提供“免费热狗”,并且扔掉达到最前面的前15个热狗。虽然不保证每次都提供最优解,但时间复杂度为“O(n)”。 - James Anderson
抛弃最后15个不是更好吗?因为较重的可能会更慢。 - Peter
@Patriker -- 我相信目标是用最少的人数减掉3000磅。虽然你可以通过将步骤4更改为“与前n-29个人交换位置”来优化算法,这将使最重的30个人排在最前面,但不是按体重严格排序。 - James Anderson

4

我会使用std::nth_element函数将20个最重的人在线性时间内分开。然后使用更复杂的方法找到并淘汰其中最重的人。


3

您可以对列表进行一次遍历,以获取平均值和标准偏差,然后使用它来估计需要前往的人数。使用partial_sort根据该数字生成列表。如果猜测过低,请在剩余部分上再次使用partial_sort并进行新的猜测。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接