길 찾기 알고리즘 | Ebs 링크 소프트웨어 세상, \”가장 빠른길을 찾아라, 최단경로 알고리즘\” 230 개의 베스트 답변

당신은 주제를 찾고 있습니까 “길 찾기 알고리즘 – EBS 링크 소프트웨어 세상, \”가장 빠른길을 찾아라, 최단경로 알고리즘\”“? 다음 카테고리의 웹사이트 th.taphoamini.com 에서 귀하의 모든 질문에 답변해 드립니다: https://th.taphoamini.com/wiki/. 바로 아래에서 답을 찾을 수 있습니다. 작성자 임웅경 이(가) 작성한 기사에는 조회수 37,939회 및 좋아요 239개 개의 좋아요가 있습니다.

Table of Contents

길 찾기 알고리즘 주제에 대한 동영상 보기

여기에서 이 주제에 대한 비디오를 시청하십시오. 주의 깊게 살펴보고 읽고 있는 내용에 대한 피드백을 제공하세요!

d여기에서 EBS 링크 소프트웨어 세상, \”가장 빠른길을 찾아라, 최단경로 알고리즘\” – 길 찾기 알고리즘 주제에 대한 세부정보를 참조하세요

EBS 링크 소프트웨어 세상, \”가장 빠른길을 찾아라, 최단경로 알고리즘\”

길 찾기 알고리즘 주제에 대한 자세한 내용은 여기를 참조하세요.

미로탐색 알고리즘 – 나무위키:대문

그래서 실시간 길찾기 알고리즘으로 다익스트라 알고리즘은 적절하지 않다. 다익스트라 알고리즘은 분신술 알고리즘이다. 초기값은 최단 경로 없음, …

+ 여기에 자세히 보기

Source: namu.wiki

Date Published: 11/2/2022

View: 7343

길찾기 알고리즘(DFS, BFS, Dijkstra) – 코딩 잡동사니

길찾기 알고리즘(DFS, BFS, Dijkstra) · 1. DFS (Depth First Search) · 2. BFS (Breadth First Search) · 3. Dijkstra (다익스트라의 최단 경로 알고리즘).

+ 여기를 클릭

Source: errorcode1001.tistory.com

Date Published: 9/8/2021

View: 1419

카카오맵이 빠르게 길을 찾아주는 방법: CCH를 이용한 개편기

하지만 탐색 알고리즘의 경우 길찾기의 응답시간이나 TPS를 가장 많이 좌우하는 부분임에도 불구하고 쉽게 개선을 할 수 없었습니다.

+ 여기에 자세히 보기

Source: tech.kakao.com

Date Published: 2/18/2022

View: 4313

Chapter 6. A* 길찾기 알고리즘 구현 – 평생 공부 블로그

A* 길찾기 알고리즘 구현 : Player.csPermalink. BFS, 다익스트라랑 거의 똑같은 구조로 흘러간다. h 를 고려한다는 것을 제외하고는 비슷 …

+ 여기에 보기

Source: ansohxxn.github.io

Date Published: 6/22/2021

View: 1188

A* 길찾기 알고리즘 (쉽고 친절한 설명) – Mawile

이번에는 길찾기알고리즘하면 제일 먼저 떠올리는 A* 알고리즘에 관하여. 이론과 실제 프로그래밍 코드로 실습을 진행하겠습니다.

+ 여기에 표시

Source: mawile.tistory.com

Date Published: 2/7/2021

View: 2935

[최단 경로 알고리즘] 가장 빠른 길 찾기

[최단 경로 알고리즘] 가장 빠른 길 찾기 … 현실 세계의 길(간선)은 음의 간선으로 표현되지 않으므로 다익스트라 알고리즘은 실제로 GPS …

+ 여기에 표시

Source: s0ng.tistory.com

Date Published: 9/7/2021

View: 7753

최단거리 길 찾기 알고리즘 – A* 알고리즘 – 푸지의 블로그

물론 길 찾기를 하는동안 목적지까지 도달 할 수 없는 경우도 판별이 가능할 것이다. 2. 개요. 이 알고리즘의 아이디어는 어떠한 노드에 대하여 시작점 …

+ 여기에 자세히 보기

Source: puzi.tistory.com

Date Published: 11/4/2021

View: 6350

길찾기 알고리즘 – 생활코딩

길찾기(Pathfinding) 알고리즘은 이동 경로를 찾는 방법입니다. 다양한 방법이 있는데요. 각 알고리즘들이 어떻게 동작하는지를 시각적으로 보여주는 …

+ 자세한 내용은 여기를 클릭하십시오

Source: opentutorials.org

Date Published: 9/30/2022

View: 8937

길찾기 알고리즘(2) – 다익스트라 알고리즘

다익스트라 알고리즘은 ‘가장 빠른 길’을 찾는 알고리즘으로 간선간 음의 값을 가지는 길은 없다고 생각합니다. 또한 플로이드-와샬보다 속도는 …

+ 더 읽기

Source: husk321.tistory.com

Date Published: 9/18/2021

View: 1087

[논문]경로 정보를 이용한 길찾기 알고리즘

A* 알고리즘은 잘 알려진 길찾기 알고리즘이다. 그러나 많은 상호 작용이 있거나 많은 장애물들이 있는 맵에서 A* 알고리즘을 실시간에 사용하는데 한계가 있을 수 …

+ 여기에 보기

Source: scienceon.kisti.re.kr

Date Published: 5/16/2022

View: 7799

주제와 관련된 이미지 길 찾기 알고리즘

주제와 관련된 더 많은 사진을 참조하십시오 EBS 링크 소프트웨어 세상, \”가장 빠른길을 찾아라, 최단경로 알고리즘\”. 댓글에서 더 많은 관련 이미지를 보거나 필요한 경우 더 많은 관련 기사를 볼 수 있습니다.

EBS 링크 소프트웨어 세상, \
EBS 링크 소프트웨어 세상, \”가장 빠른길을 찾아라, 최단경로 알고리즘\”

주제에 대한 기사 평가 길 찾기 알고리즘

  • Author: 임웅경
  • Views: 조회수 37,939회
  • Likes: 좋아요 239개
  • Date Published: 2015. 10. 22.
  • Video Url link: https://www.youtube.com/watch?v=tZu4x5825LI

길찾기 알고리즘(DFS, BFS, Dijkstra)

길찾기 알고리즘에는 DFS, BFS, 다익스트라, Best-First Search, A* 등 다양한 것이 존재합니다.

그 중 오늘은 DFS와 BFS, 다익스트라에 대해 학습해보겠습니다.

1. DFS (Depth First Search)

DFS는 깊이 우선 탐색이라고 하며, 이름에 걸맞게 어떠한 그래프를 탐색할 때 최대한 깊숙히 탐색을 한 후, 더 탐색할 수 없으면 다른 경로를 탐색하는 알고리즘입니다.

만약 위와 같은 그래프가 있고, 2차원 배열로는 저렇게 표현했다고 합시다.

시작점이 노드 A라고 했을 때, DFS 알고리즘으로 그래프를 순회한다면 어떤 순서로 순회를 할까요?

– DFS의 순회 과정 –

1) A와 연결된 노드는 B와 C 입니다. A는 자신과 연결된 2개의 노드 중 어떤 노드를 선택할 지 고릅니다.

두개의 노드 중 무엇을 선택해도 상관은 없습니다.

저는 노드를 선택해야 할 경우, 그림에서 보았을 때 가장 위쪽에 선택하는 노드를 고르도록 하겠습니다.

▶ 선택된 노드는 “B”입니다.

2) B와 연결된 노드 중 B보다 깊이가 깊은 노드는 D, E 입니다.

저는 그림에서 보았을 때 가장 위쪽에 선택하는 노드 D를 고르겠습니다.

▶ 선택된 노드는 “D”입니다.

3) D와 연결된 노드 중 D보다 깊이가 깊은 노드는 없습니다.

D에서 더 깊이 탐색을 하려고 해도 깊이가 더 깊은 노드가 없기 때문에 다시 B로 돌아옵니다.

4) B와 연결된 노드 중 B보다 깊이가 깊으며 아직 탐색하지 않은 노드는 E입니다.

▶ 선택된 노드는 “E”입니다.

5) E와 연결된 노드 중 E보다 깊이가 깊으며 아직 탐색하지 않은 노드는 F와 G입니다.

저는 그림에서 보았을 때 가장 위쪽에 선택하는 노드 G를 고르겠습니다.

▶ 선택된 노드는 “G”입니다.

6) G와 연결된 노드 중 G보다 깊이가 깊은 노드는 없습니다.

G에서 더 깊이 탐색을 하려고 해도 깊이가 더 깊은 노드가 없기 때문에 다시 E로 돌아옵니다.

7) E와 연결된 노드 중 E보다 깊이가 깊으며 아직 탐색하지 않은 노드는 F입니다.

▶ 선택된 노드는 “F”입니다.

8) F와 연결된 노드 중 F보다 깊이가 깊으며 아직 탐색하지 않은 노드는 없습니다.

F에서 더 깊이 탐색을 하려고 해도 깊이가 더 깊은 노드가 없기 때문에 다시 E로 돌아옵니다.

9) E와 연결된 노드 중 아직 탐색을 하지 않은 노드는 없습니다.

다시 B로 돌아옵니다.

10) B와 연결된 노드 중 아직 탐색을 하지 않은 노드는 없습니다.

다시 A로 돌아옵니다.

11) A와 연결된 노드 중 A보다 깊이가 깊으며 아직 탐색하지 않은 노드는 C입니다.

▶ 선택된 노드는 “C”입니다.

이러한 과정으로 그래프를 순회할 수 있습니다.

이렇게 깊이를 기준으로 탐색을 하는 길찾기 알고리즘을 DFS라고 합니다.

2. BFS (Breadth First Search)

BFS는 너비 우선 탐색이라고 하며, 이름에 걸맞게 어떠한 그래프를 탐색할 때 같은 깊이에 해당하는 노드부터 탐색하고 더 탐색할 수 없으면 더 깊은 노드들을 탐색하는 알고리즘입니다.

방금 DFS에서 사용했던 그래프와 같은 그래프와 2차원 배열입니다.

시작점이 노드 A라고 했을 때, BFS 알고리즘으로 그래프를 순회한다면 어떤 순서로 순회를 할까요?

– BFS의 순회 과정 –

1) A와 연결된 노드는 B와 C 입니다. A는 자신과 연결된 2개의 노드 중 어떤 노드를 선택할 지 고릅니다.

저는 노드를 선택해야 할 경우, 그림에서 보았을 때 가장 위쪽에 선택하는 노드를 고르도록 하겠습니다.

▶ 선택된 노드는 “B”입니다.

2) B와 같은 깊이에 해당하는 노드는 C입니다.

▶ 선택된 노드는 “C”입니다.

3) C와 같은 깊이에 해당하는 노드 중 탐색하지 않은 노드가 없습니다.

더 깊은 곳을 탐색합니다.

4) 다음 깊이에 해당하는 노드는 D, E, F 입니다.

제일 위쪽 노드인 D부터 탐색하겠습니다.

▶ 선택된 노드는 “D”입니다.

5) D와 같은 깊이에 해당하는 노드는 E, F 입니다.

저는 위쪽 노드인 E부터 탐색하겠습니다.

▶ 선택된 노드는 “E”입니다.

6) 남은 노드인 F를 선택합니다.

▶ 선택된 노드는 “F”입니다.

7) F와 같은 깊이에 해당하는 노드 중 탐색하지 않은 노드가 없습니다.

더 깊은 곳을 탐색합니다.

8) 다음 깊이에 해당하는 노드는 G 입니다.

▶ 선택된 노드는 “G”입니다.

이러한 과정으로 그래프를 순회할 수 있습니다.

이렇게 너비를 기준으로 탐색을 하는 길찾기 알고리즘을 BFS라고 합니다.

3. Dijkstra (다익스트라의 최단 경로 알고리즘)

Dijkstra는 네덜란드의 컴퓨터 과학자였던 에츠허르 다익스트라가 고안한 길찾기 알고리즘으로 그의 이름을 따서 지어진 이름입니다. BFS나 DFS와는 달리 이름만 들어서는 무슨 알고리즘인지 감이 잘 안올 것입니다.

Dijkstra 알고리즘은 BFS와 굉장히 유사하며, 노드 사이 간선(Edge)에 양의 가중치를 갖는 그래프(음의 가중치 허용 X)에서 최단 경로를 찾을 때 사용할 수 있습니다.

여기서 가중치는 DFS와 BFS에서는 등장하지 않았던 개념인데 쉽게 생각해서 그 지점까지의 거리라고 보시면 될 것 같습니다.

– Dijkstra의 탐색 과정 –

해당 알고리즘이 동작하는 과정은 아래와 같습니다.

1) 출발지를 선택하고 Distance를 0으로 설정합니다.

2) 출발지와 연결된 노드의 Distance를 업데이트합니다.

