반응형

주어진 문제는 위와 같다. 최대 100 개의 도시가 주어지고, 도시 간 이동이 가능한 버스 노선들이 주어졌을 때 모든 도시 간 이동 시의 최소 비용을 구하는 것이다. 최단 경로 탐색을 위한 알고리즘은 다익스트라, 벨만 포드, A* 탐색 등 여러 가지가 있지만 위와 같이 모든 노드 간 최단 경로를 구하기 위한 알고리즘으로 문제의 이름과 같은 플로이드 와샬 알고리즘이 있다.

 

플로이드 와샬 알고리즘에 대해 내가 기억하고 있는 것은, 노드 간 경로 가중치를 테이블에 저장하고 경로가 존재하지 않는 경우는 INF 값으로 저장한다. 그 뒤 스텝 별로 중간에 다른 노드를 경유하며 경로 가중치를 업데이트하는 방식으로 기억하고 있었다. 하지만 내가 기억하고 있는 알고리즘의 로직은 너무 모호해 문제 풀이에 적용할 수 없었고 따로 플로이드 와샬 알고리즘을 찾아본 뒤 문제를 해결했다.

 

플로이드 와샬 알고리즘

플로이드 와샬 알고리즘을 간단하게 설명하면, O(|V|^3) 의 시간 복잡도를 갖는 모든 쌍 최단 경로 탐색 알고리즘이다. 매 스텝 별로 X to Y (X, Y 는 노드) 경로의 가중치를 업데이트 해야 하므로 O(|V| ^ 2) 를 매 스텝 별로 실행하고, 각 스텝은 경유할 노드를 순차적으로 적용하므로 O(|V| ^ 2 * |V|) 의 시간 복잡도를 갖게 된다.

 

위의 그림을 통해, 알고리즘의 동작 방식을 설명할 수 있다. i, j 는 각각 노드 번호를 뜻하고 k 는 경유할 노드의 번호를 의미한다.

k = 0 일 때, 해당 테이블은 노드를 경유하지 않고 i - j 간 경로를 표현한 것이다.

k = 1 일 때, 변경된 table[2][3] 값은, 기존의 node(2) -> node(3) 의 경로 가중치는 3이고, 노드 1번을 경유한 node(2) -> node(1) -> node(3) 의 가중치 합이 4 + (-2) 이므로 새로운 값이 업데이트된 것을 볼 수 있다.

k = 2 일 때, node(4) -> node(1) 의 경로와 node(4) -> node(3) 의 경로가 2번 노드를 경유함으로 인해 가중치 값이 업데이트된 것을 확인할 수 있다.


이와 같이 경유할 노드를 변경해 가며 경로 가중치 값을 업데이트해 나가면 최종적으로 모든 노드 쌍 간의 최단 경로를 구할 수 있게 된다.

 

알고리즘을 적용한 코드는 3개의 for 문을 중첩함으로써 간단하게 구현이 가능하고, 이를 적용해 문제 해결에 사용한 코드는 다음과 같다.

 

#include <iostream>
using namespace std;

const int CITY = 100;
const int INF = 123456789;

int city, bus;
int dist[CITY + 1][CITY + 1];

void initialize() {
    for (int i = 1; i <= city; i++) {
        for (int j = 1; j <= city; j++) {
            dist[i][j] = INF;
        }
    }
}

void input() {
    cin >> city >> bus;
    initialize();

    for (int i = 0; i < bus; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        dist[a][b] = dist[a][b] < c ? dist[a][b] : c;
    }
}

void output() {
    for (int i = 1; i <= city; i++) {
        for (int j = 1; j <= city; j++) {
            if (dist[i][j] == INF) {
                cout << "0 ";
            } else {
                cout << dist[i][j] << " ";
            }
        }
        cout << "\n";
    }
}

