簡単なインタビューの質問が難しくなりました:1..100の数値を指定した場合、正確にkが指定されている場合、不足している数値を見つけます


1146

久しぶりに面接の面白い体験をしました。質問は本当に簡単に始まりました:

Q1:私たちは、数字の入った袋を持って123、...、 100。各数値は1回だけ表示されるため、100個の数値があります。これで、バッグからランダムに1つの番号が選択されます。不足している番号を見つけます。

もちろん、このインタビューの質問は以前聞いたことがあるので、次のように非常にすばやく答えました。

A1:さて、数値の合計1 + 2 + 3 + … + N(N+1)(N/2)ウィキペディア:算術級数の合計を参照)です。の場合N = 100、合計は5050です。

したがって、バッグにすべての数値が存在する場合、合計は正確にになります5050。1つの数値が欠落しているため、合計はこれより少なくなり、違いはその数値です。したがって、O(N)時間とO(1)空間でその欠けている数を見つけることができます。

この時点で、私はうまくやったと思いましたが、突然、質問は予想外に変わりました。

Q2:それは正しいですが、2つの番号が欠落している場合、これをどのように実行しますか?

これまでにこの変化を見たり聞いたり考慮したことがなかったので、慌てて質問に答えることができませんでした。インタビュアーは私の思考プロセスを知ることを強く求めたので、予想される製品と比較することで、またはおそらく1回目のパスからいくつかの情報を収集した後に2回目のパスを実行するなどして、より多くの情報を得ることができると述べましたが、私は本当に撮影していました解決策への明確な道を実際に持っているのではなく、暗闇の中で。

インタビュアーは、2番目の方程式を持つことが実際に問題を解決する1つの方法であると言って、私を励まそうとしました。この時点で私はちょっと気が動転し(答えを事前に知らなかったため)、これが一般的な(「役に立つ」)プログラミング手法であるのか、それとも単なる裏技なのかを尋ねました。

インタビュアーの答えには驚かされました。3つの欠けている数字を見つける手法を一般化できます。実際、それを一般化してk個の欠損数を見つけることができます。

Qk:バッグから正確にk個の数値が欠落している場合、どのように効率的に見つけますか?

これは数か月前のことでしたが、このテクニックが何であるかはまだわかりませんでした。Ω(N)すべての数値を少なくとも1回スキャンする必要があるため、明らかに時間の下限がありますが、インタビュアーは、解法の時間スペースの複雑さ(O(N)時間入力スキャンを除く)はNではなくkで定義されると主張しました。

したがって、ここでの質問は簡単です。

  • Q2をどのように解決しますか?
  • Q3をどのように解決しますか?
  • Qkをどのように解決しますか?

明確化

  • 通常、1..100だけでなく、1 .. NからN個の数値があります。
  • 私は明らかなセットベースのソリューションを探していません。たとえば、ビットセットを使用して、指定されたビットの値で各数値の存在/不在をエンコードし、O(N)追加のスペースでビットを使用します。Nに比例する追加のスペースはありません。
  • また、明確なソートファーストのアプローチも探していません。これとセットベースのアプローチはインタビューで言及する価値があります(実装は簡単で、Nによっては非常に実用的です)。私はHoly Grailソリューションを探しています(実装するのが実際的であるかどうかはわかりませんが、それでも望ましい漸近的な特性があります)。

繰り返しますが、もちろん、で入力をスキャンする必要O(N)がありますが、キャプチャできるのは少量の情報(Nではなくkで定義)であり、kの欠損数を何らかの方法で見つける必要があります。


7
@polygenelubricants説明をありがとうございます。「O(N)時間とO(K)スペースを使用するアルゴリズムを探しています。ここで、Kは存在しない数のカウントです」は、最初から明らかだったでしょう;-)
Dave O.

7
あなたは正確に、順番に番号にアクセスすることができないというQ1の声明で、すべきです。これはおそらくあなたには明白に思えますが、私はその質問を聞いたことがなく、「バッグ」(「マルチセット」も意味する)という用語はちょっと混乱しました。
ジェレミー

7
ここでの回答はばかげているとして、以下をお読みください:stackoverflow.com/questions/4406110/...

18
無制限の整数のスペース要件をO(1)と見なさない限り、数値を合計するソリューションにはlog(N)スペースが必要です。しかし、無制限の整数を許可すると、1つの整数だけで必要なだけのスペースを確保できます。
Udo Klein

3
ちなみに、Q1の非常に優れた代替ソリューションは、XORから1までのすべての数値を計算nし、指定された配列のすべての数値で結果をXORすることです。結局、あなたはあなたの行方不明の数を持っています。このソリューションでは、合計のようにオーバーフローを気にする必要はありません。
スベリアコフ2015

回答:


590

Dimitris Andreouの リンクの概要は次のとおりです。

i = 1、2、..、kであるi乗の合計を思い出してください。これにより、連立方程式を解く問題が減少します。

a 1 + a 2 + ... + a k = b 1

a 1 2 + a 2 2 + ... + a k 2 = b 2

...

a 1 k + a 2 k + ... + a k k = b k

ニュートンのアイデンティティを使用して、b iを計算すると、

c 1 = a 1 + a 2 + ... a k

c 2 = a 1 a 2 + a 1 a 3 + ... + a k-1 a k

...

c k = a 1 a 2 ... a k

多項式(xa 1)...(xa k)を展開すると、係数は正確にc 1、...、c kになります。Vièteの公式を参照してください。すべての多項式因子は一意に(多項式の環はユークリッド領域なので、これはiが順列まで一意に決定されることを意味します。

これは、力を覚えることで数を回復するのに十分であるという証拠を終了します。定数kの場合、これは良い方法です。

kが変化したときしかし、コンピューティングの直接的なアプローチは、c 1、...、Cのkは例えば、C以来、prohibitely高価であり、kが不足しているすべての数字、大きさのn!/(NK)の積であります!これを克服するには、Z qフィールド計算を実行します。ここで、qはn <= q <2nであるような素数です。これはBertrandの仮定によって存在します。公式はまだ成り立っており、多項式の因数分解は依然として一意であるため、証明を変更する必要はありません。また、有限フィールドでの因数分解のアルゴリズム、たとえばBerlekampCantor-Zassenhausによるアルゴリズムも必要です。

定数kの高レベルの疑似コード:

  • 指定された数値のi乗を計算します
  • 減算して、不明な数のi乗を合計します。合計をb iと呼びます。
  • ニュートンのアイデンティティを使用してb iから係数を計算します。それらをc iと呼びます。基本的に、c 1 = b 1 ; c 2 =(c 1 b 1 -b 2)/ 2; 正確な式についてはウィキペディアを参照してください
  • 多項式x k -c 1 x k-1 + ... + c kを因数分解します。
  • 多項式の根は必要な数a 1、...、a kです。

kを変化させるには、Miller-Rabinなどを使用して素数n <= q <2nを見つけ、qを法としてすべての数を減らしてステップを実行します。

編集:この回答の以前のバージョンでは、qが素数であるZ qの代わりに、特性2の有限体(q = 2 ^(log n))を使用することが可能であると述べていました。ニュートンの公式はkまでの数による除算を必要とするため、これは当てはまりません。


6
素数フィールドを使用する必要はありませんq = 2^(log n)。も使用できます。(上付き文字と下付き文字はどのようにして作成したのですか?!)
ハインリッヒアフェルムス

49
+1これは本当に、本当に賢いです。同時に、それが本当に努力する価値があるのか​​、それともかなり人為的な問題に対するこのソリューション(の一部)が別の方法で再利用できるのかは疑問です。そして、これが現実の問題であったとしても、多くのプラットフォームでは、最も簡単なO(N^2)解決策は、おそらくこの美しさよりもかなり高い可能性がありNます。これを私に思い起こさせます:tinyurl.com/c8fwgwそれにもかかわらず、素晴らしい仕事です!私はすべての数学をクロールする忍耐力を持っていなかっただろう:)
back2dos

167
これは素晴らしい答えだと思います。これは、不足している数を1を超えて拡張することは、インタビューの質問のどれほど貧弱であることも示すと思います。最初のものも一種のゲッチャですが、基本的には「面接の準備をした」ことを示すのに十分一般的です。しかし、CS専攻がk = 1(特にインタビューで「その場で」)を超えることを知ることを期待することは、少しばかげています。
corsiKa

5
これは、入力に対してリードソロモンコーディングを効果的に実行しています。
David Ehrmann、2014年

78
私はすべての数値をaに入力hash setし、1...Nルックアップを使用してスイートを繰り返し処理して数値が欠落しているかどうかを判断します。kバリエーションに関して最も一般的で、平均して最速で、最もデバッグ可能で、保守性が高く、理解しやすいソリューションです。もちろん、数学の方法は印象的ですが、途中、数学者ではなくエンジニアになる必要があります。特にビジネスが関係しているとき。
v.oddou 2014