3) 현재까지 업데이트된 Distance를 보고 가장 좋은 길(즉, 최단 거리의 길)을 선택합니다.

4) 현재까지 업데이트된 Distance와 이전 단계에서 선택된 노드의 Distance를 업데이트해줍니다.

이때, 이미 Distance가 업데이트된 노드라도 더 짧은 경로가 있다면 업데이트 해주어야 합니다.

원래 D의 Distance는 A에서 직행하는 경로로 35였지만 B를 경유하는 경로인 25가 더 가깝기 때문에 Distance를 Update

5) 현재까지 업데이트된 Distance를 보고 가장 좋은 길(즉, 최단 거리의 길)을 선택합니다.

6) 이전 단계에서 선택된 노드와 연결된 노드가 없다면 선택된 노드 다음으로 짧은 거리인 노드를 선택합니다.

7) 이전 단계에서 선택된 노드와 연결된 노드의 최단 거리를 업데이트해줍니다.

8) 선택되지 않은 노드 중 Distance가 가장 짧은 것을 선택합니다.

9) 이전 단계에서 선택된 노드와 연결된 노드의 최단 거리를 업데이트해줍니다.

10) 선택되지 않은 노드 중 Distance가 가장 짧은 것을 선택합니다.

11) 모든 노드를 확정했다면 탐색을 종료합니다.

오늘 기본적인 DFS, BFS, Dijkstra 알고리즘으로 그래프를 탐색하는 방법에 대해 알아보았습니다.

다음엔 이것을 어떻게 구현하는지에 대해 포스팅하겠습니다.

오늘도 고생 많으셨습니다 🙂

카카오맵이 빠르게 길을 찾아주는 방법: CCH를 이용한 개편기

카카오맵에서는 도보·자전거 길찾기 서비스를 제공하고 있습니다. 일반적으로 길찾기 서비스는 다음과 같은 과정을 거쳐 서비스됩니다.

도로 형상에서 그래프 형태의 도로 네트워크 구축 출도착점에서 적절한 출도착 간선 선택 경로 탐색 알고리즘으로 최단 경로 생성 경로 후처리 및 가이드 생성

이 중 도로 네트워크 관리나 출도착 간선 선택, 가이드 생성과 같은 부분은 카카오맵 이용자분들의 피드백을 빠르게 수용하여 조금이라도 더 좋은 경로로, 쉽게 이해할 수 있는 방식으로 안내를 하려고 지속적으로 개선하고 있습니다.

하지만 탐색 알고리즘의 경우 길찾기의 응답시간이나 TPS를 가장 많이 좌우하는 부분임에도 불구하고 쉽게 개선을 할 수 없었습니다. 알고리즘 자체의 한계가 뚜렷하고 우회적인 방법으로 처리하는 것에는 한계가 있기 때문에 엔진단에서부터의 개편이 필요한 상황이었습니다.

그러던 중에도 카카오맵 사용자 수는 꾸준히 늘어나고 있었고, 신규 서비스 오픈 등의 이슈가 생기면서 전면적인 개편을 진행하게 되었습니다.

너무 느린 A* 알고리즘

기존 서비스에서는 A* 알고리즘을 탐색 알고리즘으로 사용하고 있었습니다.

다익스트라 알고리즘은 확장 가능한 정점을 우선순위 큐에 넣고, 그중 가장 작은 비용의 정점을 꺼내서 검토하는 것을 반복하며, 목적지에 도달하면 종료합니다. 새로운 정점을 검토할 때마다 큐 삭제가, 확장할 때마다 큐 삽입이 발생합니다. 일반적인 경우 삽입/삭제는 보통 O(log n) 의 복잡도를 가지며, 큐의 크기 n 은 탐색 범위에 비례하기 때문에 A* 에서는 휴리스틱한 방법을 추가해 최대한 탐색 범위를 줄입니다.

가장 대표적인 휴릭스틱은 정점에서 도착지까지의 직선거리입니다. 목적지 방향으로 가는 게 최단 경로일 확률이 높기 때문에 이런 곳을 우선적으로 가보면 탐색 범위를 꽤나 많이 줄일 수 있습니다. PathFinding.js에서 두 알고리즘을 시각화해보면 꽤 유의미한 성능 향상이 있다는 것을 알 수 있습니다.

휴리스틱을 사용하면 탐색 범위가 원 형태인 r^2 에서 타원 형태인 rh 로 변경되어 많은 탐색을 줄일 수 있습니다. 하지만 목적지가 먼 경우 여전히 많은 정점을 방문하기 때문에 느립니다. 도보 길 찾기의 경우 모든 쿼리를 30km 이하로 제한했기 때문에 성능이 나쁘지 않았지만, 자전거 길 찾기는 전국 단위의 쿼리를 지원해야 했기 때문에 A*로도 수십 초 이상 필요했습니다.

이미지 출처: https://www.graphhopper.com/blog/2017/08/14/flexible-routing-15-times-faster/

기존에는 최대한 성능을 끌어올리기 위해 거리에 따라 탐색 가능한 도로 등급을 제한하거나, 직선거리에 가중치를 많이 주어 직선에서 떨어진 옆길로 빠지는 경우를 줄이는 등 극단적인 휴리스틱을 사용했었습니다. 하지만 여기서 적용한 휴리스틱대로 탐색한다고 해서 모든 경우의 좋은 경로를 우선적으로 가지는 못합니다. 직선거리가 먼 쪽으로 돌아가는 경우가 최단거리일 수 있고, 도로 등급으로 탐색 정점을 줄이는 방법은 자전거 길찾기에서 큰 도움이 되지 않기 때문입니다.

자동차의 경우 일반적으로 고속도로와 같은 큰 도로를 타기 시작하면 목적지에 다가가기 전까지 작은 도로로 빠져나오지 않고 도로의 구분도 국도, 고속도로, 이면 도로와 같이 명확해서 도로 등급 제한이 큰 효과를 발휘하지만, 자전거 도로의 경우 도로 등급의 구분이 명확하지 않고 중간중간 작은 도로로 가는 쪽이 더 좋은 경우도 많았으며 심지어는 그런 도로를 이용하지 않으면 가능한 경로가 없는 경우도 있었습니다.

더욱 근본적인 문제는 이런 꼼수를 사용하더라도 결국 ‘거리가 늘어나면 탐색 시간도 심각하게 늘어난다’라는 한계점을 벗어나지 못하기 때문에 더 빠르고 안정적인 방법으로 탐색을 하는 알고리즘이 필요하게 되었습니다.

지름길을 만들어서 찾아보자

효율적인 지름길을 만들어놓고 지름길을 통해 필요한 곳만 탐색한다면 어떨까요? Contraction Hierarchies(CH) 알고리즘은 이 간단한 아이디어에서부터 시작됩니다.

다음과 같이 일자형 그래프가 있을 때, 양 끝 점끼리의 경로를 탐색하려면 중간의 모든 정점을 거쳐서 탐색을 해봐야 합니다.

여기에 하나의 지름길을 만들어두면 어떨까요? 양 끝 점 쿼리의 경우 지름길을 타고 한 번에 탐색을 할 수 있게 됩니다. 하지만 중간에서 끝 점까지의 쿼리는 여전히 많은 정점을 탐색해봐야 하죠.

아니면 아예 모든 정점 쌍 사이에 지름길을 놓는다면 어떨까요? 쿼리는 굉장히 효율적으로 되겠지만, 간선 수가 폭발적으로 늘어나 현실적으로 말이 되지 않습니다. 이렇게 할 바에야 차라리 2차원 배열에 모든 최단 경로를 전처리하는 게 낫겠죠.

하지만 이렇게 적절하게 필요한 곳에만 지름길을 만들면 적은 수의 지름길을 추가하고도 꽤나 효율적으로 탐색할 수 있습니다.

이렇게 CH는 적절하게 지름길을 만들어두고 효율적으로 탐색을 할 수 있게 하는 알고리즘입니다. 여기서 CH는 추가적으로 정점 사이에 랭킹을 두고, 랭킹이 높아지는 쪽으로만 탐색하게 하여 더 효율적으로 탐색할 수 있게 합니다. 그렇기 때문에 Contraction(단축 경로를 생성해서) Hierarchy(계층을 두고 탐색)이라는 이름을 가지게 된 것입니다.

실시간으로 바뀌는 도로 정보 반영

CH는 그래프의 모양이 똑같더라도 간선의 비용(길이, 이동시간, 도로 상태 등등)이 달라지면 지름길도 다르게 생성이 됩니다. 따라서 갑자기 도로가 침수되어 이용을 하지 못한다거나, 인파가 많아져서 이동시간이 길어지는 등의 비용 변화가 생기면 CH는 처음부터 다시 지름길을 만들어주는 작업을 해줘야 하기 때문에 실시간성 이슈에 대응을 하기 힘듭니다.

하지만 Customizable Contraction Hierarchy는 비용에 상관없이 그래프의 연결 관계가 같다면 항상 같은 지름길이 생기고, 여기서 일부 비용이 바뀐다고 해도 그 부분과 연관된 일부 지름길의 비용만 수정하기 때문에 굉장히 빠르게 실시간 비용 변경을 적용할 수 있습니다. 이런 특성 때문에 Customizable이라는 수식어가 붙게 된 거죠.

카카오맵의 도보·자전거 길찾기에서는 실시간성 비용 변경은 없지만 충분히 추가될 수 있는 부분이고, 최단거리 외에도 편안한 길 등의 여러 가지 옵션을 제공할 때에도 CH는 모든 옵션마다 그래프 자체가 달라지지만 CCH는 같은 그래프에서 비용 값만 따로 저장을 하는 식으로 효율적으로 처리할 수 있기 때문에 CCH 알고리즘을 신규 엔진에서 사용할 탐색 알고리즘으로 정하게 되었습니다.

신규 개편에서 구현한 CCH 를 통해 수행된 탐색을 시각화하면 엄청나게 적은 범위를 탐색하고도 서울에서 부산까지의 최적경로를 탐색하는 것을 볼 수 있습니다.

빨간 부채꼴 모양은 출발지인 파주시에서부터 탐색한 정점들이고, 초록 부채꼴 모양은 도착지인 부산에서부터 탐색한 정점들입니다. 빨간 삼각형과 초록 원이 만나는 곳은 출도착지의 마지막 탐색 정점과 인접한 곳이 만나는 지점입니다.

CCH 특성상 마치 지도를 여러 파트로 쪼개놓고 경계선 부분을 따라 탐색하는 것처럼 탐색하는 것을 볼 수 있습니다.

서비스 성능 향상

“그래서 도대체 CCH를 사용하면 얼마나 효과가 있는 거지?”라는 궁금증이 있으신 분들을 위해 결과부터 말씀드리면 굉장한 수준으로 개선되었습니다.

TPS

자전거 : 70배 향상 도보 : 3배 향상

30km 거리 제한이 있는 도보 길찾기는 약 3배 정도의 성능 향상이 있었지만, 전국 단위 탐색이 가능한 자전거 길찾기는 약 70배 정도 성능이 향상됐습니다.

실제 자전거 길찾기 서버 수도 1/15로 줄어들었고, 그마저도 기존 서비스보다 수용 가능한 TPS가 훨씬 늘어난 상태입니다.

도보 길찾기의 경우에도 예전에는 성능 이슈와 필요성에 대한 의문으로 30km 거리 제한을 두었지만, 현재는 별다른 이슈 없이 의사결정만 있으면 바로 거리 제한을 해제해도 문제없을 정도로 개선되었습니다.

응답시간

응답시간 역시 자전거의 경우 기존에는 4초 넘게 탐색을 해도 결과를 못 찾는 경우가 있었는데, 현재는 높게 튀는 경우 없이 안정적이고 빠르게 개선되어 카카오맵 사용자들이 조금 더 쾌적한 환경에서 서비스를 이용할 수 있게 되었습니다.

CCH 알고리즘

CCH가 어떤 기준으로 지름길을 만들고 어떻게 비용을 전처리하며 쿼리는 어떻게 하는지 대한 상세한 내용이 궁금하신 분들을 위해 조금 더 자세하게 정리해보았습니다.

장단점

우선 CCH의 장단점부터 살펴보면 다음과 같습니다.

장점

빠른 쿼리 (놀랍게도 우선순위 큐를 사용하지 않고 탐색을 합니다.)

거리와 거의 상관없는 균일한 쿼리 성능

빠른 간선 가중치 업데이트

단점

긴 전처리 시간

간선 수 증가로 인한 메모리 증가

길찾기 옵션별 가중치 전처리로 인한 메모리 증가 (카카오맵에서의 큰길 우선, 최단거리, 편안한 길 모두 따로 전처리를 하고 있습니다.)

효율적인 1:N 탐색 불가 (출도착 후보지 개수에 비례해서 탐색 시간이 늘어납니다)

CH 알고리즘

CCH를 알아보기 전에 기본이 되는 아이디어인 CH 알고리즘을 알아놓으면 이해하기가 쉽습니다.

