web-dev-qa-db-ja.com

グラフ内の三角形の数を数えるための効率的なアルゴリズムは何ですか?

無向グラフの三角形の数を数えるための効率的なアルゴリズムは何ですか)(グラフは頂点とエッジのセットです)?私はグーグルを検索し、教科書の棚を3日間続けて毎日数時間読んでいます。

これは、そのようなアルゴリズムが必要な宿題用ですが、それを開発しても、課題には何の影響もありません。このようなアルゴリズムは外部のリソースから簡単に見つけることができると期待されていますが、私はもう終わりです。

明確にするために、グラフの三角形は長さ3のサイクルです。秘訣は、最大10,000ノードの頂点セットで動作する必要があることです。

私は現在C#で作業していますが、コピーして貼り付けるコードよりも、この問題を解決するための一般的なアプローチに関心があります。

大まかに言えば、これまでの私の試みには次のものが含まれていました。

  • 長さ3のすべての一意のサイクルを追跡した幅優先探索。これは私には良い考えのように思えましたが、機能させることができませんでした
  • グラフ内のすべてのノードをループして、3つの頂点がエッジを共有しているかどうかを確認します。これは、より大きなデータセットの実行時間が遅すぎます。 O(n ^ 3)。

アルゴリズム自体は、クラスタリング係数の計算の一部です。

27
XBigTK13X

深さ優先探索が必要になります。アルゴリズムは次のようになります。

1)現在のノードについては、訪問していないすべての隣接ノードに問い合わせます

2)これらのノードのそれぞれについて、深さ2を実行し、深さ2のノードがステップ1の現在のノードであるかどうかを確認します。

3)現在のノードを訪問済みとしてマークします

4)訪問していない隣接ノードをそれぞれ現在のノード(1つずつ)にして、同じアルゴリズムを実行します

4
evhen14

三角形のカウントは確かに難しく、計算コストがかかります。おそらくこれは、理由を理解するための良い出発点です: 次数ベースの頂点分割による大きなグラフでの効率的な三角形のカウント

適切なループは、n個のノードのそれぞれをそれぞれの隣接ノード(n *(n-1))に対してチェックし、サイクルを続行して、n個の隣接ノードの隣接ノードがnであるかどうかを確認する必要があります:(n *(n-1))(n- 1)(n-1)、これは10000nでほぼ10 ^ 16です。 100万ノードの場合、これらのループはばかげていますが、10000の場合、ブルートフォースしたい場合はまったく問題ありません:)

C#でコーディングするとおっしゃいましたが、グラフ(Cで使用可能)には、GaborCsardiによって作成された三角形をカウントするための優れたアルゴリズムがあります。 5年前のラップトップで1.3秒で10000ノードと100万エッジのランダムグラフで130万の三角形を数えました:)GaborCsardiが質問する人です:)

さまざまなプログラム的アプローチの観点から、ネットワークを保存しているデータを確認する必要があるかもしれません。隣接行列に格納されている場合、ループの数は固定されていますが、3つのエッジのネットワークのエッジリストでは、ループの数はノードの数に関係なく3の倍数です。 i-> jのすべての組み合わせをテストしなくても、ノードのネイバーをエッジリストに要求できます。

アプローチを説明し、さまざまなアルゴリズムの速度を非常に基本的な方法で測定するために、Rで教育スクリプトを作成しました。ここでのRの使用に固有の速度の問題はたくさんあります(エッジリストバージョンはエッジが多すぎるために完全に圧倒されます)が、コード例では、ブルートの速度について考える方法についていくつかのアイデアが流れるはずです-トライアングルカウントを強制します。これはRにあり、非常にきれいではありませんが、よくコメントされています。言語の壁を打ち破ることができれば幸いです。

ではごきげんよう。

# Counting triangles in a random graph using igraph and two different
# and almost equally stupid approaches looping through the 1) adjacency
# matrix and 2) the Edge-list in R.

# Use igraph and these configs
library(igraph)
V <- 100
E <-  1700

# This is the random graph that we will use
g <- erdos.renyi.game(type="gnm", n=V, p=E, directed=FALSE, loops=FALSE)

# igraph has such a fast algorythm. Long live Gabor Csardi!
benchmark <- proc.time()["elapsed"]
       triangle.count <- sum(count_triangles(g)/3)
gabor.Csardi.benchmark <- proc.time()["elapsed"] - benchmark

# For not to big networks we can plot them to get a basic feel
if(length(V(g)) < 100){
       V(g)$size <- 5
       plot(g)
}

# The adjacency matrix approach will have to deal with a crazy
# ammount of looping through pairs of matrix combinations to
# find links:

# We'll loop through each node to check it's participation in triangles
# notice that a triangle ijk will be participated in by nodes
# i, j, and k, and that each of those nodes have two triangular counts.
# All in all the structures ijk, ikj, jik, jki, kij, kji are each counted
# but shall be returned as 1 triangle. We will therefore devide our
# search-result by 6 later on.

# Store our progess in this matrix to look at how we did later on
progress <- matrix(0, nrow=length(V(g)), ncol=8)

