web-dev-qa-db-ja.com

2D配列で最大のシーケンスを見つける

私は問題を練習していて、この奇妙な問題を見つけました。それを解決する最良の方法がわかりません。

1と0のみの2D行列サイズn * nが与えられ、列方向または行方向のいずれかで1の最も長いシーケンスを見つける必要があります。

私はこの問題にかなりこだわっています。分割統治手法を使用して配列を4 n/2 * n/2配列に分割しようとしましたが、隣接するすべての行/列をチェックする必要があるため、マージするステップはまだ非常に遅くなります。迅速な解決策についてのアドバイスは素晴らしいでしょう。私はこれを見つめるのにあまりにも多くの時間を費やしました。

3

N = n * nと設定しましょう。問題を解決するためのアルゴリズムの最悪の場合の複雑さはO(N)です。これは、1と0が交互に現れる「チェッカーボード」のような行列の場合、または0のみで満たされた行列の場合でも、セルに少なくとも1回は検査する必要があります。そうしないと、最長のシーケンスの長さに関する質問に回答できません。

1つの行で最長の「1」のシーケンスを見つけることは、単純な線形スキャンであり、O(n)の反復が必要です。これをn行に実行すると、O(N) n行のそれぞれのシーケンス長を見つけるための合計操作。それらの最大値を決定するにはO(n)操作が必要なので、合計で引き続きO(N)とします。これを行うには2回(すべての行で1回、すべての列で1回)には、引き続き2 * O(N)= O(N)演算が必要です。

したがって、上記のすべての列と行に対する単純で単純な反復よりも複雑さが小さいアルゴリズムはありません。

もちろん、実際のアルゴリズムを最適化しようとすることもできます(おそらく@ErikEidtによって提案された並列処理によって)。しかし、「分割統治」戦略によって、全体の複雑さがさらに低下することはありません。

1
Doc Brown

アルゴリズム的には、常にすべての要素を調べる必要はありません。その方法を説明しますが、それが準線形時間になるかどうかはわかりません(配列要素の数に比例するよりもゆっくりと成長します)。経過実行時間の実用的な観点から、私は支配的な要因に焦点を当てます。

簡単なプログラムから始めて、それを使用して、改善された候補者が依然として正しい結果を得るかどうかをテストできます。学習していない状況では、最初のプログラムが目的に対して十分に速いかどうかを判断することもできます。

簡単なプログラムでは、各列と各行を1回ずつスキャンし、それぞれの中で最も長い1のシーケンスを数え、それらすべての中で最も高い数を保持します。

ここで最適化するための問題は、何を最適化するかということです古典的なbig-Oアルゴリズムの複雑さは、主にアレイへのプローブの数をカウントします。次に、l要素が見つかった最長の実行時間を指定すると、lnと等しい場合は完了です。残りの行と列を調べる必要はありません。それ以外の場合は、次のスパンから開始するときに、l+1要素を前方にジャンプします。それが0であるか、その行または列の終わりを超えている場合は、中間要素をプローブする必要はありません。または、前のプローブの途中でプローブすることで0が見つかった場合は、その前の要素をプローブする必要はありません0など。[統計的に、スキップできます多くの配列要素。]

しかし、最新のハードウェアで経過時間を最適化したい場合、プローブの数は重要ではありません。ネイティブコードにコンパイル(またはJITコンパイル)する言語でプログラミングしていると仮定して、インタープリターのオーバーヘッドを処理しないようにします。 (これをPythonで記述している場合は、Numpyを呼び出すたびにできるだけ多くのことを行うようにしてください。)

最近のハードウェアでは、ランタイムは、RAMからのデータをCPUキャッシュにロードすることによって支配されます。その時間内に多くの命令を実行できます。これらは重要な「プローブ」です。配列要素をテストしません。この場合、最初に最適化するのはデータ形式なので、バイトごとに8つの要素にビットパックされます。