CH 알고리즘에서는 정점에 랭크를 매기고, 랭크가 낮은 순으로 정점을 뽑아가며 지름길을 만드는 Contraction이라는 작업을 합니다. 그 후 Contraction이 완료된 정점은 전처리 과정이 끝날 때까지 임시적으로 제외합니다.

Contraction 은 현재 정점 u 가 뽑혔을 때, u 와 인접한 정점 s, t 에 대해 경로 P(s,u,t) 가 s 에서 t 로 가는 최단 경로인 경우 dist(s, u) + dist(u, t) 값을 가진 지름길을 추가하는 작업입니다.

예를 들어 아래와 같이 검은색 점선과 실선이 원본 간선인 그래프에서 정점의 랭크를 매기고 1번 정점을 Contract 하면 빨간색 실선의 지름길이 추가됩니다.

2→3 의 최단 경로가 P(2, 1, 3) 이고, 2→4 의 최단 경로가 P(2, 1, 4) 이기 때문입니다.

그러나 3→4 의 경우 P(3, 1, 4) 가 아닌 P(3, 5, 4) 가 최단 경로이기 때문에 지름길이 추가되지 않습니다.

이렇게 지름길이 생성된 그래프에서 u→v 최단 경로를 쿼리 할 때는 u, v 양쪽에서 시작하는 양방향 다익스트라로 탐색을 하는데, 이미 지름길에 랭크가 낮은 쪽으로 탐색하는 경우가 포함되어 있기 때문에 현재 정점보다 랭크가 높은 쪽으로만 탐색하도록 하여 쿼리 성능을 높입니다.

전처리

CH에서는 지름길 추가와 비용 전처리를 동시에 진행하지만, CCH 에서는 이것을 두 단계로 나눠서 처리합니다.

그래프 형태만 보고 지름길로 사용할 수 있는 간선 추가 각 간선마다의 비용 전처리

Contraction (지름길 간선 추가)

CCH의 Contraction은 비용과 상관없이 그래프의 형태만 보고 진행합니다.

CH와 비슷하게 정점마다 랭크를 매기고 Contraction을 진행하는데, 이때 CCH에서는 최단 경로와 상관없이 u 와 인접한 모든 정점들 사이에 지름길을 추가하게 됩니다.

// rank가 낮은 순으로 돌면서 contraction을 합니다. for (int rank = 0; rank < n; ++rank) { Vertex u = graph.getVertexByRank(rank); Vertex minNeighbor = graph.getUpperRankedNeighbors(u)).stream() .min(Comparator.comparing(Vertex::getRank)) .orElseGet(null); for (Vertex v : graph.getUpperRankedNeighbors(u)) { if (v != minNeighbor) { graph.addEdge(minNeighbor, v); } } } 예를 들어 1번 정점을 Contract 하면 인접한 2, 3, 4 정점들 사이에 모두 지름길을 추가해 줍니다. CH와 비교하면 3→4 간선도 추가된 것을 알 수 있습니다. Contraction이 끝나면 그래프는 아래와 같이 Chordal 한 형태가 되며, 이 특성은 비용 전처리에서 유용하게 쓰입니다. 정점에 랭크를 매기는 Ordering에 따라 지름길 간선이 추가되는 수와 형태가 매우 달라지기 때문에 효율적인 Ordering을 하는 것이 CCH에서는 중요한 과제입니다. 이 부분은 뒤의 최적화 부분에서 다루겠습니다. Metric Customization (비용 전처리) 그래프의 Chordal 한 특성에 따라 모든 아크는 특정 삼각형들의 변에 속하게 됩니다. 여기에 아크의 두 정점의 랭크와 나머지 한 정점의 랭크의 관계에 따라 각 아크에 대한 lower/intermediate/upper triangle들을 정의할 수 있습니다. 예를 들어 아래와 같은 삼각형은 각 아크에 따라 다르게 정의됩니다. 2→3 에 대해서 lower triangle 1→3 에 대해서 intermediate triangle 1→2 에 대해서 upper triangle 실제 탐색에서는 CH와 마찬가지로 랭크가 상승하는 쪽으로만 탐색을 할 것이기 때문에, 랭크가 낮은 쪽으로 가는 경우를 비용 전처리 단계에서 처리해 줘야 합니다. from, to 정점의 랭크로 비교했을 때 랭크가 낮은 아크부터 bottom-up으로 순회하며 아크의 lower triangle 을 이용하는 것이 최단 경로인지 처리를 합니다. 즉, 아크 x→y 에 대해 처리할 때 lower triangle (x, y, z_i)를 돌면서 C(x, y) = min(C(x, y), C(x, z) + C(z, y))를 계산합니다. // 편의를 위해 아크 방향성에 상관없이 같은 비용을 가진다고 가정했을 때의 Customization for (Arc arc : graph.getAllArcsSortedByRank()) { long minLowerCost = graph.getLowerTriangle(arc).stream() .map(lowerTriangle -> lowerTriangle.fromSideArc().getCost() + lowerTriangle.toSideArc().getCost()) .min(); if (minLowerCost < arc.getCost()) { arc.setCost(minLowerCost); } } 최초 Customization 이후에 일부 아크의 가중치만 바뀌는 경우, 전체 아크를 모두 업데이트하지 않고 업데이트가 필요한 아크만 골라서 Customize를 하면 빠르게 변경사항을 적용시킬 수 있습니다. 비용이 바뀐 아크들을 랭크를 기준으로 하는 우선순위 큐에 넣고 큐가 빌 때까지 위와 같은 방식으로 Customize를 진행하는데, 현재 아크의 비용이 바뀐 경우 이것에 영향을 받을만한 상위 아크들도 큐에 추가해 줍니다. x→y 아크의 비용이 변경된 경우 intermediate triangle (x, i, y)과 upper triangle (x, y, u)를 돌며 기존에 i→y나 y→u 가 x→y를 이용한 경로를 최단 경로로 사용하고 있었는지 확인합니다. 만약 그런 경우, x→y 비용 변경에 따라 해당 아크들의 비용도 변경되었을 가능성이 있어서 큐에 추가해 소음과 줘야 합니다. 혹은, 새로운 비용이 기존 비용보다 작을 경우에도 최단 경로가 갱신될 수 있기 때문에 이 경우 무조건 두 아크를 큐에 추가해 줘야 합니다. priorityQueue.addAll(toBeUpdatedArcs); while (!priorityQueue.empty()) { Arc arc = priorityQueue.poll(); long oldCost = arc.getCost(); long newCost = calcNewCost(arc); // 이전 코드와 같은 방식으로 새로 바뀔 값을 계산합니다. if (newCost != oldCost) { boolean costReduced = newCost < oldCost; for (Triangle itmTriangle : graph.getIntermediateTriangles()) { Arc fromSide = itmTriangle.fromSideArc(); Arc toSide = itmTriangle.toSideArc(); // C(i, y) == C(i, x) + oldC(x, y) 이면 업데이트 큐에 추가합니다. if (costReduced || toSide.getCost() == fromSide.getCost() + oldCost) { priorityQueue.add(toSide); } } for (Triangle itmTriangle : graph.getIntermediateTriangles()) { Arc fromSide = itmTriangle.fromSideArc(); Arc toSide = itmTriangle.toSideArc(); // C(y, u) == oldC(y, x) + C(x, u) 이면 업데이트 큐에 추가합니다. if (costReduced || toSide.getCost() == oldCost + fromSide.getCost()) { priorityQueue.add(toSide); } } arc.setCost(newCost); } } 경로 쿼리 비용 전처리 단계에서 하위 랭크 정점을 통한 경로는 전처리된 상태이기 때문에 탐색은 상위 랭크로 가는 방향으로만 해도 됩니다. CH처럼 양방향 다익스트라를 사용하여 출도착 지점으로부터 상위 랭크로 향하는 아크로만 탐색을 해도 되지만, Elimination Tree라는 개념을 이용하면 우선순위 큐 없이도 탐색을 할 수 있습니다. Elimination Tree 란 CCH 그래프에서 각 정점마다 상위 랭크인 이웃 중 랭크가 가장 작은 정점으로 가는 아크만을 남겨두어 트리 형태로 만든 것입니다. s→t 최단 경로를 탐색할 때, 다음과 같이 우선순위 큐 없이 Elimination Tree를 사용해 탐색할 수 있습니다. forward/backward 탐색에서 사용할 최단거리 저장 배열인 d_f, d_b를 ∞ 로 초기화 해놓습니다. Elimination Tree에서 s, t 의 최소 공통 조상인 x 를 구합니다. s→x 트리 경로의 정점을 순서대로 순회하며 d_f를 relax 합니다. t→x 트리 경로의 정점을 순서대로 순회하며 d_b를 relax 합니다. d_f(r) + d_b(r) 가 최소가 되는 r 을 지나는 경로가 최단경로가 됩니다. CCH에서의 Elimination Tree 높이는 굉장히 낮기 때문에 실제로는 굉장히 적은 범위만을 탐색하게 됩니다. 또한 실제 거리와는 상관없이 일정한 쿼리 성능을 가지게 됩니다. 실제 경로를 찾을 때는 CCH 그래프의 아크가 지름길일 수도 있어서 unpack 하는 과정을 거쳐야 합니다. 아래 그림과 같이 CCH 아크로 이루어진 경로 P(p(1) … p(k)) 가 있을 때 각각의 CCH 간선을 unpack 해야 하는데, u→v 아크를 unpack 할 때 해당 arc의 lower triangle (u, v, x)을 순회하며 C(u, v) = C(u, x) + C(x, v) 이면 unpack(u, x), unpack(x, v) 로 재귀적으로 경로를 구합니다. private void unpackPath(Arc arc, List resultPath) { for (Triangle lowerTriangle : graph.getLowerTriangle(arc)) { Arc fromSideArc = lowerTriangle.fromSideArc(); Arc toSideArc = lowerTriangle.toSideArc(); if (arc.getCost() == fromSideArc.getCost() + toSideArc.getCost()) { unpackPath(fromSideArc, resultPath); unpackPath(toSideArc, resultPath); return; } } resultPath.add(arc); } 최적화 Ordering Contraction 과정에서 어떻게 정점들의 순서를 매기느냐에 따라 CCH 성능이 많이 달라집니다. 좋은 Ordering일수록 추가적으로 생성되는 지름길 간선 수가 적고, Elimination Tree의 깊이도 균일하고 얕게 형성되기 때문입니다. CCH에서는 균형 잡힌 그래프 파티셔닝을 통해 Ordering을 합니다. 균형 잡힌 그래프 파티셔닝이란 그래프에서 일부 정점들을 Separator로 분류하고 삭제하여 두 개 이상의 파트로 최대한 균일하게 Separator에 속하는 정점은 최대한 적게 쪼개는 작업입니다. 예를 들어 아래의 그래프에서는 빨간 정점을 Separator로 쪼개는 것이 좋은 파티셔닝 방법입니다. 이 그래프 파티셔닝을 CCH Ordering에 사용하면 좋은 결과를 얻을 수 있습니다. 그래프를 G라고 하고, G의 separator가 S, 분리된 두 부분 그래프가 A, B 일 때 Ordering 결과를 f(G)라고 할 때 다음과 같이 재귀적으로 그래프 파티셔닝을 하며 Ordering을 합니다. f(G) = { f(A), f(B), S } 원본 논문에서는 NDMetis와 KaHIP을 파티셔닝 툴로 소개하고 있으며, 카카오에서는 CCH에서 더 유리한 네트워크 플로우 기반 파티셔닝 방식인 Inertial Flow Cutter를 오픈소스로 사용하고 있습니다. 글 윗부분에서 CCH 탐색을 시각화했을 때 그래프의 separator 을 타고 탐색을 하는 것처럼 시각화된 이유가 ordering 자체를 그래프 파티셔닝에 따라 했기 때문입니다. 같은 separator의 정점들은 elimination tree 상의 인접한 곳에 위치하게 되고, separator 을 건너뛰며 탐색하기 때문에 트리 높이가 낮아지는 것입니다. Line Graph 사용 길찾기 서비스에서 아크의 비용뿐 아니라 아크와 아크의 관계에서 존재하는 비용을 처리해야 할 때도 있습니다. 아래 그림과 같이 도로 속성이 변할 때 비용을 추가하고 싶거나 좌회전 불가와 같은 로직을 추가하고 싶으면 현재 아크뿐 아닌 직전에 지나온 아크의 정보도 필요합니다. 회전 제어 그림의 북쪽 아크의 입장에서 남쪽 아크에서 온 경우에는 문제없이 지나갈 수 있지만, 서쪽 아크에서 온 경우에는 지나갈 수 없고, 이때 비용은 ∞ 로 처리해야 합니다. 이런 아크와 아크의 관계를 처리하기 위해 기본적인 그래프를 Line Graph라는 형태로 바꿔서 처리하는데, Line Graph 란 정점이 기본 그래프의 아크가 되고, 아크가 기본 그래프의 아크와 아크의 관계가 되도록 변환한 그래프입니다. 아래의 그림처럼 검은색으로 그려진 것이 기본 그래프일 때, Line Graph는 노란색으로 그려진 그래프가 됩니다. 이렇게 Line Graph로 만들고 나면 v4→v2→v3 회전 불가 로직을 단순히 e4→e5 아크의 비용을 ∞ 로 두는 것으로 구현할 수 있습니다. 당연하게도 일반 그래프를 Line Graph 형태로 변환하게 되면 그래프의 크기가 훨씬 커지고 CCH 그래프로 지름길 간선을 추가하게 되면 그 크기는 훨씬 커지게 됩니다. Line Graph를 위한 CCH 빌드 Line Graph의 아크들을 자세히 보면 논리적으로 맞지 않는 아크들이 있습니다. e3→e1 의 경우 실제로는 (v2→v4) → (v1→v2) 인데, 이것은 존재할 수 없는 아크입니다. 하지만 CCH에서는 그래프가 무향 그래프임을 가정하고 있어서 아크 자체를 삭제할 수는 없기 때문에, 이런 아크들은 ∞ 을 부여하게 되고, 편의상 항상 무한인 아크라고 부릅니다. 이런 항상 무한인 아크들이 존재하기 때문에 CCH에서 추가되는 지름길 아크들도 항상 무한인 경우가 있고, 만약 양방향으로 항상 무한인 경우 아예 간선 자체를 없앨 수도 있습니다. 따라서 좀 더 똑똑하게 Ordering 을 하면 지름길 간선 수도 줄이고, 항상 무한인 아크 수도 늘릴 수 있습니다. Inertial Flow Cutter 는 네트워크 플로우와 컷 개념을 통해 그래프 파티셔닝을 합니다. 네트워크 플로우란 그래프에서 source와 sink 정점을 정해두고 각 아크마다 허용 가능한 플로우의 capacity 가 정해진 상태에서, source → sink로 파이프에 물을 흘려보내듯이 경로 사이의 모든 아크에 플로우를 보내는 것을 말합니다. 컷이란 그래프에서 일부 아크를 제외하여 그래프를 여러 개로 쪼개는 것을 말합니다. 이 네트워크 플로우와 컷은 아주 밀접한 관계가 있으며, 균형 잡힌 그래프 파티셔닝에서도 유용하게 사용할 수 있습니다. Line Graph에서 separator를 찾는다는 것은 일반 그래프에서의 minimum balanced cut 을 찾는 것과 같습니다. 이렇게 찾아진 separator(cut)의 순서를 아크의 방향성에 따라 묶어서 정하면 양방향으로 항상 무한인 간선의 수를 늘릴 수 있습니다. 아래 그림처럼 S_l→S_r 방향의 아크(LineGraph의 정점) (5,6), (3,4), (1,2)를 먼저 Ordering 하면, Metric Customization 단계에서 비용을 전처리 할 때 랭크가 더 낮은 곳을 통해서 갈 수 있는지 만을 체크하기 때문에 세 아크(정점) 사이의 간선은 항상 무한인 간선이 됩니다. 해당 아크를 타고 S_l → S_r 로 갈 수는 있지만, S_r → S_l 로 되돌아오는 아크들은 모두 세 아크보다 랭크가 높기 때문에 이용할 수 없기 때문에 (5,6) → (S_r→S_l) → (3,4) 와 같이 가는 방법은 존재할 수 없습니다. 즉, 그래프 파티셔닝에서 separator를 알고 있다는 가정 하에 방향성에 따라 효율적으로 ordering을 하는 것입니다. 카카오에서는 이것을 일반 그래프 기준으로 아크 기반 IFC Ordering 을 한 후, 해당 Ordering에서 separator 들을 역추적하고 위와 같은 방법으로 reordering 하여 항상 무한인 간선을 최대한 늘려서 사용하고 있습니다. 아크 기반 IFC Ordering 이 아니더라도 Line Graph 자체에서 NDMetis와 같은 방법으로 Ordering을 하고 reordering 을 해도 되지만, 아크 기반 IFC Ordering을 하는 것이 가장 성능이 좋았습니다. 수치 측정 Elimination Tree 높이 평균 : 662.43 최대 : 1,158 쿼리 중 relaxing 횟수 평균 : 66,860.65 최대 : 157,563 1:1 쿼리에서 많이 탐색해봤자 2,316 개의 정점만을 방문하고, 방문한 정점들과 인접한 정점들에 대한 relax가 많아봤자 315,126 번만 하는 것을 보면 굉장한 성능 향상이 있다는 것을 알 수 있습니다. 다익스트라 기반 알고리즘을 사용할 경우 50만 개가 넘는 정점을 방문하고, relax를 수백만 번 해도 경로를 못 찾는 경우도 많은데 CCH는 어떤 쿼리에 대해서도 안정성 있게 경로를 찾을 수 있습니다. 전처리 단계별 그래프 크기 단계 정점 수 아크 수 기본 도로 네트워크 5M 13M Line Graph 변환 13M 60M CCH Graph (NDMetis 사용) 13M 270M CCH Graph (IFC 사용) 13M 250M CCH Graph (IFC 사용 후 항상 무한인 간선 제거) 13M 119M 마치며 결론적으로 CCH는 전처리를 했을 때 늘어나는 간선 수(2배~4배)를 감당할 수 있고, 거리가 멀고 출도착 후보지가 적은 쿼리가 자주 들어오며, 길찾기 옵션 수가 너무 많지 않고, 실시간으로 비용이 업데이트되는 길찾기 서비스에 유용합니다. 카카오맵에서는 도보·자전거 길찾기 서비스가 이 특성에 아주 잘 맞았고, 여러 시행착오 끝에 실 서비스가 가능한 수준으로 개편을 할 수 있었습니다. 이 글에서는 CCH에 대해서만 중점적으로 다뤘지만 실제로는 그것 외에도 메모리 최적화, 알고리즘과 서비스를 분리할 수 있는 코드 구조 설계, CCH에는 적용할 수 없는 비즈니스 로직의 우회적인 적용 등 많은 이슈가 있었습니다. 이런 이슈들을 하나씩 처리하며 많은 수의 객체가 생성될 때는 객체에 여러 필드를 두기보다 필드의 배열 형태로 관리하는 것이 메모리에 유리하다는 것 등을 배우고 코드 구조에 대한 고민, 데이터 빌드 프로세스에 대한 고민을 하고 기존 서비스와 달라지는 결과 테스트 및 수정 등을 해보며 많은 것을 배울 수 있었습니다. 긴 글 읽어주셔서 감사합니다. 참고 – Julian Dibbelt, Ben Strasser and Dorothea Wagner. 2015. Customizable Contraction Hierarchies. Karlsruhe Institute of Technology – V. Buchhold, D. Wagner, Tim Zeitz, Michael Zündorf. 2020. Customizable Contraction Hierarchies with Turn Costs. Karlsruhe Institute of Technology – Lars Gottesbüren *, Michael Hamann, Tim Niklas Uhl and Dorothea Wagner. 2019. Faster and Better Nested Dissection Orders for Customizable Contraction Hierarchies. Karlsruhe Institute of Technology – InertialFlowCutter

