대학생활/수업

게임인공지능 6주차 - Graph, Path Finding, Shortest Path, Dijkstra

se.jeon 2023. 10. 10. 13:03
728x90
반응형

What is a Graph?

그래프 : 현상이나 사물을 정점(vertex)와 간선(edge)로 표현한 것

 

Graph G = (V, E), where V is a set of vertex and E is a set of edges

인접(Adjacent) : 두 정점이 간선으로 연결되어 있는 것

경로(Path) : 하나의 정점 v에서 시작해서 또 하나의 정점 v로 가기 위한 일련의 에지

 

지하철 노선도, 네비게이션과 같은 경우도 일종의 그래프.

그래프의 유형

- 가중치 그래프(Weighted Graph) : edge에 가중치 값이 할당된 그래프

- 방향 그래프 (Directed Graph, Di-Graph) : 방향이 존재하는 그래프

- 무방향 그래프 (Undirected Graph) : edge의 방향성이 없는 그래프

- 연결 그래프 (Connected Graph) : 모든 정점들 간에 갈 수 있는 경로 존재

- 절단 그래프 (Disconnected Graph) : 한 개의 정점이라도 서로 갈 수 있는 길이 없는 그래프

- 완전 그래프 (Complete Graph) : 모든 정점들 사이에 직접 연결된 간선이 존재하는 그래프

 

N X N 행렬

- 원소 (i, j) = 1 : 정점 i와 정점 j 사이에 간선이 있음

- 원소 (i, j) = 0 : 정점 i와 정점 j 사이에 간선이 없음

 

유향 그래프의 경우

- 원소 (i, j) = 1 : 정점 i로부터 정점 j로 연결되는 간선을 나타냄

 

가중치 그래프

- 원소 (i, j)는 1 대신에 가중치를 가짐

 

인접 리스트

- N개의 연결 리스트로 표현

- i번째 리스트 : 정점 i에 인접한 정점들을 리스트로 연결

- 가중치 그래프인 경우 : 리스트에 가중치도 보관

인접 행렬 vs 인접 리스트

두 노드가 인접해 있는지 판단 : 인접 행렬이 유리

전점 i에 인접한 모든 노드 찾기 : 인접 리스트가 유리

 

공간

- 인접 행렬 : V^2

- 인접 리스트 : 2E

 

게임에서의 그래프

- Navigation Graph (항해 그래프)

- Dependency Graph (종속 그래프)

- State Graph (상태 그래프)

Path Finding

- Depth First Search

- Breadth First Search

- Minimum Spanning Tree

 

Dijkstra 알고리즘과 Astar 알고리즘을 배울 예정

깊이우선탐색 (Depth First Search, DFS)

그래프를 가능한 깊게 이동하여 탐색하는 방식.

Stack을 이용하여 방문한 노드를 저장하고, 막다른 곳에 도달하면 Backtracking 처리한다.

#include <iostream>
#include <stack>
using namespace std;

const int MAXN = 100; // 노드의 최대 개수

int adj[MAXN][MAXN]; // 인접 행렬
bool visited[MAXN]; // 방문 여부 저장 배열
int edges[10][2] = {
    {0, 1},
    {1, 2},
    {2, 3},
    {3, 1},
    {1, 4},
    {4, 5},
    {4, 6},
    {0, 6},
    {6, 7},
    {8, 7}
};

void dfs(int start, int dest, int n) {
    stack<int> st; // 스택 생성
    st.push(start); // 시작 노드를 스택에 넣음
    visited[start] = true;  // 시작 노드를 방문

    bool isFind = false;
    while (!st.empty() && !isFind) { 
        int cur = st.top(); // 스택의 맨 위 노드를 가져옴

        // 현재 노드와 연결된 모든 노드에 대해
        bool isNext = false;
        for(int next=0; next<n; next++){
            if(adj[cur][next] && !visited[next]){ // 방문하지 않은 노드에 대해
                st.push(next);
                visited[next] = true;
                isNext = true;
                break;
            }
        }
        if(isNext == false) // 이웃 노드를 모두 방문했으면 backtracking
            st.pop();
        if (dest == st.top())
            isFind = true;
    }
    if(isFind){
        while(!st.empty()){
            int cur = st.top();
            cout << cur << " ";
            st.pop();
        }
    }
		else {
			cout << "Not Found" << endl;
		}
}