243

Muthukrishnan-Data Stream Algorithms:Puzzle 1:Finding Missing Numbersの2、3ページを読むとわかります。それはあなたが探している一般化を正確に示しています。おそらくこれがインタビュアーが読んだ内容であり、インタビュアーがこれらの質問をした理由です。

さて、もし人々がMuthukrishnanの扱いによって包含または取って代わられる答えを削除し始め、このテキストを見つけやすくするなら。:)


sdcvvcの直接関連する回答も参照してください。これには疑似コードも含まれています(ばか!これらのトリッキーな数学の公式を読む必要はありません:))(ありがとう、素晴らしい仕事!)


うーん...それは面白いです。私は数学に少し混乱したことを認めなければなりませんが、私はそれをスキミングしました。後で見るために、開いたままにしておく場合があります。:)そして、このリンクを見つけやすくするために+1します。;-)
Chris

2
Googleブックのリンクが機能しません。ここでより良いバージョン [PostScriptファイル]。
ハインリッヒアプフェルムス

9
ワオ。これが賛成されるとは思っていませんでした!前回は、私の代わりにそれを自分で解決しようとすると、それが実際にdownvotedた(Knuthの、その場合の)ソリューションへの参照を投稿:stackoverflow.com/questions/3060104/...私は喜ぶ司書の内部を、ありがとう:)
ディミトリスAndreouを

@Apfelmus、これはドラフトであることに注意してください。(もちろん、私はあなたのせいではありません。本を見つける前に、ドラフトをほぼ1年間本物と混同していました)。リンクが機能しない場合は、books.google.comにアクセスして「Muthukrishnanデータストリームアルゴリズム」を検索します(引用符なし)。これが最初に表示されます。
Dimitris Andreou 2010

2
ここでの回答はばかげているとして、以下をお読みください:stackoverflow.com/questions/4406110/...

174

数値自体と数値の2乗の両方を合計することにより、Q2を解くことができます。

その後、問題を次のように減らすことができます。

k1 + k2 = x
k1^2 + k2^2 = y

合計が期待値を下回る場所xy距離。

置換すると、次のようになります。

(x-k2)^2 + k2^2 = y

これを解決して、不足している数を特定できます。


7
+1; Mapleで選択した数値の数式を試しましたが、うまくいきます。しかし、なぜそれが機能するのか自分自身を納得させることはできませんでした。
polygenelubricants

4
@polygenelubricants:正しいことを証明したい場合は、最初に常に正しい解決策を提供することを示します(つまり、常にペアを生成し、セットから削除すると、残りのセットが観測された合計と平方和)。そこから、一意性の証明は、そのような数値のペアが1つだけ生成されることを示すのと同じくらい簡単です。
アノン。

5
方程式の性質は、その方程式からk2の2つの値を取得することを意味します。ただし、k1を生成するために使用する最初の方程式から、k2のこれらの2つの値はk1が他の値であることを意味するので、逆の方法で同じ数値である2つのソリューションがあります。k1> k2と任意に宣言した場合、2次方程式の解は1つだけなので、全体として1つの解になります。そして、明らかに質問の性質から、答えは常に存在するため、常に機能します。
クリス

3
与えられた合計k1 + k2に対して、多くのペアがあります。これらのペアは、K1 = a + bおよびK2 = abと書くことができます。ここで、a =(K1 + k2 / 2)です。aは特定の合計に対して一意です。二乗の合計(a + b)** 2 +(ab)** 2 = 2 *(a 2 + b 2)。与えられた合計K1 + K2の場合、a 2項は固定されており、b 2項のために平方和が一意になることがわかります。したがって、xとyの値は、整数のペアに対して一意です。
phkahler 2010

8
これは素晴らしいです。@ user3281743は例です。欠落している数(k1とk2)を4と6とします。Sum(1-> 10)= 55およびSum(1 ^ 2-> 10 ^ 2)= 385とします。ここでx = 55-(Sum(残りのすべての数) ))とy = 385-(Sum(残りのすべての数値の二乗))したがって、x = 10とy = 52になります。 2k ^ 2-20k + 48 = 0に簡略化します。2次方程式を解くと、4と6が答えになります。
AlexKoren 2015年

137

@j_random_hackerが指摘したように、これはO(n)時間とO(1)空間での重複の検索と非常に似ており、ここでも私の回答の適応が機能します。

「バッグ」が1から始まるA[]サイズの配列で表されると仮定すると、時間と追加の空間でN - kQkを解くことができます。O(N)O(k)

最初に、配列A[]k要素で拡張して、サイズを変更しNます。これはO(k)追加のスペースです。次に、次の疑似コードアルゴリズムを実行します。

for i := n - k + 1 to n
    A[i] := A[1]
end for

for i := 1 to n - k
    while A[A[i]] != A[i] 
        swap(A[i], A[A[i]])
    end while
end for

for i := 1 to n
    if A[i] != i then 
        print i
    end if
end for

最初のループは、k追加のエントリを配列の最初のエントリと同じように初期化します(これは、配列に既に存在していることがわかっている便利な値です-この手順の後、サイズの初期配列で欠落していたエントリN-kはすべて拡張配列にはまだありません)。

2番目のループは拡張された配列を並べ替え、要素xが少なくとも1回存在する場合、それらのエントリの1 つがの位置になるようにしますA[x]

ネストされたループがありますが、それでもO(N)時間内に実行されることに注意してください-スワップが発生するのは、iそのようなが存在する場合のみA[i] != iで、各スワップはのような要素を少なくとも1つ設定しA[i] == iます。これは、スワップの合計数(つまり、whileループ本体の実行の合計数)が最大でであることを意味しN-1ます。

3番目のループiは、値によって占有されていない配列のインデックスを出力します。iこれは、i欠落している必要があることを意味します。


4
なぜこの回答に賛成票を投じる人が少なく、正解としてマークしさえしなかったのでしょうか。Pythonのコードを次に示します。O(n)時間で実行され、追加のスペースO(k)が必要です。 pastebin.com/9jZqnTzV
wall-e

3
@cafこれは、ビットを設定し、ビットが0である場所を数えることと非常に似ています。整数配列を作成していると、より多くのメモリが占​​有されると思います。
フォックス

5
「ビットを設定し、ビットが0である場所をカウントするには、O(n)の余分なスペースが必要です。このソリューションは、O(k)の余分なスペースの使用方法を示しています。
ca

7
入力としてストリームを処理せず、入力配列を変更します(私はそれが非常に好きで、アイデアは実り多いですが)。
comco 2014年

3
@ v.oddou:いいえ、大丈夫です。スワップは変化A[i]します。つまり、次の反復では前の値と同じ2つの値が比較されません。new A[i]は最後のループのと同じになりますA[A[i]]が、new A[A[i]]新しい値になります。試してみてください。
カフェ2014

128

私は4歳の子供にこの問題を解決するように頼みました。彼は数字を並べ替えてから数えました。これにはO(キッチンフロア)のスペース要件があり、多くのボールが欠落しているのと同じくらい簡単に機能します。


20
;)あなたの4歳は5歳に近づいている必要があります。または天才です。私の4歳の娘は、まだ4まで数えることすらできません。公平を期すために、彼女が「4」の存在をようやく統合したとしましょう。そうでなければ今まではいつもそれをスキップしていました。「1、2、3、5、6、7」は彼女の通常のカウントシーケンスでした。私は彼女に鉛筆を一緒に追加するように頼みました、そして彼女はすべてをゼロから再び番号をはずすことによって1 + 2 = 3を管理するでしょう。私は実際に心配しています...: '(meh ..
v.oddou

シンプルでありながら効果的なアプローチ。
PabTorre

6
O(kitchen floor)はは-しかし、それはO(n ^ 2)ではないでしょうか?

13
O(m²)だと思います:)
Viktor Mellgren

1
@phuclv:回答には、「これにはO(キッチンフロア)のスペース要件があります」とありました。しかし、いずれの場合にも、これは並べ替えがインスタンスとすることができるで達成することがO(N)時間---参照この議論を
Anthony Labarre

36

それが最も効率的な解決策であるかどうかはわかりませんが、すべてのエントリをループし、ビットセットを使用して、どの数値が設定されているかを覚えてから、0ビットをテストします。

私は単純な解決策が好きです。合計や二乗和などを計算するよりも速いかもしれないと私は信じています。


11
私はこの明白な答えを提案しましたが、これはインタビュアーが望んだものではありません。これは私が探している答えではないことを質問で明確に述べました。別の明白な答え:最初にソートします。O(N)カウントソートもO(N log N)比較ソートもどちらも私が探しているものではありませんが、どちらも非常に単純なソリューションです。
ポリジェネルブリカント2010