Chapter 6. A* 길찾기 알고리즘 구현

using

System

;

using

System.Collections.Generic

;

using

System.Text

;

namespace

Algorithm

{

class

Pos

{

public

Pos

(

int

y

,

int

x

)

{

Y

=

y

;

X

=

x

;

}

public

int

Y

;

public

int

X

;

}

class

Player

{

public

int

PosY

{

get

;

private

set

;

}

public

int

PosX

{

get

;

private

set

;

}

Random

_random

=

new

Random

();

Board

_board

;

enum

Dir

// 반시계방향

{

Up

=

,

Left

=

1

,

Down

=

2

,

Right

=

3

}

int

_dir

=

(

int

)

Dir

.

Up

;

List

< Pos >

_points

=

new

List

< Pos >();

public

void

Initialize

(

int

posY

,

int

posX

,

Board

board

)

{

PosX

=

posX

;

PosY

=

posY

;

_board

=

board

;

AStar

();

// ⭐A * 알고리즘⭐

}

struct

PQNode

:

IComparable

< PQNode >

{

public

int

F

;

public

int

G

;

// F랑 G만 알아도 H 는 구할 수 있으니 생략

public

int

Y

;

public

int

X

;

public

int

CompareTo

(

PQNode

other

)

{

if

(

F

==

other

.

F

)

// F 값을 기준으로 크기를 비교

return

;

return

F

< other . F ? 1 : - 1 ; } } void AStar () { int [] deltaY = new int [] { - 1 , 0 , 1 , 0 }; int [] deltaX = new int [] { 0 , - 1 , 0 , 1 }; int [] cost = new int [] { 1 , 1 , 1 , 1 }; // 점수 매기기 // F = G _ H // F = 최종 점수 (작을 수록 좋음. 경로에 따라 달라짐) // G = 시작점에서 해당 좌표까지 이동하는데 드는 비용 (작을 수록 좋음. 경로에 따라 달라짐) // H = 목적지로부터 얼마나 가까운 곳인지를 따지는 보너스 점수 (작을 수록 좋음. 고정인 값) // (y, x) 이미 방문 했는지 여부 (방문 = closed 상태) bool [,] closed = new bool [ _board . Size , _board . Size ]; // 출발지에서 (y, x) 가는데에 현재까지 업뎃된 최단거리 // (y, x) 가는 길을 한번이라도 발견 했었는지 여부가 될 수도 있다. (한번이라도 예약 되있는지 여부) // 발견(예약)이 안됐다면 Int32.MaxValue 로 저장이 되어 있을 것. // F = G + H 값이 저장된다. (이 F 값이 가장 작은 정점이 방문 정점으로 선택될 것) int [,] open = new int [ _board . Size , _board . Size ]; for ( int y = 0 ; y < _board . Size ; y ++) for ( int x = 0 ; x < _board . Size ; x ++) open [ y , x ] = Int32 . MaxValue ; Pos [,] parent = new Pos [ _board . Size , _board . Size ]; // 우선순위 큐 : 예약된 것들 중 가장 좋은 후보를 빠르게 뽑아오기 위한 도구. PriorityQueue < PQNode >

pq

=

new

PriorityQueue

< PQNode >();

// 시작점 발견 (시작점 예약 진행)

open

[

PosY

,

PosX

]

=

Math

.

Abs

(

_board

.

DestY

PosY

)

+

Math

.

Abs

(

_board

.

DestX

PosX

);

// 시작점이니까 G는 0이고, H 값임.

pq

.

Push

(

new

PQNode

()

{

F

=

Math

.

Abs

(

_board

.

DestY

PosY

)

+

Math

.

Abs

(

_board

.

DestX

PosX

),

G

=

,

Y

=

PosY

,

X

=

PosX

});

parent

[

PosY

,

PosX

]

=

new

Pos

(

PosY

,

PosX

);

while

(

pq

.

Count

>

)

{

// 제일 좋은 후보를 찾는다.

PQNode

node

=

pq

.

Pop

();

// 동일한 좌표를 여러 경로로 찾아서, 더 빠른 경로로 인해서 이미 방문(closed) 된 경우 스킵

if

(

closed

[

node

.

Y

,

node

.

X

])

continue

;

// 방문 한다.

closed

[

node

.

Y

,

node

.

X

]

=

true

;

// 목적지에 도착했으면 바로 종료

if

(

node

.

Y

==

_board

.

DestY

&&

node

.

X

==

_board

.

DestX

)

break

;

// 상하좌우 등 이동할 수 있는 좌표인지 확인해서 예약(open)한다.

for

(

int

i

=

;

i

< deltaY . Length ; i ++) { int nextY = node . Y + deltaY [ i ]; int nextX = node . X + deltaX [ i ]; // 유효 범위를 벗어났으면 스킵 if ( nextY < 0 || nextY >=

_board

.

Size

||

nextX

< 0 || nextX >=

_board

.

Size

)

continue

;

// 벽으로 막혀서 갈 수 없으면 스킵

if

(

_board

.

Tile

[

nextY

,

nextX

]

==

Board

.

TileType

.

Wall

)

continue

;

// 이미 방문한 곳이면 스킵

if

(

closed

[

nextY

,

nextX

])

continue

;

// 비용 계산

int

g

=

node

.

G

+

cost

[

i

];

int

h

=

Math

.

Abs

(

_board

.

DestY

nextY

)

+

Math

.

Abs

(

_board

.

DestX

nextX

);

// 다른 경로에서 더 빠른 길을 이미 찾았으면 스킵 (업뎃 할 필요가 없으니까)

if

(

open

[

nextY

,

nextX

]

< g + h ) continue ; // 예약 진행 open [ nextY , nextX ] = g + h ; pq . Push ( new PQNode () { F = g + h , G = g , Y = nextY , X = nextX }); parent [ nextY , nextX ] = new Pos ( node . Y , node . X ); } } CalcPathFromParent ( parent ); } void CalcPathFromParent ( Pos [,] parent ) { int y = _board . DestY ; int x = _board . DestX ; while ( parent [ y , x ]. Y != y || parent [ y , x ]. X != x ) { _points . Add ( new Pos ( y , x )); Pos pos = parent [ y , x ]; y = pos . Y ; x = pos . X ; } _points . Add ( new Pos ( y , x )); _points . Reverse (); } const int MOVE_TICK = 10 ; // 10밀리세컨즈 = 0.01 초 마다 움직이게 int _sumTick = 0 ; int _lastIndex = 0 ; public void Update ( int deltaTick ) { if ( _lastIndex >=

_points

.

Count

)

return

;

_sumTick

+=

deltaTick

;

if

(

_sumTick

>=

MOVE_TICK

)

// 이부분은 0.1초마다 실행

{

_sumTick

=

;

PosY

=

_points

[

_lastIndex

].

Y

;

PosX

=

_points

[

_lastIndex

].

X

;

_lastIndex

++;

}

}

}

}

