본문 바로가기
👩‍💻Developer/Language

[C] Graph In C

by 블루누들 2022. 12. 15.

* 해당 글은 학과 수업으로 배운 내용과 코드가 포함되어 있으며 개인적 공부 목적으로 업로드하였습니다.

 

Graph는 점처럼 표시된 vertices 혹은 nodes가 line으로 표시된 edges와 연결된 형태를 나타낸다. 

각 edge는 한쌍의 vertices를 연결하고 있으며 두 vertices 사이에 최대 1개의 edge가 존재할 수 있다.

 

우리가 이 게시글에서 다룰 graph는

 - undirected : edge(B, A) = edge(A, B)

- no self-edges : (v, v) 사이에는 edge가 존재하지 않는다

graph는 vertices와 edge pair로 나타낼 수 있다. 

G = (V, E)

 

어떤 vertex의 neigbors는 해당 vertex와 edge를 통해 연결된 모든 vertices를 말한다. 

 

Lightsout

Lightsout은 n*n lights board에서 move를 통해서 직접 누른 light뿐만 아니라 주변 light까지 꺼지면 키고 켜지면 끄는 형식으로 진행하는 게임이며 모든 불이 다 꺼지면 끝나는 게임이다. 

 

이를 그래프로 생각하자면 vertex가 각 board configuration이고 각 움직임이 edge로 표현된다. 

-light를 두번 누르면 다시 이전 상태의 board로 돌아오게 된다. 

-self-edge가 없기 때문에 움직임은 무조건 새로운 board를 만든다.

 

해당 board를 풀기 위해서 우리는 모든 불을 끌 move sequence를 찾아야 한다. 

 

edge를 통해서 연결된 여러개의 vertices는 path라고 부른다. 

-즉 edge로 연결된 vertices series를 의미한다. 

-따라서 두 vertices 사이에 path는 다양하게 여러개 있을 수 있다. 

-따라서 board를 solve하는 것은 path를 찾는것과 마찬가지이다. 

 

n*n lightout에서 우리는 n*n 개의 move를 만들 수 있고 2^(n*n) board configuration이 존재할 수 있다.

graph로 나타내면

2^(n*n) vertices가 존재할 수 있고 n*n * 2^(n*n) / 2 edges가 존재할 수 있다. 

-undirected이기 때문에 2로 나눠준다. 

 

Graph는 conceptual model이 될 수도 있고 data structure만 될 수 도 있다. 

 

Data structure로 나타내기 위해서는

-graph 자체

-graph vertices (vertex type) : 이는 unsigned int로 정의된다

- graph edges

들을 나타내야 한다. 

 

graph를 활용한 기본 operation에는

-graph_new(n) : n vertices를 가진 새로운 graph를 만드는 것

-graph_size(G) : G의 vertices 개수를 return 하는 것

-graph_hasedge(G, v,w): G가 (v,w) 사이에 edge를 가지고 있는지 확인하는 것

-graph_addedge(G, v, w) : graph G에 (v, w) edge를 더한다. 

-graph_free(G) : graph를 free 시킨다. 

typedef unsigned int vertex;
typedef struct graph_header* graph_t;

graph_t graph_new(unsigned int numvert);
//@ensures \result!=NULL;

void graph_free(graph G);
//@requires G!=NULL;

unsigned int graph_size(graph_t G);
//@requires G!=NULL;

bool graph_hasedge(graph_t G, vertex v, vertex w);
//@requires G!=NULL;
//@requires v<graph_size(G) && w<graph_size(G);

void graph_addedge(graph_t G, vertex v, vertex w);
//@requires G!=NULL;
//@requires v<graph_size(G) && w<graph_size(G);
//@requires v!= w && !graph_hasedge(G, v,w); //오직 새로운 edge만 추가 가능

예시로 새로운 graph를 만들어보자

graph_t G = graph_new(5);
graph_addedge(G, 0, 1);
graph_addedge(G, 0, 4);
graph_addedge(G, 1, 2);
graph_addedge(G, 1, 4);
graph_addedge(G, 2, 3);
graph_addedge(G, 2, 4);

