6

最近、電話インタビューを行いました。プロセスの一部として問題のコーディングが含まれていました。
問題は のバリエーションでしたFind the most closest common ancestor of a treeが、ひねりがありました。ツリーはグラフによく似ていました。つまり、子ノードを接続できました。例:

     A   
   /  
  B 
  | \ 
  C  E  
  |  |  
  D  F  
  \  /  
   G   

この場合、このツリーとノードが与えられFD結果として得られる最も近い共通の祖先は になりますB。2 番目のひねりは、ツリーが配列として提示されたことです。実装 するメソッドには次の入力があり
public String getCA(String[] nodes, String[][] parentNodes, String targetNode1, String targetNode2)
ました。 正直なところ、私は完全にパニックに陥り(すでにかなり緊張していました)、答えを見つけるのに本当に長い時間がかかりました.nodes{"G", "F", "E", "D", "C", "B", "A"}parentNodes{{"F","D"},{"E"}, {"B"}, {"C"}, {"B"}, {"A"}, null}
nodes[i]parentNodes[i]

これは再帰的に解決されると思いますが、私が知る限り、うまくいく反復的な解決策を思いつきました.ノードをキューにプッシュし、最初のターゲットノード、次に2番目のノードのパスを上に移動します。すでに遭遇したノードを見つけたら、それを解決策と見なします (問題を解決するためにコメントを追加しました)。

public String getCA(String[] nodes, String[][] parentNodes, String targetNode1, String targetNode2) {  
        if(nodes == null || parentNodes == null){  
            throw new IllegalArgumentException();  
        }  

        Map<String, String[]> map = new HashMap<String, String[]>();  
        for(int i = 0; i < nodes.length; i++){  
            map.put(nodes[i], parentNodes[i]);  
        }  
        //These are the parents visited as we go up  
        Set<String> parentsSeen = new HashSet<String>();  
        parentsSeen.add(targetNode1);  

        Queue<String> path = new LinkedList<String>();  
        String[] parents = map.get(targetNode1);  
        //The root is the common parent  
        if(parents == null){  
            return targetNode1;  
        }   

        //Build the path up  
        for(String parent:parents){  
            path.add(parent);  
        }  
        while(!path.isEmpty()){  
            String currentParent = path.remove();  
            parentsSeen.add(currentParent);  
            parents = map.get(currentParent);  
            if(parents == null){  
                continue;   
            }  
            for(String parent:parents){  
                path.add(parent);  
            }  
        }  

        parents = map.get(targetNode2);  
        //It is the root, so it is the common parent  
        if(parents == null){  
            return targetNode2;  
        }  
        //Start going up for the targetNode2. The first parent that we have already visited is the common parent  
        for(String parent:parents){  
            if(parentsSeen.contains(parent)){  
                return parent;  
            }  
            path.add(parent);  
        }  

        while(!path.isEmpty()){  
            String currentParent = path.remove();  
            if(parentsSeen.contains(currentParent)){  
                return currentParent;  
            }             
            parents = map.get(currentParent);  
            if(parents == null){  
                continue;  
            }  
            for(String parent:parents){  
                path.add(parent);  
            }  
        }  
        return null;            
    }

前進の電話がかかってきませんでした。私は「独学」であるため、ここでどのように失敗したかを理解したいと思います。これは技術的な問題であるため、主観的な問題ではないと思います。ここで経験豊富な人々から助けを得ることができれば幸いです。
では、同僚のプログラマーとして、この問題をどのように処理し、私のソリューションをどのように評価しますか? スキルを向上させるために何をする必要がありますか?
できるだけ率直に話すことができます。何がうまくいかなかったのかを理解し、学ぶことができる限り、私は満足しています

4

7 に答える 7

4

ここで「最も近い」とはどういう意味か、私にはわかりません。次のグラフについて考えてみます。

    I
   /\
  /  \
 /    \
H      E
|\    /|
| \  / |
G  \/  D
|  /\  |
| /  F C
|/    \|
A      B