A* 길찾기 알고리즘 (쉽고 친절한 설명)

728×90

🔥 소개

안녕하세요~!

이번에는 길찾기알고리즘하면 제일 먼저 떠올리는 A* 알고리즘에 관하여

이론과 실제 프로그래밍 코드로 실습을 진행하겠습니다.

사실 아시는분은 아시겠지만, 예전에 A* 알고리즘에 관하여 포스팅을 올려놨었는데요..

일단 그 글은 현재 지웠습니다.

설명이 아예없기때문에.. 지금 봐보니까 진짜 불친절하더라구요…ㅠㅠ

이번 포스팅에서는 매우 자세하고 친절하게 알려드리고 있습니다!

Image from GeeksforGeeks

🔥 참고

기본적인 알고리즘의 원리는 GeeksforGeeks 를 참고했습니다.

🔥 A* 알고리즘 필수 단어

A* 알고리즘 : 출발 꼭짓점에서부터 목표 꼭짓점까지 가는 최단 경로를 찾아내는(다시 말해 주어진 목표 꼭짓점까지 가는 최단 경로임을 판단할 수 있는 테스트를 통과하는) 휴리스틱기반의 그래프 탐색 알고리즘.

열린목록 : 아직 방문하지 않은 노드의 집합.

닫힌목록 : 이미 방문했거나 갈수없는 노드의 집합.

부모노드 : 전에 방문했던 노드.

노드 : 그래프상에서의 현재의 위치또는 정점.

🔥 A* 알고리즘의 원리

A* 알고리즘의 작동방식은 휴리스틱기반의 계산방식 에 있습니다.

탐색하는 방법

순서 행위 1 그래프상에서의 모든 정점의 부모노드를 -1로 초기화하고, 비용은 INF(무한대)값으로 설정한다. 2 시작노드의 비용을 0으로 초기화하고, 시작노드의 부모노드는 시작노드를 가리킨다. 3 현재노드를 기준으로 모든방향의 노드에 대한 비용을 계산한다. 4 비용이 계산된 노드들중 최소비용을 가지는 노드가 다음노드가 된다. 5 다음노드의 부모노드는 현재노드를 가리킨다. 6 3~5을 반복한다. 7 만약 다음노드가 도착노드라면, 탐색을 중지하고 최적의 경로를 계산한다.

최적의 경로를 계산하는 방법

순서 행위 1 도착노드를 스택에 푸쉬(push)한다. (현재 가리키고있는 노드는 부모노드이다.) 2 현재 가리키고있는 노드의 부모노드를 스택에 푸쉬(push)한다. 3 2를 반복한다. 4 만약 현재 가리키고있는 노드와 부모노드가 같다면(시작노드라면) 3을 중지한다. 5 그러면 이 스택은 최적의 경로에 대한 정보를 담고 있게된다.

🔥 휴리스틱 기법

우선 탐색하는방법 에서 소개했던 “비용을 계산한다.” 이 부분에 대한 계산방법을 담고있습니다.

비용을 계산하는 방법은

비용(f) = 다음노드에서 도착노드까지와의 거리(h) + 다음노드 방향에 대한 비용(g)

입니다.

더보기 (모바일의 경우 수학공식이 이상하게 표기되니, 되도록이면 데스크탑환경에서 읽어주시기 바랍니다.)

다음노드에서 도착노드까지와의 거리

우선 다음노드의 절대좌표를 \( (x_{a}, y_{a}) \) 라고 가정하고,

도착노드의 절대좌표를 \( (x_{b}, y_{b}) \) 라고 가정하겠습니다.

이랬을때, “다음노드에서 도착노드까지와의 거리”는 다음과 같습니다.

cost = \( \sqrt{(x_{a} – x_{b})^{2} + (y_{a} – y_{b})^{2}} \)

다음노드 방향에 대한 비용

우선 직선방향은 1.000 이 됩니다.

그 이유를 알기위해서 다음과 같은 직각삼각형이 있다고 가정해봅시다.

우리가 앞으로 다뤄야할 가상의 노드(정점)들은 모두 위의 직각삼각형과 같이 일정한 거리를 두고있습니다.

말그대로 현재노드를 A라고 다음노드를 B라고 했을때 이 A와 B사이의 상대적 거리 쉽게말해서

현재노드와 다음노드사이의 상대적 거리 를 간단하게 1.000 로 두겠다는 의미입니다.

대각선방향에서 달라지는 점은 다음노드가 C가 된다는것 이외에는 달라지지않습니다.

똑같이 다음노드가 C이기 때문에 그 이동비용은 1.414(\( \sqrt{2} \)) 가 됩니다.

🔮 A* 길찾기 알고리즘을 직접 설계해보자!

A* 길찾기 알고리즘을 만들기 전에 저는 개발환경을 구성하고자합니다.

저의 개발환경입니다.

🕹️ 개발환경

혹시 개발환경이 맞지않아 작동이 안되는 상황도 있기때문에 이 점을 감안해주시기 바랍니다.

통합개발환경 Visual Studio 2022 Current (v143) 컴파일러 MSVC 프로그래밍 언어 C++20 운영체제 Windows 11 home 작성일자 2021-11-21 파일 기본 인코딩 UTF-8 윈도우 패키지 x64

🕹️ 프레임워크

이번에 구현할 A* 길찾기 알고리즘의 프레임워크 설계도입니다.

이런식으로 프레임워크를 설계한이유는 쉽게 접근하는데 용이하고,

소스코드가 간결해지며 코드의 가독성이 증가하게됩니다.

FileClass는 맵정보가 담긴 파일을 관리합니다.

HeuristicClass는 맵정보를 기반으로 최적의 경로탐색을 수행합니다.

AstarClass는 FileClass와 HeuristicClass를 적절히 사용하여 A* 길찾기 알고리즘을 수행합니다.

🕹️ 프로젝트 시작하기

우선 저는 “빈 프로젝트”를 선택하고 다음(N)을 클릭하겠습니다.

그다음, 적절한 프로젝트의 이름을 설정하고 만들기(C)를 클릭하겠습니다.

그다음, 솔루션을 마우스우클릭한후 속성(R)을 클릭하겠습니다.

속성 페이지창이 뜨면 다음과 같이 설정해준후, 적용(A)을 누르고, 확인을 누릅니다.

🕹️ 솔루션 정렬

저는 다음과 같은방식으로 프로젝트의 솔루션을 정렬했습니다.

🔮 A* 길찾기 알고리즘를 직접 만들어보자!

TypeDecl.h

우선 본격적인 A* 길찾기 알고리즘을 수행하기전에 몇가지의 구조체및 선언을 만들어야합니다.

이 TypeDecl.h 헤더파일은 앞으로 사용할 모든 헤더파일에 포함될것입니다.

/* < TypeDecl.h > */ #pragma once #include #include #include constexpr float INF = (987654321.0000f); #define BLOCK int _ = _getch() using Vec2Map = std::vector>; /* Vec2MapInfo: 맵정보, 맵의 최대크기, 맵의 출발지점, 맵의 도착지점 */ struct Vec2MapInfo { Vec2Map mapInfo; int map_col; int map_row; int beg_x; int beg_y; int dest_x; int dest_y; }; /* CostCoord: 한 정점의 좌표와 비용 */ struct CostCoord { float cost; int x, y; }; /* Coord: 한 정점의 좌표 */ struct Coord { int x, y; }; /* CellDetails: 비용과 부모노드 */ struct CellDetails { float f, g, h; int parent_x, parent_y; }; /* PointDecl: 맵에 표기 되어있는 문자의 역할 */ struct PointDecl { std::string BeginPoint, DestinationPoint, Void, Wall, TracedPath; };

이제 텍스트파일을 기반으로 맵에 대한 정보를 불러오는 기능을 만들어보도록 하겠습니다.

FileClass.h

FileClass 클래스의 선언입니다.

우선 RoadMap함수는 두번째 파라미터로 맵정보를 불러올 맵의 텍스트파일경로를 넣어주면 됩니다.

그리고 외부에서 그 맵정보를 불러오고싶을때,

GetVec2Map함수를 수행하면 해당 맵에 대한 정보를 불러올 수 있습니다.

/* < FileClass.h > */ #pragma once #include “TypeDecl.h” #include #include #include class FileClass { public: FileClass(); FileClass(const FileClass&); ~FileClass(); bool RoadMap(PointDecl, const char*); Vec2MapInfo GetVec2Map();

FileClass 클래스는 멤버변수로 Vec2MapInfo라는 구조체를 가집니다.

이 구조체는 외부에서 맵정보를 가져오고싶을때, GetVec2Map함수를 통해 가져올 수 있습니다.

private: Vec2MapInfo m_MapInfo; };

FileClass.cpp

이거는 FileClass 클래스의 정의입니다.

우선 맵정보를 가지고있는 멤버변수 m_MapInfo라는 구조체를 초기화합니다.

/* < FileClass.cpp > */ #include “FileClass.h” FileClass::FileClass() { memset(&this->m_MapInfo, 0x00, sizeof(Vec2MapInfo)); return; } FileClass::FileClass(const FileClass&) { return; } FileClass::~FileClass() { return; }

그다음 본격적으로 RoadMap이라는 함수를 정의합니다.

이 함수는 우선 읽기모드(std::ifstream)로 텍스트파일을 열게됩니다.

이제 열었으니, 멤버변수 fail()과 is_open()으로 이 텍스트파일이 정상적으로 존재하는 파일인지 확인합니다.

bool FileClass::RoadMap(PointDecl ptDecl, const char* mapFileName) { std::ifstream mapFile(mapFileName); if (mapFile.fail()) { return false; } if (!mapFile.is_open()) { return false; }

우선 맵의 가로길이를 mapInfo.map_col라는 변수에 저장하고,

맵의 세로길이를 mapInfo.map_row라는 변수에 저장합니다.

이제 제네릭타입이 std::string인 Map이라는 2차원벡터에 세로길이가 mapInfo.map_row,

가로길이가 mapInfo.map_col만큼 빈 데이터공간을 생성합니다.

빈공간을 생성하게되면 이제 본격적으로 파일에서 맵정보를 읽어와야합니다.

여기서 중요한점이 있는데,

만약 그 점이 시작지점이거나 도착지점이면 반드시 빈 공간(Void)으로 바꿔주어야 한다는겁니다.

그 이유는 밑에서 HeuristicClass를 다루게되면 아시게될것입니다.

Vec2MapInfo mapInfo; mapFile >> mapInfo.map_col >> mapInfo.map_row; Vec2Map Map(mapInfo.map_row, std::vector(mapInfo.map_col)); for (int y = 0; y < mapInfo.map_row; ++y) { for (int x = 0; x < mapInfo.map_col; ++x) { mapFile >> Map[y][x]; if (Map[y][x] == ptDecl.BeginPoint) { mapInfo.beg_x = x; mapInfo.beg_y = y; Map[y][x] = ptDecl.Void; } if (Map[y][x] == ptDecl.DestinationPoint) { mapInfo.dest_x = x; mapInfo.dest_y = y; Map[y][x] = ptDecl.Void; } } } mapInfo.mapInfo.clear(); mapInfo.mapInfo = Map; this->m_MapInfo = mapInfo; return true; } Vec2MapInfo FileClass::GetVec2Map() { return (this->m_MapInfo); }

HeuristicClass.h

이 HeuristicClass 클래스가 우리가 만들 A* 길찾기 알고리즘의 핵심이 될 부분입니다.

HeuristicClass 클래스는 그래프의 탐색을 하거나,

탐색이 끝난 그래프를 외부에서 가져오기 쉽게 가공하는 역할을 합니다.

우선 우리는 열린목록에 대한 자료구조를 set으로 하게될겁니다.

그 이유는

1. set은 중복된 원소를 가지지않는다.

2. 사용자가 직접 정렬방식을 정할 수 있다.

때문입니다.

less구조체의 역할은 나중에 열린목록에서

제네릭타입이 CostCoord인 set을 비용이 적은순으로 정렬할것이기 때문에 추가로 오버로딩했습니다.