그림으로 나타내면 아래와 같이 나타낼 수 있다. 

이는 graph_hasedge(G, 3, 2) == true이지만

graph_hasedge(G, 3, 1) == false가 된다. 

 

Neighbors

neighbors 역시 비슷하게 interface로 나타낼 수 있다. 

abstrac type of neighbors: neighbors_t

Operations:

-graph_get_neighbors(G, v) : G의 vertex v의 neighbor를 return할 수 있다.

-graph_hasmore_neighbors(nbors) : 추가적인 neighbors가 있는지 체크한다.

-graph_next_neighbor(nbors) : 다음 neighbor를 return한다.

-graph_free_neighbors(nbors): unexamined 된 이웃들을 free 시킨다. 

typedef struct neighbor_header* neighbors_t;

neighbors_t graph_get_neighbors(graph_t G, vertex v);
//@requires G!=NULL && v<graph_size(G);
//@ensures \result !=NULL;

bool graph_hasmore_neighbors(neighbors_t nbors);
//@requires nbors!=NULL;

vertex graph_next_neighbor(neighbors_t nbors);
//@requires nbors != NULL;
//@requires graph_hasmore_neighbors(nbors);

void graph_free_neighbors(neighbors_t nbors);
//@requires nbors != NULL;

위 만들어놓은 graph를 활용해 예시를 들어보자 

neighbors_t n4 = graph_get_neighbors(G, 4);

-> n4는 0,1,2를 가지게 된다.

vertex a = graph_next_neighbor(n4);

-> a 는 0,1,2 모두가 될 수 있다. 

-> 여기서 1이라고 가정해보자

vertex a = graph_next_neighbor(n4);

-> a는 0, 2가 될 수 있다. 

-> 여기서 0이라고 가정해보자

vertex a = graph_next_neighbor(n4);

-> a는 2여야만 한다. 

graph_hasmore_neighbors(n4);

-> 모든 vertex를 다 보았으므로 false를 return 해야한다.

 

이러한 graph를 실제로 implement하기 위해서는

-linked list of edges

-hash set이 필요하다

 

Cost Operations

만약 graph가 v개의 vertices가 있다면 edges는

-0개에서 v(v-1)/2개가 될수 있다. 

따라서 e∈ O(v^2)이다.

 

각 linked list 와 hash set edges의 cost를 살펴보면

  Linked list of edges Hash set of edges
graph_hasedge O(e) O(1) avg
graph_addedge O(1) O(1) avg+ amortized

특정 vertex의 neighbor를 찾는 것은 모든 edge를 확인해야 하는 것과 같다. 

-> graph_get_neighbors는 O(e)의 cost를 가진다는 뜻이다. 

 

하나의 vertex에 총 몇개의 neighbors가 있을 수 있을까?

최대 v-1: 만약 vertex가 다른 모든 vertices와 edge가 있을 경우

최대 e: graph edges보다 많은 neighbors는 있을 수 없다. 

 

따라서 하나의 vertex는 O(min(v,e)) neighbors가 존재한다. 

위의 표와 종합해보면

  linked list of edges hash set of edges
graph_hasedge O(e) O(1) avg
graph_addedge O(1)  O(1) avg+amort
graph_get_neighbors O(e) O(e)
iterating through neighbors O(min(v,e)) O(min(v,e))

 

graph를 나타내는 두가지의 representation을 살펴보자

 

Adjacency Matrix Representation

-graph를 v*v boolean matrix로 나타낼 수 있다. 

만약 i 와 j 사이에 edge가 있다면 M[i,j]== true 

그렇지 않다면 M[i,j] == false

이때 M을 adjacency matrix라고 부른다. 

 

matrix로 각 operation cost를 계산해보면

graph_hasedge(G, v, w) = O(1)

graph_addedge(G, v, w) = O(1)

graph_get_neighbors(G,v) = O(v)

-v vertex의 row를 다 거쳐야 한다. 

 

필요한 space: O(v^2)

 

Adjacency List Representation

