4 minute read


오늘도 항해 99 코테 스터디 5번째 TIL입니다.
주말인데 재밌는 문제가 나왔습니다. 난이도도 무려 골드 3입니다.

[챌린저 Day 13] 미로보수

https://www.acmicpc.net/problem/30689

그래프에서 사이클이 있는지 판단하고 그 사이클을 없앨 수 있는 최소한의 비용을 구하는 문제입니다.
그래프 사이클 판단은 https://www.acmicpc.net/problem/16724문제에서 경험한 적이 있기에
기억을 더듬어 문제를 풀어보았습니다.

사이클을 판단하는 문제의 가장 큰 아이디어는 방문처리하는 isVisited배열과 dfs가 진행중인지 아닌지 판단하는 isFinished(보통은 진행중인지를 나타내는 isExcute를 사용…)배열!
2개의 배열을 사용합니다.
그래서 dfs하는 도중인데 방문한 노드가 다음 확인하려는 노드이다?
그러면 사이클이 감지가 된것입니다.

더이상 갈 곳이 없으면 백트래킹으로 dfs가 끝났다는것을 저장하면 됩니다!

풀면서 중요한 점은 사이클의 비용만 연산을 해야합니다.
예를 들어

D R R
D L R
R U D
같은 경우에는 (1,2)(1,3)(2,3)(2,2) 가 사이클입니다. (왼쪽 위에를 1, 1로 잡고 (x, y)로 생각하면)
하지만 (1,1)에서 dfs를 실행하면 경로에 (1,1)이 포함되어 (1,1)이 최소비용이라면 잘못된 오답을 반환할 수 있습니다.
따라서 모든 좌표에서 dfs를 수행하는데 사이클만 연산을 해야합니다.

처음 생각난 방법은 방문을 판단하는 isVisited배열이 방문한 횟수라고 생각해서 2이상이면 사이클이라 판단하는 식으로 구현했습니다.
즉 2바퀴를 도는거죠!
코드는 다음과 같습니다.

#include<iostream>
#include<utility>
#include<algorithm>
#define MAX 2001
using namespace std;

pair<int, int> arr[MAX][MAX];
int cost[MAX][MAX];
int result = 0;
int isVisited[MAX][MAX];  // 방문 여부
bool isFinished[MAX][MAX]; // 해당 dfs가 끝났는지 여부
bool isCycle = false;

bool isRange(int x, int y, int m, int n)
{
    return x > 0 && y > 0 && x <= m && y <= n;
}

void dfs(int x, int y, int m, int n)
{
    isVisited[y][x]++;

    int nextX = x + arr[y][x].second;
    int nextY = y + arr[y][x].first;

    if (!isRange(nextX, nextY, m, n))
    {
        isFinished[y][x] = true;
        return;
    }

    if (isFinished[nextY][nextX])
    {
        isFinished[y][x] = true;
        return;
    }

    if (isVisited[nextY][nextX] >= 2)
    {
        isCycle = true;
        result = cost[y][x];
        isFinished[y][x] = true;
        return;
        // 사이클 발생
    }
    else
    {
        dfs(nextX, nextY, m, n);
    }
    isFinished[y][x] = true;

    if (isCycle && isVisited[y][x] >= 2)
    {
        result = min(result, cost[y][x]);
    }
}

void Init(int n, int m)
{
    result = 0;
    isCycle = false;
}

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

    int n, m, answer = 0;
    char c;
    cin >> n >> m;

    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= m; ++j)
        {
            cin >> c;

            if (c == 'D')
                arr[i][j] = { 1, 0 };
            else if (c == 'U')
                arr[i][j] = { -1, 0 };
            else if (c == 'L')
                arr[i][j] = { 0, -1 };
            else if (c == 'R')
                arr[i][j] = { 0, 1 };
        }
    }

    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= m; ++j)
        {
            cin >> cost[i][j];
        }
    }

    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= m; ++j)
        {
            Init(n, m);

            dfs(j, i, m, n);

            if (isCycle)
            {
                answer += result;
            }
        }
    }
    cout << answer;
}

문제를 풀긴했지만 2바퀴돌리는것보다 좋은방법이 없을까? 생각했는데 stack을 이용한 방법을 생각했습니다.
매 실행마다 stack에 좌표를 넣고 사이클이 판단되는 시점(isVisited[nextY][nextX] 이 true 인 곳)에 해당 좌표를 기억했다가
dfs종료 후 stack을 빼주면서 저장한 좌표와 같은 점이라면 그 이후는 연산을 안하는거죠!
조금은 연산이 줄어 빠를거라 생각했습니다.

코드는 다음과 같습니다.

#include<iostream>
#include<utility>
#include<algorithm>
#include<stack>
#define MAX 2001
using namespace std;

pair<int, int> arr[MAX][MAX];
int cost[MAX][MAX];
bool isVisited[MAX][MAX];  // 방문 여부
bool isFinished[MAX][MAX]; // 해당 dfs가 끝났는지 여부
bool isCycle = false;
stack<pair<int, int>> s;
pair<int, int> top;

bool isRange(int x, int y, int m, int n)
{
    return x > 0 && y > 0 && x <= m && y <= n;
}

void dfs(int x, int y, int m, int n)
{
    isVisited[y][x] = true;
    s.push({ x, y });
    int nextX = x + arr[y][x].second;
    int nextY = y + arr[y][x].first;

    if (!isRange(nextX, nextY, m, n))
    {
        isFinished[y][x] = true;
        return;
    }

    if (isFinished[nextY][nextX])
    {
        isFinished[y][x] = true;
        return;
    }

    if (isVisited[nextY][nextX])
    {
        // 사이클 발생
        isCycle = true;
        isFinished[y][x] = true;
        top = { nextX, nextY };
        return;
    }
    else
    {
        dfs(nextX, nextY, m, n);
    }
    isFinished[y][x] = true;
}

void Init(int n, int m)
{
    while (!s.empty())s.pop();
    isCycle = false;
}

int GetMinCost()
{
    int value = 100000000;
    while (!s.empty())
    {
        pair<int, int> p = s.top();
        s.pop();
        value = min(value, cost[p.second][p.first]);
        if (p == top)
        {
            break;
        }
    }
    return value;
}

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

    int n, m, answer = 0;
    char c;
    cin >> n >> m;

    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= m; ++j)
        {
            cin >> c;

            if (c == 'D')
                arr[i][j] = { 1, 0 };
            else if (c == 'U')
                arr[i][j] = { -1, 0 };
            else if (c == 'L')
                arr[i][j] = { 0, -1 };
            else if (c == 'R')
                arr[i][j] = { 0, 1 };
        }
    }

    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= m; ++j)
        {
            cin >> cost[i][j];
        }
    }

    for (int i = 1; i <= n; ++i)
    {
        for (int j = 1; j <= m; ++j)
        {
            Init(n, m);

            dfs(j, i, m, n);

            if (isCycle)
            {
                answer += GetMinCost();
            }
        }
    }
    cout << answer;
}

이 코드도 정답처리가 되긴합니다.
다만 이전 2바퀴도는것보다 거의 동일합니다…


느낀 점

더 효율적이라고 생각했는데 시간차이가 별로 안났습니다.
그리고 코드가 너무 더러워서 좀 정리해서 제출한 코드가 있는데 괄호만 좀 정리하고 if문 좀 정리했는데
시간이 약간약간씩 달라지는거 보면 제가 생각한 1번째와 2번째 풀이가 거의 똑같다고 생각이 되네요

그래도 여러 방면에서 생각해본게 언젠가는 도움이 될겁니다.

Comments