/* < HeuristicClass.h > */ #pragma once #include “TypeDecl.h” #include #include #include #include #include class HeuristicClass { private: struct less { constexpr bool operator()(const CostCoord& _Left, const CostCoord& _Right) const { return _Left.cost < _Right.cost; } }; public: HeuristicClass(); HeuristicClass(const HeuristicClass&); ~HeuristicClass(); bool BeginSearch(PointDecl, Vec2MapInfo); std::queue GetQueue();

이제 아래 4개의 멤버함수는 외부에서 접근할 수 없도록, 내부에서만 사용할것이기때문에 private으로 설정했습니다.

멤버변수 m_TrackedPath는 그래프상에서 최적의 경로를 담고있는 큐입니다.

이 멤버변수는 GetQueue함수로 외부에서 가져갈 수 있습니다.

private: bool isUnBlockedNode(PointDecl, Vec2Map, int, int); bool isDestinationNode(int, int, int, int); bool isInGround(int, int, int, int); void TraceMinimumPath(std::vector>, Coord); private: const int m_dx1[4] = { 0, 0, 1, -1 }; const int m_dy1[4] = { -1, 1, 0, 0 }; const int m_dx2[4] = { 1, -1, -1, 1 }; const int m_dy2[4] = { -1, 1, -1, 1 }; std::queue m_TrackedPath; };

HeuristicClass.cpp

먼저 생성자와 소멸자는 아무 역할을 하지않고 기본적인 선언과 정의만 해두었습니다.

BeginSearch함수는 이제 그래프를 탐색하면서 모든 노드들에 대한 최적의 경로를 만들어냅니다.

우선 탐색전에 이미 출발지점과 도착지점이 같은지,

출발지점이나 도착지점이 그래프내에 존재하는지,

도착지점이나 출발지점이 벽으로 막혀져있는지 확인해야합니다.

/* < HeuristicClass.cpp > */ #include “HeuristicClass.h” HeuristicClass::HeuristicClass() { return; } HeuristicClass::HeuristicClass(const HeuristicClass&) { return; } HeuristicClass::~HeuristicClass() { return; } bool HeuristicClass::BeginSearch(PointDecl ptDecl, Vec2MapInfo mapInfo) { if (this->isDestinationNode(mapInfo.dest_x, mapInfo.dest_y, mapInfo.beg_x, mapInfo.beg_y)) { return true; } if (!this->isUnBlockedNode(ptDecl, mapInfo.mapInfo, mapInfo.beg_x, mapInfo.beg_y) || !this->isUnBlockedNode(ptDecl, mapInfo.mapInfo, mapInfo.dest_x, mapInfo.dest_y)) { return false; } if (!this->isInGround(mapInfo.map_col, mapInfo.map_row, mapInfo.beg_x, mapInfo.beg_y) || !this->isInGround(mapInfo.map_col, mapInfo.map_row, mapInfo.dest_x, mapInfo.dest_y)) { return false; }

이제 초기화를 해야합니다.

CellDetails는 노드의 비용과 부모노드에 대한 정보를 담고있습니다.

부모노드는 모두 -1로 초기화하고, 비용은 모두 INF(무한대)로 해줍시다.

그다음 출발지점의 비용을 0.0f로하고 출발지점의 부모노드는 출발노드를 가리키게됩니다.

이제 닫힌목록과 열린목록을 생성하고, 열린목록에는 출발노드를 집어넣습니다.

int st_x = mapInfo.beg_x; int st_y = mapInfo.beg_y; std::vector> cell(mapInfo.map_row, std::vector(mapInfo.map_col)); for (int y = 0; y < mapInfo.map_row; ++y) { for (int x = 0; x < mapInfo.map_col; ++x) { cell[y][x].f = cell[y][x].g = cell[y][x].h = INF; cell[y][x].parent_x = cell[y][x].parent_y = -1; } } cell[st_y][st_x].f = cell[st_y][st_x].g = cell[st_y][st_x].h = 0.0f; cell[st_y][st_x].parent_x = st_x; cell[st_y][st_x].parent_y = st_y; std::vector> closedList(mapInfo.map_row, std::vector(mapInfo.map_col)); CostCoord bVertex; bVertex.cost = 0.0f; bVertex.x = st_x; bVertex.y = st_y; std::set openList; openList.insert(bVertex);

여기는 bfs와 매우 유사합니다.

openList는 항상 제일 적은 비용의 노드를 첫번째주소로 가리키게 됩니다(전에 선언한 less구조체에 의해).

먼저 닫힌목록의 현재노드를 방문처리해주고, 이제 직선방향에 대한 탐색과 대각선방향에 대한 탐색을 시작합니다.

직선방향탐색과 대각선방향탐색의 다른점은 g를 업데이트할때말고는 전부 똑같습니다.

일단 다음노드가 그래프내부에 있다면 2개의 분기로 나눠집니다.

1. 다음노드가 도착노드인가?

2. 다음노드가 닫힌목록에 존재하지않은 노드인 동시에, 벽으로 막혀져있지않은가?

우리는 만약 1번분기로 간다면 다음노드의 부모노드는 현재노드가되고, 그래프의 최적화경로를 탐색하기 시작합니다.

2번분기로 간다면 n, g, f를 각각 업데이트하고, 다음노드의 f가 한번도 업데이트되지않았거나, 새로 업데이트될 f(nf)보다 기존의 f(f)가 크다면 cellDetails를 업데이트하고, 열린목록에 다음노드를 삽입합니다.

while (!openList.empty()) { CostCoord current = *openList.begin(); openList.erase(openList.begin()); int cur_x = current.x; int cur_y = current.y; closedList[cur_y][cur_x] = true; float nf, ng, nh; for (int i = 0; i < 4; ++i) { int new_x = cur_x + this->m_dx1[i]; int new_y = cur_y + this->m_dy1[i]; if (this->isInGround(mapInfo.map_col, mapInfo.map_row, new_x, new_y)) { if (this->isDestinationNode(mapInfo.dest_x, mapInfo.dest_y, new_x, new_y)) { cell[new_y][new_x].parent_x = cur_x; cell[new_y][new_x].parent_y = cur_y; this->TraceMinimumPath(cell, { mapInfo.dest_x, mapInfo.dest_y }); return true; } else if (!closedList[new_y][new_x] && this->isUnBlockedNode(ptDecl, mapInfo.mapInfo, new_x, new_y)) { ng = cell[cur_y][cur_x].g + 1.000f; nh = ((float)std::sqrt((float)std::pow(new_x – mapInfo.dest_x, 2) + (float)std::pow(new_y – mapInfo.dest_y, 2))); nf = ng + nh; if (cell[new_y][new_x].f == INF || cell[new_y][new_x].f > nf) { cell[new_y][new_x].g = ng; cell[new_y][new_x].h = nh; cell[new_y][new_x].f = nf; cell[new_y][new_x].parent_x = cur_x; cell[new_y][new_x].parent_y = cur_y; CostCoord bVertex; bVertex.cost = nf; bVertex.x = new_x; bVertex.y = new_y; openList.insert(bVertex); } } } } for (int i = 0; i < 4; ++i) { int new_x = cur_x + this->m_dx2[i]; int new_y = cur_y + this->m_dy2[i]; if (this->isInGround(mapInfo.map_col, mapInfo.map_row, new_x, new_y)) { if (this->isDestinationNode(mapInfo.dest_x, mapInfo.dest_y, new_x, new_y)) { cell[new_y][new_x].parent_x = cur_x; cell[new_y][new_x].parent_y = cur_y; this->TraceMinimumPath(cell, { mapInfo.dest_x, mapInfo.dest_y }); return true; } else if (!closedList[new_y][new_x] && this->isUnBlockedNode(ptDecl, mapInfo.mapInfo, new_x, new_y)) { ng = cell[cur_y][cur_x].g + 1.414f; nh = ((float)std::sqrt((float)std::pow(new_x – mapInfo.dest_x, 2) + (float)std::pow(new_y – mapInfo.dest_y, 2))); nf = ng + nh; if (cell[new_y][new_x].f == INF || cell[new_y][new_x].f > nf) { cell[new_y][new_x].g = ng; cell[new_y][new_x].h = nh; cell[new_y][new_x].f = nf; cell[new_y][new_x].parent_x = cur_x; cell[new_y][new_x].parent_y = cur_y; CostCoord bVertex; bVertex.cost = nf; bVertex.x = new_x; bVertex.y = new_y; openList.insert(bVertex); } } } } } return false; }

이제 그래프에서 탐색된 내용(cell)을 기반으로 최적화된 경로를 만들어 내어야 합니다.

우선 처음 스택에는 도착노드의 좌표를 삽입합니다.

그후, 현재노드의 부모노드가 현재노드와 같아질때(출발노드가 이에 해당)까지

스택에 현재노드의 부모노드를 삽입합니다.

굳이 큐는 사용하지않아도 되지만 외부에서 가져갈때 좀더 사용하기 쉽도록 큐로 옮겼습니다.

void HeuristicClass::TraceMinimumPath(std::vector> cell, Coord dest) { std::stack TrackingStack; std::queue TrackingQueue; int cur_x, cur_y; cur_x = dest.x; cur_y = dest.y; TrackingStack.push({ cur_x, cur_y }); while (!(cell[cur_y][cur_x].parent_x == cur_x && cell[cur_y][cur_x].parent_y == cur_y)) { int temp_x = cell[cur_y][cur_x].parent_x; int temp_y = cell[cur_y][cur_x].parent_y; cur_x = temp_x; cur_y = temp_y; TrackingStack.push({ cur_x, cur_y }); } while (!TrackingStack.empty()) { TrackingQueue.push(TrackingStack.top()); TrackingStack.pop(); } this->m_TrackedPath = TrackingQueue; return; } std::queue HeuristicClass::GetQueue() { return (this->m_TrackedPath); } bool HeuristicClass::isUnBlockedNode(PointDecl ptDecl, Vec2Map Map, int cur_x, int cur_y) { return (Map[cur_y][cur_x] == ptDecl.Void); } bool HeuristicClass::isDestinationNode(int dest_x, int dest_y, int cur_x, int cur_y) { return ((dest_x == cur_x) && (dest_y == cur_y)); } bool HeuristicClass::isInGround(int map_col, int map_row, int cur_x, int cur_y) { return (((0 <= cur_x) && (cur_x < map_col)) && ((0 <= cur_y) && (cur_y < map_row))); } AstarClass.h AstarClass 클래스는 FileClass 클래스와 HeuristicClass 클래스를 적절히 이용해서 메인함수에서 사용하기 쉽게 브릿지역할을 하게됩니다. /* < AstarClass.h > */ #pragma once #include “FileClass.h” #include “HeuristicClass.h” #include “TypeDecl.h” #include class AstarClass { public: AstarClass(); AstarClass(const AstarClass&); ~AstarClass(); bool Initialize(PointDecl, const char*); void Shutdown(); void Run();

멤버변수 m_PtDecl은 그래프에서의 문자열에 따라 그에 맞는 역할을 수행하도록 도와주는 구조체입니다.

private: FileClass* m_file_class; HeuristicClass* m_heuristic_class; PointDecl m_PtDecl; };

AstarClass.cpp

생성자가 호출되면 FileClass 클래스와 HeuristicClass 클래스를 가리키는 포인터들을 모두 0으로 초기화합니다.

Initialize함수는 이 포인터들에게 메모리공간을 모두 나눠주고, 각각에 맞는 역할을 수행하도록하는 역할입니다.

Shutdown함수는 이 포인터들의 메모리공간을 다시 반납하고 0으로 초기화하는 역할입니다.

/* < AstarClass.cpp > */ #include “AstarClass.h” AstarClass::AstarClass() { this->m_file_class = 0; this->m_heuristic_class = 0; } AstarClass::AstarClass(const AstarClass&) { return; } AstarClass::~AstarClass() { return; } bool AstarClass::Initialize(PointDecl ptDecl, const char* mapFileName) { this->m_PtDecl = ptDecl; bool result; this->m_file_class = new FileClass; if (!this->m_file_class) { return false; } result = this->m_file_class->RoadMap(ptDecl, mapFileName); if (!result) { return false; } this->m_heuristic_class = new HeuristicClass; if (!this->m_heuristic_class) { return false; } return true; } void AstarClass::Shutdown() { if (this->m_file_class) { delete this->m_file_class; this->m_file_class = 0; } if (this->m_heuristic_class) { delete this->m_heuristic_class; this->m_heuristic_class = 0; } return; }

Run함수에서는 먼저 FileClass 클래스의 멤버함수인 GetVec2Map함수를 호출하여 맵정보를 불러옵니다.

그다음 HeuristicClass 클래스에서 해당맵정보를 바탕으로 그래프탐색을 시작합니다.

그래프탐색이 끝나면,

그래프에서 최적의 경로에 대한 정보를 가지고있는 큐를 GetQueue함수를 이용해 얻을 수 있습니다.