# Loop through all nodes to find triangles in an adjacency matrix
benchmark <- proc.time()["elapsed"] # Measure time for this loop
for(i in 1:length(V(g))){
       # Node i has connections to these nodes:
       i.neighbors <- as.vector( neighborhood(g, 1, nodes=i)[[1]] )
       i.neighbors <- setdiff(i.neighbors, c(i)) # i should not be part of its own neighborhood

       # for each i, tri is the number of triangles that i is involved in
       # using any j or any k. For a triangle {1,2,3}, tri will be 2 for
       # i==1, since i is part of both triangles {1,2,3} and {1,3,2}:
       tri <- 0

       for(j in i.neighbors)
       {
              # Node j has connections to these nodes:
              j.neighbors <- as.vector( neighborhood(g, 1, nodes=j)[[1]] )
              j.neighbors <- setdiff(j.neighbors, c(j)) # j should not be part of its own neighborhood

              # Were any of j's neighbors also a neighbor of i?
              k <- intersect(i.neighbors, j.neighbors)

              tri <- tri + length(k)
       }

       # Save our findings to the progress matrix
       progress[i,1] <- tri
       progress[i,7] <- proc.time()["elapsed"] - benchmark
}
progress[,2] <- sapply(1:length(progress[,1]), function(x) sum(progress[,1][1:x]))
progress[,3] <- round(progress[,2] / 6, digits=2)

# The Edge-list approach uses a list of all edges in the network to loop through instead
# Here, I suppose, a lot of the extra speed could arise from R being better at looping
# with lapply() and at finding data in a data.frame that the brute-force loop above is.
el <- as.data.frame(as.matrix(get.edgelist(g, )))

# This is ugly. Make the edgelist contain all edges as both i->j and j->i. In
# the igraph object, they are only given as low i to high j by get.edgelist()
  el.rev <- data.frame(el[,2], el[,1])
  names(el) <- names(el.rev) <- c("i","j")
  el <- rbind(el, el.rev)

# these nodes are connected (we'd only need to bother abouth non isolates)
nodes <- sort(unique(c(el$i, el$j)))
tri <- 0

# Loop through connected nodes to find triangles in Edge-list
benchmark <- proc.time()["elapsed"] # Measure time for this loop
for(i in nodes){
       i.neighbors <- el[el$i==i,]$j
       # i's neighbors are the $j:s of the edgelist where $i:s are i. 

       k.list <- unlist(lapply(i.neighbors, function(x) intersect(i.neighbors,el[el$i==x, ]$j)))
       # lists nodes that can be a k in an ijk-triangle for each of i's neighboring j:s
       # If 1 has neighbors 2 and 3, el[el$i==x, ]$j) will be first, the neighbors of 2 and then
       # the neighbors of 3. When intersected with the neighbors of i, k:s will be found. If
       # {1,2,3} is a triangle one k will be 3 for {i=1, j=2}, and another k will be 2 for {i=1, j=3}

       # k.list might be NULL
       tri.for.this.i <- (as.numeric(length(k.list)) / 2)
       # Here we devide by two since i can be in a triangle with j and k lik {ijk} and {ikj}
       # We will later have to devide by 3 more, since each triangle will be counted for
       # each node i that we loop through

       # Save the counting to the progress
       tri <- tri.for.this.i + tri
       progress[i,4] <- as.numeric(tri.for.this.i)
       mm <- c(mm, i)
       progress[i,8] <- proc.time()["elapsed"] - benchmark
}
progress[,5] <- sapply(1:length(progress[,4]), function(x) sum(progress[,4][1:x]))
progress[,6] <- round(progress[,5] / 3, digits=2)

# Fix the results into a usable format
results <- data.frame(c("igraph", "adjacency-loop", "Edge-loop"),
                      c(triangle.count, max(progress[,3]), max(progress[,6])),
                      c(gabor.Csardi.benchmark, (max(progress[,7]) - min(progress[,7])), (max(progress[,8]) - min(progress[,8]))))
row.names(results) <- c("igraph", "Adjacensy-loop", "Edge-loop")
names(results) <- c("Routine", "Triangle count", "Execution-time")

# Now we have run two approaches of more or less the same thing.
# Not only should the igraph triangle.count, and the two loops
# be identical, but the progress of the two methods should too.
progress[,3] == progress[,6]
plot(progress[,6], type="l", col="blue")
lines(progress[,7], col="green")

# Look at the result:
View(results)
2
nJGL

グラフの表現方法によって異なります。

隣接行列Aがある場合、三角形の数はtr(A ^ 3)/ 6、つまり対角要素の合計の1/6倍である必要があります(分割によって方向と回転が処理されます)。

隣接リストがある場合は、すべてのノードから開始して、深さ3の検索を実行します。そのノードに到達する頻度を数えます->もう一度6で割ります。

1
Michael Nett

ここで引用されているように: https://math.stackexchange.com/a/1170

三角形の検索とカウントで知られている最速のアルゴリズムは、高速行列積に依存し、O(nω)時間計算量を持ちます。ここで、ω<2.376は高速行列積の指数です。ただし、このアプローチでは、θ(n2)空間が複雑になります。

0
Yash Sharma