web-dev-qa-db-ja.com

ランダムDAGの生成

有向非巡回グラフの問題を解決しています。

しかし、いくつかの有向非巡回グラフでコードをテストするのに問題があります。テストグラフは大きく、(明らかに)非巡回である必要があります。

非巡回有向グラフを生成するためのコードを書くためにたくさんのことを試みました。しかし、私は毎回失敗しました。

使用できる非循環有向グラフを生成するための既存の方法はありますか?

24

私はこれを行うCプログラムを作り上げました。重要なのはノードを「ランク付け」することであり、onlyはランクの低いノードからランクの高いノードにエッジを描画します。

私が書いたプログラムは DOT言語 で印刷します。

これがコード自体であり、その意味を説明するコメントが付いています。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define MIN_PER_RANK 1 /* Nodes/Rank: How 'fat' the DAG should be.  */
#define MAX_PER_RANK 5
#define MIN_RANKS 3    /* Ranks: How 'tall' the DAG should be.  */
#define MAX_RANKS 5
#define PERCENT 30     /* Chance of having an Edge.  */

int main (void)
{
  int i, j, k,nodes = 0;
  srand (time (NULL));

  int ranks = MIN_RANKS
              + (Rand () % (MAX_RANKS - MIN_RANKS + 1));

  printf ("digraph {\n");
  for (i = 0; i < ranks; i++)
    {
      /* New nodes of 'higher' rank than all nodes generated till now.  */
      int new_nodes = MIN_PER_RANK
                      + (Rand () % (MAX_PER_RANK - MIN_PER_RANK + 1));

      /* Edges from old nodes ('nodes') to new ones ('new_nodes').  */
      for (j = 0; j < nodes; j++)
        for (k = 0; k < new_nodes; k++)
          if ( (Rand () % 100) < PERCENT)
            printf ("  %d -> %d;\n", j, k + nodes); /* An Edge.  */

      nodes += new_nodes; /* Accumulate into old node set.  */
    }
  printf ("}\n");
  return 0;
}

そして、これがテスト実行から生成されたグラフです。

A randomly generated DAG

53
ArjunShankar

https://mathematica.stackexchange.com/questions/608/how-to-generate-random-directed-acyclic-graphs への答えが適用されます:のエッジの隣接行列表現がある場合グラフの場合、行列が下三角行列の場合、必然的にDAGになります。

同様のアプローチは、ノードの任意の順序を取り、ノードxからyまでのエッジを検討することです。 x <yの場合のみ。その制約は、構造によってDAGnessも取得する必要があります。構造体を使用してノードを表す場合、メモリ比較はノードを順序付ける任意の方法の1つです。

基本的に、擬似コードは次のようになります。

for(i = 0; i < N; i++) {
    for (j = i+1; j < N; j++) {
        maybePutAnEdgeBetween(i, j);
    }
}