アレイ全体が大きすぎてCPUキャッシュに収まらない場合、次の最適化手順は、アレイをCPUに1回または2回だけロードすることです。行指向のデータの場合、行のスキャンは問題ありませんが、列のスキャンは、行ごとにロードされたデータを再利用して列ごとの部分的な結果を維持するか、列に2回目のパスを行いますが、1つのキャッシュラインのストライプを処理します一度に広い(8 x 64ビットパックされた列?CPUのキャッシュラインサイズは何でも)ので、各キャッシュラインを一度だけロードできます。

これは、アレイを物理的に取得するのにかかる時間を考慮する必要がないことを前提としていますRAM(仮想メモリではありません)、すべてがRAM atこれらの仮定が成り立たない場合、支配的なのは、データをRAMにできるだけ早く、キャッシュ読み込みの説明と同様に1回だけ取得することです。どちらの方法でも、レッスンを使用しています。 Bツリーの。

これで、CPUコア全体の並列化を検討できます。コアはキャッシュメモリを共有しますか?コアを並行して有効に使用することは微妙なことです。各コアは個別のアレイ行で機能しますが、ロードされたデータを列ストライプに再利用するのは難しい作業です。

GPUベクトル化を使用することも興味深いかもしれません。

l+1 skipプローブは、キャッシュラインの読み込みまたはRAM読み込みのレベルで適用された場合でも役立ちますが、特に2次元でスキャンする必要があるため、複雑になります。それでも、長さlのランを見つけることは、これ以上データを調べる必要がないことを意味します。

1
Jerry101

わかりました、私はいくつかの魔法の解決策を見るのを楽しみにしていますが、それまでは:

物事を分割することで私が見ることができる唯一の利点は、並列処理を適用できることです。 CPUを並列で使用していない場合、分割統治のポイントはないようです。行よりも小さい、または列よりも小さいサブディビジョンには、人為的な境界をまたぐソリューションを再構築する必要があります。一度に1つの行全体を解いてから、一度に1つの列全体を解いて、最​​良の答えを選びます。

(行または列を解くには、ゼロが表示されるたびにカウンターをリセットするスキャンをスキャンします。ゼロが表示されるたびにカウンターをインクリメントします。表示される最高のカウンター値の場所(および値と水平または垂直の方向)をキャプチャします)。

並列処理を適用する場合は、行をCPUCountグループに分割し、各グループを別のCPUにファームし、列をCPUCountグループに分割し、同様にして、最善の方法を使用します。行と列の回答は互いに独立しているため、それらは解決する必要のある別々の問題にすぎません。

0
Erik Eidt

他の回答で与えられた時間の複雑さを改善しない1つのアプローチを次に示しますO(n)しかしmightにはいくつかのパフォーマンス上の利点があります。基本的な考え方は、行列の行(または列)を一連のバイトに変換します。たとえば、nが24の場合、3バイトが必要です。次に、ルックアップテーブルを使用して、その8つのセルチャンクで使用できるシーケンスと継続を決定できます。これが表に必要なものです。

FF: (8,S,E)
FE: (7,S)
FD: (6,S);(1,E)
FC: (6,S)
...
A1: (1,S);(1);(1,E)
3F: (6,E)
...
0F: (4,E)
...

最初の行はバイト0xFFまたはバイナリ:11111111。このエントリは、長さ8のシーケンスが1つあり、8セルチャンクの開始と終了に隣接していることを示しています。 0xFD11111101には2つのシーケンスがあり、最初の6つのセルと最後の1つのセルです。 A1には3つのシーケンスがあり、すべて1セル長で、最初は1つ、中央は1つ、最後は1つです。注:バイトの最長の中間シーケンス以上を格納する理由はありません。

完全なテーブルを取得したら、それをプログラムにハードコードできます。たとえば、3つの要素すべて(開始、最長の内部、終了)を返すスイッチステートメントを作成できます。

次に、トリッキーな部分について、行の8セルバイトを反復処理します。チャンク間の継続を追跡する必要があります。たとえば、最初のセルが0xFD、これまでのところ、最も長い完全なシーケンスは6ロングです。しかし、最後の1セルを無視することはできません。次のチャンクを読み取るまで、そのシーケンスがどれだけ長いかわかりません。

これが単純なカウントアルゴリズムよりも速いと主張するつもりはありませんが、このような問題を解決する別の方法については、概念レベルから言及する価値があると思います。

0
JimmyJames