int main() {
    int nodeNum = 9 ; // n: 노드의 개수
    int edgeNum = 10;

    // 에지 정보 초기화
    for(int i=0; i<nodeNum; i++){
        for(int j=0; j<nodeNum; j++){
            adj[i][j] = 0;
        }
    }

    // 에지 정보 설정
    for (int i = 0; i < edgeNum; i++) {
        int u, v;
        u = edges[i][0];
        v = edges[i][1];
        adj[u][v] = 1;
    }

    // DFS 실행
    int start = 0;
    int dest = 5;
    dfs(start, dest, nodeNum);
    return 0;
}
#include <iostream>
#include <vector>
#include <stack>
using namespace std;

const int MAXN = 100; // 노드의 최대 개수

vector<int> adj[MAXN+1]; // 인접 리스트
bool visited[MAXN+1]; // 방문 여부 저장 배열

void dfs(int start, int dest) {
    stack<int> st; // 스택 생성
    st.push(start); // 시작 노드를 스택에 넣음
    visited[start] = true;

    bool isFind = false;
    while (!st.empty() && !isFind) { // 스택이 빌 때까지 반복
        int cur = st.top(); // 스택의 맨 위 노드를 가져옴

       // 현재 노드와 연결된 모든 노드에 대해
       bool isNext = false;
       for (int v : adj[cur]) {
            if (!visited[v]) { // 아직 방문하지 않은 노드인 경우
                st.push(v);    // 해당 노드를 스택에 넣음
                visited[v] = true; // 해당 노드를 방문
                isNext = true;
                break;
            }
        }
        if(isNext == false) // 이웃 노드를 모두 방문했으면 backtracking
            st.pop();
        if (dest == st.top())
            isFind = true;
    }
    if(isFind){
        while(!st.empty()){
            int cur = st.top();
            cout << cur << " ";
            st.pop();
        }
    }
		else {
			cout << "Not Found" << endl;
		}
}

int edges[10][2] = {
    {0, 1},
    {1, 2},
    {2, 3},
    {3, 1},
    {1, 4},
    {4, 5},
    {4, 6},
    {0, 6},
    {6, 7},
    {8, 7}
};

int main() {
    int nodeNum = 9; // 노드의 개수
    int edgeNum = 10; // 간선의 개수

    // 간선 정보 입력받기
    for (int i = 0; i < edgeNum; i++) {
        int u = edges[i][0];
        int v = edges[i][1];
        adj[u].push_back(v);
    }

    // DFS 실행
    int start = 0;
    int dest = 5;
    dfs(start, dest);

    return 0;
}
#include <iostream>
#include <vector>
using namespace std;

const int MAXN = 100; // 노드의 최대 개수

int graph[MAXN+1][MAXN+1]; // 그래프 정보 저장 배열
bool visited[MAXN+1]; // 방문 여부 저장 배열

void dfs(int u, int n) {
    visited[u] = true; // 현재 노드 방문 처리
    cout << u << " "; // 현재 노드 출력

    // 현재 노드와 연결된 모든 노드에 대해
    for (int v = 1; v <= n; v++) {
        if (graph[u][v] && !visited[v]) { // 아직 방문하지 않은 노드인 경우
            dfs(v, n); // 해당 노드로 이동하여 재귀호출
        }
    }
}
#include <iostream>
#include <stack>
#include <vector>

using namespace std;

const int MAX = 100;

int graph[MAX][MAX];
bool visited[MAX][MAX];
int dy[] = {0, 0, 1, -1}; // 우, 좌, 하, 상
int dx[] = {1, -1, 0, 0};

void dfs(int sx, int sy, int tx, int ty) {
    // 스택에 현재 정점 push
    stack<pair<int, int>> s;
    s.push({sy, sx});
    visited[sy][sx] = true;

    bool isFind = false;
    while (!s.empty() && !isFind) {
        // 스택의 top에 있는 정점 get
        int cy = s.top().first;
        int cx = s.top().second;

        // (cx, cy)에 인접한 4방향 정점 중 방문하지 않은 정점을 스택에 push
        bool isNext = false;
        for (int i = 0; i < 4; i++) {
            int nx = cx + dx[i];
            int ny = cy + dy[i];
            if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue; // 배열 범위 체크
            if (graph[ny][nx] == 0 || visited[ny][nx]) continue; // 벽이거나 이미 방문한 정점인 경우
            s.push({ny, nx});
            visited[ny][nx] = true;
            isNext = true;
            break;
        }
        if(isNext == false) // 이웃 노드를 모두 방문했으면 backtracking
            s.pop();
        if (ty == s.top().first && tx == s.top().second)
            isFind = true;
    }
    if(isFind){
        while(!s.empty()){
            cout << "( " << s.top().first << " , " << s.top().second << " )" << endl;
        }
    }
}