ここで、[〜#〜] n [〜#〜]はグラフ内のノードの数です。

擬似コードは、Nノードが与えられた場合の潜在的なDAGの数が

2^(n*(n-1)/2),

あるので

n*(n-1)/2

順序対(「Nは2を選択」)、およびそれらの間にエッジを配置するかどうかを選択できます。

12
dyoo

したがって、これらすべての合理的な答えをまとめようとすると、次のようになります。

(以下では、生成されたグラフの頂点の数にVを使用し、エッジの数にEを使用し、E≤V(V-1)/ 2と仮定します。)

個人的には、最も有用な答えは、 http://condor.depaul.edu/rjohnson/source/graph_ge.c のコードを指しているFlaviusによるコメントにあると思います。そのコードは本当に単純で、コメントで簡単に説明できます。これを再現します。

To generate a directed acyclic graph, we first
generate a random permutation dag[0],...,dag[v-1].
(v = number of vertices.)
This random permutation serves as a topological
sort of the graph. We then generate random edges of the
form (dag[i],dag[j]) with i < j.

実際、コードが行うことは、以下を繰り返し実行することにより、エッジの要求数を生成することです。

  1. [0、V)の範囲で2つの数値を生成します。
  2. それらが等しい場合はそれらを拒否します。
  3. 最初のものが大きい場合はそれらを交換します。
  4. 以前に生成したことがある場合は、それらを拒否します。

このソリューションの問題は、Eがエッジの最大数V(V-1)/ 2に近づくと、より多くのエッジを拒否する必要があるため、アルゴリズムがますます遅くなることです。より良い解決策は、すべてのV(V-1)/ 2の可能なエッジのベクトルを作成することです。ランダムにシャッフルします。シャッフルされたリストの最初の(要求されたエッジ)エッジを選択します。

貯水池サンプリングアルゴリズム は、kの端点を推定できるため、空間O(E)でこれを実行できます。th kの値からのエッジ。したがって、実際にソースベクトルを作成する必要はありません。ただし、それでもO(V2)時間。

または、 Fisher-Yates シャッフル(または、必要に応じてクヌースシャッフル)を実行して、E回の反復後に停止することもできます。ウィキペディアで提示されているFYシャッフルのバージョンでは、これにより末尾のエントリが生成されますが、アルゴリズムは逆方向でも同様に機能します。

// At the end of this snippet, a consists of a random sample of the
// integers in the half-open range [0, V(V-1)/2). (They still need to be
// converted to pairs of endpoints).
vector<int> a;
int N = V * (V - 1) / 2;
for (int i = 0; i < N; ++i) a.Push_back(i);
for (int i = 0; i < E; ++i) {
  int j = i + Rand(N - i);
  swap(a[i], a[j]);
a.resize(E);

これにはO(E)時間のみが必要ですが、O(N2) スペース。実際、これはO(E)スペースに少し工夫を凝らして改善できますが、SOコードスニペットは小さすぎて結果を含めることができないため、 O(E)スペースとO(E log E)時間でより単純なものを提供します。少なくとも次のクラスDAGがあると仮定します。

class DAG {
  // Construct an empty DAG with v vertices
  explicit DAG(int v);

  // Add the directed Edge i->j, where 0 <= i, j < v
  void add(int i, int j);
};

今ここに行きます:

// Return a randomly-constructed DAG with V vertices and and E edges.
// It's required that 0 < E < V(V-1)/2.
template<typename PRNG>
DAG RandomDAG(int V, int E, PRNG& prng) {
  using dist = std::uniform_int_distribution<int>;
  // Make a random sample of size E
  std::vector<int> sample;
  sample.reserve(E);
  int N = V * (V - 1) / 2;
  dist d(0, N - E);  // uniform_int_distribution is closed range
  // Random vector of integers in [0, N-E]
  for (int i = 0; i < E; ++i) sample.Push_back(dist(prng));
  // Sort them, and make them unique
  std::sort(sample.begin(), sample.end());
  for (int i = 1; i < E; ++i) sample[i] += i;
  // Now it's a unique sorted list of integers in [0, N-E+E-1]
  // Randomly shuffle the endpoints, so the topological sort
  // is different, too.
  std::vector<int> endpoints;
  endpoints.reserve(V);
  for (i = 0; i < V; ++i) endpoints.Push_back(i);
  std::shuffle(endpoints.begin(), endpoints.end(), prng);
  // Finally, create the dag
  DAG rv;
  for (auto& v : sample) {
    int tail = int(0.5 + sqrt((v + 1) * 2));
    int head = v - tail * (tail - 1) / 2;
    rv.add(head, tail);
  }
  return rv;
}
3
rici

ランダムな有向グラフを生成してから、深さ優先探索を実行してサイクルを検索できます。サイクルを見つけたら、エッジを削除してサイクルを中断します。

これは最悪の場合のO(VE)だと思います。各DFSはO(V)を取り、各DFSは少なくとも1つのエッジを削除します(したがって最大E)

すべてのV ^ 2の可能なエッジを均一にランダムに選択して有向グラフを生成し、ランダムな順序でDFSを実行し、ランダムなエッジを削除すると、すべての可能なダグにわたって均一な分布(または少なくともそれに近い)が得られます。

3
Andrew Tomazos

非常に単純なアプローチは次のとおりです。

  1. 下の対角行列のインデックスを反復処理してエッジをランダムに割り当てます(上記のリンクで提案されているように: https://mathematica.stackexchange.com/questions/608/how-to-generate-random-directed-acyclic -グラフ

  2. これにより、おそらく複数のコンポーネントを含むDAGが得られます。素集合データ構造を使用して、コンポーネント間にエッジを作成することでマージできるコンポーネントを提供できます。

素集合はここで説明されています: https://en.wikipedia.org/wiki/Disjoint-set_data_structure

2
Michael Lewis

nノードとノードの各ペア間のエッジを含むグラフを作成しますn1およびn2 if n1 != n2およびn2 % n1 == 0

1
Dietmar Kühl

私は最近、受け入れられた答えを再実装しようとしましたが、それが不確定であることがわかりました。 min_per_rankパラメーターを適用しないと、ノードが0のグラフになってしまう可能性があります。

これを防ぐために、forループを関数でラップしてから、各ランクの後にmin_per_rank満足しました。 JavaScriptの実装は次のとおりです。

https://github.com/karissa/random-dag

そして、受け入れられた回答のメインループを置き換えるいくつかの疑似Cコード。

int pushed = 0

int addRank (void) 
{
  for (j = 0; j < nodes; j++)
    for (k = 0; k < new_nodes; k++)
      if ( (Rand () % 100) < PERCENT)
        printf ("  %d -> %d;\n", j, k + nodes); /* An Edge.  */

  if (pushed < min_per_rank) return addRank()
  else pushed = 0

  return 0
}
1
karissa

接続されていない可能性のあるランダムDAGを生成する

接続されていない可能性のあるランダムDAGを生成するための簡単なアルゴリズムを次に示します。

_const randomDAG = (x, n) => {
    const length = n * (n - 1) / 2;

    const dag = new Array(length);

    for (let i = 0; i < length; i++) {
        dag[i] = Math.random() < x ? 1 : 0;
    }

    return dag;
};

const dagIndex = (n, i, j) => n * i + j - (i + 1) * (i + 2) / 2;

const dagToDot = (n, dag) => {
    let dot = "digraph {\n";

    for (let i = 0; i < n; i++) {
        dot += `    ${i};\n`;

        for (let j = i + 1; j < n; j++) {
            const k = dagIndex(n, i, j);
            if (dag[k]) dot += `    ${i} -> ${j};\n`;
        }
    }

    return dot + "}";
};

const randomDot = (x, n) => dagToDot(n, randomDAG(x, n));

new Viz().renderSVGElement(randomDot(0.3, 10)).then(svg => {
    document.body.appendChild(svg);
});_
_<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/viz.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/full.render.js"></script>_

このコードスニペットを数回実行すると、接続されていないDAGが表示される場合があります。

では、このコードはどのように機能しますか?

有向非巡回グラフ(DAG)は、単なる トポロジカルソート 無向グラフです。 n頂点の無向グラフは、頂点からそれ自体への繰り返しエッジまたはエッジをカウントせずに、最大n * (n - 1) / 2エッジを持つことができます。これで、下の頂点から上の頂点へのエッジのみを持つことができます。したがって、すべてのエッジの方向が事前に決定されます。

これは、n * (n - 1) / 2エッジの重みの1次元配列を使用してDAG全体を表すことができることを意味します。エッジの重みが_0_の場合、エッジが存在しないことを意味します。したがって、0または1のランダムな配列を作成するだけで、それがランダムなDAGです。

i頂点のDAG内の頂点jから頂点nまでのエッジ。ここで_i < j_は、インデックスkにエッジの重みがあります。 k = n * i + j - (i + 1) * (i + 2) / 2


接続されたDAGの生成

ランダムDAGを生成したら、次の関数を使用して接続されているかどうかを確認できます。

_const isConnected = (n, dag) => {
    const reached = new Array(n).fill(false);

    reached[0] = true;

    const queue = [0];

    while (queue.length > 0) {
        const x = queue.shift();

        for (let i = 0; i < n; i++) {
            if (i === n || reached[i]) continue;
            const j = i < x ? dagIndex(n, i, x) : dagIndex(n, x, i);
            if (dag[j] === 0) continue;
            reached[i] = true;
            queue.Push(i);
        }
    }

    return reached.every(x => x); // return true if every vertex was reached
};
_

接続されていない場合、その 補集合は常に接続されます

_const complement = dag => dag.map(x => x ? 0 : 1);

const randomConnectedDAG = (x, n) => {
    const dag = randomDAG(x, n);
    return isConnected(n, dag) ? dag : complement(dag);
};
_

30%のエッジを持つランダムDAGを作成すると、その補集合には70%のエッジがあることに注意してください。したがって、xの唯一の安全な値は50%です。ただし、エッジのパーセンテージよりも接続性を重視する場合、これは取引を妨げるものではありません。

最後に、すべてをまとめます。

_const randomDAG = (x, n) => {
    const length = n * (n - 1) / 2;

    const dag = new Array(length);

    for (let i = 0; i < length; i++) {
        dag[i] = Math.random() < x ? 1 : 0;
    }

    return dag;
};

const dagIndex = (n, i, j) => n * i + j - (i + 1) * (i + 2) / 2;

const isConnected = (n, dag) => {
    const reached = new Array(n).fill(false);

    reached[0] = true;

    const queue = [0];

    while (queue.length > 0) {
        const x = queue.shift();

        for (let i = 0; i < n; i++) {
            if (i === n || reached[i]) continue;
            const j = i < x ? dagIndex(n, i, x) : dagIndex(n, x, i);
            if (dag[j] === 0) continue;
            reached[i] = true;
            queue.Push(i);
        }
    }

    return reached.every(x => x); // return true if every vertex was reached
};

const complement = dag => dag.map(x => x ? 0 : 1);

const randomConnectedDAG = (x, n) => {
    const dag = randomDAG(x, n);
    return isConnected(n, dag) ? dag : complement(dag);
};

const dagToDot = (n, dag) => {
    let dot = "digraph {\n";

    for (let i = 0; i < n; i++) {
        dot += `    ${i};\n`;

        for (let j = i + 1; j < n; j++) {
            const k = dagIndex(n, i, j);
            if (dag[k]) dot += `    ${i} -> ${j};\n`;
        }
    }

    return dot + "}";
};

const randomConnectedDot = (x, n) => dagToDot(n, randomConnectedDAG(x, n));

new Viz().renderSVGElement(randomConnectedDot(0.3, 10)).then(svg => {
    document.body.appendChild(svg);
});_
_<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/viz.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/full.render.js"></script>_

このコードスニペットを数回実行すると、他のDAGよりもはるかに多くのエッジを持つDAGが表示される場合があります。


特定の割合のエッジを持つ接続されたDAGを生成する

接続性と特定の割合のエッジの両方に関心がある場合は、次のアルゴリズムを使用できます。

  1. 完全に接続されたグラフから始めます。
  2. エッジをランダムに削除します。
  3. エッジを削除した後、グラフがまだ接続されているかどうかを確認します。
  4. 接続されていない場合は、そのエッジを追加し直します。

このアルゴリズムは、以前の方法ほど効率的ではないことに注意してください。

_const randomDAG = (x, n) => {
    const length = n * (n - 1) / 2;

    const dag = new Array(length).fill(1);

    for (let i = 0; i < length; i++) {
        if (Math.random() < x) continue;
        dag[i] = 0;
        if (!isConnected(n, dag)) dag[i] = 1;
    }

    return dag;
};

const dagIndex = (n, i, j) => n * i + j - (i + 1) * (i + 2) / 2;

const isConnected = (n, dag) => {
    const reached = new Array(n).fill(false);

    reached[0] = true;

    const queue = [0];

    while (queue.length > 0) {
        const x = queue.shift();

        for (let i = 0; i < n; i++) {
            if (i === n || reached[i]) continue;
            const j = i < x ? dagIndex(n, i, x) : dagIndex(n, x, i);
            if (dag[j] === 0) continue;
            reached[i] = true;
            queue.Push(i);
        }
    }

    return reached.every(x => x); // return true if every vertex was reached
};

const dagToDot = (n, dag) => {
    let dot = "digraph {\n";

    for (let i = 0; i < n; i++) {
        dot += `    ${i};\n`;

        for (let j = i + 1; j < n; j++) {
            const k = dagIndex(n, i, j);
            if (dag[k]) dot += `    ${i} -> ${j};\n`;
        }
    }

    return dot + "}";
};

const randomDot = (x, n) => dagToDot(n, randomDAG(x, n));

new Viz().renderSVGElement(randomDot(0.3, 10)).then(svg => {
    document.body.appendChild(svg);
});_
_<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/viz.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/2.1.2/full.render.js"></script>_

お役に立てば幸いです。

0
Aadit M Shah

@ArjunShankarが提供したコードは、多くの冗長エッジを持つDAGを生成しました。

Pythonに変換し、いくつかの機能を統合して、ランダムDAGの推移的なセットを作成しました。このようにして、生成されたグラフには、同じ到達可能性を持つエッジの最小数が含まれます。

推移グラフは、出力をモデルコードに貼り付けることにより、 http://dagitty.net/dags.html で視覚化できます。 (右にあります)。

@ArjunShankarコードは、最小数のエッジ(最後のコード)でのみDAGを生成するように調整できることに注意してください。

以下、既存のDAGの推移グラフを生成します。

import random

class Graph:
    nodes = []
    edges = []
    removed_edges = []

    def remove_Edge(self, x, y):
        e = (x,y)
        try:
            self.edges.remove(e)
            # print("Removed Edge %s" % str(e))
            self.removed_edges.append(e)
        except:
            return

    def Nodes(self):
        return self.nodes

    # Sample data
    def __init__(self):
        self.nodes = []
        self.edges = []


def get_random_dag():
    MIN_PER_RANK = 1    # Nodes/Rank: How 'fat' the DAG should be
    MAX_PER_RANK = 2
    MIN_RANKS = 6   # Ranks: How 'tall' the DAG should be
    MAX_RANKS = 10
    PERCENT = 0.3  # Chance of having an Edge
    nodes = 0

    ranks = random.randint(MIN_RANKS, MAX_RANKS)

    adjacency = []
    for i in range(ranks):
        # New nodes of 'higher' rank than all nodes generated till now
        new_nodes = random.randint(MIN_PER_RANK, MAX_PER_RANK)

        # Edges from old nodes ('nodes') to new ones ('new_nodes')
        for j in range(nodes):
            for k in range(new_nodes):
                if random.random() < PERCENT:
                    adjacency.append((j, k+nodes))

        nodes += new_nodes

    # Compute transitive graph
    G = Graph()
    # Append nodes
    for i in range(nodes):
        G.nodes.append(i)
    # Append adjacencies
    for i in range(len(adjacency)):
        G.edges.append(adjacency[i])

    N = G.Nodes()
    for x in N:
        for y in N:
            for z in N: 
                if (x, y) != (y, z) and (x, y) != (x, z):
                    if (x, y) in G.edges and (y, z) in G.edges:
                        G.remove_Edge(x, z)

    # Print graph
    for i in range(nodes):
        print(i)
    print()
    for value in G.edges:
        print(str(value[0]) + ' ' + str(value[1]))

get_random_dag()

以下、最初の図に、@ ArjunShankarコードによって生成された多くの冗長エッジを持つランダムDAGが表示されている場合があります。 2番目の図は、ランダムDAGの推移的なセットです。

Random DAG

Transitive Graph

推移的

def get_random_dag():
    MIN_PER_RANK = 1    # Nodes/Rank: How 'fat' the DAG should be
    MAX_PER_RANK = 3
    MIN_RANKS = 15   # Ranks: How 'tall' the DAG should be
    MAX_RANKS = 20
    PERCENT = 0.3  # Chance of having an Edge
    nodes = 0
    node_counter = 0

    ranks = random.randint(MIN_RANKS, MAX_RANKS)

    adjacency = []
    rank_list = []
    for i in range(ranks):
        # New nodes of 'higher' rank than all nodes generated till now
        new_nodes = random.randint(MIN_PER_RANK, MAX_PER_RANK)

        list = []
        for j in range(new_nodes):
            list.append(node_counter)
            node_counter += 1
        rank_list.append(list)

        print(rank_list)

        # Edges from old nodes ('nodes') to new ones ('new_nodes')
        if i > 0:
            for j in rank_list[i - 1]:
                for k in range(new_nodes):
                    if random.random() < PERCENT:
                        adjacency.append((j, k+nodes))

        nodes += new_nodes

    for i in range(nodes):
        print(i)
    print()
    for Edge in adjacency:
        print(str(Edge[0]) + ' ' + str(Edge[1]))
    print()
    print()
0
WillEnsaba