生成随机DAG

时间:2012-10-08 22:26:34

标签: c++ c algorithm graph cycle

我正在解决有向无环图上的问题。

但是我在一些有向无环图上测试我的代码时遇到了麻烦。测试图应该很大,并且(显然)非循环。

我尝试了很多代码来编写用于生成非循环有向图的代码。但我每次都失败了。

是否有一些现有方法可以生成我可以使用的非循环有向图?

9 个答案:

答案 0 :(得分:47)

我制作了一个C程序来做到这一点。关键是“排名”节点,从较低排名的节点绘制边缘到较高排名的节点。

我写的程序打印在the DOT language

以下是代码本身,其中的注释解释了它的含义:

#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

答案 1 :(得分:12)

https://mathematica.stackexchange.com/questions/608/how-to-generate-random-directed-acyclic-graphs的答案适用:如果你有一个图的边缘的邻接矩阵表示,那么如果矩阵是下三角形,那么它必然是一个DAG。

类似的方法是对节点进行任意排序,然后仅在 x&lt; x 时考虑从节点 x y 的边缘。 ÿ。这种约束也应该通过构造得到你的DAGness。如果您使用结构来表示节点,则内存比较将是订购节点的一种任意方式。

基本上,伪代码类似于:

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

有序对(&#34; N选择2&#34;),我们可以选择是否具有它们之间的边缘。

答案 2 :(得分:3)

您可以生成随机有向图,然后对周期进行深度优先搜索。当你找到一个循环时,通过删除边缘来打破它。

我认为这是最糟糕的情况O(VE)。每个DFS取O(V),每个DFS移除至少一个边(所以最大E)

如果通过均匀随机选择所有V ^ 2可能的边生成有向图,并且您以随机顺序DFS并删除随机边 - 这将使您在所有可能的范围内均匀分布(或至少接近它)的DAG。

答案 3 :(得分:3)

所以,试着将所有这些合理的答案放在一起:

(下面,我用V表示生成图中的顶点数,E用边数表示,我们假设E≤V(V-1)/ 2。)

就我个人而言,我认为最有用的答案是Flavius的评论,他指的是http://condor.depaul.edu/rjohnson/source/graph_ge.c的代码。该代码非常简单,并且通过注释方便地描述,我重现:

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. 如果他们相等就拒绝他们;
  3. 如果第一个更大,则交换它们;
  4. 如果之前已经生成它们,请拒绝它们。
  5. 此解决方案的问题在于,当E接近最大边数V(V-1)/ 2时,算法变得越来越慢,因为它必须拒绝越来越多的边缘。更好的解决方案是制作所有V(V-1)/ 2个可能边缘的矢量;随机洗牌;并选择随机列表中的第一个(请求的边)边。

    reservoir sampling algorithm让我们在空间O(E)中这样做,因为我们可以从k的值推导出k th 边缘的端点。因此,我们实际上不必创建源向量。但是,它仍然需要O(V 2 )时间。

    或者,可以进行Fisher-Yates shuffle(或Knuth shuffle,如果您愿意),在E迭代后停止。在维基百科中提供的FY shuffle版本中,这将生成尾随条目,但该算法也可以向后运行:

    // 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(N 2 )空间。事实上,这可以通过一些技巧改进到O(E)空间,但是SO代码片段太小而不能包含结果,因此我将在O(E)空间和O(E)中提供更简单的代码片段。记录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;
    }
    

答案 4 :(得分:2)

一种非常简单的方法是:

  1. 通过迭代下对角矩阵的索引来随机分配边(如上面的链接所示:https://mathematica.stackexchange.com/questions/608/how-to-generate-random-directed-acyclic-graphs

  2. 这将为您提供可能包含多个组件的DAG。您可以使用Disjoint-set数据结构为组件提供可以通过在组件之间创建边来合并的组件。

  3. 此处描述了不相交集:https://en.wikipedia.org/wiki/Disjoint-set_data_structure

答案 5 :(得分:1)

如果nn1,请创建一个包含n2个节点以及每对节点n1 != n2n2 % n1 == 0之间边缘的图表。

答案 6 :(得分:1)

我最近尝试重新实施已接受的答案,发现它是不确定的。如果不强制执行min_per_rank参数,最终可能会得到一个包含0个节点的图形。

为了防止这种情况,我将for循环包装在一个函数中,然后检查以确保在每个排名之后min_per_rank满足。{1}}。这是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
}

答案 7 :(得分:0)

@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()

下面,您可能会在第一张图中看到随机DAG,它具有@ArjunShankar代码生成的许多冗余边。第二个数字是随机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()

答案 8 :(得分:0)

生成可能未连接的随机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)只是topologically sorted无向图。 n个顶点的无向图最多可以包含n * (n - 1) / 2个边,不计算重复的边或从顶点到自身的边。现在,您只能具有从较低顶点到较高顶点的边。因此,所有边缘的方向都是预定的。

这意味着您可以使用n * (n - 1) / 2边缘权重的一维数组来表示整个DAG。边缘权重0表示该边缘不存在。因此,我们只创建一个零或一的随机数组,这就是我们的随机DAG。

i个顶点的DAG中,从顶点j到顶点n的边,其中i < j的边权重为索引k,其中{ {1}}。


生成连接的DAG

一旦生成随机DAG,就可以使用以下功能检查它是否已连接。

k = n * i + j - (i + 1) * (i + 2) / 2

如果未连接,则其complement will always be connected

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
};

请注意,如果我们创建具有30%边缘的随机DAG,则其补码将具有70%边缘。因此,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); }; 的唯一安全值为50%。但是,如果您对连接性的关注程度超过边缘的百分比,那么这不应该成为破坏交易的事情。

最后,将它们放在一起。

x
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);
});

如果多次运行此代码段,则DAG的边缘可能会比其他边缘多得多。


生成具有一定百分比的边的连接的DAG

如果您同时考虑连接性和一定比例的边缘,则可以使用以下算法。

  1. 从完全连接的图开始。
  2. 随机删除边缘。
  3. 移除边后,检查图形是否仍处于连接状态。
  4. 如果不再连接,则重新添加该边缘。

应该注意的是,这种算法的效率不如以前的方法。

<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>
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);
});

希望有帮助。