ここでは、AとB、HとEの2つの共通の祖先があります。HはAとBの両方から距離2です。EはAから距離1ですが、Bから距離3です。どちらを選択しますか?

さらに、その質問に対するあなたの答えが何であれ、一方から祖先のセットを見つけて、もう一方からBFSを実行することは機能しません。Aのすべての祖先を検索してからBからBFSを実行すると、最初にHが検索され、Bのすべての祖先を検索してからAからBFSを実行すると、最初にEが検索されます。敵対者として、私はAとBを切り替えて、どのような選択(2/2または1/3のどちらが良いか)に関してアルゴリズムを失敗させることができます。

したがって、正しいアルゴリズムは、祖先セットの計算とBFSよりも複雑である必要があります。そして、その選択方法を教えてくれない限り、正しいアルゴリズムを特定できるかどうかはわかりません。

于 2012-08-28T03:58:46.927 に答える
2

とてもいい質問です。あなたは本当にこれを書くことに力を入れています。インタビューで申し訳ありませんが、このようなことが起こると本当にイライラするに違いありません。問題について見てみましょう。

頭に浮かぶアイデアは次のとおりです。両方のターゲット ノードからルートに到達し、2 つのリストを作成するまで、再帰的に上に移動できます。最後に、両方のリストで最初の共通ノードを見つけます。

public static List<String> visitUpwards(Map<String, String[]> mapping, String current) {
    List<String> list = new ArrayList<String>();
    list.add(current);
    if(mapping.get(current) == null) return list;
    else {           
       for(String s: mapping.get(current)) {              
          list.addAll(visitUpwards(mapping, s));                            
       }
       return list;
    }
}  

編集: よく考えてみると、これはあまり良い考えではありませんでした。これは、基本的に DFS 検索を上向きに行うため、最初の共通の祖先を見つけるのが難しくなるためです。より良いアイデアは、代わりに各ターゲットに対して BFS を実行することです (これはあなたのソリューションに似ているように見えるかもしれません:D):

public static Set<String> BFS(Map<String, String[]> mapping, String node) {
    Queue<String> queue = new LinkedList<String>();
    Set<String> result = new LinkedHashSet<String>();
    queue.add(node);
    while(!queue.isEmpty()) {
        String current = queue.remove();
        result.add(current);
        if(mapping.get(current) != null)
            for(String s: mapping.get(current)) {
                queue.add(s);
            }
    }
    return result;
}

次に、別のマップを使用して最初の共通の祖先を見つけます。

Set<String> list1 = BFS(mapping, "D");      
Set<String> list2 = BFS(mapping, "G");
System.out.println(list1);
System.out.println(list2);

Map<String, Boolean> src = new HashMap<String, Boolean>();

for(String s: list1) {
    src.put(s, true);
}

for(String s: list2) {
    if(src.get(s) != null && src.get(s)) {
        System.out.println(s);
        break;
    }
}
于 2012-08-27T20:21:59.483 に答える
1

会社があなたに電話をかけなかった理由はわかりません。ただし、詳細で低レベルのロジックをすべて備えた単一の大きなメソッドを使用するのではなく、コードを小さくて意味のあるメソッドに抽出することで、コードに大きなメリットがあります。また、コードをコンパイルしていくつかのテストケースで実行しようとはしていませんが、簡単に読んだだけでは、コードが正しいとはまったく確信していません。最も近いコメントの祖先がまたはである可能性がありますtargetNode1 targetNode2たとえばtargetNode2、の親である場合targetNode1)。(これは、「最も近い共通の祖先」を定義する方法の詳細に依存すると思います。その場合の意味を明確にする必要があります。)

  • ノードから親へのマップを使用することは良い考えです。ただし、そのマップを意味のある名前の小さなメソッドにビルドするコードを抽出することはできます。
  • グラフを介した幅優先探索を実装するメソッドを定義できます(これは基本的にここで行っていることです)。とで同じ方法を使用しtargetNode1ますtargetNode2
于 2012-08-27T19:59:32.540 に答える
1