너비우선탐색(Breadth First Search, BFS)

- 폭을 우선적으로 탐색

- DFS는 최적 경로 탐색을 보장하지 않음

- BFS는 목표 노드를 찾는 경로가 가능한 가장 적은 에지를 포함하도록 보장

- FIFO 큐 사용

#include <iostream>
#include <queue>
using namespace std;

const int MAXN = 100; // 노드의 최대 개수

int adj[MAXN][MAXN]; // 인접 행렬
bool visited[MAXN]; // 방문 여부 저장 배열
int parent[MAXN];

int edges[10][2] = {
    {0, 1},
    {1, 2},
    {2, 3},
    {3, 1},
    {1, 4},
    {4, 5},
    {4, 6},
    {0, 6},
    {6, 7},
    {8, 7}
};

void bfs(int start, int dest, int n) {
    queue<int> q; // 큐 생성
    q.push(start); // 시작 노드를 큐에 넣음
    visited[start] = true; // 시작 노드 방문 처리

    bool isFind = false;
    while (!q.empty() && !isFind) { // 큐가 비거나 목적지를 찾지 못하는 경우
        int cur = q.front(); // 큐의 맨 앞 노드를 가져옴
        q.pop(); // 해당 노드를 큐에서 제거
 
        // 현재 노드와 연결된 모든 노드에 대해
        for (int i = 0; i < n; i++) {
            if (adj[cur][i] && !visited[i]) { // 인접한 노드 중에서 아직 방문하지 않은 노드인 경우
                q.push(i); // 해당 노드를 큐에 넣음
                visited[i] = true; // 해당 노드 방문 처리
                parent[i] = cur;   // 해당 노드의 parent를 cur 노드로 설정
                if (i == dest){
                    isFind = true;
                    break;
                }
            }
        }
    }
    if(isFind){
        int cur = dest;
        cout << cur << " ";
        while(true){        
            cur = parent[cur];
            cout << cur << " ";
            if(cur == start)
                break;        
        }
    }
}

int main() {
    int nodeNum = 9 ; // 노드의 개수
    int edgeNum = 10; // 긴산의 개수

    // 에지 정보 초기화
    for(int i=0; i<nodeNum; i++){
        for(int j=0; j<nodeNum; j++){
            adj[i][j] = 0;
        }
    }

    // 에지 정보 설정
    for (int i = 0; i < edgeNum; i++) {
        int u, v;
        u = edges[i][0];
        v = edges[i][1];
        adj[u][v] = 1;
    }

    // DFS 실행
    int start = 0;
    int dest = 5;
    bfs(start, dest, nodeNum);

    return 0;
}
#include <iostream>
#include <queue>
using namespace std;

const int MAXN = 100; // 노드의 최대 개수

int adj[MAXN][MAXN]; // 인접 행렬
bool visited[MAXN]; // 방문 여부 저장 배열
int parent[MAXN];

int edges[10][2] = {
    {0, 1},
    {1, 2},
    {2, 3},
    {3, 1},
    {1, 4},
    {4, 5},
    {4, 6},
    {0, 6},
    {6, 7},
    {8, 7}
};

void bfs(int start, int dest, int n) {
    queue<int> q; // 큐 생성
    q.push(start); // 시작 노드를 큐에 넣음
    visited[start] = true; // 시작 노드 방문 처리

    bool isFind = false;
    while (!q.empty() && !isFind) { // 큐가 비거나 목적지를 찾지 못하는 경우
        int cur = q.front(); // 큐의 맨 앞 노드를 가져옴
        q.pop(); // 해당 노드를 큐에서 제거
 
        // 현재 노드와 연결된 모든 노드에 대해
        for (int i = 0; i < n; i++) {
            if (adj[cur][i] && !visited[i]) { // 인접한 노드 중에서 아직 방문하지 않은 노드인 경우
                q.push(i); // 해당 노드를 큐에 넣음
                visited[i] = true; // 해당 노드 방문 처리
                parent[i] = cur;   // 해당 노드의 parent를 cur 노드로 설정
                if (i == dest){
                    isFind = true;
                    break;
                }
            }
        }
    }
    if(isFind){
        int cur = dest;
        cout << cur << " ";
        while(true){        
            cur = parent[cur];
            cout << cur << " ";
            if(cur == start)
                break;        
        }
    }
}

