イントロレベルのCSコースの問題セットをまとめる作業をしていて、表面的には非常に単純に見える質問を思いつきました。
両親の名前、生年月日、死亡日が記載された人のリストが表示されます。あなたは、生涯のある時点で、誰が親、祖父母、曽祖父母などであったかを知ることに興味があります。この情報で各人に整数としてラベルを付けるアルゴリズムを考案します(0は、その人が子供、1はその人が親であったことを意味し、2はその人が祖父母であったことを意味します。
簡単にするために、ファミリーグラフは無向バージョンがツリーであるDAGであると想定できます。
ここでの興味深い課題は、ツリーの形状だけを見てこの情報を判断することはできないということです。たとえば、私には8人の曽祖父母がいますが、私が生まれたときは誰も生きていなかったので、彼らの生涯ではどれも曽祖父母ではありませんでした。
この問題に対して私が思いつくことができる最良のアルゴリズムは、時間O(n2)、ここでnは人数です。考え方は単純です。各人からDFSを開始し、その人の死亡日の前に生まれた家系図の中で最も遠い子孫を見つけます。ただし、これが問題の最適な解決策ではないと確信しています。たとえば、グラフが2つの親とそのnの子だけである場合、問題はO(n)で簡単に解決できます。私が望んでいるのは、O(n2)またはそのランタイムがグラフの形状に対してパラメータ化されているため、O(n2)最悪の場合。
今朝考えたところ、@ AlexeyKukanovも同じような考えを持っていたことがわかりました。しかし、私のものはもっと肉付けされていて、もう少し最適化されているので、とにかく投稿します。
このアルゴリズムはO(n * (1 + generations))
であり、どのデータセットでも機能します。現実的なデータの場合、これはO(n)
です。
O(n)
作業が行われます。O(1)
です。次に、子への再帰呼び出しごとに、O(generations)
作業を実行して、その子のデータを自分のデータにマージする必要があります。各人は、データ構造でそれらに遭遇すると呼び出され、O(n)
呼び出しと総費用O(n * (generations + 1))
のために各親から1回呼び出すことができます。O(n * (generations + 1))
です。これらすべての操作の合計はO(n * (generations + 1))
です。
現実的なデータセットの場合、これはO(n)
であり、定数はかなり小さくなります。
更新:これは私が思いついた最善の解決策ではありませんが、それに関連するコメントが非常に多いため、残しました。
一連のイベント(誕生/死)、親の状態(子孫、親、祖父母などなし)、および生命の状態(生きている、死んでいる)があります。
次のフィールドを持つ構造にデータを保存します。
_mother
father
generations
is_alive
may_have_living_ancestor
_
イベントを日付で並べ替えてから、イベントごとに次の2つのロジックコースのいずれかを受講します。
_Birth:
Create new person with a mother, father, 0 generations, who is alive and may
have a living ancestor.
For each parent:
If generations increased, then recursively increase generations for
all living ancestors whose generations increased. While doing that,
set the may_have_living_ancestor flag to false for anyone for whom it is
discovered that they have no living ancestors. (You only iterate into
a person's ancestors if you increased their generations, and if they
still could have living ancestors.)
Death:
Emit the person's name and generations.
Set their is_alive flag to false.
_
最悪のケースは、誰もが生きている祖先をたくさん持っている場合、O(n*n)
です。ただし、一般に、O(n log(n))
であるソート前処理ステップがあり、次にO(n * avg no of living ancestors)
になります。これは、ほとんどの場合、合計時間がO(n log(n))
になる傾向があることを意味します。人口。 (@Alexey Kukanovが修正してくれたおかげで、並べ替えの前段階を正しくカウントできませんでした。)
私のおすすめ:
O(N)
。descendant_birthday[0]
_に追加します(必要に応じてそのベクトルを増やします)。このフィールドがすでに設定されている場合は、新しい日付が早い場合にのみ変更してください。descendant_birthday[i]
_日付について、上記と同じルールに従って、親のレコードの_descendant_birthday[i+1]
_を更新します。O(C*N)
であり、Cは、指定された入力の「家族の深さ」の最大値(つまり、最長の_descendant_birthday
_ベクトルのサイズ)です。現実的なデータの場合、(他の人がすでに指摘しているように)正確性を失うことなく、ある程度の妥当な定数で制限できるため、Nに依存しません。descendant_birthday[i]
_がまだ死亡日よりも早い最大のi
で「各人にラベルを付けます」。また、O(C*N)
。したがって、現実的なデータの場合、問題の解決策は線形時間で見つけることができます。 @btillyのコメントで示唆されているような不自然なデータの場合でも、Cは大きくなる可能性があり、縮退した場合はNのオーダーでさえあります。これは、ベクトルサイズに上限を設けるか、@ btillyのソリューションのステップ2でアルゴリズムを拡張することで解決できます。
入力データの親子関係が(問題ステートメントに記述されているように)名前で提供されている場合、ハッシュテーブルはソリューションの重要な部分です。ハッシュがないと、関係グラフを作成するためにO(N log N)
が必要になります。他のほとんどの提案された解決策は、関係グラフがすでに存在することを前提としているようです。
birth_date
でソートされた人のリストを作成します。 death_date
でソートされた別の人のリストを作成します。発生したイベントのリストを取得するために、これらのリストから人々をポップして、時間を論理的に移動することができます。
個人ごとに、is_alive
フィールドを定義します。これは、最初はすべての人にとってFALSEになります。人々が生まれて死ぬとき、それに応じてこの記録を更新してください。
has_a_living_ancestor
と呼ばれる、個人ごとに別のフィールドを定義します。最初は全員に対してFALSEに初期化されます。出生時に、x.has_a_living_ancestor
はx.mother.is_alive || x.mother.has_a_living_ancestor || x.father.is_alive || x.father.has_a_living_ancestor
に設定されます。したがって、ほとんどの人(全員ではない)にとって、これは出生時にTRUEに設定されます。
課題は、has_a_living_ancestor
をFALSEに設定できる機会を特定することです。人が生まれるたびに、祖先を介してDFSを実行しますが、ancestor.has_a_living_ancestor || ancestor.is_alive
が真である祖先のみを実行します。
そのDFS中に、生きている祖先がなく、現在は死んでいる祖先が見つかった場合は、has_a_living_ancestor
をFALSEに設定できます。これは、has_a_living_ancestor
が古くなることもあると思いますが、すぐに見つけられることを願っています。
以下は、各子が最大で1つの親を持つグラフで機能するO(n log n)アルゴリズムです(編集:このアルゴリズムは、O(n log n)パフォーマンスの2つの親の場合には拡張されません)。追加の作業でパフォーマンスをO(n log(最大レベルラベル))に改善できると私は信じていることは注目に値します。
1つの親ケース:
各ノードxについて、トポロジカルの逆順で、生年月日とxから削除された世代数の両方で厳密に増加している二分探索木T_xを作成します。 (T_xには、xをルートとする祖先グラフのサブグラフに最初に生まれた子c1と、このサブグラフにある次の最初に生まれた子c2が含まれているため、c2の「曽祖父母レベル」はc1よりも厳密に大きくなります。このサブグラフで次に生まれた子c3は、c3のレベルがc2のレベルよりも厳密に大きくなるようにします。)T_xを作成するには、以前に作成されたツリーT_wをマージします。ここでwはxの子です(トポロジの逆順で繰り返します)。
マージの実行方法に注意すれば、そのようなマージの総コストは、祖先グラフ全体でO(n log n)であることを示すことができます。重要なアイデアは、各マージ後、各レベルの最大1つのノードがマージされたツリーに残ることに注意することです。各ツリーT_wにh(w) log nのポテンシャルを関連付けます。ここで、h(w)は、wからの最長パスの長さに等しくなります。葉に。
子ツリーT_wをマージしてT_xを作成すると、すべてのツリーT_wが「破棄」され、ツリーT_xの構築に使用するためにそれらが格納するすべての可能性が解放されます。そして、(log n)(h(x))ポテンシャルを持つ新しいツリーT_xを作成します。したがって、私たちの目標は、ツリーT_wからT_xを作成するために最大でO((log n)(sum_w(h(w))-h(x) +定数))時間を費やすことです。マージの償却コストはO(log n)のみになります。これは、h(w)がT_xの開始点として最大になるように、ツリーT_wを選択し、次にT_wを変更してT_xを作成します。T_xに対してこのような選択を行った後、2つのバイナリ検索ツリーをマージするための標準アルゴリズムと同様のアルゴリズムを使用して、他の各ツリーを1つずつT_xにマージします。
基本的に、マージは、T_wの各ノードyを反復処理し、生年月日でyの先行zを検索し、xからzよりも多くのレベルが削除されている場合はyをT_xに挿入することによって実行されます。次に、zがT_xに挿入された場合、zのレベルよりも厳密に大きい最低レベルのT_x内のノードを検索し、間にあるノードをスプライスして、T_xが生年月日とレベルの両方で厳密に順序付けられるという不変条件を維持します。これは、T_wの各ノードに対してO(log n)のコストがかかり、T_wには最大でO(h(w))ノードがあるため、すべてのツリーをマージするための総コストはO(( log n)(sum_w(h(w)))、h(w ')が最大になるように、子w'を除くすべての子wを合計します。
T_xの各要素に関連付けられたレベルを、ツリー内の各ノードの補助フィールドに格納します。 T_xを作成した後、xの実際のレベルを把握できるように、この値が必要です。 (技術的な詳細として、実際には各ノードのレベルとその親のレベルの差をT_xに格納して、ツリー内のすべてのノードの値をすばやくインクリメントできるようにします。これは標準のBSTトリックです。)
それでおしまい。初期ポテンシャルが0で、最終ポテンシャルが正であることに注意してください。したがって、償却された境界の合計は、ツリー全体にわたるすべてのマージの総コストの上限になります。 xがコストO(log n)で死ぬ前に生まれたT_xの最新の要素を二分探索することにより、BST T_xを作成すると、各ノードxのラベルが見つかります。
O(n log(max level label))へのバインドを改善するために、ツリーを遅延マージし、必要に応じてツリーの最初のいくつかの要素のみをマージして、現在のノードのソリューションを提供できます。スプレーツリーなど、参照の局所性を利用するBSTを使用すると、上記の限界を達成できます。
うまくいけば、上記のアルゴリズムと分析は、少なくとも従うのに十分明確です。説明が必要な場合はコメントしてください。
私は、各人のマッピング(世代->その世代の最初の子孫が生まれた日付)を取得することが役立つだろうという予感があります。
日付は厳密に増加している必要があるため、バイナリ検索(または適切なデータ構造)を使用して、O(log n)時間で最も遠い生きている子孫を見つけることができます。
問題は、これらのリストを(少なくとも単純に)マージするとO(世代数)になるため、最悪の場合はO(n ^ 2)になる可能性があることです(AとBがCとDの親であり、親であると考えてください)。 EとFの...)。
私はまだ最良のケースがどのように機能するかを理解し、最悪のケースをよりよく特定しようとする必要があります(そしてそれらの回避策があるかどうかを確認します)
最近、プロジェクトの1つにリレーションシップモジュールを実装しました。このモジュールでは、すべてがデータベースにあり、アルゴリズムは2nO(m)(mは最大分岐係数)が最適だと思います。最初のラウンドでは関係グラフを作成し、2番目のラウンドではすべての人を訪問するため、操作を2回Nに乗算しました。 2つのノードごとに双方向の関係を保存しました。ナビゲート中は、一方向のみを使用して移動します。ただし、2つの操作セットがあります。1つは子のみをトラバースし、もう1つは親のみをトラバースします。
Person{
String Name;
// all relations where
// this is FromPerson
Relation[] FromRelations;
// all relations where
// this is ToPerson
Relation[] ToRelations;
DateTime birthDate;
DateTime? deathDate;
}
Relation
{
Person FromPerson;
Person ToPerson;
RelationType Type;
}
enum RelationType
{
Father,
Son,
Daughter,
Mother
}
この種の双向グラフのように見えます。ただし、この場合、最初にすべてのPersonのリストを作成してから、リストリレーションを作成し、各ノード間にFromRelationsとToRelationsを設定できます。次に、あなたがしなければならないのは、すべての人について、タイプ(Son、Daughter)のToRelationsのみをナビゲートするだけです。そして、あなたは日付を持っているので、あなたはすべてを計算することができます。
コードの正しさをチェックする時間がありませんが、これでその方法がわかります。
void LabelPerson(Person p){
int n = GetLevelOfChildren(p, p.birthDate, p.deathDate);
// label based on n...
}
int GetLevelOfChildren(Person p, DateTime bd, DateTime? ed){
List<int> depths = new List<int>();
foreach(Relation r in p.ToRelations.Where(
x=>x.Type == Son || x.Type == Daughter))
{
Person child = r.ToPerson;
if(ed!=null && child.birthDate <= ed.Value){
depths.Add( 1 + GetLevelOfChildren( child, bd, ed));
}else
{
depths.Add( 1 + GetLevelOfChildren( child, bd, ed));
}
}
if(depths.Count==0)
return 0;
return depths.Max();
}
これが私の刺し傷です:
class Person
{
Person [] Parents;
string Name;
DateTime DOB;
DateTime DOD;
int Generations = 0;
void Increase(Datetime dob, int generations)
{
// current person is alive when caller was born
if (dob < DOD)
Generations = Math.Max(Generations, generations)
foreach (Person p in Parents)
p.Increase(dob, generations + 1);
}
void Calculate()
{
foreach (Person p in Parents)
p.Increase(DOB, 1);
}
}
// run for everyone
Person [] people = InitializeList(); // create objects from information
foreach (Person p in people)
p.Calculate();