より最適な解決策は、ブックキーピングにブール値の配列を使用することです。ノードごとに 1 つ。それらをすべてfalseに初期化します。ノードにアクセスするときは、それらをtrueに設定します。今日と同じように、交互に「木」を上っていきます。すでにアクセスしたノード (配列でtrueに設定) に到達すると、共通の祖先が見つかりました。

編集:より良いアプローチは、各起点ノードに一意の識別子を持つ整数の配列を持つことです。誰がすでにノードを訪問したかを知る必要があるループがある場合、それは自分自身である可能性があります。

于 2012-08-27T20:10:21.407 に答える
1

グラフの次のバリエーションを検討してください...

              A
              |
          X---B---Y
           \ / \ /
            C   E  
            |   |
            D   F
             \ /
              G

nodes={"G", "F", "E", "D", "C", "B", "A", "X", "Y"}

parentNodes={{"F","D"},{"E"}, {"Y","B"}, {"C"}, {"X","B"}, {"A"}, null, {"B"}, {"B"}}

"C"探索する2つの親ノードがあるため、ノードに到達するとプログラムが困難になると思います。再帰アルゴリズムに頼るか、反復アルゴリズムを使用して未調査のノードのスタックのようなものを管理する必要があります。

イテラトンに偏っているように見えるので、次のようなことを試すことができます。

配列 (または同様のデータ構造) を作成します。各配列要素には、次の 3 つの情報が保持されます。

 nodeName: string - name of a node from your `nodes` string
 pathLength[2]: integer -  array of minimum path length from refrence node (1 or 2).

スタックを作成します。各スタック要素には、次の 3 つの情報が含まれます。

 nodeName: string - name of a node to "explore"
 lengthToHere: integer - length of path from reference node to `nodeName`
 pathNumber: integer - 1 for first reference node, 2 for second.

アルゴリズム:

 Initialize array with nodeName from nodes and set each pathLength to "infinity"
 Push both starting reference nodes onto the stack: 

       push("F", 0, 1)
       push "D", 0, 2)

Repeat until stack is empty:
  - pop(nodeName, lengthToHere, pathNumber)
  - update pathLength[pathNumber] of element having nodeName in array with minimum of
            its current value and lengthToHere
  - for each parent of nodeName push(parent, lengthToHere + 1, pathNumber)

開始参照ノードからのすべてのパスが評価されると、スタックは空になります。共通の先祖がある場合、特定の nodeName の両方の pathLength 値は無限大より小さくなります。これらの値を合計して、この共通の祖先までのパスの合計の長さを求めます。合計が最小の nodeName を報告します。

于 2012-08-28T01:42:04.960 に答える
0

特定のターゲットノードに対して、次の手順xy実行します。

  • xとそのすべての祖先をセットに追加しSます。
  • yが入っているかどうかを確認し、そうでない場合は、Sの親が入っているかどうかを確認yS、そうでない場合は、親が入っているかどうかを確認Sします。

実行時間はO(n)です。

于 2012-08-28T08:44:39.963 に答える
0

私は何かを誤解しているかもしれませんが、アルゴリズムのこの部分はノードの調査に多くの時間を浪費する可能性があると思います:

   while(!path.isEmpty()){  
        String currentParent = path.remove();  
        parentsSeen.add(currentParent);  
        parents = map.get(currentParent);  
        if(parents == null){  
            continue;   
        }  
        for(String parent:parents){  
            path.add(parent);  
        }  
    }  

これはツリーの幅優先検索のように見えますが、子が複数の親を持つため、同じノードを複数回検索することになる場合があります。

parentSeen セットのチェックを追加することで、無駄な時間を止めることができます。

    while(!path.isEmpty()){  
        String currentParent = path.remove();  
        if (parentsSeen.contains(currentParent)) {
            continue;
        }
        parentsSeen.add(currentParent);  
        parents = map.get(currentParent);  
        if(parents == null){  
            continue;   
        }  
        for(String parent:parents){  
            path.add(parent);  
        }  
    }  
于 2012-08-28T16:47:33.190 に答える