飛行機があり、燃料が少ないとしましょう。飛行機が3000ポンドの乗客の体重を落とさない限り、次の空港に到達することはできません。最大数の命を救うために、私たちは最初に最も重い人々を飛行機から追い出したいです。
そして、そうそう、飛行機には何百万人もの人々がいます。そして、リスト全体をソートする必要はなく、最も重い乗客を見つけるための最適なアルゴリズムが欲しいです。
これは、C++でコーディングしようとしているもののプロキシの問題です。乗客マニフェストに対して重量で「partial_sort」を実行したいのですが、必要な要素の数がわかりません。独自の「partial_sort」アルゴリズム(「partial_sort_accumulate_until」)を実装できますが、標準のSTLを使用してこれを行う簡単な方法があるかどうか疑問に思っています。
1つの方法は、 最小ヒープ (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万人の乗客がおり、最軽量の重量が100ポンドであると仮定すると、_n log k
_はかなり小さい数になります。
乗客の体重をランダムに取得すると、パフォーマンスが大幅に向上します。推奨エンジンにこのようなものを使用します(数百万のリストから上位200のアイテムを選択します)。通常、実際にヒープに追加されるアイテムは50,000または70,000になります。
かなり似たようなものが表示されると思います。候補者の大部分は、すでにヒープにいる最軽量の人よりも軽いため拒否されます。 Peek
はO(1)
操作です。
ヒープ選択とクイック選択のパフォーマンスの詳細については、「 理論と実践が一致する場合 」を参照してください。ショートバージョン:アイテムの合計数の1%未満を選択している場合、ヒープ選択はクイック選択よりも明らかに勝者です。 1%を超える場合は、クイック選択または Introselect などのバリアントを使用します。
ただし、これはプロキシの問題には役立ちません。
1,000,000人の乗客が3000ポンドの重量を落とすには、各乗客が失う(3000/1000000)= 1人あたり0.003ポンドでなければなりません。それは、すべての人のシャツ、靴、またはおそらく爪の切り抜きを捨てることで達成でき、全員を救います。これは、飛行機がより多くの燃料を使用したために必要な重量損失が増加する前に、効率的な収集と投棄を前提としています。
実際には、彼らはもはや船上で爪切りを許可していませんので、それは出ています。
以下は、単純なソリューションのかなり単純な実装です。 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)
を実行します。
すべての乗客が協力すると仮定します:パラレルソートネットワークを使用します。 ( this も参照)
更新: 代替ビデオ (1:00にジャンプ)
ペアの人々に比較交換を依頼する-これより速くなることはできません。
@Blastfurnaceは正しい軌道に乗っていました。ピボットがウェイトしきい値である場合は、クイック選択を使用します。各パーティションは、1セットの人をセットに分割し、各セットの合計体重を返します。最も重い人に対応するバケットの重量が3000ポンドを超え、そのセットに含まれる最も低いバケットの人が1人になるまで(つまり、それ以上分割できなくなるまで)、適切なバケットを分割し続けます。
このアルゴリズムは線形時間で償却されますが、2次の最悪の場合です。 線形時間アルゴリズムだけだと思います。
このアルゴリズムを説明する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]
人々の体重のように、最大値と最小値が基数ソートを使用してO(n)でソートする可能性が高いことをよく理解していると仮定します。次に、リストの最も重い端から最も軽いものに向かって単純に作業します。合計実行時間:O(n)。残念ながら、STLには基数ソートの実装はありませんが、書くのは非常に簡単です。
超並列トーナメントソート:-
病気の両側に標準的な3つの座席があると仮定します:
窓側の席にいる人よりも重い場合は、窓側の席の乗客に中央の席に移動するように依頼します。
中央の座席の乗客に、通路の座席の乗客が重い場合は交換するように依頼します。
左側の通路の座席の乗客に、右側の通路の座席の乗客と交換するよう依頼します。
右側の通路の座席で乗客をバブルソートします。 (n行に対してnステップかかります)。 -右側の通路の座席の乗客に、前の人とn -1回交換するように依頼します。
5 3000ポンドに達するまでドアから追い出します。
乗客の負荷が非常に少ない場合は、3ステップ+ nステップ+ 30ステップ。
2通路の飛行機の場合、指示はより複雑ですが、パフォーマンスはほぼ同じです。
「ソート済み」とは異なる中止ルールで部分的なクイックソートを使用しないのはなぜですか。それを実行してから、上位半分のみを使用し、この上位半分内の重みに少なくとももう投げ捨てられるべき重みが含まれなくなるまで続けます。再帰の1ステップ戻り、リストをソートします。その後、ソートされたリストの上位から人を追い出し始めることができます。
おそらくstd::nth_element
最も重い20人を線形時間で分割します。次に、より複雑な方法を使用して、最も重い重いものを見つけ出します。
@Jamesはコメントに答えがあります: std::priority_queue
コンテナを使用できる場合、または std::make_heap
と std::pop_heap
(および std::Push_heap
)std::vector
のようなものを使用する場合。
リストを1回通過して平均と標準偏差を取得し、それを使用して行かなければならない人の数を概算できます。 partial_sortを使用して、その番号に基づいてリストを生成します。推測が低かった場合、新しい推測で残りの部分に対してpartial_sortを再度使用します。
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)である必要があり、これは人数に比例します。