void AstarClass::Run() { std::queue queue; bool result; Vec2MapInfo mapInfo = this->m_file_class->GetVec2Map(); if (mapInfo.mapInfo.empty()) { return; } result = this->m_heuristic_class->BeginSearch(this->m_PtDecl, mapInfo); switch (result) { case false: std::cout << "올바른 맵의 정보가 아닙니다." << std::endl; break; case true: queue = this->m_heuristic_class->GetQueue(); break; }

만약 그래프탐색이 성공적으로 끝난다면(result == true)기존에불러왔던 맵의 그래프에다가 최적의 경로를 순서대로 덮어씌우고, 맵의 그래프를 콘솔창으로 출력합니다.

if (result) { while (!queue.empty()) { Coord coord = queue.front(); queue.pop(); mapInfo.mapInfo[coord.y][coord.x] = this->m_PtDecl.TracedPath; } for (int y = 0; y < mapInfo.map_row; ++y) { for (int x = 0; x < mapInfo.map_col; ++x) { std::cout << mapInfo.mapInfo[y][x] << ' '; } std::cout << std::endl; } } return; } main.cpp 메인함수가 포함된 이 소스에서는 그래프의 문자열이 의미하는 바를 정의하고, AstarClass 클래스를 동작시킵니다. "BLOCK;"은 그냥 빌드에서 실행하면 결과값이 안보이기 때문에 콘솔창을 한번 멈추는 역할을 합니다. /* < main.cpp > */ #include “AstarClass.h” int main(int argc, char** argv) { AstarClass* astar_class = new AstarClass; PointDecl pDecl; pDecl.BeginPoint = “@”; pDecl.DestinationPoint = “$”; pDecl.TracedPath = “*”; pDecl.Void = “+”; pDecl.Wall = “#”; if (astar_class->Initialize(pDecl, “sample_map.txt”)) { astar_class->Run(); } astar_class->Shutdown(); delete astar_class; astar_class = 0; BLOCK; return 0; }

🧼 실제로 실행을 시켜보자!

우선 실행을 시키기 전에 맵의 그래프정보가 담긴 텍스트파일이 필요합니다.

간단하게 만들어보겠습니다.

sample_map.txt

10 5 # @ # + # + # + # # # + # + + + + + + + # + # + # # # + # + # + # + # $ # # # + # + + + # + + + + +

10은 맵의 가로길이, 5는 맵의 세로길이를 의미합니다.

밑에는 맵에 대한 정보입니다.

맵에 대한 그래프의 제네릭타입을 std::string으로 했기때문에 한 블럭을 문자열로 하셔도 상관없습니다.

이제 실행시켜보겠습니다!

실행화면

다음과 같이 너무 잘 작동합니다~

와우~!!!

꾸웃!

🧼 마치며…

그대로 복붙만 하는것보다는 이해를 하시면서 공부하는것을 추천드립니다..

이렇게 추천드리는 이유는 “이해한다는것”과 “따라한다는것”은 여러 상황에 대한 유연성과 프로그래밍적인 사고력을 길러준다는 측면에 매우 다릅니다.

이해가 안가시는분은 고민하지마시고 댓글로 질문부탁드립니다.

확인하는대로 답변드리겠습니다!

그럼 안녕~!!

728×90

[최단 경로 알고리즘] 가장 빠른 길 찾기

728×90

반응형

최단 경로 알고리즘

– 말 그대로 가장 짧은 경로를 찾는 알고리즘

– ‘한 지점에서 다른 특정 지점까지의 최단 경로’, ‘모든 지점에서 다른 모든 지점까지의 최단 경로’ 등의 사례가 존재

– 최단 경로를 모두 출력하는 문제보다는 단순히 최단 거리를 출력하도록 요구하는 문제가 많음

– 그리디 알고리즘과 다이나믹 프로그래밍 알고리즘이 최단 경로 알고리즘에 그대로 적용됨

음의 간선

– 0보다 작은 값을 가지는 간선

– 현실 세계의 길(간선)은 음의 간선으로 표현되지 않으므로 다익스트라 알고리즘은 실제로 GPS 소프트웨어의 기본 알고리즘으로 채택됨

다익스트라 최단 경로 알고리즘

– 그래프에서 여러 개의 노드가 있을 때, 특정한 노드에서 출발하여 다른 노드로 가는 각각의 최단 경로를 구해주는 알고리즘

– ‘음의 간선’이 없을 때 정상적으로 동작

– ‘가장 비용이 적은 노드’를 선택해 과정을 반복하기 때문에 그리디 알고리즘으로 분류

– 최단 경로를 구하는 과정에서 ‘각 노드에 대한 현재까지의 최단 거리’ 정보를 항상 1차원 리스트에 저장하며 리스트를 계속 갱신

– 한 단계당 하나의 노드에 대한 최단 거리를 확실히 찾는 것으로 이해할 수 있음

다익스트라 최단 경로 알고리즘의 원리

1. 출발 노드를 설정

2. 최단 거리 테이블을 초기화

3. 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택

4. 해당 노드를 거쳐 다른 노드로 가는 비용을 계산하여 최단 거리 테이블을 갱신

5. 위 과정에서 3과 4를 반복

간단한 다익스트라 알고리즘

– O(V^2)의 시간 복잡도 (V는 노드의 개수)

– 처음에 각 노드에 대한 최단 거리를 담는 1차원 리스트 선언

– 단계마다 ‘방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택’하기 위하여 매 단계마다 1차원 리스트의 모든 원소를 확인(순차 탐색)

import sys input = sys.stdin.readline INF = int(1e9) # 무한을 의미하는 값으로 10억을 설정 # 노드의 개수, 간선의 개수를 입력받기 n, m = map(int, input().split()) # 시작 노드 번호를 입력받기 start = int(input()) # 각 노드에 연결되어 있는 노드에 대한 정보를 담는 리스트를 만들기 graph = [[] for i in range(n + 1)] # 방문한 적이 있는지 체크하는 목적의 리스트를 만들기 visited = [False] * (n + 1) # 최단 거리 테이블을 모두 무한으로 초기화 distance = [INF] * (n + 1) # 모든 간선 정보를 입력받기 for _ in range(m): a, b, c = map(int, input().split()) # a번 노드에서 b번 노드로 가는 비용이 c라는 의미 graph[a].append((b, c)) # 방문하지 않은 노드 중에서, 가장 최단 거리가 짧은 노드의 번호를 반환 def get_smallest_node(): min_value = INF index = 0 # 가장 최단 거리가 짧은 노드(인덱스) for i in range(1, n + 1): if distance[i] < min_value and not visited[i]: min_value = distance[i] index = i return index def dijkstra(start): # 시작 노드에 대해서 초기화 distance[start] = 0 visited[start] = True for j in graph[start]: distance[j[0]] = j[1] # 시작 노드를 제외한 전체 n - 1개의 노드에 대해 반복 for i in range(n - 1): # 현재 최단 거리가 가장 짧은 노드를 꺼내서, 방문 처리 now = get_smallest_node() visited[now] = True # 현재 노드와 연결된 다른 노드를 확인 for j in graph[now]: cost = distance[now] + j[1] # 현재 노드를 거쳐서 다른 노드로 이동하는 거리가 더 짧은 경우 if cost < distance[j[0]]: distance[j[0]] = cost # 다익스트라 알고리즘을 수행 dijkstra(start) # 모든 노드로 가기 위한 최단 거리를 출력 for i in range(1, n + 1): # 도달할 수 없는 경우, 무한(INFINITY)이라고 출력 if distance[i] == INF: print("INFINITY") # 도달할 수 있는 경우 거리를 출력 else: print(distance[i]) 간단한 다익스트라 알고리즘의 시간 복잡도 - 총 O(V)번에 걸쳐서 최단 거리가 가장 짧은 노드를 매번 선형 탐색해야 하고, 현재 노드와 연결된 노드를 매번 일일이 확인하기 때문 - 전체 노드의 개수가 5,000개 이하라면 간단한 다익스트라 알고리즘으로 문제를 풀 수 있음 - 하지만 노드의 개수가 10,000개를 넘어가는 문제라면 해결하기 어려움 우선순위 큐 - 우선순위가 가장 높은 데이터를 가장 먼저 삭제 - 데이터를 우선순위에 따라 처리하고 싶을 때 사용 - 일반적으로 PriorityQueue 보다는 heapq가 더 빠르게 동작하기 때문에 수행 시간이 제한된 상황에서는 heapq 사용을 권장 - 우선순위 큐 라이브러리에 데이터의 묶음을 넣으면 첫 번째 원소를 기준으로 우선순위를 설정 - 내부적으로 최소 힙 혹은 최대 힙을 이용 - 힙을 이용하는 경우 모든 원소를 저장한 뒤에 우선순위에 맞게 빠르게 뽑아낼 수 있으므로 힙은 우선순위 큐를 구현하는데 많이 사용 - 우선순위 큐를 이용하여 시작 노드로부터 거리가 짧은 노드 순서대로 큐에서 나올 수 있도록 다익스트라 알고리즘을 작성 최소 힙 - 값이 낮은 데이터가 먼저 삭제 (최대 힙은 값이 큰 데이터가 먼저 삭제) - 파이썬 라이브러리에서는 기본적으로 최소 힙 구조를 이용 - 다익스트라 최단 경로 알고리즘에서는 비용이 적은 노드를 우선하여 방문하므로 최소 힙 구조를 기반으로 하는 파이썬의 우선순위 큐 라이브러리를 그대로 사용하면 적합 - 최소 힙을 최대 힙처럼 사용하기 위하여 일부러 우선순위에 해당하는 값에 음수 부호를 붙여 넣었다가 나중에 우선순위 큐에서 꺼낸 다음 다시 음수 부호를 붙여 원래의 값으로 돌리는 방식도 존재 - 힙에서 원소를 꺼내면 '가장 값이 작은 원소'가 추출됨 개선된 다익스트라 알고리즘 - 최악의 경우에도 시간 복잡도 O(ElogV)를 보장 (V는 노드의 개수, E는 간선의 개수) - 힙 자료구조를 사용 - 간단한 다익스트라 알고리즘과 비교할 때 get_smallest_node() 함수를 작성할 필요가 없음 - 최단 거리가 가장 짧은 노드를 선택하는 과정을 다익스트라 최단 경로 함수 안에서 우선순위 큐를 이용하는 방식으로 대체 가능 import heapq import sys input = sys.stdin.readline INF = int(1e9) # 무한을 의미하는 값으로 10억을 설정 # 노드의 개수, 간선의 개수를 입력받기 n, m = map(int, input().split()) # 시작 노드 번호를 입력받기 start = int(input()) # 각 노드에 연결되어 있는 노드에 대한 정보를 담는 리스트를 만들기 graph = [[] for i in range(n + 1)] # 최단 거리 테이블을 모두 무한으로 초기화 distance = [INF] * (n + 1) # 모든 간선 정보를 입력받기 for _ in range(m): a, b, c = map(int, input().split()) # a번 노드에서 b번 노드로 가는 비용이 c라는 의미 graph[a].append((b, c)) def dijkstra(start): q = [] # 시작 노드로 가기 위한 최단 경로는 0으로 설정하여, 큐에 삽입 heapq.heappush(q, (0, start)) distance[start] = 0 while q: # 큐가 비어있지 않다면 # 가장 최단 거리가 짧은 노드에 대한 정보 꺼내기 dist, now = heapq.heappop(q) # 현재 노드가 이미 처리된 적이 있는 노드라면 무시 if distance[now] < dist: continue # 현재 노드와 연결된 다른 인접한 노드들을 확인 for i in graph[now]: cost = dist + i[1] # 현재 노드를 거쳐서, 다른 노드로 이동하는 거리가 더 짧은 경우 if cost < distance[i[0]]: distance[i[0]] = cost heapq.heappush(q, (cost, i[0])) # 다익스트라 알고리즘을 수행 dijkstra(start) # 모든 노드로 가기 위한 최단 거리를 출력 for i in range(1, n + 1): # 도달할 수 없는 경우, 무한(INFINITY)이라고 출력 if distance[i] == INF: print("INFINITY") # 도달할 수 있는 경우 거리를 출력 else: print(distance[i]) 개선된 다익스트라 알고리즘의 시간 복잡도 - 간단한 다익스트라 알고리즘에 비해 훨씬 빠름 플로이드 워셜 알고리즘 - 모든 지점에서 다른 모든 지점까지의 최단 경로를 모두 구해야 하는 경우에 사용할 수 있는 알고리즘 - 단계마다 거쳐 가는 노드를 기준으로 알고리즘을 수행 - 매번 방문하지 않은 노드 중에서 최단 거리를 갖는 노드를 찾을 필요가 없음 - 2차원 리스트에 최단 거리 정보를 저장 (모든 노드에 대하여 다른 모든 노드로 가는 최단 거리 정보를 담아야 하기 때문 - 2차원 리스트를 처리해야 하므로 N번의 단계에서 O(N^2)의 시간이 소요 - 노드의 개수가 N일 때 N번 만큼의 단계를 반복하며 점화식에 맞게 2차원 리스트를 갱신하기 때문에 다이나믹 프로그래밍 INF = int(1e9) # 무한을 의미하는 값으로 10억을 설정 # 노드의 개수 및 간선의 개수를 입력받기 n = int(input()) m = int(input()) # 2차원 리스트(그래프 표현)를 만들고, 모든 값을 무한으로 초기화 graph = [[INF] * (n + 1) for _ in range(n + 1)] # 자기 자신에서 자기 자신으로 가는 비용은 0으로 초기화 for a in range(1, n + 1): for b in range(1, n + 1): if a == b: graph[a][b] = 0 # 각 간선에 대한 정보를 입력 받아, 그 값으로 초기화 for _ in range(m): # A에서 B로 가는 비용은 C라고 설정 a, b, c = map(int, input().split()) graph[a][b] = c # 점화식에 따라 플로이드 워셜 알고리즘을 수행 for k in range(1, n + 1): for a in range(1, n + 1): for b in range(1, n + 1): graph[a][b] = min(graph[a][b], graph[a][k] + graph[k][b]) # 수행된 결과를 출력 for a in range(1, n + 1): for b in range(1, n + 1): # 도달할 수 없는 경우, 무한(INFINITY)이라고 출력 if graph[a][b] == 1e9: print("INFINITY", end=" ") # 도달할 수 있는 경우 거리를 출력 else: print(graph[a][b], end=" ") print() 플로이드 워셜 알고리즘의 점화식 - Dab = min(Dab, Dak + Dkb) - A에서 B로 가는 최소 비용과 A에서 K를 거쳐 B로 가는 비용을 비교하여 더 작은 값으로 갱신하겠다는 말 728x90 반응형