각 vertex v별로 그 neighbors를 list에 저장해놓은 형태를 

adjacency list of v라고 한다. 

각 operation cost를 계산해보면

graph_hasedge(G, v, w) = O(min(v,e))

graph_addedge(G, v, w) = O(1)

graph_get_neighbors(G,v) = O(v)

 

필요한 space: O(v+e)

- v element array 와 2e list items

O(v+e) 는 O(max(v,e))로 쓰인다. 

 

matrix와 list별 cost를 정리해보면

  Adjacency Matrix Adjacency List
space O(v^2) O(v+e)
graph_hasedge O(1) O(min(v,e))
graph_addedge O(1) O(1)
graph_get_neighbors O(v) O(1)
iterating through neighbors O(min(v,e)) O(min(v,e))

그렇다면 어떤 representation을 언제 써야할까?

dense graph: edge가 많은 경우 

sparse graph: 상대적으로 edge가 적은 경우

 

Dense graph의 cost

  Adjacency matrix Adjacency list
space O(v^2) O(v+e) -> O(v^2)
graph_hasedge O(1) O(min(v,e)) -> O(v)
graph_addedge O(1) O(1)
graph_get_neighbors O(v) O(1)
iterating through neighbors O(min(v,e)) -> O(v) O(min(v,e)) -> O(v)

graph_has_edge는 matrix가 빠르고

graph_get_neighbors는 list가 빠르다.

따라서 dense graphs는 두 representations은 같은 비용을 가지지만 graph_hasedge는 matrix가 더 빠르다. 

-> matrix를 사용하는 것이 더 효율적이다. 

 

Sparse graph의 cost

  Adjacency matrix Adjacency list
space O(v^2) O(v+e) -> O(v)
graph_hasedge O(1) O(min(v,e)) -> O(v)
graph_addedge O(1) O(1)
graph_get_neighbors O(v) O(1)
iterating through neighbors O(min(v,e)) -> O(v) O(min(v,e)) -> O(v)

list가 훨씬 더 적은 space를 요구한다는 것을 확인할 수 있다. 

graph_has_edge는 matrix가 빠르고

graph_get_neighbors는 list가 빠르다.

따라서 sparse graphs의 경우 두 representations은 같은 비용을 가지지만 list가 더 적은 space가 사용되기 때문에 list를 사용하는 것이 더 효율적이다. 

 

Adjacency list는 vertices가 각 node인 null-terminated list와 같다. 

이 graph data structure의 경우

graph vertices개수와 그 개수만큼의 array인 adjacency list가 존재한다

typedef struct adjlist_node adjlist;
struct adjlist_node{
  vertex vert;
  adjlist* next;
};

typedef struct graph_header graph;
struct graph_header{
  unsigned int size;
  adjlist** adj;
};

이 interface는 또한 vertex를 아래와 같이 정의한다.

typedef unsigned int vertex;

bool is_vertex(graph* G, vertex v){
  REQUIRES(G !=NULL);
  return v <G->size; //v가 unsigned int 타입이라서 0<= v는 따로 쓰지 않아도 된다. 
}

vertex는 0과 graph size사이에 있는 값이어야 valid하다

 

graph의 경우 

-non-NULL

-length of adjacency listr가 graph size와 동일해야 하고

-각 adjacency list가 valid 해야한다. 

bool is_graph(graph* G){
  if(G==NULL) return false;
  //@assert(G->size==\length(G->adj));
  for (unsigned int i=0; i<G->size; i++){
    if(!is_adjlist(G, i, G->adj[i])) return false;
  }
  return true;
}

각 adjacency list의 경우

-Null-terminated

-각 vertex가 valid해야하고

-self-edges가 없어야하고

-모든 outgoing edges는 다시 돌아올 edge가 있어야 하고

-duplicate edge가 없어야 한다. 