@polygenelubricants:質問のどこに言ったかわかりません。ビットセットが結果であると考える場合、2番目のパスはありません。複雑さは(インタビュアーが言うように、Nが一定であると考える場合、複雑さは「Nではなくkで定義される」)O(1)であり、より「クリーンな」結果を作成する必要がある場合、 O(k)を取得します。これは、クリーンな結果を作成するために常にO(k)を必要とするため、取得できる最良の方法です。
Chris Lercher、2010

「私は明らかなセットベースのソリューションを探しているわけではないことに注意してください(たとえば、ビットセットを使用する」。元の質問の最後の2番目の段落
hrnt

9
@hmt:はい、質問は数分前に編集されました。私は、インタビュー対象者に期待するだろうと答えているだけです...次善の解決策を人工的に構築します(あなたが何をしても、O(n)+ O(k)時間に勝ることはできません)。 tは私にとって意味があります。ただし、O(n)の追加スペースを用意する余裕がない場合を除きますが、質問については明確ではありません。
Chris Lercher、2010

3
さらに明確にするために、質問をもう一度編集しました。私はフィードバック/回答に感謝します。
polygenelubricants

33

私は数学をチェックしていませんが、Σ(n^2)計算Σ(n)と同じパスで計算すると、2つの欠落した数値を取得するのに十分な情報が得られると思いΣ(n^3)ます。3つある場合も同様です。


15

数値の合計に基づくソリューションの問題は、指数が大きい数値を保存および処理するコストを考慮していないことです...実際には、非常に大きなnで機能するには、大きな数のライブラリが使用されます。 。これらのアルゴリズムのスペース使用率を分析できます。

sdcvvcとDimitris Andreouのアルゴリズムの時間と空間の複雑さを分析できます。

ストレージ:

l_j = ceil (log_2 (sum_{i=1}^n i^j))
l_j > log_2 n^j  (assuming n >= 0, k >= 0)
l_j > j log_2 n \in \Omega(j log n)

l_j < log_2 ((sum_{i=1}^n i)^j) + 1
l_j < j log_2 (n) + j log_2 (n + 1) - j log_2 (2) + 1
l_j < j log_2 n + j + c \in O(j log n)`

そう l_j \in \Theta(j log n)

使用されている総ストレージ: \sum_{j=1}^k l_j \in \Theta(k^2 log n)

使用容量:計算にa^j時間がかかると仮定するとceil(log_2 j)、合計時間:

t = k ceil(\sum_i=1^n log_2 (i)) = k ceil(log_2 (\prod_i=1^n (i)))
t > k log_2 (n^n + O(n^(n-1)))
t > k log_2 (n^n) = kn log_2 (n)  \in \Omega(kn log n)
t < k log_2 (\prod_i=1^n i^i) + 1
t < kn log_2 (n) + 1 \in O(kn log n)

使用された合計時間: \Theta(kn log n)

この時間とスペースに問題がなければ、単純な再帰アルゴリズムを使用できます。b!iをバッグのi番目のエントリ、nを削除前の数、kを削除の数とします。Haskell構文では...

let
  -- O(1)
  isInRange low high v = (v >= low) && (v <= high)
  -- O(n - k)
  countInRange low high = sum $ map (fromEnum . isInRange low high . (!)b) [1..(n-k)]
  findMissing l low high krange
    -- O(1) if there is nothing to find.
    | krange=0 = l
    -- O(1) if there is only one possibility.
    | low=high = low:l
    -- Otherwise total of O(knlog(n)) time
    | otherwise =
       let
         mid = (low + high) `div` 2
         klow = countInRange low mid
         khigh = krange - klow
       in
         findMissing (findMissing low mid klow) (mid + 1) high khigh
in
  findMising 1 (n - k) k

使用されるストレージ:O(k)リストO(log(n))用、スタック用:O(k + log(n)) このアルゴリズムはより直感的で、同じ時間の複雑さを持ち、使用するスペースが少なくなります。


1
+1、見栄えは良いですが、スニペット#1の4行目から5行目までを失ってしまいました。ありがとう!
j_random_hacker

isInRangeあるO(対数N)ではなくO(1) :これは、比較しなければならないので、それは、範囲内1..nの数を比較O(ログn)のビット。このエラーが残りの分析にどの程度影響するかはわかりません。
jcsahnwaldtによると、GoFundMonicaは2018年

14

ちょっと待って。質問のとおり、バッグには100個の数字があります。ループの最大100-k回の反復でセットを使用し、セットから数値を削除できるため、kがどんなに大きくても、問題は一定の時間で解決できます。100は一定です。残りの数のセットはあなたの答えです。

解を1からNまでの数値に一般化すると、Nが定数でないこと以外は何も変化しないため、O(N-k)= O(N)時間になります。たとえば、ビットセットを使用する場合、ビットをO(N)時間で1に設定し、数値を反復処理し、ビットを0に設定して(O(Nk)= O(N))、次に答えがあります。

面接官が最終セットの内容をO(N)時間ではなくO(k)時間で出力する方法を尋ねていたようです。明らかに、ビットが設定されている場合は、すべてのNビットを反復処理して、数値を出力するかどうかを決定する必要があります。ただし、セットの実装方法を変更すると、k回の反復で数値を出力できます。これは、ハッシュセットと二重にリンクされたリストの両方に格納されるオブジェクトに数値を入れることによって行われます。ハッシュセットからオブジェクトを削除すると、リストからも削除されます。回答はリストに残され、長さはkになります。


9
この答えは単純すぎます。単純な答えは機能しないことは誰もが知っています。;)しかし真剣に、元の質問はおそらくO(k)スペース要件を強調する必要があります。
DK。

問題は単純ではありませんが、マップにはO(n)の追加メモリを使用する必要があります。一定の時間と一定の記憶で私が解決したバストの問題
Mojo Risin

3
最小の解が少なくともO(N)であることを証明できると思います。少ないので、いくつかの番号でLOOKさえしなかったことを意味し、順序が指定されていないため、すべての番号を調べることは必須です。
v.oddou 2014

入力をストリームと見なし、nが大きすぎてメモリに保持できない場合、O(k)メモリ要件は意味があります。ただし、ハッシュは引き続き使用できます。k^ 2バケットを作成し、それぞれに単純な合計アルゴリズムを使用するだけです。これはk ^ 2のメモリにすぎず、成功の可能性を高めるためにいくつかのバケットを使用できます。
Thomas Ahle

8

2(および3)の不足している数の問題を解決するには、を変更しますquickselect。これO(n)は、パーティションがインプレースで行われる場合、平均で実行され、定数メモリを使用します。

  1. ランダムなピボットに関するセットを、ピボットより小さい数値を含むpパーティションlr、ピボットより大きい数値を含むパーティションに分割します。

  2. ピボット値を各パーティションのサイズと比較して、2つの欠落している数値がどのパーティションにあるかを判別します(p - 1 - count(l) = count of missing numbers in lおよび n - count(r) - p = count of missing numbers in r

  3. a)各パーティションに1つの数値が欠落している場合は、合計の差のアプローチを使用して、欠落している各数値を見つけます。

    (1 + 2 + ... + (p-1)) - sum(l) = missing #1 そして ((p+1) + (p+2) ... + n) - sum(r) = missing #2

    b)1つのパーティションに両方の番号がなく、パーティションが空の場合、不足している番号は、どちらの パーティションに番号がない(p-1,p-2)(p+1,p+2)によって異なります。

    1つのパーティションに2つの数値がないが空ではない場合、そのパーティションに再帰します。

欠落している数が2つしかないため、このアルゴリズムは常に少なくとも1つのパーティションを破棄するためO(n)、クイック選択の平均時間の複雑さは保持されます。同様に、3つの欠落した数値がある場合、このアルゴリズムは各パスで少なくとも1つのパーティションを破棄します(2つの欠落した数値と同様に、最大で1つのパーティションに複数の欠落した数値が含まれるため)。ただし、不足している数値がさらに追加されると、パフォーマンスがどの程度低下するかはわかりません。

インプレースパーティション分割を使用しない実装があるため、この例はスペース要件を満たしていませんが、アルゴリズムのステップを示しています。

<?php

  $list = range(1,100);
  unset($list[3]);
  unset($list[31]);

  findMissing($list,1,100);

  function findMissing($list, $min, $max) {
    if(empty($list)) {
      print_r(range($min, $max));
      return;
    }

    $l = $r = [];
    $pivot = array_pop($list);

    foreach($list as $number) {
      if($number < $pivot) {
        $l[] = $number;
      }
      else {
        $r[] = $number;
      }
    }

    if(count($l) == $pivot - $min - 1) {
      // only 1 missing number use difference of sums
      print array_sum(range($min, $pivot-1)) - array_sum($l) . "\n";
    }
    else if(count($l) < $pivot - $min) {
      // more than 1 missing number, recurse
      findMissing($l, $min, $pivot-1);
    }

    if(count($r) == $max - $pivot - 1) {
      // only 1 missing number use difference of sums
      print array_sum(range($pivot + 1, $max)) - array_sum($r) . "\n";
    } else if(count($r) < $max - $pivot) {
      // mroe than 1 missing number recurse
      findMissing($r, $pivot+1, $max);
    }
  }

デモ


セットの分割は、線形空間を使用するようなものです。少なくとも、ストリーミング設定では機能しません。
Thomas Ahle

@ThomasAhle en.wikipedia.org/wiki/Selection_algorithm#Space_complexityを参照してください。セットを所定の位置に分割するには、O(1)の追加スペースのみが必要であり、線形スペースは必要ありません。ストリーミング設定ではO(k)追加スペースになりますが、元の質問ではストリーミングについて言及していません。
FuzzyTree 2016

直接ではありませんが、通常はストリーミングの定義である「O(N)で入力をスキャンする必要がありますが、キャプチャできるのは少量の情報(kではなく、kで定義)のみです」です。サイズNの配列がない限り、パーティション分割のためにすべての数を移動することは実際には不可能です。質問に多くの回答があるだけで、この制約を無視しているようです。
Thomas Ahle

1
しかし、あなたが言うように、より多くの数が追加されるとパフォーマンスが低下する可能性がありますか?また、線形時間中央値アルゴリズムを使用して、常に完全なカットを取得することもできますが、kの数値が1、...、nにうまく分散している場合は、プルーニングする前に「深い」logkレベルを実行する必要はありません。枝は?
Thomas

2
最悪の場合の実行時間は確かにnlogkです。これは、入力全体をほとんどのlogk時間で処理する必要があるため、幾何学的シーケンス(多くてもn要素で始まるシーケンス)になるためです。単純な再帰を使用して実装すると、スペース要件がログに記録されますが、実際のクイック選択を実行して各パーティションの正しい長さを確保することにより、O(1)にすることができます。
emu

7

これは、kビットの追加ストレージを使用するソリューションです。巧妙なトリックはなく、単純明快です。実行時間O(n)、余分なスペースO(k)。最初に解決策を読んだり、天才であったりせずにこれが解決できることを証明するだけです:

void puzzle (int* data, int n, bool* extra, int k)
{
    // data contains n distinct numbers from 1 to n + k, extra provides
    // space for k extra bits. 

    // Rearrange the array so there are (even) even numbers at the start
    // and (odd) odd numbers at the end.
    int even = 0, odd = 0;
    while (even + odd < n)
    {
        if (data [even] % 2 == 0) ++even;
        else if (data [n - 1 - odd] % 2 == 1) ++odd;
        else { int tmp = data [even]; data [even] = data [n - 1 - odd]; 
               data [n - 1 - odd] = tmp; ++even; ++odd; }
    }

    // Erase the lowest bits of all numbers and set the extra bits to 0.
    for (int i = even; i < n; ++i) data [i] -= 1;
    for (int i = 0; i < k; ++i) extra [i] = false;

    // Set a bit for every number that is present
    for (int i = 0; i < n; ++i)
    {
        int tmp = data [i];
        tmp -= (tmp % 2);
        if (i >= even) ++tmp;
        if (tmp <= n) data [tmp - 1] += 1; else extra [tmp - n - 1] = true;
    }

    // Print out the missing ones
    for (int i = 1; i <= n; ++i)
        if (data [i - 1] % 2 == 0) printf ("Number %d is missing\n", i);
    for (int i = n + 1; i <= n + k; ++i)
        if (! extra [i - n - 1]) printf ("Number %d is missing\n", i);

    // Restore the lowest bits again.
    for (int i = 0; i < n; ++i) {
        if (i < even) { if (data [i] % 2 != 0) data [i] -= 1; }
        else { if (data [i] % 2 == 0) data [i] += 1; }
    }
}

よろしいです(data [n - 1 - odd] % 2 == 1) ++odd;か?
チャールズ

2
これがどのように機能するか説明できますか?分からない。
Teepeemm 2014

一時的なストレージに(n + k)ブール値の配列を使用できれば、解決策は非常に簡単になりますが、それは許可されていません。そこで、配列の最初に偶数を、最後に奇数を入れて、データを並べ替えます。これで、それらのn個の数値の最下位ビットを一時記憶に使用できます。これは、偶数と奇数の数がいくつあるかを知っており、最下位ビットを再構築できるためです。これらのnビットとk個の追加ビットは、まさに私が必要とした(n + k)ブール値です。
gnasher729 2014年

2
データが大きすぎてメモリに保持できず、ストリームとしてしか見なかった場合、これは機能しません。でも美味しくハッキー:)
Thomas Ahle

スペースの複雑さはO(1)になる可能性があります。最初のパスでは、「余分」を使用せずに、このアルゴリズムですべての数値<(n-k)を処理します。2番目のパスでは、パリティビットを再びクリアし、最初のkの位置をインデックス番号(nk)..(n)に使用します。
emu 2016年

5

すべての番号が存在するかどうかを確認できますか?はいの場合、これを試すことができます:

S =バッグ内のすべての数値の合計(S <5050)
Z =不足している数値の合計5050-S

欠番がある場合xy、その後:

x = Z-yおよび
max(x)= Z-1

だからあなたはから1までの範囲をチェックしmax(x)て数を見つけます


1
max(x)平均、ときx数は?
Thomas Ahle

2
彼はおそらく一連の数字の最大値を意味します
JavaHopper

2つ以上の数値がある場合、このソリューションは
無効に

4

このアルゴリズムは質問1で機能する可能性があります。

  1. 最初の100個の整数のxorを事前計算します(val = 1 ^ 2 ^ 3 ^ 4 .... 100)
  2. xor要素が入力ストリームから取得され続けるとき(val1 = val1 ^ next_input)
  3. 最終的な答え= val ^ val1

またはさらに良い:

def GetValue(A)
  val=0
  for i=1 to 100
    do
      val=val^i
    done
  for value in A:
    do
      val=val^value 
    done
  return val

このアルゴリズムは、実際には2つの欠落した数値に対して拡張できます。最初のステップは同じままです。2つの欠落した数値でGetValueを呼び出すと、結果はa1^a22つの欠落した数値になります。まあ言ってみれば

val = a1^a2

次に、valからa1とa2をふるいにかけるために、valの任意のセットビットを取得します。ithビットがvalに設定されているとしましょう。つまり、a1とa2はithビット位置で異なるパリティを持っています。次に、元の配列に対して別の反復を行い、2つのxor値を保持します。1つはi番目のビットが設定されている数値用で、もう1つはi番目のビットが設定されていない数値用です。これで、数値のバケットが2つa1 and a2あり、異なるバケットにあることが保証されています。次に、各バケットで欠落している要素を1つ見つけるために行ったのと同じことを繰り返します。


これはの問題を解決するだけk=1ですよね?しかし、私はxorオーバーサムを使用するのが好きです、それは少し速いようです。
Thomas Ahle

@ThomasAhleはい。私はそれを私の答えで呼びました。
bashrc

正しい。k = 2の場合、「2次」xorがどのようなものであるか考えていますか?合計に二乗を使用するのと同様に、xorに「二乗」できますか?
Thomas Ahle

1
@ThomasAhle 2つの欠落した数値で機能するように変更されました。
bashrc 2016年

これが私のお気に入りの方法です:)
ロバート・キング'25

3

両方のリストの合計と両方のリストの積があれば、Q2を解くことができます。

(l1はオリジナル、l2は変更されたリストです)

d = sum(l1) - sum(l2)
m = mul(l1) / mul(l2)

算術級数の合計は最初と最後の項の平均のn倍であるため、これを最適化できます。

n = len(l1)
d = (n/2)*(n+1) - sum(l2)

これで、次のことがわかりました(aとbが削除された数値の場合):

a + b = d
a * b = m

したがって、次のように再配置できます。

a = s - b
b * (s - b) = m

そして乗算する:

-b^2 + s*b = m

右側がゼロになるように再配置します。

-b^2 + s*b - m = 0

次に、2次式で解くことができます。

b = (-s + sqrt(s^2 - (4*-1*-m)))/-2
a = s - b

Python 3コードの例:

from functools import reduce
import operator
import math
x = list(range(1,21))
sx = (len(x)/2)*(len(x)+1)
x.remove(15)
x.remove(5)
mul = lambda l: reduce(operator.mul,l)
s = sx - sum(x)
m = mul(range(1,21)) / mul(x)
b = (-s + math.sqrt(s**2 - (-4*(-m))))/-2
a = s - b
print(a,b) #15,5

sqrt関数、reduce関数、sum関数の複雑さがわからないので、このソリューションの複雑さを計算できません(誰かが知っている場合は、以下にコメントしてください)。


計算にどのくらいの時間とメモリを使用しますx1*x2*x3*...か?
Thomas

@ThomasAhleリストの長さはO(n)-timeおよびO(1)-spaceですが、実際には(少なくともPythonでは)乗算はO(n ^ 1.6)-timeの長さなので数と数は、長さのO(log n)-spaceです。
Tuomas Laakkonen

@ThomasAhleいいえ、log(a ^ n)= n * log(a)なので、数値を格納するためのO(l log k)スペースがあります。したがって、長さlと元の長さkの数のリストが与えられた場合、O(l)-spaceがありますが、定数因数(log k)は、それらすべてを書き込むだけの場合よりも低くなります。(私の方法は、質問に答える特に良い方法だとは思いません。)
Tuomas Laakkonen

3

Q2の場合、これは他のソリューションより少し非効率的なソリューションですが、O(N)ランタイムがあり、O(k)スペースを取ります。

アイデアは、元のアルゴリズムを2回実行することです。最初の1つでは、不足している総数を取得します。これにより、不足している数の上限が得られます。この番号を呼び出しましょうN。欠落している2つの数値は合計してNになるため、最初の数値は区間内にのみ存在でき[1, floor((N-1)/2)]、2番目の数値は内に配置され[floor(N/2)+1,N-1]ます。

したがって、最初の間隔に含まれていないすべての数値を破棄して、もう一度すべての数値をループします。あるものは、あなたはそれらの合計を追跡します。最後に、欠落している2つの数値の1つがわかります。

この方法は一般化できると思います。入力に対する1回のパスで複数の検索が「並行して」実行される可能性がありますが、まだその方法はわかりません。


Ahahaはい、これはQ2で私が思いついたのと同じ解決策です。N/ 2未満のすべての数値のマイナスを取り、合計をもう一度計算するだけですが、これはさらに優れています!
xjcl

2

これは複雑な数学の方程式や理論がなくてもできると思います。以下は、インプレースおよびO(2n)時間の複雑性ソリューションの提案です。

入力フォームの仮定:

バッグ内の数の数= n

不足している数の数= k

バッグの数は、長さnの配列で表されます

アルゴの入力配列の長さ= n

配列内の欠落しているエントリー(バッグから取り出された数値)は、配列内の最初のエレメントの値で置き換えられます。

例えば。バッグは最初は[2,9,3,7,8,6,4,5,1,10]のように見えます。4を取り出すと、4の値は2(配列の最初の要素)になります。したがって、4を取り出すと、バッグは[2,9,3,7,8,6,2,5,1,10]のようになります。

このソリューションの重要な点は、配列がトラバースされるときに、そのINDEXの値を否定することにより、訪問した番号のINDEXにタグを付けることです。

    IEnumerable<int> GetMissingNumbers(int[] arrayOfNumbers)
    {
        List<int> missingNumbers = new List<int>();
        int arrayLength = arrayOfNumbers.Length;

        //First Pass
        for (int i = 0; i < arrayLength; i++)
        {
            int index = Math.Abs(arrayOfNumbers[i]) - 1;
            if (index > -1)
            {
                arrayOfNumbers[index] = Math.Abs(arrayOfNumbers[index]) * -1; //Marking the visited indexes
            }
        }

        //Second Pass to get missing numbers
        for (int i = 0; i < arrayLength; i++)
        {                
            //If this index is unvisited, means this is a missing number
            if (arrayOfNumbers[i] > 0)
            {
                missingNumbers.Add(i + 1);
            }
        }

        return missingNumbers;
    }

これはメモリを使いすぎます。
Thomas Ahle

2

このようなストリーミングアルゴリズムを一般化する一般的な方法があります。アイデアは、少しランダム化を使用して、うまくいけばk要素を独立したサブ問題に「拡散」し、元のアルゴリズムが問題を解決するというものです。この手法は、とりわけ、スパース信号の再構成に使用されます。

不足しているすべての数値が異なるバケットにハッシュされている場合、配列のゼロ以外の要素には不足している数値が含まれます。

特定のペアが同じバケットに送信される確率1/uは、ユニバーサルハッシュ関数の定義よりも低くなります。約k^2/2ペアがあるので、エラー確率は最大でk^2/2/u=1/2です。つまり、少なくとも50%の確率で成功し、uチャンスを増やします。

このアルゴリズムはk^2 lognビットのスペースを取ることに注意してください(logn配列バケットごとにビットが必要です)。これは、@ Dimitris Andreouの回答で必要なスペースと一致します(特に、多項式因数分解のスペース要件は偶然にもランダム化されます)。このアルゴリズムにも定数があります。kパワーサムの場合の時間ではなく、更新ごとの時間。

実際、コメントで説明されているトリックを使用することで、電力合計法よりもさらに効率的にすることができます。


注:マシンの方が速い場合xorは、ではなく、各バケットでを使用することもできsumます。
Thomas Ahle

興味深いですが、これはスペース制約を尊重していると思いますk <= sqrt(n)-少なくともu=k^2?k = 11とn = 100の場合、121のバケットがあり、アルゴリズムは、ストリームから各#を読み取るときにチェックする100ビットの配列を持つのと同様になります。増やすuと成功の可能性が高まりますが、スペースの制約を超える前に増やすことができる量には制限があります。
FuzzyTree 2016

1
問題は、nよりもはるかに大きい場合に最も理にかなっているkと思いますが、実際にk lognは一定の時間の更新を行いながら、説明したハッシュと非常に似た方法でスペースを削減できます。これは、総和法のようにgnunet.org/eppstein-set-reconciliationに記述されていますが、基本的には、テーブルハッシュのような強力なハッシュ関数を使用して「2つのk」バケットにハッシュします。 。デコードするには、そのバケットを識別し、別のバケツを解放し、その上でどの(おそらく)、そのバケットの両方から要素を削除
トーマスAhle

2

Q2の非常にシンプルなソリューションですが、まだ誰も答えていないことに驚いています。Q1の方法を使用して、欠落している2つの数値の合計を見つけます。それをSで表すと、欠落している数値の1つはS / 2より小さく、もう1つはS / 2より大きくなります(そうです)。1からS / 2までのすべての数値を合計し、それを(Q1のメソッドと同様に)式の結果と比較して、欠落している数値間の小さい方を見つけます。Sからそれを引いて、より大きな欠損数を見つけます。


これはスヴァローゼンの答えと同じだと思いますが、あなたはそれをより良い言葉で説明しました。Qkに一般化する方法はありますか?
John McClane

他の答えを逃してすみません。$ Q_k $に一般化できるかどうかはわかりませんが、その場合、不足している最小の要素を特定の範囲にバインドできません。一部の要素は$ S / k $よりも小さくなければならないことを知っていますが、それは複数の要素に当てはまる可能性があります
Gilad Deutsch

1

とてもいい問題です。Qkにはセット差を使用するつもりです。Rubyのように、多くのプログラミング言語でもサポートされています。

missing = (1..100).to_a - bag

これはおそらく最も効率的な解決策ではありませんが、この場合にこのようなタスク(既知の境界、低い境界)に直面した場合に実際に使用するソリューションです。数のセットが非常に大きい場合は、もちろん、より効率的なアルゴリズムを検討しますが、それまでは、単純なソリューションで十分です。


1
これはスペースを使いすぎます。
Thomas Ahle

@ThomasAhle:なぜ毎秒の回答に無駄なコメントを追加するのですか?スペースを使いすぎているとはどういう意味ですか?
DarkDust

「Nに比例する追加のスペースはありません。」このソリューションはまさにそれを行います。
Thomas Ahle

1

あなたは ブルームフィルター。バッグの各番号をブルームに挿入し、見つからないものを報告するまで完全な1-kセットを繰り返します。これは、すべてのシナリオで答えを見つけることができるわけではありませんが、十分な解決策となる場合があります。


削除を可能にするカウントブルームフィルターもあります。次に、すべての番号を追加して、ストリームに表示される番号を削除できます。
Thomas Ahle

これはおそらくもっと実用的な答えの1つですが、ほとんど注目されません。
ldog

1

私はその質問に対して別のアプローチをとり、面接担当者が彼が解決しようとしているより大きな問題の詳細について調査します。問題とそれを取り巻く要件によっては、明らかなセットベースのソリューションが適切な場合もあれば、リストの生成と選択後の生成のアプローチが適切でない場合もあります。

たとえば、インタビュアーがnメッセージをディスパッチし、を知る必要がある場合などです。その後、セットには欠落している要素のリストが含まれ、追加の処理を行う必要はありません。kであり、応答につながらなかったを知る必要があり、の後にできるだけ少ない実時間でそれを知る必要がある場合がありますn-k応答が到着した。また、メッセージチャネルの性質上、フルボアで実行している場合でも、最後の応答が到着してから最終結果が生成されるまでの時間に影響を与えずに、メッセージ間で処理を実行する十分な時間があるとしましょう。その時間を使用して、送信された各メッセージの識別ファセットをセットに挿入し、対応する応答が到着するたびにそれを削除することができます。最後の応答が到着したら、行われるべき唯一のことは、識別子をセットから削除することです。これは、一般的な実装では、O(log k+1)k

全体が実行されるため、これは確かに、事前に生成された数値のバッグをバッチ処理するための最速のアプローチではありませんO((log 1 + log 2 + ... + log n) + (log n + log n-1 + ... + log k))。しかし、それはk(事前にわかっていなくても)の任意の値で機能し、上記の例では、最も重要な間隔を最小化する方法で適用されました。


これは、O(k ^ 2)の追加メモリしかない場合に機能しますか?
Thomas Ahle

1

対称性(グループ、数学言語)の観点から考えることで、ソリューションに動機を与えることができます。一連の数値の順序に関係なく、答えは同じでなければなりません。k不足している要素を特定するのに役立つ関数を使用する場合は、どの関数にその特性があるかを検討する必要があります。対称です。関数s_1(x) = x_1 + x_2 + ... + x_nは対称関数の例ですが、より高次の関数もあります。特に、基本的な対称関数について考えます。2次の基本対称関数はs_2(x) = x_1 x_2 + x_1 x_3 + ... + x_1 x_n + x_2 x_3 + ... + x_(n-1) x_n、2つの要素のすべての積の合計です。同様に、3次以上の基本対称関数についても同様です。それらは明らかに対称です。さらに、それらはすべての対称関数のビルディングブロックであることがわかります。

それに注意することで、基本的な対称関数を作成することができますs_2(x,x_(n+1)) = s_2(x) + s_1(x)(x_(n+1))。さらに考えてみるとs_3(x,x_(n+1)) = s_3(x) + s_2(x)(x_(n+1))、それが納得できるようになり、1回のパスで計算できるようになります。

どの項目が配列から欠落していたかをどのように確認しますか?多項式について考えてください(z-x_1)(z-x_2)...(z-x_n)0いずれかの数値を入力した場合に評価されますx_i。多項式を展開すると、が得られz^n-s_1(x)z^(n-1)+ ... + (-1)^n s_nます。基本対称関数もここに表示されますが、根に順列を適用しても多項式は同じままであるため、これは本当に驚きではありません。

したがって、他の人が述べたように、多項式を作成して因数分解して、セットに含まれていない数値を特定することができます。

最後に、大量のメモリのオーバーフローが懸念される場合(n番目の対称多項式はの次数になります100!)、100より大きい素数mod ppあるこれらの計算を実行できます。その場合、多項式を評価し、mod p再び評価することがわかります0入力がセット内の数値である場合はtoになり、入力がセット内の数値でない場合はゼロ以外の値に評価されます。ただし、他の人が指摘したようにk、ではなくNに依存する多項式から値を取得するには、多項式を因数分解する必要がありmod pます。


1

さらに別の方法は、残差グラフフィルタリングを使用することです。

1から4までの数字があり、3が欠落しているとします。バイナリ表現は次のとおりです。

1 = 001b、2 = 010b、3 = 011b、4 = 100b

そして、次のようなフローグラフを作成できます。

                   1
             1 -------------> 1
             |                | 
      2      |     1          |
0 ---------> 1 ----------> 0  |
|                          |  |
|     1            1       |  |
0 ---------> 0 ----------> 0  |
             |                |
      1      |      1         |
1 ---------> 0 -------------> 1

フローグラフにはxノードが含まれ、xはビット数であることに注意してください。また、エッジの最大数は(2 * x)-2です。

したがって、32ビット整数の場合、O(32)スペースまたはO(1)スペースが必要になります。

1、2、4から始まる各数値の容量を削除すると、残差グラフが残ります。

0 ----------> 1 ---------> 1

最後に、次のようなループを実行します。

 result = []
 for x in range(1,n):
     exists_path_in_residual_graph(x)
     result.append(x)

これで、結果にはresult欠落していない数値も含まれます(誤検知)。ただし、欠落している要素がある場合、k <=(結果のサイズ)<= nk

与えられたリストを最後にもう一度見て、結果が欠落しているかどうかをマークします。

したがって、時間の複雑さはO(n)になります。

最後に、それはノードを取ることによって、偽陽性の数(および必要なスペース)を低減することが可能です00011110だけではなくの01


グラフ図がわかりません。ノード、エッジ、数値は何を表していますか?一部のエッジが方向付けられ、他のエッジが方向付けられないのはなぜですか?
dain

実際、私はあなたの答えをまったく理解していません。もう少し明確にしてもらえますか?
dain

1

O(k)の意味を明確にする必要があるでしょう。

これは、任意のkの自明な解決策です。数値セット内の各vに対して、2 ^ vの合計を累積します。最後に、iを1からNにループします。2^ iとのビットANDの合計がゼロの場合、iは欠落します。(または、数値で、合計の床を2 ^ iで割ったものが偶数の場合。またはsum modulo 2^(i+1)) < 2^i。)

簡単ですよね?O(N)時間、O(1)ストレージ、および任意のkをサポートします。

実際のコンピュータではそれぞれO(N)スペースを必要とする膨大な数を計算している場合を除きます。実際、このソリューションはビットベクトルと同じです。

そのため、賢く、合計と二乗の合計と立方体の合計を計算し、v ^ kの合計まで計算して、ファンシーな計算を行って結果を抽出することができます。しかし、それらも大きな数であり、疑問を投げかけています。私たちが話している操作の抽象的なモデルは何ですか?O(1)スペースにどれだけ収まるか、必要なサイズの数を合計するのにどのくらいの時間がかかりますか?


いい答えだ!1つの小さなこと:「2 ^ iを法とする和がゼロの場合、iが欠落している」は正しくありません。しかし、何が意図されているかは明らかです。「2 ^(i + 1)を法とする和が2 ^ iより小さい場合、iが欠落している」のは正しいと思います。(もちろん、ほとんどのプログラミング言語では、モジュロ計算の代わりにビットシフトを使用します。プログラミング言語は、通常の数学表記よりも表現力が高い場合があります。:
jcsahnwaldtは、GoFundMonicaの

1
ありがとう、あなたは完全に正しいです!修正されましたが、私は怠惰で数学の表記法からはずれていました...ああ、私もそれを台無しにしました。再度修正中...
2018年

1

これは、sdcvvc / Dimitris Andreouの回答のように複雑な数学に依存せず、cafやパネル大佐が行ったように入力配列を変更せず、Chris Lercher、JeremyP、および他の多くがしました。基本的に、私はQ2に関するSvalorzen / Gilad Deutchのアイデアから始め、それを一般的なケースQkに一般化し、Javaで実装して、アルゴリズムが機能することを証明しました。

アイデア

任意の間隔Iがあり、そこに少なくとも1つの欠落した数値が含まれていることがわかっていると仮定します。入力配列を1回通過した後、Iからの数値のみを見ると、Iから欠落している数値の合計Sと数量Qの両方を取得できます。私たちは、単に減算することによってこれを行うIの長たちから数に遭遇するたびにI(取得するためのQを)とのすべての数字の前に計算合計を減少させることによってIその遭遇数(取得するための各時間でSを)。

次にSQを見てみましょう。場合Q = 1、それはその後、ということを意味し、私は一つだけ欠番のが含まれており、この数は、明らかであるS。私はに終了済みのマークを付け(プログラムでは「あいまいでない」と呼ばれます)、それ以上の検討から除外します。一方、Q> 1の場合、Iに含まれる欠落した数値の平均A = S / Qを計算できます。すべての数値が異なるため、そのような数値の少なくとも1つはAよりも厳密に小さく、少なくとも1つはAよりも厳密に大きくなります。今私たちはAに分割しますそれぞれが少なくとも1つの欠落している数を含む2つの小さな間隔に。整数の場合、どの間隔にAを割り当てるかは問題ではないことに注意してください。

次の配列パスでは、間隔ごとにSQを個別に(ただし同じパスで)計算し、その後、マーク間隔をQ = 1に、分割間隔をQ> 1に設定します。新しい「あいまいな」区間がなくなるまでこのプロセスを続けます。つまり、各区間には正確に1つの欠落した数値が含まれるため、分割するものは何もありません(そしてSを知っているので、常にこの数値を知っています)。すべての可能な数値を含む唯一の「全範囲」の間隔から始めます(質問の[1..N]など)。

時間と空間の複雑さの分析

プロセスが停止するまでに必要なパスの総数pは、欠落数カウントkより大きくなることはありません。不等式p <= kは厳密に証明できます。一方、kの大きな値に役立つ経験的な上限p <log 2 N + 3もあります。入力配列が属する間隔を決定するために、入力配列の各番号に対してバイナリ検索を行う必要があります。これにより、時間の複雑さにlog k乗数が追加されます。

全体として、時間の複雑さはO(N᛫min(k、log N)᛫log k)です。大きなkの場合、これはO(N᛫k)であるsdcvvc / Dimitris Andreouの方法よりもはるかに優れていることに注意してください。

その作業のために、アルゴリズムは最大k間隔で格納するためにO(k)の追加スペースを必要とします。これは、「ビットセット」ソリューションのO(N)よりもはるかに優れています。

Java実装

上記のアルゴリズムを実装するJavaクラスを次に示します。常に、不足している数値の並べ替えられた配列を返します。その上、最初のパスで計算するため、欠落している数のカウントkは必要ありません。数値の全範囲はminNumberおよびmaxNumberパラメータによって指定されます(たとえば、質問の最初の例では1および100)。

public class MissingNumbers {
    private static class Interval {
        boolean ambiguous = true;
        final int begin;
        int quantity;
        long sum;

        Interval(int begin, int end) { // begin inclusive, end exclusive
            this.begin = begin;
            quantity = end - begin;
            sum = quantity * ((long)end - 1 + begin) / 2;
        }

        void exclude(int x) {
            quantity--;
            sum -= x;
        }
    }

    public static int[] find(int minNumber, int maxNumber, NumberBag inputBag) {
        Interval full = new Interval(minNumber, ++maxNumber);
        for (inputBag.startOver(); inputBag.hasNext();)
            full.exclude(inputBag.next());
        int missingCount = full.quantity;
        if (missingCount == 0)
            return new int[0];
        Interval[] intervals = new Interval[missingCount];
        intervals[0] = full;
        int[] dividers = new int[missingCount];
        dividers[0] = minNumber;
        int intervalCount = 1;
        while (true) {
            int oldCount = intervalCount;
            for (int i = 0; i < oldCount; i++) {
                Interval itv = intervals[i];
                if (itv.ambiguous)
                    if (itv.quantity == 1) // number inside itv uniquely identified
                        itv.ambiguous = false;
                    else
                        intervalCount++; // itv will be split into two intervals
            }
            if (oldCount == intervalCount)
                break;
            int newIndex = intervalCount - 1;
            int end = maxNumber;
            for (int oldIndex = oldCount - 1; oldIndex >= 0; oldIndex--) {
                // newIndex always >= oldIndex
                Interval itv = intervals[oldIndex];
                int begin = itv.begin;
                if (itv.ambiguous) {
                    // split interval itv
                    // use floorDiv instead of / because input numbers can be negative
                    int mean = (int)Math.floorDiv(itv.sum, itv.quantity) + 1;
                    intervals[newIndex--] = new Interval(mean, end);
                    intervals[newIndex--] = new Interval(begin, mean);
                } else
                    intervals[newIndex--] = itv;
                end = begin;
            }
            for (int i = 0; i < intervalCount; i++)
                dividers[i] = intervals[i].begin;
            for (inputBag.startOver(); inputBag.hasNext();) {
                int x = inputBag.next();
                // find the interval to which x belongs
                int i = java.util.Arrays.binarySearch(dividers, 0, intervalCount, x);
                if (i < 0)
                    i = -i - 2;
                Interval itv = intervals[i];
                if (itv.ambiguous)
                    itv.exclude(x);
            }
        }
        assert intervalCount == missingCount;
        for (int i = 0; i < intervalCount; i++)
            dividers[i] = (int)intervals[i].sum;
        return dividers;
    }
}

公平を期すために、このクラスはNumberBagオブジェクトの形式で入力を受け取ります。NumberBag配列の変更とランダムアクセスは許可されていません。また、配列が順次走査で要求された回数もカウントされます。またIterable<Integer>、プリミティブint値のボックス化を回避int[]し、テストの準備のために大きな値の一部をラップできるため、大規模な配列テストに適しています。所望であれば、交換することは困難ではないNumberBagことにより、int[]又はIterable<Integer>タイプfindforeachのものにするためのループ、それに2つを変更することにより、署名。

import java.util.*;

public abstract class NumberBag {
    private int passCount;

    public void startOver() {
        passCount++;
    }

    public final int getPassCount() {
        return passCount;
    }

    public abstract boolean hasNext();

    public abstract int next();

    // A lightweight version of Iterable<Integer> to avoid boxing of int
    public static NumberBag fromArray(int[] base, int fromIndex, int toIndex) {
        return new NumberBag() {
            int index = toIndex;

            public void startOver() {
                super.startOver();
                index = fromIndex;
            }

            public boolean hasNext() {
                return index < toIndex;
            }

            public int next() {
                if (index >= toIndex)
                    throw new NoSuchElementException();
                return base[index++];
            }
        };
    }

    public static NumberBag fromArray(int[] base) {
        return fromArray(base, 0, base.length);
    }

    public static NumberBag fromIterable(Iterable<Integer> base) {
        return new NumberBag() {
            Iterator<Integer> it;

            public void startOver() {
                super.startOver();
                it = base.iterator();
            }

            public boolean hasNext() {
                return it.hasNext();
            }

            public int next() {
                return it.next();
            }
        };
    }
}

テスト

これらのクラスの使用法を示す簡単な例を以下に示します。

import java.util.*;

public class SimpleTest {
    public static void main(String[] args) {
        int[] input = { 7, 1, 4, 9, 6, 2 };
        NumberBag bag = NumberBag.fromArray(input);
        int[] output = MissingNumbers.find(1, 10, bag);
        System.out.format("Input: %s%nMissing numbers: %s%nPass count: %d%n",
                Arrays.toString(input), Arrays.toString(output), bag.getPassCount());

        List<Integer> inputList = new ArrayList<>();
        for (int i = 0; i < 10; i++)
            inputList.add(2 * i);
        Collections.shuffle(inputList);
        bag = NumberBag.fromIterable(inputList);
        output = MissingNumbers.find(0, 19, bag);
        System.out.format("%nInput: %s%nMissing numbers: %s%nPass count: %d%n",
                inputList, Arrays.toString(output), bag.getPassCount());

        // Sieve of Eratosthenes
        final int MAXN = 1_000;
        List<Integer> nonPrimes = new ArrayList<>();
        nonPrimes.add(1);
        int[] primes;
        int lastPrimeIndex = 0;
        while (true) {
            primes = MissingNumbers.find(1, MAXN, NumberBag.fromIterable(nonPrimes));
            int p = primes[lastPrimeIndex]; // guaranteed to be prime
            int q = p;
            for (int i = lastPrimeIndex++; i < primes.length; i++) {
                q = primes[i]; // not necessarily prime
                int pq = p * q;
                if (pq > MAXN)
                    break;
                nonPrimes.add(pq);
            }
            if (q == p)
                break;
        }
        System.out.format("%nSieve of Eratosthenes. %d primes up to %d found:%n",
                primes.length, MAXN);
        for (int i = 0; i < primes.length; i++)
            System.out.format(" %4d%s", primes[i], (i % 10) < 9 ? "" : "\n");
    }
}

大規模アレイテストは次の方法で実行できます。

import java.util.*;

public class BatchTest {
    private static final Random rand = new Random();
    public static int MIN_NUMBER = 1;
    private final int minNumber = MIN_NUMBER;
    private final int numberCount;
    private final int[] numbers;
    private int missingCount;
    public long finderTime;

    public BatchTest(int numberCount) {
        this.numberCount = numberCount;
        numbers = new int[numberCount];
        for (int i = 0; i < numberCount; i++)
            numbers[i] = minNumber + i;
    }

    private int passBound() {
        int mBound = missingCount > 0 ? missingCount : 1;
        int nBound = 34 - Integer.numberOfLeadingZeros(numberCount - 1); // ceil(log_2(numberCount)) + 2
        return Math.min(mBound, nBound);
    }

    private void error(String cause) {
        throw new RuntimeException("Error on '" + missingCount + " from " + numberCount + "' test, " + cause);
    }

    // returns the number of times the input array was traversed in this test
    public int makeTest(int missingCount) {
        this.missingCount = missingCount;
        // numbers array is reused when numberCount stays the same,
        // just Fisher–Yates shuffle it for each test
        for (int i = numberCount - 1; i > 0; i--) {
            int j = rand.nextInt(i + 1);
            if (i != j) {
                int t = numbers[i];
                numbers[i] = numbers[j];
                numbers[j] = t;
            }
        }
        final int bagSize = numberCount - missingCount;
        NumberBag inputBag = NumberBag.fromArray(numbers, 0, bagSize);
        finderTime -= System.nanoTime();
        int[] found = MissingNumbers.find(minNumber, minNumber + numberCount - 1, inputBag);
        finderTime += System.nanoTime();
        if (inputBag.getPassCount() > passBound())
            error("too many passes (" + inputBag.getPassCount() + " while only " + passBound() + " allowed)");
        if (found.length != missingCount)
            error("wrong result length");
        int j = bagSize; // "missing" part beginning in numbers
        Arrays.sort(numbers, bagSize, numberCount);
        for (int i = 0; i < missingCount; i++)
            if (found[i] != numbers[j++])
                error("wrong result array, " + i + "-th element differs");
        return inputBag.getPassCount();
    }

    public static void strideCheck(int numberCount, int minMissing, int maxMissing, int step, int repeats) {
        BatchTest t = new BatchTest(numberCount);
        System.out.println("╠═══════════════════════╬═════════════════╬═════════════════╣");
        for (int missingCount = minMissing; missingCount <= maxMissing; missingCount += step) {
            int minPass = Integer.MAX_VALUE;
            int passSum = 0;
            int maxPass = 0;
            t.finderTime = 0;
            for (int j = 1; j <= repeats; j++) {
                int pCount = t.makeTest(missingCount);
                if (pCount < minPass)
                    minPass = pCount;
                passSum += pCount;
                if (pCount > maxPass)
                    maxPass = pCount;
            }
            System.out.format("║ %9d  %9d  ║  %2d  %5.2f  %2d  ║  %11.3f    ║%n", missingCount, numberCount, minPass,
                    (double)passSum / repeats, maxPass, t.finderTime * 1e-6 / repeats);
        }
    }

    public static void main(String[] args) {
        System.out.println("╔═══════════════════════╦═════════════════╦═════════════════╗");
        System.out.println("║      Number count     ║      Passes     ║  Average time   ║");
        System.out.println("║   missimg     total   ║  min  avg   max ║ per search (ms) ║");
        long time = System.nanoTime();
        strideCheck(100, 0, 100, 1, 20_000);
        strideCheck(100_000, 2, 99_998, 1_282, 15);
        MIN_NUMBER = -2_000_000_000;
        strideCheck(300_000_000, 1, 10, 1, 1);
        time = System.nanoTime() - time;
        System.out.println("╚═══════════════════════╩═════════════════╩═════════════════╝");
        System.out.format("%nSuccess. Total time: %.2f s.%n", time * 1e-9);
    }
}

イデオーネでお試しください


0

あなたが任意に大きな整数のと関数を利用できることを考えると、私はO(k)時間とO(log(k))空間のアルゴリズムを持っているfloor(x)と思いlog2(x)ます:

あなたは持っているkビット長整数(それゆえlog8(k)スペース)追加x^2:xはあなたが袋の中に見つける次の番号で、s=1^2+2^2+...これはかかるO(N)時間を(インタビュアーのための問題ではありませんこれを)。最後に、j=floor(log2(s))あなたが探している最大の数を取得します。次にs=s-j、上記をもう一度行います。

for (i = 0 ; i < k ; i++)
{
  j = floor(log2(s));
  missing[i] = j;
  s -= j;
}

さて、通常は2756-bit整数用のfloor関数とlog2関数はなく、double用です。そう?単純に、2バイト(または1、3、または4)ごとにこれらの関数を使用して目的の数値を取得できますが、これによりO(N)時間の複雑さが増します。


0

これは愚かに聞こえるかもしれませんが、最初に提示された問題では、バッグに残っているすべての数値を確認して、実際にそれらを足し合わせて、その方程式を使用して欠落している数値を見つける必要があります。

したがって、すべての数値を確認できるので、不足している数値を探してください。2つの数値が欠落している場合も同様です。かなりシンプルだと思います。バッグに残っている数を確認するときに、数式を使用しても意味がありません。


2
それらを合計することの利点は、すでに見た数値を覚えておく必要がないことです(たとえば、追加のメモリ要件はありません)。そうでない場合、唯一のオプションは、表示されたすべての値のセットを保持してから、そのセットを繰り返し処理して、欠落している値を見つけることです。
Dan Tao

3
この質問は通常、O(1)空間の複雑さの規定で尋ねられます。

最初のN個の数値の合計はN(N + 1)/ 2です。N = 100の場合、Sum = 100 *(101)/ 2 = 5050;
tmarthal 2011

0

これは次のように一般化できると思います:

算術級数と乗算の合計の初期値としてS、Mを示します。

S = 1 + 2 + 3 + 4 + ... n=(n+1)*n/2
M = 1 * 2 * 3 * 4 * .... * n 

これを計算するための公式について考えるべきですが、それは重要ではありません。とにかく、1つの番号が欠落している場合は、すでにソリューションを提供しています。ただし、2つの数値が不足している場合は、S1とM1で新しい合計と倍数の合計を示します。これは次のようになります。

S1 = S - (a + b)....................(1)

Where a and b are the missing numbers.

M1 = M - (a * b)....................(2)

S1、M1、M、Sを知っているので、上記の方程式は、欠落している数であるaとbを見つけるために解ける。

不足している3つの数値について:

S2 = S - ( a + b + c)....................(1)

Where a and b are the missing numbers.

M2 = M - (a * b * c)....................(2)

解ける方程式が2つあるだけで、未知数は3です。


ただし、乗算はかなり大きくなります。また、2つ以上の欠落した数値に一般化するにはどうすればよいですか。
Thomas

私は、N = 3で欠落している数値= {1、2}の非常に単純なシーケンスでこれらの式を試しました。エラーは式(2)にあるはずなので、私は働きませんでしたM1 = M / (a * b)その回答を参照しください)。その後、正常に動作します。
dma_k 2016

0

これが効率的かどうかはわかりませんが、この解決策を提案したいと思います。

  1. 100要素のxorを計算する
  2. 98個の要素のxorを計算します(2つの要素が削除された後)
  3. ここで(1の結果)XOR(2の結果)は、2つの欠落しているNOのXOR
    を提供します。式の差分を合計して、差分がdであるとしましょう。

次に、ループを実行して、可能なペア(p、q)を取得します。どちらも[1、100]にあり、合計をdにします。

ペアが取得されたら、(結果3)XOR p = qであるかどうかを確認し、そうである場合は完了です。

私が間違っている場合は修正してください。これが正しい場合は、時間の複雑さについてもコメントしてください。


2
sumとxorが2つの数値を一意に定義するとは思いません。ループを実行して、合計がdになるすべての可能なkタプルを取得するには、時間がかかりますO(C(n、k-1))= O(n <sup> k-1 </ sup>)。これは、k> 2の場合、悪い。
Teepeemm、2014

0

ほとんどの場合Q1とQ2O(log n)で実行できます。

私たちの仮定memory chipの配列で構成さnの数test tubes。そして、x試験管内の数字x milliliterは薬液で表されます。

プロセッサがであるとしlaser lightます。レーザーを照らすと、レーザーはその長さに垂直にすべてのチューブを通過します。薬液を通過するたびに、光度が低下し1ます。そして、特定のミリリットルマークで光を通過させることは、の操作ですO(1)

次に、試験管の真ん中にレーザーを当てて、光度の出力を取得すると

  • 事前に計算された値(数値が欠落していないときに計算された値)と等しい場合、不足している数値はより大きいですn/2
  • 出力が小さい場合は、少なくとも1つの欠落数値が未満ですn/21またはで明るさが低下していないか確認することもでき2ます。それまでに減少した場合、1欠落している数値の1つはより小さくn/2、もう1つはより大きいn/2。それまでに減少した2場合、両方の数値はより小さいですn/2

上記のプロセスを何度も繰り返して、問題のドメインを絞り込むことができます。各ステップで、ドメインを半分に小さくします。そして最後に、結果を得ることができます。

言及する価値のある並列アルゴリズム(興味深いので)、

  • いくつかの並列アルゴリズムによるソート、たとえば、並列マージはO(log^3 n)時間内に実行できます。そして、不足している番号は、O(log n)時間内のバイナリ検索によって見つけることができます。
  • 理論的には、nプロセッサがある場合、各プロセスは入力の1つをチェックし、番号を識別するいくつかのフラグを設定できます(便利なように配列で)。そして次のステップで、各プロセスは各フラグをチェックし、最後にフラグが付いていない番号を出力できます。プロセス全体にはO(1)時間がかかります。追加のO(n)スペース/メモリ要件があります。

上記の2つの並列アルゴリズムは、コメントに記載されているように追加のスペースが必要になる場合があることに注意してください。


test-tube-laserメソッドは本当に興味深いものですが、ハードウェアの指示にうまく対応できずO(logn)、コンピューター上にある可能性が非常に低いことに同意してください。
SirGuy 2017

1
あなたの並べ替え方法に関しては、それはに依存する余分なスペースを必要Nとし、O(N)時間よりも(への依存性の観点からN)、私たちはより良くしようとしています。
SirGuy 2017

@SirGuyテストチューブの概念と並列処理のメモリコストについてのあなたの懸念に感謝します。私の投稿は、問題についての私の考えを共有することです。GPUプロセッサは現在、並列処理を可能にしています。テストチューブのコンセプトが将来利用できなくなるかどうかは誰にもわかりません。
shuva 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.