최단거리 길 찾기 알고리즘 – A* 알고리즘

1. 목적

출발점에서 목적지까지 길 찾기를 하고자 할때 중간 중간 장애물을 피해가며 목적지까지 도달하는 알고리즘이 필요해졌다. 물론 길 찾기를 하는동안 목적지까지 도달 할 수 없는 경우도 판별이 가능할 것이다.

2. 개요

이 알고리즘의 아이디어는 어떠한 노드에 대하여 시작점으로부터 온 거리(g)와 앞으로 목적지까지 가는데 남은 거리(h : heuristic)를 더한 값(f)이 가장 적은 노드를 선택하여 목적지 까지 탐색하는 것이다.

여기서 중요한것은 목적지까지 남은 거리(h)를 적절하게 추정하여 설정하는 것이다.

그림1. 가장 작은 f값을 선택해가며 진행한다

그리고 두개의 목록으로 노드들을 관리해야 하는데 그것이 열린목록(Open list)과 닫힌목록(Close list)이다.

열린목록은 현재 조사중인 노드에 인접한 리스트로 최단거리로써 선택 가능성이 열려있는 노드들의 집합이다.

닫힌목록은 조사가 끝난 노드들의 집합으로 다시 조사할 필요가 없다.

3. 설계

각 노드는 위에서 언급한 g, h, f의 값과 자신의 노드id, 어디로부터 왔는지 부모노드의 id를 저장한다.

열린목록은 일반적인 list를 이용하는 것이 아닌 가장 최선의 방법일 가능성이 높은 즉, f의 값이 가장 작은 노드를 먼저 검색할 것이기 때문에 priority queue를 사용한다.

닫힌목록은 일반적인 queue로 생성한다.

전체적인 흐름은 시작노드를 열린목록에 넣는것으로 시작한다.

그리고 다음을 반복한다.

1. 열린목록의 노드를 꺼내어 접근할 수 있는 노드를 모두 검사한다.

2-1. 해당 노드가 닫힌목록에 있는 노드라면 무시한다.

2-2. 해당 노드가 열린목록에 있는 노드라면 f값을 비교해 더 작은 노드의 정보로 갱신한다.

2-3. 해당 노드가 열린목록 혹은 닫힌목록에 있지 않는 노드라면 열린목록에 넣는다.

3. 1번에서 꺼낸 노드는 닫힌목록으로 이동한다.

4. 목표 노드가 열린목록에 들어왔다면 중지한다.

나머지는 목표 노드로부터 부모노드를 찾아 시작노드까지 조사해 길을 완성한다.

만약 목표 노드가 들어오기전에 열린 목록이 먼저 비었다면 길이 없다는 뜻이다.

4. 예시

다음 예는 대각 이동 불가, h는 목표까지의 x의 차이와 y의 차이를 더한 값, 열린목록에서 같은 f값이 들어왔다면 먼저 들어온 노드에 우선권등 조건을 부여하였다.

그림2. a*알고리즘의 예시

출처 : aidev.co.kr/game/501

en.wikipedia.org/wiki/A*_search_algorithm

반응형

길찾기 알고리즘(2) – 다익스트라 알고리즘

728×90

다익스트라 알고리즘

다익스트라 알고리즘은 최단거리를 구하는 알고리즘중 가장 대표적인 알고리즘으로 아래와 같은 조건에서 사용하면 좋습니다.

– 간선간 음의 가중치가 없을때

– 시작지점이 한개의 정점으로 정해졌을 때 (시작점이 여러개라면 다익스트라를 여러번 돌려 해결이 가능합니다)

다익스트라 알고리즘은 ‘가장 빠른 길’을 찾는 알고리즘으로 간선간 음의 값을 가지는 길은 없다고 생각합니다. 또한 플로이드-와샬보다 속도는 빠르지만 시작지점 1개에 대해서만 빠른 길을 찾는 알고리즘입니다.

조금 풀어쓴 다익스트라 알고리즘

예시 다익스트라 경로

1. 시작점 start, 도착점 finish, 각 경로와 가중치가 주어지게 됩니다.

– 입력을 전부 받아오면 다익스트라를 위한 배열을 아래 표처럼 INF로 초기화해줍니다.

1 2 3 4 5 INF INF INF INF INF

2. 시작점에서 시작점으로 가는 비용은 0이니 시작 지점을 0으로 지정해 줍니다.

1 2 3 4 5 0 INF INF INF INF

– 이후 큐에 시작 지점을 넣어준 뒤 경로 탐색을 시작합니다.

3. 큐에서 원소를 하나 빼내고 경로가 담긴 배열과 비교해 배열을 업데이트 해줍니다.

1 2 3 4 5 0 3 2 INF INF

– 이후 이 지점들을 큐에 넣습니다.

4. 3번과 같은 과정으로 다음 지점인 2, 3 에서 갈 수 있는 값을 업데이트 해 줍니다. 이때는 2와 3까지 가는데 걸리는 비용을 반영해줘야 합니다.

1 2 3 4 5 0 3 2 5 (2+3) 10 (3+7)

– 위 예시의 경우 [1 -> 2 -> 5]의 경로를 통해 5까지 가는 비용을 10이라고 해주고 4까지의 비용은 [1 -> 3 -> 4], 5로 업데이트 해 줍니다.

– 이후 4, 5번 지점을 큐에 넣습니다.

5. 큐에 4, 5번이 있으니 4와 5에서 갈 수 있는 지점을 탐색합니다. 그런데 5는 갈 수 있는게 없어 작업을 하지 않고 4에서 출발하는 경로만 탐색하게 됩니다.

1 2 3 4 5 0 3 2 5 8

– 여기서 [1 -> 2 -> 5]는 10의 비용이지만 [1 -> 3 -> 4 -> 5]가 8의 비용으로 더 빨리 갈 수 있으니 8로 업데이트 해 줍니다.

– 더이상 경로가 없어 탐색은 중지합니다.

– 결과로 나오는 위 배열이 시작지점 1에서 각 지점까지의 비용을 나타내 줍니다.

우선순위 큐?

아래 코드를 보시면 우선순위 큐를 이용해서 문제를 풀이 했습니다. 우선순위 큐를 이용하게 되면 저장시 최소 비용이 top에 올 수 있게 만들 수 있어 기본 선형의 알고리즘보다 빠른 탐색이 가능합니다. 기존 선형탐색을 통해서 최소 거리를 구한다고 하면 시간복잡도가 O(N^2)이 나오지만 우선순위 큐에서는 O(NlogN)의 복잡도로 처리가 됩니다.

(간선의 수를 E라고 하면 시간복잡도는 O(ElogN) 이 됩니다)

C++로 만든 간단한 코드

#include #include #include using namespace std; // arr은 문제의 시작 지점이 인덱스로 가는데까지 비용을 저장 int arr[100]; // 입력에서 인덱스가 출발지가 되고 <도착지, 비용> 순으로 받아오게 됩니다. vector> v[100]; void Djikstra(int start){ // 큐에는 시간, 출발지 순으로 데이터가 들어가게 됩니다. // 때문에 우선순위 큐는 시간에 따라서 우선순위를 만들어 줍니다. priority_queue> q; arr[start] = 0; q.push(make_pair(0, start)); while(!q.empty()){ int cur = q.top().second; int time = -q.top().first; q.pop(); if(time > arr[cur]) continue; for(int i = 0; i < v[cur].size(); i++){ int nextCur = v[cur][i].first; int nextTime = v[cur][i].second; if(arr[nextCur] > time + nextTime){ arr[nextCur] = time + nextTime; q.push(make_pair(-arr[nextCur], nextCur)); } } } }

* 실제로 사용하는 경우 입력을 받은 뒤 비용을 저장하는 인덱스인 arr을 int의 최대값으로 초기화해야 합니다!

위 코드에서 주의해야 할 점은 우선순위 큐에 값을 넣을때 마이너스(-)를 붙여서 넣는다는 것입니다. C++의 queue에 있는 priority_queue의 경우 ‘높은 값’이 top로 오게 됩니다. 그대로 사용한다면 꺼낼때 오히려 ‘제일 먼’ 값을 고르게 되는 것이죠. 때문에 앞에 -를 붙여 우선순위 큐 안에 절댓값이 작은 값이 top로 오게 해주며 pop을 할 때 -를 붙여서 pop을 하면 문제 없이 사용이 가능합니다.

또한 if(time > arr[cur]) 를 넣어서 속도에서 조금 더 이득을 볼 수 있게 했습니다.

다익스트라 알고리즘을 이용한 문제들

1. 백준 1753 (다익스트라)

가장 기본적인 문제로 다익스트라에 대한 개념만 잡고 간다면 무난히 풀 수 있는 문제입니다.

2. 백준 11779 (다익스트라 경로추적)

-풀이

기존 다익스트라에서 경로를 저장할 수 있어야 하며 간단하게 저장용 배열, 출력용 배열 1개씩 추가를 하면 풀이를 할 수 있습니다.

3. 백준 5719

-풀이

아이디어는 금방 생각 나지만 첫 다익스트라를 한 후 경로를 어떻게 없앨지 고민을 해야 하는 문제입니다. 저같은 경우 BFS를 통해 경로를 역추적한 후 bool 배열을 하나 더 선언해 최단거리를 삭제할 수 있게 처리했습니다.

728×90

[논문]경로 정보를 이용한 길찾기 알고리즘

A* 알고리즘은 잘 알려진 길찾기 알고리즘이다. 그러나 많은 상호 작용이 있거나 많은 장애물들이 있는 맵 에서 A* 알고리즘을 실시간에 사용하는데 한계가 있을 수 있다. 그래서 게임에서는 최단의 경로를 찾는 대신에 게임 플레이어에게 자연스럽게 보이는 경로를 빠르게 찾을 필요가 있다.

PC들의 이동 경로를 실시간으로 샘풀링하여 항해메시를 생성하는 방법의 기대효과는?

키워드에 대한 정보 길 찾기 알고리즘

다음은 Bing에서 길 찾기 알고리즘 주제에 대한 검색 결과입니다. 필요한 경우 더 읽을 수 있습니다.

이 기사는 인터넷의 다양한 출처에서 편집되었습니다. 이 기사가 유용했기를 바랍니다. 이 기사가 유용하다고 생각되면 공유하십시오. 매우 감사합니다!

사람들이 주제에 대해 자주 검색하는 키워드 EBS 링크 소프트웨어 세상, \”가장 빠른길을 찾아라, 최단경로 알고리즘\”

  • EBS
  • 소프트웨어
  • 다큐멘터리
  • 링크
  • 알고리즘

EBS #링크 #소프트웨어 #세상, #\”가장 #빠른길을 #찾아라, #최단경로 #알고리즘\”


YouTube에서 길 찾기 알고리즘 주제의 다른 동영상 보기

주제에 대한 기사를 시청해 주셔서 감사합니다 EBS 링크 소프트웨어 세상, \”가장 빠른길을 찾아라, 최단경로 알고리즘\” | 길 찾기 알고리즘, 이 기사가 유용하다고 생각되면 공유하십시오, 매우 감사합니다.

See also  세월 의 섬 | 마법인형🔵 로스트아크 세월의섬 섬의 마음 획득 방법 1784 투표 이 답변

Leave a Comment