bool is_adjlist(graph* G, vertex v, adjlist* L){
 REQUIRES(G!=NULL)
 //@requires (G->size == \length(G, adj));
 if (!is_acyclic(L)) return false;
 while(L!=NULL){
   vertex w = L->vert; //w is a neighbor of v
   
   //neighbors are legal vertices
   if (!is_vertex(G, w)) return false;
   
   //no self-edges
   if (v==w) return false;
   
   //every outgoing edge has corresponding edge coming back to it
   if (!is_in_adjlist(G->adj[w],v)) return false;
   
   //Edges aren't duplicated
   if (is_in_adjlist(L->next, w)) return false;
   
   L = L->next;
  }
  return true;
}

Basic operations

-graph_size: size를 return한다. (O(1))

unsigned int graph_size(graph* G){
  REQUIRES(is_graph(G));
  return G->size;
}

-graph_new : empty adjacency list를 만든다.

O(v) : v 개의 positions을 만들어야 한다.

graph* graph_new(unsigned int size){
  graph* G = xmalloc(sizeof(graph));
  G->size = size;
  G->adj = xcalloc(size, sizeof(adjlist*));
  ENSURES(is_graph(G));
  return G;
}

-graph_free는 모든 adjacency list, array, graph header를 free해야 한다. 

O(v+e): adjacency lists에 있는 2e nodes + v array positions

void graph_free(graph* G){
  REQUIRES(is_graph(G));
  for (unsigned int i=0; i<G->size; i++){
    adjlist* L = G->adj[i];
    while (L!=NULL){
      adjlist*tmp = L->next;
      free(L)
      L = L->next;
    }
  }
  free(G->adj);
  free(G);
}

-graph_hasedge(G, v, w)는 v positions의 adjacency list에 w를 Linear search로 찾아나간다. 

O(min(v,e))

bool graph_hasedge(graph* G, vertex v, vertex w){
  REQUIRES(is_graph(G));
  REQUIRES(is_vertex(G, v)&&is_vertex(G,w));
  
  for (adjlist* L = G->adj[v]; L!=NULL; L = L->next){
    if(L->vert==w) return true;
  }
  return false;
}

-graph_addedge(G, v,w): v와 w 사이에 edge를 추가한다.

-w를 v의  neighbor로, v를 w의 neighbor로 

O(1)

void graph_addedge(graph* G, vertex v, vertex w){
  REQUIRES(is_graph(G));
  REQUIRES(is_vertex(G,v) && is_vertex(G,w));
  REQUIRES(v!=w && !graph_hasedge(G, v, w));
  
  adjlist*L;
  
  L = xmalloc(sizeof(adjlist));
  L->vert = w;
  L->next = G->adj[v];
  G->adj[v] = L;
  
  L = xmalloc(sizeof(adjlist));
  L->vert = v;
  L->next = G->adj[w];
  G->adj[w] = L;
  
  ENSURES(is_graph(G));
  ENSURES(graph_hasedge(G, v, w));
  }

vertex의 adjaceny list 를 neighbors의 representation으로 나타낼 수 있다. 

typedef struct neighbor_header neighbors;
struct neighbor_header{
  adjlist* next_neighbor;
}

graph_get_neighbors(G,v)는

새로운 neighbor struct를 만들고 next_neighbor가 adjacency list v를 point하도록 하고 이 struct를 return 해야 한다. 

O(1)

neighbors* graph_get_neighbors(graph* G, vertex v){
   REQUIRES (is_graph(G) && is_vertex(G, v));
   
   neighbors* nbors = xmalloc(sizeof(neighbors));
   nbors->next_neighbor = G->adj[v];
   ENSURES(is_neighbors(nbors));
   return nbors;
 }

graph_next_neighbor는 말그대로 next_neighbor를 return하고 이것이 adjacency list에서 다음 field를 point하도록 한다. 

O(1)

vertex graph_next_neighbor(neighbors* nbors){
  REQUIRES(is_neighbors(nbors));
  REQUIRES(graph_hasmore_neighbors(nbors));
  
  vertex v = nbors->next_neighbor->vert;
  nbors->next_neighbor = nbors->next_neighbor->next;
  return v;
}

graph_hasmore_neighbors: adjacency list의 끝에 다다랐는지 확인한다. 