void solve() {
    for (int k = 1; k <= city; k++) {
        for (int i = 1; i <= city; i++) {
            for (int j = 1; j <= city; j++) {
                if (i == j)
                    continue;
                int k_dist = dist[i][k] + dist[k][j];
                dist[i][j] = dist[i][j] < k_dist ? dist[i][j] : k_dist;
            }
        }
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    input();

    solve();

    output();
}

 

# Reference.

 

플로이드-워셜 알고리즘 - 위키백과, 우리 모두의 백과사전

컴퓨터 과학에서, 플로이드-워셜 알고리즘(Floyd-Warshall Algorithm)은 변의 가중치가 음이거나 양인 (음수 사이클은 없는) 가중 그래프에서 최단 경로들을 찾는 알고리즘이다.[1][2] 알고리즘을 한 번

ko.wikipedia.org

'Algorithm > Baekjoon' 카테고리의 다른 글

[BOJ # 1874] 스택 수열  (0) 2021.09.21
[BOJ # 1600] 말이 되고픈 원숭이  (0) 2021.09.19
<BOJ 2508> 나무 자르기  (0) 2019.02.14
반응형

주어진 문제는 위와 같다. 수열의 크기 n 이 주어지고, 수열을 이루는 1 ~ n 까지 정수가 하나씩 순서대로 주어진다. 이 때, 스택을 가지고 주어진 수열을 만들기 위해 필요한 연산을 한 줄에 하나씩 출력한다.

 

스택은 LIFO 구조로, push / pop 연산을 통해 자료를 저장하고 뺄 수 있다. 스택으로 수열을 만들기 위해서 필요한 조건은, 주어진 정수가 항상 스택에 저장된 top 값보다 크거나 같아야 한다는 것이다. 만약 스택의 top 에 저장된 값보다 작은 정수가 주어졌을 때는 그 값을 찾기 위해 스택에서 여러 번 pop 을 해야 하므로, 이 문제에서 주어진 조건을 만족할 수 없다.

 

때문에 문제의 풀이 방식은 간단하다. 주어진 정수가 스택에 저장된 top 의 값보다 크다면 값들을 push 해 주어진 정수가 스택에 저장되도록 한다. 그 뒤 스택의 top 에 있는 값을 꺼내면 된다. 만약 주어진 정수가 스택에 저장된 top 값과 같다면 그 값을 pop 연산을 통해 바로 꺼낼 수 있다. 이 때 문제는, 이미 스택에 push 된 값이 다시 push 될 수 있다는 점이다.

 

예를 들어, [1, 2, 3, 4] 의 순서로 push 가 이뤄지고 [4, 3] 순서로 pop 이 되었을 때 스택의 top 에 저장된 값은 '2' 이다. 이 때 '6' 이라는 정수가 주어지면 스택의 top 보다 큰 '3' 부터 '6' 까지 [3, 4, 5, 6] 순서로 push 를 하게 된다. 하지만 이 경우에 이미 [3, 4] 라는 값들은 push / pop 이 된 값이므로 이를 다시 수행하지 않도록 따로 기록을 해야 한다.

 

그래서 이 문제에서 추가적으로 활용하는 변수로 스택에 마지막으로 push 된 값을 기록한다. 만약 위의 경우에 마지막으로 push 한 값이 '4' 임을 안다면 '6' 이 주어졌을 때, [5, 6] 만 push 를 하면 됨을 알 수 있다. 따라서 중복되는 push / pop 연산이 이루어지지 않는다.

 

이러한 풀이로 작성한 소스 코드는 다음과 같다.

 

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

int N, num, cur;
stack<int> s;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    string ans = "";
    cin >> N;

    for (int i = 0; i < N; i++) {
        cin >> num;

        if (cur < num) {
            for (int k = cur + 1; k <= num; k++) {
                s.push(k);
                ans += "+\n";
            }
            cur = num;
        }
        else if (s.top() != num) {
            ans = "NO\n";
            break;
        }

        s.pop();
        ans += "-\n";
    }

    cout << ans;
}

문제 풀이 중간에 불가능한 값이 입력될 경우 ans 라는 문자열을 "NO" 로 바꾸고 반복을 종료한다. 그렇지 않을 경우 정상적으로 연산의 순서가 ans 에 저장될 것이다. 이를 콘솔에 출력함으로써 주어진 문제에 대한 답을 얻을 수 있다.

'Algorithm > Baekjoon' 카테고리의 다른 글

[BOJ # 11404] 플로이드  (0) 2021.09.23
[BOJ # 1600] 말이 되고픈 원숭이  (0) 2021.09.19
<BOJ 2508> 나무 자르기  (0) 2019.02.14
반응형

주어진 문제는 위와 같다. 2차원 행렬에서 (0, 0) -> (M - 1, N - 1) 까지의 최단 거리를 구하는 문제에 특별한 이동 방식이 추가된 형태이다. 특별한 이동 방식은 체스에서 나이트가 움직이는 방식과 같고 문제에서 정의한 K 만큼 이러한 방식으로 이동할 수 있다.

 

일반적인 BFS 문제는 큐를 사용해 각각의 이동으로 인한 현재 위치를 저장하고 그 위치가 목적지에 도달하면 종료하게 된다. 이 때, 동일한 위치에 대해서 재방문을 할 필요가 없기 때문에 각 위치 (x, y) 의 방문 여부를 저장한다. 이 문제는 이에 더해 그 위치까지 몇 번의 이동 능력을 사용했는 지가 중요하기 때문에 (x, y) 위치 + 이동 능력 사용 횟수 를 기준으로 방문 여부를 체크하면 해결할 수 있다.

 

이러한 풀이로 작성한 소스 코드는 다음과 같다.

 

#include <iostream>
#include <queue>

using namespace std;

const int MAX_SIZE = 200;

struct POINT {
    int r, c;
    int horse, cnt;

    POINT(int r, int c, int horse, int cnt) : r(r), c(c), horse(horse), cnt(cnt) {}
};

int dr[4] = {1, 0, -1, 0}, dc[4] = {0, 1, 0, -1};
int hr[8] = {-1, -2, -2, -1, 1, 2, 2, 1}, hc[8] = {-2, -1, 1, 2, 2, 1, -1, -2};

int N, M, K;
int map[MAX_SIZE][MAX_SIZE];
bool vis[MAX_SIZE][MAX_SIZE][31];

void input() {
    cin >> K;
    cin >> M >> N;

    for (int i = 0; i < N; i++) {
        for (int j = 0; j < M; j++) {
            cin >> map[i][j];
        }
    }
}

int solve() {
    queue<POINT> que;

    que.push(POINT(0, 0, 0, 0));
    while (que.size()) {
        POINT top = que.front();
        que.pop();

        if (top.r == N - 1 && top.c == M - 1) {
            return top.cnt;
        }

        if (vis[top.r][top.c][top.horse])
            continue;
        vis[top.r][top.c][top.horse] = true;

        if (top.horse < K) {
            for (int k = 0; k < 8; k++) {
                int nr = top.r + hr[k], nc = top.c + hc[k];
                if (nr < 0 || nr >= N || nc < 0 || nc >= M)
                    continue;
                if (map[nr][nc] || vis[nr][nc][top.horse + 1])
                    continue;
                que.push(POINT(nr, nc, top.horse + 1, top.cnt + 1));
            }
        }
        for (int k = 0; k < 4; k++) {
            int nr = top.r + dr[k], nc = top.c + dc[k];
            if (nr < 0 || nr >= N || nc < 0 || nc >= M)
                continue;
            if (map[nr][nc] || vis[nr][nc][top.horse])
                continue;
            que.push(POINT(nr, nc, top.horse, top.cnt + 1));
        }
    }

    return -1;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    input();
    cout << solve();

    return 0;
}

 

solve() 함수에서 기존 BFS 소스 코드와 차이점이 있다면, if (top.horse < K) 로 시작되는 분기문이다. POINT 구조체의 horse 는 현재까지 이동 능력의 사용 횟수이며 이 횟수가 K 보다 작다면 특별한 이동 방식으로 이동할 수 있도록 한다. 또한, bool 타입 3차원 행렬 vis 에서 마지막 인덱스로 horse 값을 사용해 해당 위치까지 가는 동안 사용한 이동 능력의 횟수를 확인해 재방문을 막는다.

'Algorithm > Baekjoon' 카테고리의 다른 글

[BOJ # 11404] 플로이드  (0) 2021.09.23
[BOJ # 1874] 스택 수열  (0) 2021.09.21
<BOJ 2508> 나무 자르기  (0) 2019.02.14
반응형

문제 링크 icpc.me/2508


이 문제는 높이가 서로 다른 나무 N 그루에서 나무를 M 미터만큼 벌목하고자 하는 문제이다.

벌목기를 사용하려고 하는데, 벌목기의 높이 H 를 지정할 수 있고 H 보다 더 큰 나무를 모두 절단해

각 나무의 높이 - H 만큼의 나무를 얻을 수 있다.

이 때 M 미터만큼 벌목을 하기 위해서 H 를 얼마로 설정해야 하는가를 찾는 문제이다.


처음 접근 방식은 N 그루의 나무 중 가장 높이가 큰 나무의 높이로부터 H 를 순차적으로 찾는 방식이었다.

이렇게 할 경우 간단한 문제는 해결이 가능하지만 나무의 개수가 많아질수록 시간이 오래 걸려 시간 초과가 발생하였다.


더보기

// 순차 탐색 


#include <stdio.h>

#include <stdlib.h>



int main() {

int tree, obj;

int *tree_height;

int max_height;

int total;

int prev;

scanf("%d %d", &tree, &obj);

tree_height = calloc(sizeof(int), tree);

for (int i = 0; i < tree; i++) {

scanf("%d", tree_height + i);

}

max_height = tree_height[0];

prev = -1;

while (1) {

total = 0;

for (int i = 0; i < tree; i++) {

if (tree_height[i] - max_height > 0)

total += tree_height[i] - max_height;

}

if (total > obj) {

if (prev < obj && prev != -1)

break;

else max_height++;

}

else if (total < obj) {

if (prev > obj && prev != -1)

break;

else max_height--;

}

else {

break;

}

prev = total;

}

printf("%d\n", max_height);

}


시간 초과가 발생한 후 다음으로 시도한 방법은 나무를 높이 순으로 정렬한 후 순차탐색을 하는 것이었다.

이렇게 할 경우, 마찬가지로 벌목기의 높이 H 를 줄여가며 확인을 해야 하지만 나무가 높이 순으로 정렬되어 있기 때문에

벌목기가 나무보다 높아질 경우 그 뒤의 나무는 자를 수 있는 지를 확인할 필요가 없다.


더보기

// 정렬 후 순차탐색


#include <iostream>

#include <vector>

#include <algorithm>


using namespace std;


bool comp(int a, int b) {

return a > b;

}


int main() {

ios_base::sync_with_stdio(false);

cin.tie(NULL);

cout.tie(NULL);


vector<int> v;

int n, m, input;


cin >> n >> m;

for (int i = 0; i < n; i++) {

cin >> input;

v.push_back(input);

}


sort(v.begin(), v.end(), &comp);


int max = v[0];

int sum;

while (1) {

sum = 0;

for (int i = 0; i < n; i++) {

if (v[i] <= max)

break;

sum += v[i] - max;

}

if (sum >= m)

break;

max--;

}

cout << max << "\n";

}


두번째 소스 코드 또한 마찬가지로 시간 초과가 발생했다.

아무래도 H 를 바꿔가며 나무를 일일히 확인하는 방법은 시간적으로 많이 소모된다고 생각이 들었다. 그래서 생각해낸 방법이

두번째 방법과 유사하지만 나무를 확인하는 횟수를 줄이는 것이었다.


간단하게 설명하자면, 위의 소스코드는 H 를 변경할 때마다 모든 나무에서 몇 m 씩만큼의 나무를 얻을 수 있는 지를 계산해야 한다.

하지만 이 과정은 나무가 높이 순으로 정렬되어 있기 때문에 불필요한 과정이었다.

이전 단계 (즉, 이전 벌목기의 높이 H) 에서 벌목 가능한 나무를 X 그루라고 하면, 단계가 변할 때마다 총 얻을 수 있는 나무의 양은 X 만큼

증가하게 된다.

추가적으로 H 가 감소했으니, 이전 단계에서 벌목 가능한 나무의 위치 바로 다음 위치부터 있는 나무만을 확인하면 된다.


이렇게 작성한 소스 코드는 다음과 같다.


더보기

// 정렬 후 순차 탐색

// 이전 단계의 탐색 위치를 저장


#include <iostream>

#include <algorithm>

#include <vector>


using namespace std;


bool comp(long long a, long long b) {

return a > b;

}


int main() {

ios_base::sync_with_stdio(false);

cin.tie(NULL);

cout.tie(NULL);


vector<long long> v;

long long n, m, input;


cin >> n >> m;

for (int i = 0; i < n; i++) {

cin >> input;

v.push_back(input);

}


sort(v.begin(), v.end(), &comp);


long long max = v[0];

long long sum = 0;

long long index = 0;

while (1) {

sum += index; // sum 을 단계 별로 초기화하지 않고 기존의 값에 H 가 변화함에 따라 추가되는 나무를 더해준다.

for (int i = index; i < n; i++) {

if (v[i] <= max) {

index = i;

break;

}

sum += v[i] - max;

}

if (sum >= m)

break;

max--;

}

cout << max << "\n";

}


이렇게 풀었을 때, 더이상 시간초과는 발생하지 않았지만 왠지 모르게 입력되는 값이 커질수록 결과값이 이상하게 출력되었다.

처음에 long long 을 사용하지 않고 int 형을 사용해 자료형 문제로 생각하고 long long 으로 바꾼 후 다시 제출을 했지만

마찬가지로 36% 정도에서 틀렸다는 결과가 발생했다.


그래서 예외 상황이 생길 수 있나 생각했는데 상황을 찾지 못해 결국 풀이 방식을 바꾸는 쪽을 생각했다.


데이터를 정렬하지 않고 문제를 푸는 방법을 생각하게 되었고, 정렬을 하지 않게 되면 이 알고리즘에서 시간을 줄일 수 있는 방법이

H 의 값을 변경하는 횟수를 줄이는 것이었다.


이 알고리즘의 속도는 H 를 얼마나 많이 변경하느냐와 H 에 따라 벌목할 수 있는 나무를 얼마나 탐색하느냐 이 두 가지에 따라 결정된다.

즉, O(H * N) 이 된다고 볼 수 있다. 이 때, N 을 줄이는 것은 앞서 시도한 방법들에서 실패했으니 H 를 줄이는 방법을 생각해 보았다.


H 를 줄이는 방법은 H 를 N 그루의 나무 중 최대 높이부터 1씩 감소시켜 나가는 것이 아니라 최대 높이와 최소 높이를 기준으로

이분 탐색을 하는 방법이었다.


이분 탐색을 함으로써 O(H * N) 은 O(logH * N) 으로 바꿀 수 있게 되고, H 는 최대 1,000,000,000 의 값을 갖는다고 문제의 조건으로

주어졌으니 log(1,000,000,000) ≒ 29 로 알고리즘의 성능은 O(N) 으로 표현할 수 있게 된다.


소스 코드는 다음과 같다.


더보기

// 이분 탐색


#include <iostream>


#define _MAX(A, B) A > B ? A : B


using namespace std;


long long arr[1000001];


int main() {

ios_base::sync_with_stdio(false);

cin.tie(NULL);

cout.tie(NULL);


long long n, m, max = 0;

cin >> n >> m;


for (long long i = 0; i < n; i++) {

cin >> arr[i];

max = _MAX(max, arr[i]);

}


long long low = 0, mid, high = max;

long long sum = 0;


while (low < high) {

sum = 0;

mid = (high + low + 1) / 2;

for (long long i = 0; i < n; i++) {

sum += _MAX(0, arr[i] - mid);

}

if (sum < m) high = mid - 1;

else low = mid;

}


cout << low << "\n";

}


'Algorithm > Baekjoon' 카테고리의 다른 글

[BOJ # 11404] 플로이드  (0) 2021.09.23
[BOJ # 1874] 스택 수열  (0) 2021.09.21
[BOJ # 1600] 말이 되고픈 원숭이  (0) 2021.09.19

+ Recent posts