int main() {
    int nodeNum = 9 ; // 노드의 개수
    int edgeNum = 10; // 긴산의 개수

    // 에지 정보 초기화
    for(int i=0; i<nodeNum; i++){
        for(int j=0; j<nodeNum; j++){
            adj[i][j] = 0;
        }
    }

    // 에지 정보 설정
    for (int i = 0; i < edgeNum; i++) {
        int u, v;
        u = edges[i][0];
        v = edges[i][1];
        adj[u][v] = 1;
    }

    // DFS 실행
    int start = 0;
    int dest = 5;
    bfs(start, dest, nodeNum);

    return 0;
}

최소신장트리(Minimum Spanning Tree)

트리 : 사이클이 없는 연결 그래프

신장트리 : 주어진 그래프에서 모든 정점들과 간선으로 구성된 트리 (그래프에서 사이클을 제거한, 절단된 그래프)

최소신장트리 : 신장트리들 중 간선의 합이 최소인 트리 (엣지의 가중치를 전부 재봤더니 가장 작더라)

 

그래프에서 스패닝 트리는 여러개 나올 수 있다.

그래프 알고리즘의 중요한 개념. 과거에는 유용하게 쓰였으나, 현재는 사용되지 않는 경향.

통신에서는 유용하게 사용한다. 게임에서는 거의 사용하지 않는 경향.

AStar 알고리즘보다 효율이 좋은 경우가 사실상 나오지 않음.

1. 정렬함
2. 쭉 돌면서 선 연결함
3. 선 연결하다가 잉? 이거 사이클인디? 하면 선 삭제함
4. 반복
5. 예시의 경우 9-11-13 라인은 13을 그리지 않음, 만약 13이 10이여서 9-11-10일 경우 11을 그리지 않음

Shortest Path

조건

- 간선 가중치가 있는 유향 그래프

- 무향 그래프 : 양방향 유향 그래프로 가정

 

두 정점 사이의 최단 경로

- 두 정점 사이의 경로들 중 간선의 가중치 합이 최소인 경로

 

단일 시작점 최단 경로

- 단일 시작점으롭터 각 정점에 이르는 최단 경로

- Dijkstra's Algorithm

 

목표지향 최단 경로

- A* Algorithm

 

모든 쌍 최단 경로

- 모든 정점 쌍 사이의 최단 경로를 모두 구한다

- 플로이드-워샬 알고리즘

Shortest Path - Edge Relax (엣지 완화 알고리즘)

알고리즘이 진행되는 동안 시작 노드에서 목적 노드까지 Best Path Found So Far (BFSP)/에 대한 정보 수집 및 갱신

사이클이 생기면 안 되기 때문에 가중치를 비교 한 다음 짧은것을 택한다.

if(TotalCostToThisNode[t] > TotalCostToThisNode[n] + EdgeCost(n-to-t))
{
	TotalCostToThisNode[t] = TotalCostToThisNode[n] + EdgeCost(n-to-t));
    Parent(t) = n;
}

Shortest Path - Dijkstra's algorithm

- 비용기반 방향 그래프 G = (V, E)에서 시작 노드부터 다른 노드까지의 최단 경로를 구할 수 있다

- SPT(Shortest Path Tree) 관리 : 시작 노드 s로부터의 최종 최단경로 가중치가 이미 결정된 정점들을 관리
- 우선순위 큐(Priority Queue)를 이용

  : 최단 경로 추정 가중치가 가장 작은 정점을 선택

  : u를 S에 추가

  : u와 연결된 모든 간선을 완화

 

영상 시청 - Dijkstra's Algorithm (UNITY3D)
https://youtu.be/dhvf9KCAsVg?si=zEjZlXJl6MPVGmHT

 

728x90
반응형