O(1)

bool graph_hasmore_neighbors(neighbors* nbors){
  REQUIRES(is_neighbors(nbors));
  return nbors->next_neighbor !=NULL;
}

graph_free_neighbors는 neighbor header만 free시킨다.

(adjacency list를 free하지 않도록 해야한다. )

O(1)

void graph_free_neighbors(neighbors* nbors){
  REQUIRES(is_neighbors(nbors));
  free(nbors);
}

cost를 종합해보자면

  Adjacency list
space O(v+e)
graph_new O(v)
graph_free O(v+e)
graph_size O(1)
graph_hasedge O(min(v,e))
graph_addedge O(1)
graph_get_neighbors O(1)
graph_hasmore_neighbors O(1)
graph_next_neighbor O(1)
graph_free_neighbor O(1)

 

graph interface를 활용해서 graph를 print할 수 있는 client function을 작성해보자

void graph_print(graph_t G){
  for (vertex v = 0; v < graph_size(G); v++){
    printf("Vertices connected to %u: ", v);
    neighbors_t nbors = graph_get_neighbors(G, v);
    while(graph_hasmore_neighbors(nbors)){
       vertex w = graph_next_neighbors(nbors);
       printf("%u", w);
    }
    graph_free_neighbors(nbors);
    printf("\n");
  }
}

adjacency list representation을 고려하여 v vertices와 e edges가 있다고 했을때

void graph_print(graph_t G){
  for (vertex v = 0; v < graph_size(G); v++){     //v times
    printf("Vertices connected to %u: ", v);      //O(1)          O(v)
    neighbors_t nbors = graph_get_neighbors(G, v);  //O(1)        O(v) 
    while(graph_hasmore_neighbors(nbors)){         //O(min(v,e))  O(v min(v,e))
       vertex w = graph_next_neighbors(nbors);     //O(1)         O(v min(v,e))
       printf("%u", w);
    }
    graph_free_neighbors(nbors);                   //O(1)         O(v min(v,e))
    printf("\n");
  }
}

cost: O(vmin(v,e))

 

하지만 이때 전체 graph_print는 모든 edge를 두번씩 방문한다는 점을 생각해야 한다. 

따라서 내부 while loop는 O(e)임을 알 수 있다. 

void graph_print(graph_t G){
  for (vertex v = 0; v < graph_size(G); v++){     //v times
    printf("Vertices connected to %u: ", v);      //O(1)          O(v)
    neighbors_t nbors = graph_get_neighbors(G, v);  //O(1)        O(v) 
    while(graph_hasmore_neighbors(nbors)){         //O(e)         O(v + e)
       vertex w = graph_next_neighbors(nbors);     //O(1)         O(v + e)
       printf("%u", w);
    }
    graph_free_neighbors(nbors);                   //O(1)         O(v + e)
    printf("\n");
  }
}

따라서 실제 cost는 O(v+e)이다.

 

같은 graph_print를 matrix representation으로 본다면

void graph_print(graph_t G){
  for (vertex v = 0; v < graph_size(G); v++){     //v times
    printf("Vertices connected to %u: ", v);      //O(1)          O(v)
    neighbors_t nbors = graph_get_neighbors(G, v);  //O(v)        O(v^2) 
    while(graph_hasmore_neighbors(nbors)){         //O(e)         O(v^2 + e)
       vertex w = graph_next_neighbors(nbors);     //O(1)         O(v^2 + e)
       printf("%u", w);
    }
    graph_free_neighbors(nbors);                   //O(1)         O(v^2 + e)
    printf("\n");
  }
}

matrix representation에서의 graph_print의 실제 cost는 O(v^2+e)이다. 

'👩‍💻Developer > Language' 카테고리의 다른 글

[C] Search in Graph  (0) 2022.12.16
[C]Bytecode In C  (1) 2022.12.15
[C] C에서 숫자란?  (0) 2022.12.14
[C/C++] C언어의 Memory model  (1) 2022.11.29
[C] C언어에 대해 알아보자  (0) 2022.11.25

댓글