N자리의 정수 중, 신기한 소수를 구하는 문제이다.

신기한 소수란, 정수의 가장 앞에서부터 1자리, 2자리, 3자리 .... N자리까지의 숫자가 모두 소수인 수이다.

 

예를 들어, 7331의 경우 앞에서부터 1자리인 7도 소수이며, 앞에서부터 2자리인 73도 소수이고, 앞에서부터 3자리인 733과 앞에서부터 4자리인 7331도 소수이다.

 

이러한 수를 모두 출력하면 된다.

 

문제 풀이

이 문제의 규칙을 한 번 생각해보자.

 

7331처럼, 신기한 소수가 되는 수는 7, 73, 733, 7331 이 모두 소수이다.

 

4개의 숫자를 생각해보면, 73은 7의 뒤에 한자리의 숫자가 더해진 형태이며 733은 73의 뒤에 한자리의 숫자가 더해진 형태이다. 7331또한, 733뒤에 한자리의 숫자가 더해진 형태이다.

 

즉, N자리의 신기한 소수는 (N - 1)자리의 신기한 소수의 뒤에 한 자리의 숫자가 더해진 형태가 된다.

이를 기반으로 1자리 수 부터 탐색을 해보자.

 

1자리의 신기한 소수는 2, 3, 5, 7이 있다.

 

2자리의 신기한 수는 1자리의 신기한 수의 뒤에 1자리의 숫자를 더한 형태였다.

그렇다면, 21, 22, 23, 24, 25, 26, 27,28, 29, 31, 32, 33, 34, 35, 36,37, 38, 39 .... 등의 숫자들이 신기한 수의 후보가 될 수 있다. 이 수들 중에 소수인 수가 있다면 해당 수는 신기한 소수일 것이다.

 

그렇다면, 해당 숫자들을 대상으로 소수 판별을 해야한다.

소수가 되기 위해선 첫 번째로, 가장 마지막 자리가 짝수여서는 안된다.

그러므로, 22, 24, 26, 28 ...등은 후보에서 제외할 수 있다.

 

남은 숫자 21, 23, 25, 27, 29, 31, 33 등의 숫자를 대상으로 소수 판별을 해보자.

소수를 판별하는 방법은 간단하다. N이라는 숫자가 있을 때, 2~sqrt(N)까지 반복문을 돌면서 N을 해당 수로 나누어보면 된다. 

 

예를 들어, 15가 소수인지 판별하고 싶다면 2~sqrt(15)까지의 정수에 대해 15를 나누었을 때, 나머지가 0이되는 경우가 있는지를 탐색하면 된다.

 

15 % 2 = 1

15 % 3 = 0

 

=> 15가 3으로 나누어지므로, 3이라는 약수가 존재함을 알 수 있다. 이로 인해, 15는 소수가 아님을 알 수 있다.

 

이렇게, 신기한 수의 뒤에 한자리 수 하나를 더하고 해당 수가 소수인지 아닌지를 판별하는 것을 한자리의 신기한 소수부터 시작해서 계속 반복하면 된다. 계속 반복하여 N자리의 소수를 모두 구한 뒤 출력하면 된다.

 

풀이 코드

int Digit = 0;
std::cin >> Digit;

먼저, 목표로 하는 자리 수를 입력받아준다.

std::priority_queue<int, std::vector<int>, std::greater<int>> Numbers;
for (int i = 2; i <= 9; i++)
{
    if (isPrimeNumber(i) == true)
    {
        Numbers.push(i);
    }
}

다음은 우선순위 큐에 한자리의 신기한 소수를 모두 삽입해준다.

isPrimeNumber는 해당 수가 소수인지 판별해주는 함수이다. 내부 구현 코드는 잠시 뒤에 보도록 하자.

if (Digit == 1)
{
    PrintAll(Numbers);
    return 0;
}

만약, 입력받은 자리수가 1이었다면, 그대로 모든 우선순위 큐의 모든 원소를 출력해주면 된다.

int Threshold = static_cast<int>(std::pow(10, Digit - 1));

이건, 미리 설정한 한계 수치이다.

예를 들어, 입력받은 자리수가 4라고 한다면 3자리 숫자에 대한 탐색이 끝나는 순간에 탐색을 멈추어야 한다.

 

3자리 숫자의 뒤에 한 자리 숫자를 더해 4자리 숫자를 구하고, 4자리 숫자의 뒤에 한자리 숫자를 더해 5자리 숫자를 구하는 방식이기 때문에, 현재 참조하는 숫자가 4자리 숫자라면 더이상 탐색을 진행할 필요가 없어진다.

 

Digit가 4인 경우에, Threshold는 1000이 되고, 현재 숫자가 1000보다 크거나 같다면 반복문을 종료하도록 할 것이다.\

while (true)
{
    int CurNum = Numbers.top();
    if (CurNum >= Threshold)
    {
        break;
    }

    Numbers.pop();

    CurNum *= 10;
    int NextNum = 0;

    for (int i = 0; i <= 9; i++)
    {
        if (i % 2 == 0)
        {
            continue;
        }

        NextNum = CurNum + i;

        if (isPrimeNumber(NextNum) == true)
        {
            Numbers.push(NextNum);
        }
    }
}

우선순위 큐의 원소중 최소값을 기준으로 Threshold 보다 큰지 작은지를 검사하였다.
최소값이 Threshold보다 더 작다면, 그대로 반복문을 종료해준다.

 

그렇지 않다면, 우선순위 큐의 최소값을 pop한 뒤, 해당 숫자의 뒤에 1을 더한 숫자를 모두 검사하여, 소수인 숫자는 다시 우선순위 큐에 삽입하도록 하였다.

 

2는 23, 29를 우선순위 큐에 삽입하며, 3은 31과 37을 우선순위 큐에 삽입한다. 5는 53과 59를 우선순위 큐에 삽입하며, 7은 71, 73, 79를 우선순위 큐에 삽입한다.

 

한 자리 수에 대한 탐색이 모두 끝나게 되면, 두 자리의 숫자를 대상으로 탐색을 하게 된다.

23은 233,239를 우선순위 큐에 삽입하고, 29는 293을 우선순위 큐에 삽입한다. 31은 311,313,317을 우선순위 큐에 삽입하며, 37은 37와 3789를 우선순위 큐에 삽입한다.

 

이 과정을 N자리 수의 신기한 소수를 모두 구할 때까지 반복하는 것이다.

PrintAll(Numbers);

return 0;

반복문이 끝난 뒤, 우선 순위 큐에 있는 모든 원소를 출력해주면 끝난다.

bool isPrimeNumber(int _Num)
{
    int NumSqrt = std::sqrt(_Num);

    for (int i = 2; i <= NumSqrt; i++)
    {
        if (_Num % i == 0)
        {
            return false;
        }
    }

    return true;
}

위의 코드는 소수를 판별하는 코드이다. 2~sqrt(N)까지 반복문을 돌며, _Num의 약수인 수가 있는지 확인하는 것이다.

void PrintAll(std::priority_queue<int, std::vector<int>, std::greater<int>>& _Numbers)
{
    while (_Numbers.size() > 0)
    {
        std::cout << _Numbers.top() << std::endl;
        _Numbers.pop();
    }
}

위 코드는 우선 순위 큐의 모든 원소를 출력하는 함수이다.

하나씩 pop해주며, top을 계속 출력하도록 하였다.

 

코드 전문

#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <cmath>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}

void PrintAll(std::priority_queue<int, std::vector<int>, std::greater<int>>& _Numbers)
{
    while (_Numbers.size() > 0)
    {
        std::cout << _Numbers.top() << std::endl;
        _Numbers.pop();
    }
}

bool isPrimeNumber(int _Num)
{
    int NumSqrt = std::sqrt(_Num);

    for (int i = 2; i <= NumSqrt; i++)
    {
        if (_Num % i == 0)
        {
            return false;
        }
    }

    return true;
}

int main()
{
    Init();

    int Digit = 0;
    std::cin >> Digit;

    std::priority_queue<int, std::vector<int>, std::greater<int>> Numbers;
    for (int i = 2; i <= 9; i++)
    {
        if (isPrimeNumber(i) == true)
        {
            Numbers.push(i);
        }
    }

    if (Digit == 1)
    {
        PrintAll(Numbers);
        return 0;
    }

    int Threshold = static_cast<int>(std::pow(10, Digit - 1));

    while (true)
    {
        int CurNum = Numbers.top();
        if (CurNum >= Threshold)
        {
            break;
        }

        Numbers.pop();

        CurNum *= 10;
        int NextNum = 0;

        for (int i = 0; i <= 9; i++)
        {
            if (i % 2 == 0)
            {
                continue;
            }

            NextNum = CurNum + i;

            if (isPrimeNumber(NextNum) == true)
            {
                Numbers.push(NextNum);
            }
        }
    }

    PrintAll(Numbers);

    return 0;
}

 

여러 개의 집이 순서대로 놓여 있고, 이 집의 외벽에 색을 칠하고자 한다.

하나의 집에 인접한 두 집과 같은 색이 아니도록 칠하고자 한다.

 

각 집마다 색을 칠할 때의 비용이 상이하게 책정되어 있다.

인접한 집과 다른 색상이라는 조건을 유지하며 색을 칠할 때 그 비용의 최소를 구하자.

 

문제 풀이

이 문제는 모든 경우를 탐색하며 그 비용을 구해야 한다.

하지만, 일반적인 완전탐색 방식으로 모든 경우를 탐색하면 말도 안되는 경우의 수가 나온다.

그러므로 다이나믹 프로그래밍을 사용하여 효율적으로 비용의 최소 값을 구해야 한다.

 

먼저, 첫 번째 집의 색상을 하나로 고정하여 시작하는 DP를 3번 반복하는 것이다. 

 

왜 3번을 할까? 첫 번째 집과 마지막 집의 색이 같으면 안된다는 조건 때문이다.

그렇기 때문에 첫 번째 집을 Red로 칠하는 경우, Green으로 칠하는 경우, Blue로 칠하는 경우를 하나씩 확인해야 한다.

 

그럼, 이제 DP를 해보자.

먼저, DP는 2차원 배열로 만들 것이다.

DP[i][j] 가 있다면, i는 집의 순번이고 j는 색상이다. (0은 R, 1은 G, 2는 B)

저장될 값은 최소 비용이다.

 

DP[3][2] 라고 한다면, 3번째 집을 Blue로 칠할 때의 최소 비용이다.

DP[10][1]은 10번째 집을 Green으로 칠할 때의 최소 비용이다.

이렇게 집의 위치와 색상을 함께 고려할 것이다.

 

이 때, DP[i][0] 은 어떻게 구해야 할까?

i번째 집을 빨간색으로 칠한다고 하면, 일단 i-1번째 집은 빨간색이어선 안된다.

또한, i - 1 개의 집을 칠하는데 들어간 총 비용 + i번째 집을 빨간색으로 칠하는 비용이다.

i - 1 개의 집을 칠하는데 들어가는 총 비용은 i - 1 번째 집이 파란색이냐, 초록색이냐에 따라 다를 수 있다.

 

그러므로, DP[i][0] 은 std::min(DP[i - 1][1] + Cost[i][j], DP[i - 1][2] + Cost[i][j]) 가 될 것이다.

DP[i][0]에 저장할 값은 최소비용이므로, 이전의 집이 파란색일 때의 총 비용과 초록색일 때의 총 비용중 최소 값을 기준으로 현재 집을 칠하는 비용을 더해서 총 비용의 최소값을 갱신하는 것이다.

 

이렇게 마지막 집까지 모두 갱신했다고 해보자.

그러면, 마지막 집이 R인 경우의 총 비용, G인 경우의 총 비용, B인 경우의 총 비용 이렇게 3개의 값을 구할 수 있다.

이 3개의 비용 중 가장 작은 비용이 첫 번째 집을 R로 칠하는 경우 중 최소 비용인 것이다.

 

하지만, 마지막 집은 첫 번째 집과 같은 색일 수 없다는 조건이 있으므로, 마지막 집이 R인 경우의 총 비용은 제외하고, 마지막 집이 G인 경우의 총 비용과 B인 경우의 총 비용만 고려하여 최소를 구하면 된다.

 

첫 번째 집을 R로 칠할 때의 최소 비용, G로 칠할 때의 최소 비용, B로 칠할 때의 최소비용 총 3개를 구한 뒤 셋 중 최소비용을 구하면 그 값이 답이 된다.

 

풀이 코드

#define BIGNUMBER 999999999

먼저, 최소값을 갱신하기 전 초기값을 잡기 위해 충분히 큰 값을 하나 정의해주었다.

int NumOfHouse = 0;
std::cin >> NumOfHouse;

std::vector<std::vector<int>> Cost(NumOfHouse, std::vector<int>(3, 0));
for (int i = 0; i < NumOfHouse; i++)
{
    for (int j = 0; j < 3; j++)
    {
        std::cin >> Cost[i][j];
    }
}

이후 입력을 받아주었다. 모든 비용을 Cost에 저장해주었다. 

Cost[i][j]는 i번째 집을 j색(0 = R, 1 = G, 2 = B)으로 칠하는 비용이다.

int Answer = BIGNUMBER;

std::vector<std::vector<int>> DP(NumOfHouse, std::vector<int>(3, BIGNUMBER));
for (int i = 0; i < 3; i++)
{
    for (int j = 0; j < DP.size(); j++)
    {
        std::fill(DP[j].begin(), DP[j].end(), BIGNUMBER);
    }

    DP[0][0] = Cost[0][0];
    DP[0][1] = Cost[0][1];
    DP[0][2] = Cost[0][2];

    for (int j = 0; j < 3; j++)
    {
        if (i == j)
        {
            continue;
        }

        DP[1][j] = DP[0][i] + Cost[1][j];
    }

    for (int j = 2; j < NumOfHouse; j++)
    {
        DP[j][0] = std::min(DP[j - 1][1] + Cost[j][0], DP[j - 1][2] + Cost[j][0]);
        DP[j][1] = std::min(DP[j - 1][0] + Cost[j][1], DP[j - 1][2] + Cost[j][1]);
        DP[j][2] = std::min(DP[j - 1][0] + Cost[j][2], DP[j - 1][1] + Cost[j][2]);
    }

    for (int j = 0; j < 3; j++)
    {
        if (j == i)
        {
            continue;
        }

        Answer = std::min(Answer, DP[NumOfHouse - 1][j]);
    }
}

먼저,std::fill을 사용하여 DP배열을 초기화해주었다.

하나의 배열을 재활용 할 것이기 때문에 초기화 코드를 넣어주었다.

 

DP는 위에서 말했듯이, 첫번째 집이 R인 경우, G인 경우, B인 경우 총 3개의 경우를 진행할 것이다.

 

DP[0][0], DP[0][1], DP[0][2] 는 Cost[0][0], Cost[0][1], Cost[0][2] 로 초기화 해 주었다.

 

이후 반복문으로 DP[1]도 갱신해주었다. 

그리고 이후, DP[2]부터 DP[NumOfHouse]까지 모두 갱신해주었다.

 

DP[1]만 따로 갱신한 이유는 첫번째 집을 칠하는 색을 하나로 정해두었기 때문에, DP[1]은 첫번째 집이 어떤 색인지 고려하여 값을 계산할 필요가 없기 때문이다. 첫번째 집이 빨간색이라면 그에 맞게 DP[1]을 갱신하였고, 파란색이라면 그에 맞게 갱신한 것이다.

 

DP배열을 모두 갱신한 뒤에, DP배열의 마지막에 저장된 총 비용 중 최소값을 Answer에 저장해주었다.

 

이를, 총 3번(첫 번째 집이 R인경우, G인 경우, B인 경우) 실행하였고, 최종적으로 Answer에 저장된 값이 정답이 된다.

 

std::cout << Answer;

return 0;

마지막으로 답을 출력해주면 된다.

 

 

코드 전문

더보기
#include <iostream>
#include <vector>
#include <algorithm>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}

#define BIGNUMBER 999999999

int main()
{
    Init();

    int NumOfHouse = 0;
    std::cin >> NumOfHouse;

    std::vector<std::vector<int>> Cost(NumOfHouse, std::vector<int>(3, 0));
    for (int i = 0; i < NumOfHouse; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            std::cin >> Cost[i][j];
        }
    }

    int Answer = BIGNUMBER;

    std::vector<std::vector<int>> DP(NumOfHouse, std::vector<int>(3, BIGNUMBER));
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < DP.size(); j++)
        {
            std::fill(DP[j].begin(), DP[j].end(), BIGNUMBER);
        }

        DP[0][0] = Cost[0][0];
        DP[0][1] = Cost[0][1];
        DP[0][2] = Cost[0][2];

        for (int j = 0; j < 3; j++)
        {
            if (i == j)
            {
                continue;
            }

            DP[1][j] = DP[0][i] + Cost[1][j];
        }

        for (int j = 2; j < NumOfHouse; j++)
        {
            DP[j][0] = std::min(DP[j - 1][1] + Cost[j][0], DP[j - 1][2] + Cost[j][0]);
            DP[j][1] = std::min(DP[j - 1][0] + Cost[j][1], DP[j - 1][2] + Cost[j][1]);
            DP[j][2] = std::min(DP[j - 1][0] + Cost[j][2], DP[j - 1][1] + Cost[j][2]);
        }

        for (int j = 0; j < 3; j++)
        {
            if (j == i)
            {
                continue;
            }

            Answer = std::min(Answer, DP[NumOfHouse - 1][j]);
        }
    }

    std::cout << Answer;

    return 0;
}

 

 

3개의 돌 그룹이 있다. 각 그룹에는 돌이 A개, B개, C개씩 포함되어있다.

이 때, 두 개의 돌 그룹을 선택하여 돌의 개수를 바꾸는 연산을 진행한다.

개수를 바꾸는 진행은 두 그룹의 돌 개수가 X, Y 이고, X < Y라고 했을 때, X = 2 * X , Y = Y - X 의 형식으로 진행한다.

이 연산을 반복하여 A,B,C의 개수를 모두 동일하게 만들 수 있다면 1을 출력하고, 아니라면 0을 출력하면 된다.

 

문제 풀이

이 문제는 모든 경우를 탐색하는 방법 말고는 더 좋은 방법은 딱히 없는 듯 하다.

방문 체크를 하며, 이미 탐색한 경우는 제외하고 모든 경우에 대해 탐색하도록 하였다.

 

최초에 A, B, C가 주어진다.

 

연산은 두 그룹을 선택했을 때, 더 작은 쪽의 값을 작은 쪽에 더하고, 큰 쪽에서 빼는 연산을 하게 된다.

같은 양을 양쪽에 빼고 더하기 때문에, 전체 총량 (A + B + C)는 변하지 않는다.

 

즉, A와 B만 알면 사실 C는 구할 수 있다.

이 아이디어를 기준으로, 방문 체크는 3차원 배열이 아닌 2차원 배열을 사용하여 2개의 돌을 기준으로만 하게 된다.

 

또한, 1, 2, 3 과 3, 2, 1은 사실 같은 경우라고 할 수 있다.

문제에서 구하는 것은 연산을 반복했을 때, 모든 돌 그룹의 돌 개수가 같아질 수 있는지 여부를 반환하는 것이기 때문에 1,2,3 에서 true가 나온다면 3, 2,1에서도 당연히 true가 나올 수 밖에 없다.

 

그렇기 때문에, 돌 개수를 정렬하여 탐색을 할 것이다.

(1, 2, 3 과 3, 2, 1을 같은 경우라고 가정하기 위해)

 

처음 A, B, C가 있다면, 이 중 2개의 돌 A, B만 선택하여 Queue에 삽입한다.

이후, Queue에서는 front를 pop한 뒤, A,B,C를 기준으로 BFS를 진행할 것이다.

 

1. A, B를 선택하는 경우

2. A, C를 선택하는 경우

3. B, C를 선택하는 경우

 

세 경우를 탐색하며 계속 BFS를 진행하며, A,B,C가 같아지는 타이밍이 온다면 1을 출력하고 종료하도록 하였다.

만약, BFS가 모두 끝날 때 까지 A,B,C가 같아지는 타이밍을 발견하지 못한다면 0을 출력하도록 하였다.

 

또한, A, B, C 가 절대 같아질 수 없는 경우가 한 가지 있는데, A + B + C의 값이 3의 배수가 아닌 경우이다.

A, B, C가 모두 X라는 값으로 같은 값을 가진다면 A + B + C 는 3X 가 된다.

즉, A, B, C 가 같다면, A + B + C 는 반드시 3의 배수여야 한다.

 

그러므로, A + B + C가 3의 배수가 아니라면 바로 0을 출력하고 종료하도록 예외처리를 해주었다.

 

풀이 코드

int A = 0;
int B = 0;
int C = 0;

std::cin >> A >> B >> C;

int SumStone = A + B + C;

if (SumStone % 3 != 0)
{
    std::cout << 0;
    return 0;
}

먼저, 입력을 받아준 뒤 세 수의 합이 3의 배수가 아니라면 0을 출력한 뒤 종료하도록 하였다.

std::queue<std::pair<int, int>> BFS;

if (A > B)
{
    std::swap(A, B);
}

BFS.push({A, B});

다음은 queue에 첫 원소인 {A, B}를 삽입해 주었다.

이 때, A와 B를 오름차순으로 정렬하여 넣어주어야 한다.

while (BFS.size() > 0)
{
    std::pair<int, int> CurStone = BFS.front();
    BFS.pop();

    int A = CurStone.first;
    int B = CurStone.second;
    int C = SumStone - A - B;

    if (A == B && B == C)
    {
        std::cout << 1;
        return 0;
    }

    if (A != B)
    {
        InsertQueue(A, B, BFS);
    }

    if (B != C)
    {
        InsertQueue(B, C, BFS);
    }

    if (A != C)
    {
        InsertQueue(A, C, BFS);
    }
}

BFS내부 코드이다.

 

먼저, C를 구해준다. 그리고, A와 B와 C가 모두 같은지 검사해준다. 만약 같다면 1을 출력하고 그대로 종료한다.

같지 않다면, 그대로 너비 우선 탐색을 진행한다.

 

두 그룹을 선택했을 때, 돌의 개수가 같다면 선택할 수 없으므로 두 그룹의 돌 개수가 다른 경우에만 탐색을 진행하도록 하였다.

void InsertQueue(int _Left, int _Right, std::queue<std::pair<int, int>>& _Queue)
{
    if (_Left > _Right)
    {
        std::swap(_Left, _Right);
    }

    _Right -= _Left;
    _Left *= 2;

    if (isVisit[_Left][_Right] == false)
    {
        isVisit[_Left][_Right] = true;
        _Queue.push({ _Left, _Right });
    }
}

이는 queue에 원소를 삽입하는 함수이다.

동일한 코드를 여러 번 작성하는 것이 불편하니, 함수로 만들어주었다.

 

먼저, 선택된 두 숫자를 오름차순으로 정렬하였다.

그리고, 연산을 진행하였다. 큰 수에서는 작은 수를 빼주고, 작은 수에는 작은 수를 더해주었다.

 

그렇게 나온 경우를 이미 탐색한 경험이 없다면, 방문 체크를 한 뒤 queue에 삽입해주었다.

std::cout << 0;

return 0;

만약, 반복문이 끝날 때 까지 프로그램이 종료되지 않았다면 세 그룹의 돌 개수가 같은 경우를 발견하지 못한 것이므로 0을 출력하고 끝내주었다.

 

 

코드 전문

더보기
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <set>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}

std::vector<std::vector<bool>> isVisit(1501, std::vector<bool>(1501, false));

void InsertQueue(int _Left, int _Right, std::queue<std::pair<int, int>>& _Queue)
{
    if (_Left > _Right)
    {
        std::swap(_Left, _Right);
    }

    _Right -= _Left;
    _Left *= 2;

    if (isVisit[_Left][_Right] == false)
    {
        isVisit[_Left][_Right] = true;
        _Queue.push({ _Left, _Right });
    }
}

int main()
{
    Init();

    int A = 0;
    int B = 0;
    int C = 0;

    std::cin >> A >> B >> C;

    int SumStone = A + B + C;

    if (SumStone % 3 != 0)
    {
        std::cout << 0;
        return 0;
    }

    std::queue<std::pair<int, int>> BFS;

    if (A > B)
    {
        std::swap(A, B);
    }

    BFS.push({A, B});

    while (BFS.size() > 0)
    {
        std::pair<int, int> CurStone = BFS.front();
        BFS.pop();

        int A = CurStone.first;
        int B = CurStone.second;
        int C = SumStone - A - B;

        if (A == B && B == C)
        {
            std::cout << 1;
            return 0;
        }

        if (A != B)
        {
            InsertQueue(A, B, BFS);
        }

        if (B != C)
        {
            InsertQueue(B, C, BFS);
        }

        if (A != C)
        {
            InsertQueue(A, C, BFS);
        }
    }

    std::cout << 0;

    return 0;
}

 

N의 길이를 가지는 좋은 수열 중, 하나의 수로 표현했을 때 가장 작은 수가 되는 좋은 수열을 구하자.

 

좋은 수열이란, 나쁜 수열이 아닌 수열이다.

나쁜 수열이란, 특정 부분 수열이 반복되는 부분 수열이 존재하는 수열이다.

 

예를 들어, 123123 은 123이라는 부분 수열이 반복되는 123123이라는 부분 수열이 존재한다.

312123 은, 12라는 부분 수열이 반복되는 1212라는 부분 수열이 존재한다.

11은, 1이라는 부분 수열이 반복되는 11이라는 부분 수열이 존재한다.

 

문제 풀이

이 문제는 특별한 규칙을 찾을 수는 없었다. 하나하나 경우의 수를 탐색하는 방법말고는 다른 방법은 딱히 없는 듯 하다.

본인은 백트래킹을 활용하여 문제를 해결하였다.

 

문자열의 맨 뒤에 1, 2, 3을 더한 뒤, 해당 수열이 좋은 수열인지 탐색하는 것이다.

 

그렇다면, 해당 수열이 나쁜 수열인지 좋은 수열인지 판단하는 방법이 필요하다.

본인은 맨 뒤에서부터 수열의 절반만큼 탐색하며 판단하였다.

 

예를 들어 "12323" 을 판별한다고 해보자.

 

맨 뒤에서 1글자를 기준으로 비교하게 되면, "12323이렇게 파란색 문자열과 빨간색 문자열을 비교하게 된다.

두 문자열이 같다면, 나쁜 수열인 것이다.

 

한 글자를 비교했을 때, 좋은 수열이었다면 이번엔 두 글자를 비교해보는 것이다. "12323" 이렇게 말이다.

이번엔, 파란색 문자열과 빨간색 문자열이 동일하므로, 나쁜 수열임을 판단할 수 있다.

 

하지만 끝을 기준으로 비교하게 되면 "132321" 이렇게 중간에 수열이 반복되는 경우는 탐지할 수가 없을 것 처럼 보인다. 하지만, 아니다. 왜냐하면, 재귀함수를 통해 1글자씩 더해가며 판별을 반복할 것이기 때문이다. 

 

맨 처음엔, 빈 문자열에 1을 더한다.

문자열 : "1"

 

위의 문자열을 판별한다.

좋은 수열인 경우, 문자열의 뒤에 "1"을 더한다

문자열 : "11"

 

또 위의 문자열을 판별한다.

좋은 수열인 경우, 문자열의 뒤에 "1"을 더한다.

하지만, 현재 문자열은 나쁜 수열이다.그러므로, 뒤의 1을 뺀 뒤, 2를 더한다. (만약 맨 뒤가 2인 상태에서 나쁜 수열로 판단된다면, 2를 빼고 3을 더한다. )

문자열 : "12"

 

위의 문자열을 판별한다.좋은 수열인 경우, 문자열의 뒤에 "1"을 더한다.

문자열 : "121"

 

이 과정을 의사 코드로 작성하면, 아래와 같다.

void Recursive(std::string _Str, int _MaxLength)
{
	if (_Str이 나쁜 수열이면?)
	{
            return;
	}

	if (길이가 MaxLength까지 왔는데, 나쁜 수열이 아니다?)
	{
            정답 발견!
            return;
	}
    
	Recursive(_Str + "1", _MaxLength);
	Recursive(_Str + "2", _MaxLength);
	Recursive(_Str + "3", _MaxLength);
}

 

이렇게 재귀함수를 활용하여, 문자열의 길이를 1씩 증가시키며 좋은 수열을 찾는 것이다.

그러므로, 중간에 반복되는 수열이 있는 경우는 있을 수가 없다.

 

(중간에 있는 반복되는 수열은 현재 문자열보다 길이가 짧은 문자열에서는 가장 뒤에 있는 반복되는 수열일 수 있다. 거기서 반복되는 수열이 발견되면서 return 해버리기 때문에, 중간에서 발견되는 반복되는 수열은 있을 수 없다.)

 

또한, 재귀함수를 보면, 1을 우선적으로 더하고 그 이후 2를 더하고 마지막에 3을 더하고 있다.즉, 가장 먼저 발견되는 좋은 수열이 정답이 될 수 밖에 없다.

 

1-> 11 -> 12 -> 121->  1211 -> 1212 -> 1213 ... 이런 식으로 숫자 크기대로 탐색하기 때문이다.

 

풀이 코드

int Length = 0;
std::cin >> Length;

먼저, 수열의 길이를 입력받는다.

 

Recursive("", Length);

std::cout << Answer;

return 0;

이후, 재귀함수를 실행한 뒤에 답을 출력할 것이다.

 

void Recursive(std::string _Str, int _MaxLength)
{
    if (Answer != "")
    {
        return;
    }

    if (Check(_Str) == false)
    {
        return;
    }

    if (_Str.size() >= _MaxLength)
    {
        Answer = _Str;
        return;
    }
    
    Recursive(_Str + "1", _MaxLength);
    Recursive(_Str + "2", _MaxLength);
    Recursive(_Str + "3", _MaxLength);
}

Answer이 ""이 아니라면, 이미 답을 구했다는 의미이므로 모든 재귀함수를 종료시킨다.

 

Check는 해당 수열이 좋은 수열인지 판단하는 함수이다. 나쁜 수열이라면 return해주었다.

 

길이가 MaxLength될 때 까지 나쁜 수열로 판별되지 않은 수열이 있다면 정답으로 저장해주었다.

 

문자열의 맨 뒤에 1을 붙이는 경우를 우선적으로 재귀적 탐색한 뒤, 정답을 발견하지 못하면 2를 붙이며 탐색하고, 그 때도 발견하지 못하면 3을 붙여 순차적으로 탐색하도록 하였다.

 

bool Check(const std::string& _Str)
{
    int Size = _Str.size();

    for (int i = 1; i <= Size / 2; i++)
    {
        int Start = Size - i * 2;
        
        if (_Str.substr(Start, i) == _Str.substr(Start + i, i))
        {
            return false;
        }
    }

    return true;
}

Check함수의 내부 코드이다.

문자열의 끌을 기준으로, 1자리, 2자리 3자리.... (문자열의 길이 / 2)자리의 수열이 앞에서 반복되고 있는 지를 판별하였다.

 

12131213

12131213

12131213

12131213

 

위와 같은 순서로, 파란색 문자열과 빨간색 문자열을 비교한 것이다.

위에서도 말했지만, 중간에서 반복되는 부분 수열은, 해당 수열이 끝에 있을 때 이미 걸러지기 때문에 중간에서는 반복되는 수열이 발견될 수 없다.

 

(123231 에서, 23과 23이 반복되고 있지만,이는 12323 에서 이미 걸러지고 return 되므로 123231이라는 문자열은 탐색되지 않는다.)

 

 

코드 전문

더보기
#include <iostream>
#include <vector>
#include <set>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}

std::string Answer = "";

bool Check(const std::string& _Str)
{
    int Size = _Str.size();

    for (int i = 1; i <= Size / 2; i++)
    {
        int Start = Size - i * 2;
        
        if (_Str.substr(Start, i) == _Str.substr(Start + i, i))
        {
            return false;
        }
    }

    return true;
}

void Recursive(std::string _Str, int _MaxLength)
{
    if (Answer != "")
    {
        return;
    }

    if (Check(_Str) == false)
    {
        return;
    }

    if (_Str.size() >= _MaxLength)
    {
        Answer = _Str;
        return;
    }
    
    Recursive(_Str + "1", _MaxLength);
    Recursive(_Str + "2", _MaxLength);
    Recursive(_Str + "3", _MaxLength);
}

int main()
{
    Init();

    int Length = 0;
    std::cin >> Length;

    Recursive("", Length);

    std::cout << Answer;

    return 0;
}

 

트리의 특정 간선에 연결된 두 노드 중, 한 쪽의 가중치에는 -1을 하고 다른 쪽의 가중치에는 +1을 하는 작업을 반복했을 때, 모든 노드의 가중치를 0으로 만들 수 있을까?

 

만약, 만들 수 없다면 -1을 출력하면 되고 만들 수 있다면 몇 번을 반복해야 모든 노드의 가중치가 0이 되는지 구하면 된다.

 

문제 풀이

언뜻 보기엔, 매우 복잡해보이지만 생각보다는 간단한 문제이다.

핵심은 트리의 가장 말단 노드부터 0으로 만드는 것이다.

 

DFS든 BFS든 트리의 말단을 찾아가서, 말단을 0으로 만들면서 루트 노드까지 올라왔을 때 모든 노드의 가중치를 확인하면 된다.

 

그림을 보면서 이해해보자.

 

문제의 첫 번째 입력을 그림으로 그려보면 아래와 같다.

노드 안의 숫자는 (번호 / 가중치) 이다.

이제, 말단에서부터 가중치를 0으로 만들어보자.

 

2번 노드에 대해 갱신하면, 위와 같아진다.

다음은 4번 노드에 대해서도 갱신해보자.

위와 같이 값이 갱신될 것이다.

이제, 말단 노드가 모두 0으로 되었으니, 0이 아닌 노드 중 가장 말단에 있는 노드를 0으로 만들어보자.

 

3번 노드를 갱신하고 나니, 루트 노드까지 갱신되었다.

노드의 모든 가중치를 보면 0으로 갱신되어 있는 것을 볼 수 있다.

 

이처럼, 말단노드부터 위로 올라오며 0으로 갱신하다 보면, 모든 노드를 갱신하게 된다.

모든 노드를 갱신한 뒤에, 모든 노드의 가중치가 0인지 아닌지를 확인하면 된다.

 

가중치가 0이 아니라면, -1과 +1을 하는 작업을 몇 번 반복했는지를 출력하면 되는데, 이건 노드를 0으로 만들 때, 해당 노드의 가중치의 절대값에 대한 누적합을 구하면 된다.

 

참고로 위의 과정은 루트 노드에서부터 시작해야 한다.

하지만, 입력으로 주어지는 트리는 양방향 트리이므로, 어떤 노드든 사실 루트 노드가 될 수 있다.

그러므로 아무 노드에서나 시작해도 문제는 없다.

 

하지만, 4번 노드에서부터 시작하도록 했는데 입력으로 4번 노드가 주어지지 않는다면?

이런 문제를 방지하기 위해, 0번 노드에서부터 시작하는 것이 가장 무난하다. 

 

풀이 코드

std::vector<long long> CastA(a.size());
for(int i = 0; i < a.size(); i++)
{
    CastA[i] = a[i];
}

std::vector<std::vector<int>> Link(a.size());

for(int i = 0; i < edges.size(); i++)
{
    int Node_1 = edges[i][0];
    int Node_2 = edges[i][1];

    Link[Node_1].push_back(Node_2);
    Link[Node_2].push_back(Node_1);
}
    
std::vector<bool> isVisit(a.size(), false);

CastA는 주어진 노드의 가중치를 long long 자료형으로 변환하여 새로 저장한 것이다.

왜냐하면, 기존 배열에 값을 더하고 빼며 계산할 것인데 이 과정에서 int 자료형의 범위를 초과하는 경우가 발생할 수 있기 때문이다.

 

Link는 각 노드에서 연결된 노드들을 저장한 배열이다.

 

isVisit는 DFS를 위해 만든 방문체크배열이다.

 

DFS(CastA, Link, isVisit, 0, -1);

for(int i = 0; i < CastA.size(); i++)
{
    if(CastA[i] != 0)
    {
        Count = -1;
        break;
    }
}

return Count;

다음은 DFS를 돌린 다음, 모든 노드의 가중치가 0인지를 확인하는 과정이다.

(참고로 Count는 전역변수이다.)

 

void DFS(std::vector<long long>& _Weight, const std::vector<std::vector<int>>& _Link,  std::vector<bool>& _isVisit, int _CurIndex, int _PrevIndex)
{
    _isVisit[_CurIndex] = true;
    
    for(int i = 0; i < _Link[_CurIndex].size(); i++)
    {
        int NextIndex = _Link[_CurIndex][i];
        
        if(_isVisit[NextIndex] == false)
        {
            DFS(_Weight, _Link, _isVisit, NextIndex, _CurIndex);
        }
    }
    
    if(_PrevIndex != -1)
    {
        Count += abs(_Weight[_CurIndex]);

        _Weight[_PrevIndex] += _Weight[_CurIndex];
        _Weight[_CurIndex] = 0;
    }
}

DFS내부 코드는 이와 같다.

 

먼저, 방문체크를 해준 다음 연결된 노드 중 이동할 수 있는 노드에 대해 이동해주었다.

이후, 말단 노드에서는 더이상 이동할 곳이 없기 때문에 반복문을 탈출하게 되는데, 이 때, 해당 노드의 가중치를 0으로 만드는 과정을 거치게 된다. 말단 노드에서부터 위로 올라오며 재귀적으로 노드의 가중치를 0으로 만드는 것이다.

 

 이렇게 하면, 올바른 답을 출력할 수 있게 된다.

 

 

코드 전문

더보기
#include <string>
#include <vector>

using namespace std;

long long Count = 0;

void DFS(std::vector<long long>& _Weight, const std::vector<std::vector<int>>& _Link,  std::vector<bool>& _isVisit, int _CurIndex, int _PrevIndex)
{
    _isVisit[_CurIndex] = true;
    
    for(int i = 0; i < _Link[_CurIndex].size(); i++)
    {
        int NextIndex = _Link[_CurIndex][i];
        
        if(_isVisit[NextIndex] == false)
        {
            DFS(_Weight, _Link, _isVisit, NextIndex, _CurIndex);
        }
    }
    
    if(_PrevIndex != -1)
    {
        Count += abs(_Weight[_CurIndex]);

        _Weight[_PrevIndex] += _Weight[_CurIndex];
        _Weight[_CurIndex] = 0;
    }
}

long long solution(vector<int> a, vector<vector<int>> edges) 
{
    std::vector<long long> CastA(a.size());
    for(int i = 0; i < a.size(); i++)
    {
        CastA[i] = a[i];
    }
    
    std::vector<std::vector<int>> Link(a.size());
    
    for(int i = 0; i < edges.size(); i++)
    {
        int Node_1 = edges[i][0];
        int Node_2 = edges[i][1];
        
        Link[Node_1].push_back(Node_2);
        Link[Node_2].push_back(Node_1);
    }
    
    std::vector<bool> isVisit(a.size(), false);
    
    DFS(CastA, Link, isVisit, 0, -1);
    
    for(int i = 0; i < CastA.size(); i++)
    {
        if(CastA[i] != 0)
        {
            Count = -1;
            break;
        }
    }
    
    return Count;
}

 

n개의 자연수를 더해서, s가 될 수 있는 자연수 집합 중에서 원소들의 곱이 가장 큰 집합을 구하면 된다.

 

문제 풀이

이 문제는 어렵게 생각하면 매우 어렵지만 쉽게 생각하면 매우 쉽다.

결론부터 말하면, 최고의 집합은 원소들간의 편차가 가장 작은 집합이다.

 

예를 들어, n이 3이고 s가 10이라고 한다면 최고의 집합은 {3, 3, 4}가 된다.

n이 5이고 s가 40이라고 한다면, {8, 8, 8, 8, 8} 이 된다.

n이 4이고, s가 35라고 한다면 {8, 9, 9, 9}가 된다.

 

왜 그렇게 되는지는 사실 수학적인 증명까지는 잘 모르지만, 직접 몇 가지 경우에 대해 계산해본 결과 위처럼 편차가 가장 고른 숫자 집합이 최고의 집합이 된다는 것을 확인해볼 수 있었다.

 

그렇다면, n과 s를 사용해서 편차가 가장 고른 숫자 집합을 만들면 된다는 것이다.

어떻게 만들면 될까?

 

1. n개 만큼의 원소를 저장할 수 있는 배열을 만든다.

2. 배열의 각 원소를 s / n 으로 저장한다.

3. 배열의 가장 마지막 원소부터 (s % n) 개만큼 앞에 있는 원소까지 1씩 더해준다.

 

위의 과정을 거치면 최고의 집합을 구할 수 있다.

가장 값이 고른 숫자 집합을 만들려면, 당연히 n으로 s를 나누면 된다.

하지만 모든 숫자의 합이 s가 되기 위해선, 나머지가 0이어야만 한다. 하지만, 항상 나머지가 0이되는 것은 아니다.

나머지가 1보다 큰 상황에선 나머지의 수와 동일한 갯수만큼 각 숫자에 1씩 더해주면, 가장 편차가 적은 집합을 구성할 수 있게 된다.

 

풀이 코드

if(s < n)
{
    return {-1};
}

 

먼저, 예외처리이다. 만약, s가 n보다 작다면?
s 를 n으로 나눈 몫이 0이 된다. 최고의 집합은 모든 원소가 자연수가 되어야 하는데, s < n 인 상황에선 원소에 0이 포함되므로 s < n 인 경우에는 최고의 집합이 존재하지 않는다.

 

vector<int> Answer(n, 0);
    
int Share = s / n;
int Remain = s % n;

for(int i =  Answer.size() - 1; i  >= 0; i--)
{
    Answer[i] = Share;

    if(Remain > 0)
    {
        Answer[i] += 1;
        Remain--;
    }
}

이후, 위에서 말했던 것처럼 배열에 몫을 저장해주었고, 배열의 가장 뒤에서부터 나머지의 개수만큼 원소에 1을 더해주었다.

 

return Answer;

반복문이 끝난 후 배열을 반환해주면 된다.

 

 

코드 전문

더보기
#include <string>
#include <vector>

using namespace std;

vector<int> solution(int n, int s) 
{
    if(s < n)
    {
        return {-1};
    }
   
    vector<int> Answer(n, 0);
    
    int Share = s / n;
    int Remain = s % n;
    
    for(int i =  Answer.size() - 1; i  >= 0; i--)
    {
        Answer[i] = Share;
        
        if(Remain > 0)
        {
            Answer[i] += 1;
            Remain--;
        }
    }
        
    return Answer;
}

 

 
특정 과목을 수강하기 위한 선행과목이 있다고 했을 때, 각 과목은 최소 몇학기에 수강할 수 있는가를 구하는 문제이다.
A->B 의 선행관계가 성립된다면, A와 B를 같은 학기에 들을 수 없기 때문에 A를 1학기에 들었다면, B는 최소 2학기에 들을 수 있다.

 

문제 풀이

해당 문제는 위상 정렬을 활용한 대표적인 문제이다.
위상 정렬을 모른다면, 아래 링크를 클릭하여 위상 정렬을 알아보고 오자.
알고리즘 - 위상 정렬 (tistory.com)
 
위상정렬을 알고 있다면, 어려운 것 없이 해결할 수 있다.
진입차수가 0이되는 과목에 대해 queue에 {과목, 수강학기} 페어를 값으로 삽입한 뒤, 추가적으로 진입차수가 0이 되는 과목들은 {과목, 수강학기 + 1}의 값으로 queue에 삽입하며 queue가 빌 때 까지 반복하면 된다.
 

풀이 코드

int NumSubject = 0;
int NumLink = 0;
std::cin >> NumSubject >> NumLink;

std::vector<std::vector<int>> Links(NumSubject);
std::vector<int> InDegree(NumSubject, 0);

for (int i = 0; i < NumLink; i++)
{
    int First = 0;
    int Second = 0;
    std::cin >> First >> Second;

    Links[First - 1].push_back(Second - 1);
    InDegree[Second - 1]++;
}

먼저, 입력을 받아주었다.
Link배열은 인덱스 i에 대해, i를 선행해야 시작할 수 있는 작업들을 모아둔 배열이다.
예를 들어, 1->2, 1->3  의 선행관계가 성립된다면, Link[1] 은 {2, 3}이 되는 것이다.
 
InDegree는 진입차수이다. 인덱스 i에 대해, i를 시작하기 위한 선행 작업이 몇 개가 있느냐를 저장하는 배열이다.
 

std::queue<std::pair<int, int>> Sort;
std::vector<int> Semesters(NumSubject);

for (int i = 0; i < NumSubject; i++)
{
    if (InDegree[i] == 0)
    {
         Sort.push({i, 1});
         Semesters[i] = 1;
    }
}

위상정렬을 실행하기 위한 queue를 만들었다.
queue의 원소는 {과목, 수강학기} 이다.
최초에 진입차수가 0인 과목은 1학기에 바로 들을 수 있다는 뜻이기 때문에 {과목, 1}로 queue에 삽입해주었다.
 
Order는 해당 과목을 최소 몇 학기에 수강할 수 있는가를 저장하는 배열이다. 
 

while (Sort.size() > 0)
{
    int Index = Sort.front().first;
    int CurSemester = Sort.front().second;
    Sort.pop();

    for (int i = 0; i < Links[Index].size(); i++)
    {
        int CurIndex = Links[Index][i];
        InDegree[CurIndex]--;

        if (InDegree[CurIndex] == 0)
        {
            Sort.push({ CurIndex, CurSemester + 1});
            Semesters[CurIndex] = CurSemester + 1;
        }
    }
}

이제, 본격적으로 위상정렬을 해보자.
queue에서 가장 앞의 원소 하나를 빼준다.
 
이후, 해당 과목을 선행과목으로 갖는 과목들의 진입차수를 1씩 빼주었다.
진입차수가 0이 되는 과목이 생긴다면, 해당 과목을 queue에 삽입해주었다.
해당 과목은 다음 학기에 들을 수 있으므로, 현재학기 + 1로 삽입해주었다.
 

for (int i = 0; i < NumSubject; i++)
{
    std::cout << Semesters[i] << " ";
}

return 0;

모든 작업이 끝나고, 배열의 원소를 순서대로 출력해주면 된다.
 

 

코드 전문

더보기
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}

int main()
{
    Init();

    int NumSubject = 0;
    int NumLink = 0;
    std::cin >> NumSubject >> NumLink;
    
    std::vector<std::vector<int>> Links(NumSubject);
    std::vector<int> InDegree(NumSubject, 0);

    for (int i = 0; i < NumLink; i++)
    {
        int First = 0;
        int Second = 0;
        std::cin >> First >> Second;

        Links[First - 1].push_back(Second - 1);
        InDegree[Second - 1]++;
    }

    std::queue<std::pair<int, int>> Sort;
    std::vector<int> Semesters(NumSubject);

    for (int i = 0; i < NumSubject; i++)
    {
        if (InDegree[i] == 0)
        {
            Sort.push({i, 1});
            Semesters[i] = 1;
        }
    }

    while (Sort.size() > 0)
    {
        int Index = Sort.front().first;
        int CurSemester = Sort.front().second;
        Sort.pop();

        for (int i = 0; i < Links[Index].size(); i++)
        {
            int CurIndex = Links[Index][i];
            InDegree[CurIndex]--;

            if (InDegree[CurIndex] == 0)
            {
                Sort.push({ CurIndex, CurSemester + 1});
                Semesters[CurIndex] = CurSemester + 1;
            }
        }
    }
    
    for (int i = 0; i < NumSubject; i++)
    {
        std::cout << Semesters[i] << " ";
    }

    return 0;
}

 

 

 

매초 외부와 2칸 이상 접촉된 치즈가 녹아내린다.

모든 치즈가 녹아내리는데, 몇 초가 걸리는지 출력하면 된다.

 

문제 풀이

 

문제의 핵심은 치즈 내부와 외부를 어떻게 구분하느냐이다.

위의 그림처럼 치즈가 배치되어 있다면, 빨간 영역이 외부이고 파란 영역이 내부일 것이다.

외부를 구하는 법은 간단하다.

(0,0)을 시작으로 BFS를 돌리면 된다.

 

문제 조건을 보면, 가장자리에는 치즈를 배치하지 않는다는 조건이 있다.

즉, (0,0)은 항상 외부에 속하는 영역이고 이 점을 기준으로 BFS를 돌리면 외부를 파악할 수 있다.

 

BFS를 통해, 외부를 판별한 뒤 외부와 2칸 이삭 접촉한 치즈를 제거하면 된다.

 

이 과정이 1번 실행될 때마다, 1초가 지난다고 가정하며 치즈가 사라질 때까지 반복하면 된다.

 

풀이 코드

int Height = 0;
int Width = 0;
std::cin >> Height >> Width;

std::vector<std::vector<int>> Board(Height, std::vector<int>(Width, 0));
std::vector<std::vector<bool>> isVisit(Height, std::vector<bool>(Width, false));

for (int i = 0; i < Height; i++)
{
    for (int j = 0; j < Width; j++)
    {
        std::cin >> Board[i][j];
    }
}

먼저 입력을 모두 받아준다.

int TimeCount = 0;

while (BFSFunc(Board, isVisit) < Height * Width)
{
    TimeCount++;
}

이후, BFS를 돌려주며 시간을 1초씩 늘려주었다.

BFSFunc는 외부를 확인한 뒤, 치즈를 제거하는 과정까지 포함된 함수이다.

반환값은 외부의 칸 개수이다. 외부의 칸 개수가 전체 맵의 크기(높이 * 너비)와 같다면 치즈가 모두 녹았다고 판단하는 것이다.

std::vector<int> DirX = { 1, 0, -1, 0 };
std::vector<int> DirY = { 0, 1, 0, -1 };

int BFSFunc(std::vector<std::vector<int>>& _Board, std::vector<std::vector<bool>>& _isVisit)
{
    for (int i = 0; i < _isVisit.size(); i++)
    {
        std::fill(_isVisit[i].begin(), _isVisit[i].end(), false);
    }

    std::queue<std::pair<int, int>> BFS;
    BFS.push({ 0, 0 });
    _isVisit[0][0] = true;

    std::set<std::pair<int, int>> CheesePos;

    int EmptyCount = 1;

    while (BFS.size() > 0)
    {
        std::pair<int, int> CurPos = BFS.front();
        BFS.pop();

        for (int i = 0; i < 4; i++)
        {
            int NextX = CurPos.second + DirX[i];
            int NextY = CurPos.first + DirY[i];

            if (NextX < 0 || NextY < 0 || NextX >= _Board[0].size() || NextY >= _Board.size())
            {
                continue;
            }

            if (_Board[NextY][NextX] == 0 && _isVisit[NextY][NextX] == false)
            {
                _isVisit[NextY][NextX] = true;
                BFS.push({ NextY, NextX });

                EmptyCount++;
            }

            if (_Board[NextY][NextX] == 1)
            {
                CheesePos.insert({ NextY, NextX });
            }
        }
    }

    std::set<std::pair<int, int>>::iterator CurIter = CheesePos.begin();
    std::set<std::pair<int, int>>::iterator EndIter = CheesePos.end();

    while (CurIter != EndIter)
    {
        std::pair<int, int> CurPos = *CurIter;

        int Count = 0;

        for (int k = 0; k < 4; k++)
        {
            int NextX = CurPos.second + DirX[k];
            int NextY = CurPos.first + DirY[k];

            if (_isVisit[NextY][NextX] == true)
            {
                Count++;
            }

            if (Count >= 2)
            {
                _Board[CurPos.first][CurPos.second] = 0;
                break;
            }
        }

        CurIter++;
    }

    return EmptyCount;
}

 

isVisit 벡터의 경우, 계속 새로 만들어주는것보다 값만 바꿔주는 것이 더 빠를 것이라고 판단하여 함수의 처음 부분에 std::fill을 이용해 모든 값을 false로 초기화해주었다.

 

이후 평범하게 BFS를 돌려주었다.

isVisit가 true인 부분은 외부라고 판단할 수 있을 것이다.

 

이후, 치즈가 있는 칸을 전부 돌아주며 상하좌우 4방향중 2칸 이상이 외부인 치즈를 제거해주었다.

std::cout << TimeCount;

return 0;

  마지막으로 반환해주면 끝이다.

 

 

코드 전문

더보기
#include <iostream>
#include <vector>
#include <queue>
#include <set>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}


std::vector<int> DirX = { 1, 0, -1, 0 };
std::vector<int> DirY = { 0, 1, 0, -1 };

int BFSFunc(std::vector<std::vector<int>>& _Board, std::vector<std::vector<bool>>& _isVisit)
{
    for (int i = 0; i < _isVisit.size(); i++)
    {
        std::fill(_isVisit[i].begin(), _isVisit[i].end(), false);
    }

    std::queue<std::pair<int, int>> BFS;
    BFS.push({ 0, 0 });
    _isVisit[0][0] = true;

    std::set<std::pair<int, int>> CheesePos;

    int EmptyCount = 1;

    while (BFS.size() > 0)
    {
        std::pair<int, int> CurPos = BFS.front();
        BFS.pop();

        for (int i = 0; i < 4; i++)
        {
            int NextX = CurPos.second + DirX[i];
            int NextY = CurPos.first + DirY[i];

            if (NextX < 0 || NextY < 0 || NextX >= _Board[0].size() || NextY >= _Board.size())
            {
                continue;
            }

            if (_Board[NextY][NextX] == 0 && _isVisit[NextY][NextX] == false)
            {
                _isVisit[NextY][NextX] = true;
                BFS.push({ NextY, NextX });

                EmptyCount++;
            }

            if (_Board[NextY][NextX] == 1)
            {
                CheesePos.insert({ NextY, NextX });
            }
        }
    }

    std::set<std::pair<int, int>>::iterator CurIter = CheesePos.begin();
    std::set<std::pair<int, int>>::iterator EndIter = CheesePos.end();

    while (CurIter != EndIter)
    {
        std::pair<int, int> CurPos = *CurIter;

        int Count = 0;

        for (int k = 0; k < 4; k++)
        {
            int NextX = CurPos.second + DirX[k];
            int NextY = CurPos.first + DirY[k];

            if (_isVisit[NextY][NextX] == true)
            {
                Count++;
            }

            if (Count >= 2)
            {
                _Board[CurPos.first][CurPos.second] = 0;
                break;
            }
        }

        CurIter++;
    }

    return EmptyCount;
}

int main()
{
    Init();

    int Height = 0;
    int Width = 0;
    std::cin >> Height >> Width;

    std::vector<std::vector<int>> Board(Height, std::vector<int>(Width, 0));
    std::vector<std::vector<bool>> isVisit(Height, std::vector<bool>(Width, false));

    for (int i = 0; i < Height; i++)
    {
        for (int j = 0; j < Width; j++)
        {
            std::cin >> Board[i][j];
        }
    }

    int TimeCount = 0;

    while (BFSFunc(Board, isVisit) < Height * Width)
    {
        TimeCount++;
    }

    std::cout << TimeCount;

    return 0;
}

 

 
규칙에 맞게 트리를 구성한 뒤, 트리에서 가장 긴 너비와 해당 너비의 레벨을 출력하면 된다.
 

문제 풀이

19
1 2 3
2 4 5
3 6 7
4 8 -1
5 9 10
6 11 12
7 13 -1
8 -1 -1
9 14 15
10 -1 -1
11 16 -1
12 -1 -1
13 17 -1
14 -1 -1
15 18 -1
16 -1 -1
17 -1 19
18 -1 -1
19 -1 -1

 
위의 입력을 기준으로 이해해보자.
 

 
트리는 위 그림과 같이 구성될 것이다.
여기서, 우리가 구해야 하는 것은, 특정 레벨에서의 너비이다.
 
너비를 구하는 법은 우측에 있는 노드의 위치에서 좌측에 있는 노드의 위치를 빼고 1을 더하는 것이다.
그렇다면, 각 노드의 위치를 구하는 것이 먼저일 것이다.
 
위치는 어떻게 구해야 할까?
 
문제의 규칙중에, 특정 노드의 왼쪽 서브트리는  노드보다 왼쪽에 위치하며, 오른쪽 서브트리는 노드보다 오른쪽에 위치한다는 조건이 있다.

그림에서 볼 수 있듯이, 왼쪽 서브트리의 모든 노드는 나보다 왼쪽에 있으며, 오른쪽 서브트리의 모든 노드는 나보다 우측에 있다.
 
또한, 모든 열에는 하나의 노드만 존재한다는 조건도 있는데 이 조건까지 합치게 되면, 하나의 규칙을 발견할 수 있다.
 
어떤 노드의 위치는 왼쪽에 있는 노드의 개수 + 1이 된다는 것이다.

1번 노드의 왼쪽에는 9개의 노드가 존재한다.
그러므로, 1번 노드의 위치는 10이 된다.
 
그렇다면, 특정 노드의 위치를 구하기 위해선 왼쪽에 있는 노드의 개수를 구해야 한다는 것이다.
 
왼쪽에 있는 노드의 개수를 어떻게 구해야 할까?
왼쪽 서브트리의 노드 개수를 구하면 된다. 우리는 BFS든 DFS든 탐색 알고리즘을 이용하여, 특정 트리의 노드 개수를 구할 수 있다.
 
왼쪽 서브트리의 루트 노드를 기준으로 BFS든 DFS든 실행하게 되면, 노드의 개수를 구할 수 있을 것이다.
하지만, 왼쪽 서브트리만 구한다고 해서 왼쪽에 있는 모든 노드의 개수를 구할 수는 없다. 

3번 노드를 기준으로 보자.
왼쪽 서브트리의 노드 개수는 총 4개이다.
 
하지만, 실제로 전체 트리에서 3번 노드보다 왼쪽에 있는 노드는 위의 그림에서 파란색 원으로 감싸고 있는 만큼이다.
총 14개가 존재한다.
 
즉, 서브트리의 개수만 구하게 되면 특정 노드에선 반례가 존재할 수 있다는 것이다.
그러므로, 서브트리의 개수와 함께 고려해야 하는 것이 하나 있다.
 
바로, 부모 노드의 위치이다.
 
특정 노드의 왼쪽 서브트리는 해당 노드보다 왼쪽에 위치하며, 오른쪽 서브트리는 해당 노드보다 오른쪽에 위치한다.
노드가 없이 비어있는 열은 없다.
 
위의 조건들을 깊게 생각해보자.
왼쪽 서브트리의 모든 노드는 나보다 왼쪽에 위치하고, 오른쪽 서브트리의 모든 노드는 나보다 오른쪽에 위치한다.
또한, 그 사이에 공백이 존재하지도 않는다.
 
그렇다면, 나의 위치는 왼쪽 서브트리중 가장 오른쪽에 있는 노드보다 1만큼 크며, 오른쪽 서브트리중 가장 왼쪽에 있는 노드보다 1만큼 작다고 할 수 있을 것이다. 
 
이를 바꿔말하면, 특정 노드에서 왼쪽 서브트리중 가장 왼쪽에 있는 노드는 특정 노드의 부모노드의 위치 + 1 이라고도 표현할 수 있을 것이다.

 
그림에서 알 수 있듯이, 3번 노드를 기준으로 보았을 때 왼쪽 서브트리중 가장 왼쪽에 있는 노드(16번 노드)는 3번 노드의 부모노드 (1번 노드)보다 1만큼 큰 위치값을 가지고 있다.
 
3번 노드에서 16번 위치 값을 빼면, 나오는 값은 3번 노드와 16번 노드 사이에 있는 노드 개수 + 1이 나올 것이다.
=> 3번노드 위치 - 16번노드 위치 = 3번과 16번 사이 노드의 개수 + 1 = 왼쪽 서브트리의 노드 개수
 
앞에서, 16번 노드의 위치는 1번 노드의 위치 + 1이라고 하였으므로, 식을 다시 아래와 같이 정리할 수 있을 것이다.
=> 3번 노드 위치 - 1번 노드의 위치 - 1 = 왼쪽 서브트리의 노드 개수
=> 3번 노드 위치 = 1번 노드의 위치 + 왼쪽 서브트리의 노드 개수 + 1
 
그렇다면, 모든 노드에 대해 부모 노드의 위치 + 왼쪽 서브트리의 노드 개수 + 1이라는 공식을 사용해도 될까?
역시나 안된다.
 
위의 그림에서 2번 노드를 보자.
2번 노드의 부모노드의 위치는 10이며, 왼쪽 서브트리의 노드 개수는 2개이다.
그렇다면 2번 노드의 위치가 13이어야 하는데, 실제로는 3이다.
 
그러므로, 2번 노드의 경우엔 다른 방법으로 위치를 구해야 한다.
어렵지 않다. 위에서 생각했던 것과 동일한 논리로 생각해보면, 부모의 위치 - 우측 서브트리의 노드 개수 - 1이 나의 위치가 된다.
 
이를 정리해보자.
 
내가 부모 노드의 왼쪽에 있다면, 나의 위치는 부모의 위치 - 우측 서브트리의 노드 개수 - 1 이다.
내가 부모 노드의 오른쪽에 있다면, 나의 위치는 부모의 위치 + 좌측 서브트리의 노드 개수 + 1 이다.
 
이 공식을 이용하여, 모든 노드의 위치를 구할 수 있고, 모든 노드의 위치를 구했다면 모든 레벨에 대해 너비의 최소, 최대 또한 구할 수 있게 된다.
 

코드 풀이

struct Node
{
    int Level = 0;
    int Position = 0;
    
    int Parent = -1;
    int LeftChild = 0;
    int RightChild = 0;
};

 
먼저, 노드의 구조체를 선언하였다.
노드에는 레벨과 위치, 부모의 인덱스, 왼쪽 자식의 인덱스, 오른쪽 자식의 인덱스 정보가 담겨있다.
 
부모의 인덱스는 처음에 -1로 초기화해주었는데, 이는 루트노드를 구별하기 위해서이다.
뒤에서 모든 자식 노드의 부모노드를 갱신해줄 것인데, 루트노드의 경우엔 부모노드가 없으므로 값이 갱신되지 않아 -1의 값을 보유하게 된다. 이 값으로 루트노드를 판별할 것이다.
 

int NumNode = 0;
std::cin >> NumNode;

std::vector<Node> Nodes(NumNode + 1);
for (int i = 1; i <= NumNode; i++)
{
    int Index = 0;
    int LeftChild = 0;
    int RightChild = 0;

    std::cin >> Index >> LeftChild >> RightChild;

    Nodes[Index].LeftChild = LeftChild;
    if (LeftChild != -1)
    {
        Nodes[LeftChild].Parent = Index;
    }

    Nodes[Index].RightChild = RightChild;
    if (RightChild != -1)
    {
        Nodes[RightChild].Parent = Index;
    }
}

이는 입력받는 코드이다.
입력받은 대로, 왼쪽 자식과 오른쪽 자식을 저장해주었으며, 각 자식 노드의 부모를 현재 노드의 인덱스로 갱신해주었다.
 

for (int i = 1; i <= NumNode; i++)
{
    Nodes[i].Position = GetPosition(Nodes, i);
}

이후, GetPosition이라는 함수를 통해, 모든 노드의 position을 갱신해주었다.
GetPosition 함수 내부를 보자.
 

int GetPosition(std::vector<Node>& _Nodes, int _Index)
{
    int Parent = _Nodes[_Index].Parent;

    if(_Nodes[_Index].Position != 0)
    {
        return _Nodes[_Index].Position;
    }

    //내가 오른쪽 자식일 때.
    if (Parent != -1 && _Nodes[Parent].RightChild == _Index)
    {
        _Nodes[_Index].Position = GetPosition(_Nodes, Parent) + GetLeftSize(_Nodes, _Index) + 1;
        return _Nodes[_Index].Position;
    }

    //내가 왼쪽 자식일 때
    if (Parent != -1 && _Nodes[Parent].LeftChild == _Index)
    {
        _Nodes[_Index].Position = GetPosition(_Nodes, Parent) - GetRightSize(_Nodes, _Index) - 1;
        return _Nodes[_Index].Position;
    }

    //루트노드면?
    _Nodes[_Index].Position = GetLeftSize(_Nodes, _Index) + 1;
    return GetLeftSize(_Nodes, _Index) + 1;
}

위에서 설명했던 것을 동일하게 구현하였다.
 
GetLeftSize와 GetRightSize 함수 내부 코드는 뒤에서 보도록 하고, 의미만 먼저 알아보도록 하다.
GetLeftSize는 왼쪽 서브트리의 노드 개수이며, GetRightSize는 오른쪽 서브트리의 노드 개수이다.
 
현재, 내가 부모 노드의 오른쪽 자식이라고 한다면, 나의 위치는 부모 노드의 위치 + 왼쪽 서브트리의 개수  + 1이 된다.
반면 내가 부모 노드의 왼쪽 자식이라고 한다면, 나의 위치는 부모 노드의 위치 + 우측 서브트리의 개수  - 1이 된다.
 
이 공식을 그대로 적용하였다.
 
반면, 루트노드의 경우 부모노드가 없으므로 좌측 서브트리의 개수 + 1만 해주면 된다.

int GetLeftSize(std::vector<Node>& _Nodes, int _Index)
{
    if (_Nodes[_Index].LeftChild == -1)
    {
        return 0;
    }

    int LeftLeftSize = GetLeftSize(_Nodes, _Nodes[_Index].LeftChild);
    int LeftRightSize = GetRightSize(_Nodes, _Nodes[_Index].LeftChild);

    return LeftLeftSize + LeftRightSize + 1;
}

int GetRightSize(std::vector<Node>& _Nodes, int _Index)
{
    if (_Nodes[_Index].RightChild == -1)
    {
        return 0;
    }

    int LeftLeftSize = GetLeftSize(_Nodes, _Nodes[_Index].RightChild);
    int LeftRightSize = GetRightSize(_Nodes, _Nodes[_Index].RightChild);

    return LeftLeftSize + LeftRightSize + 1;
}

위 코드는 왼쪽 서브트리의 노드 개수를 구하는 함수와 오른쪽 서브트리의 노드 개수를 구하는 함수이다.
각 자식에 대해 재귀적으로 함수를 호출하여 노드 개수를 구하고 있다.
 
DFS방식과 동일하다고 보면 된다.
특정 노드의 서브트리의 노드 개수는 서브트리의 루트노드의 왼쪽 서브트리 노드 개수 + 오른쪽 서브트리 노드 개수 + 1 이기 때문에, 이를 이용하여 노드 개수를 구하였다. (1을 더하는 이유는 서브트리의 루트노드도 포함해야 하기 때문)
 

std::vector<std::pair<int, int>> MinMaxWidth(NumNode + 1, { INT_MAX, INT_MIN });

int RootNodeIndex = GetRootNode(Nodes, 1);
Nodes[RootNodeIndex].Level = 1;

std::queue<int> BFS;
BFS.push(RootNodeIndex);

모든 노드의 위치를 구하였다면, 이젠 너비를 구할 차례이다.
 
먼저, 너비를 구하기 위해 MinMaxWidth라는 자료구조를 만들었다.
해당 자료구조는 특정 레벨에서 가장 왼쪽에 있는 위치 값과 가장 오른쪽에 있는 위치 값을 저장할 벡터이다.
이 벡터의 인덱스는 레벨을 의미한다.
 
그리고, 레벨이 1인 루트 노드부터 시작해야 하기 때문에 루트노드를 구해서 루드노드의 level을 1로 초기화해주고 BFS를 시작하도록 하였다.
 

int GetRootNode(std::vector<Node>& _Nodes, int _Index)
{
    if (_Nodes[_Index].Parent == -1)
    {
        return _Index;
    }

    return GetRootNode(_Nodes, _Nodes[_Index].Parent);
}

참고로 루트노드를 구하는 함수는 위와 같다. 아무 노드나 하나 잡아서 부모를 타고 계속 올라가면 된다.
부모가 -1인 노드가 루트노드이므로 해당 인덱스를 반환하면 된다.
 

while (BFS.size() > 0)
{
    int CurIndex = BFS.front();
    BFS.pop();

    int Parent = Nodes[CurIndex].Parent;
    if (Parent != -1)
    {
        Nodes[CurIndex].Level = Nodes[Parent].Level + 1;
    }

    int CurLevel = Nodes[CurIndex].Level;

    MinMaxWidth[CurLevel].first = std::min(Nodes[CurIndex].Position, MinMaxWidth[CurLevel].first);
    MinMaxWidth[CurLevel].second = std::max(Nodes[CurIndex].Position, MinMaxWidth[CurLevel].second);

    if (Nodes[CurIndex].LeftChild != -1)
    {
        BFS.push(Nodes[CurIndex].LeftChild);
    }

    if (Nodes[CurIndex].RightChild != -1)
    {
        BFS.push(Nodes[CurIndex].RightChild);
    }
}

위 코드는 BFS의 구현 코드이다.
노드의 Level을 부모 노드의 Level + 1로 갱신해준 뒤, 노드의 위치값을 이용해 MinMaxWidth의 값을 갱신해주었다.
왼쪽 자식과 오른쪽 자식을 계속 queue에 삽입해주며 완전탐색을 돌려주었다.
 

//레벨, 길이
std::pair<int, int> Answer;

for (int i = 1; i <= NumNode; i++)
{
    int Width = MinMaxWidth[i].second - MinMaxWidth[i].first + 1;

    if (Width > Answer.second)
    {
        Answer.first = i;
        Answer.second = Width;
    }
}

MinMaxWidth가 모두 갱신되었으니, 이제 MinMaxWidth에 저장된 값을 이용해 Width의 최대값을 구할 차례이다.
MinMaxWidth의 값은 특정 레벨의 가장 왼쪽 위치값과 가장 오른쪽 위치값이므로 second - first + 1이 너비가 된다.
 
너비의 최대값을 구해주었다.
 

std::cout << Answer.first << " " << Answer.second;

return 0;

마지막으로 출력해주면 된다.
 

 

코드 전문

더보기
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <algorithm>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}

struct Node
{
    int Level = 0;
    int Position = 0;
    
    int Parent = -1;
    int LeftChild = 0;
    int RightChild = 0;
};

//전방선언
int GetRightSize(std::vector<Node>& _Nodes, int _Index);

//구현부
int GetLeftSize(std::vector<Node>& _Nodes, int _Index)
{
    if (_Nodes[_Index].LeftChild == -1)
    {
        return 0;
    }

    int LeftLeftSize = GetLeftSize(_Nodes, _Nodes[_Index].LeftChild);
    int LeftRightSize = GetRightSize(_Nodes, _Nodes[_Index].LeftChild);

    return LeftLeftSize + LeftRightSize + 1;
}

int GetRightSize(std::vector<Node>& _Nodes, int _Index)
{
    if (_Nodes[_Index].RightChild == -1)
    {
        return 0;
    }

    int LeftLeftSize = GetLeftSize(_Nodes, _Nodes[_Index].RightChild);
    int LeftRightSize = GetRightSize(_Nodes, _Nodes[_Index].RightChild);

    return LeftLeftSize + LeftRightSize + 1;
}

int GetPosition(std::vector<Node>& _Nodes, int _Index)
{
    int Parent = _Nodes[_Index].Parent;

    if(_Nodes[_Index].Position != 0)
    {
        return _Nodes[_Index].Position;
    }

    //내가 오른쪽 자식일 때.
    if (Parent != -1 && _Nodes[Parent].RightChild == _Index)
    {
        _Nodes[_Index].Position = GetPosition(_Nodes, Parent) + GetLeftSize(_Nodes, _Index) + 1;
        return _Nodes[_Index].Position;
    }

    //내가 왼쪽 자식일 때
    if (Parent != -1 && _Nodes[Parent].LeftChild == _Index)
    {
        _Nodes[_Index].Position = GetPosition(_Nodes, Parent) - GetRightSize(_Nodes, _Index) - 1;
        return _Nodes[_Index].Position;
    }

    //루트노드면?
    _Nodes[_Index].Position = GetLeftSize(_Nodes, _Index) + 1;
    return GetLeftSize(_Nodes, _Index) + 1;
}

int GetRootNode(std::vector<Node>& _Nodes, int _Index)
{
    if (_Nodes[_Index].Parent == -1)
    {
        return _Index;
    }

    return GetRootNode(_Nodes, _Nodes[_Index].Parent);
}

int main()
{
    Init();

    int NumNode = 0;
    std::cin >> NumNode;

    std::vector<Node> Nodes(NumNode + 1);
    for (int i = 1; i <= NumNode; i++)
    {
        int Index = 0;
        int LeftChild = 0;
        int RightChild = 0;

        std::cin >> Index >> LeftChild >> RightChild;

        Nodes[Index].LeftChild = LeftChild;
        if (LeftChild != -1)
        {
            Nodes[LeftChild].Parent = Index;
        }

        Nodes[Index].RightChild = RightChild;
        if (RightChild != -1)
        {
            Nodes[RightChild].Parent = Index;
        }
    }

    for (int i = 1; i <= NumNode; i++)
    {
        Nodes[i].Position = GetPosition(Nodes, i);
    }
    
    //특정 레벨의 가장 왼쪽 위치값, 가장 오른쪽 위치값
    std::vector<std::pair<int, int>> MinMaxWidth(NumNode + 1, { INT_MAX, INT_MIN });
    
    int RootNodeIndex = GetRootNode(Nodes, 1);
    Nodes[RootNodeIndex].Level = 1;

    std::queue<int> BFS;
    BFS.push(RootNodeIndex);

    while (BFS.size() > 0)
    {
        int CurIndex = BFS.front();
        BFS.pop();

        int Parent = Nodes[CurIndex].Parent;
        if (Parent != -1)
        {
            Nodes[CurIndex].Level = Nodes[Parent].Level + 1;
        }

        int CurLevel = Nodes[CurIndex].Level;

        MinMaxWidth[CurLevel].first = std::min(Nodes[CurIndex].Position, MinMaxWidth[CurLevel].first);
        MinMaxWidth[CurLevel].second = std::max(Nodes[CurIndex].Position, MinMaxWidth[CurLevel].second);

        if (Nodes[CurIndex].LeftChild != -1)
        {
            BFS.push(Nodes[CurIndex].LeftChild);
        }

        if (Nodes[CurIndex].RightChild != -1)
        {
            BFS.push(Nodes[CurIndex].RightChild);
        }
    }

    //레벨, 길이
    std::pair<int, int> Answer;

    for (int i = 1; i <= NumNode; i++)
    {
        int Width = MinMaxWidth[i].second - MinMaxWidth[i].first + 1;

        if (Width > Answer.second)
        {
            Answer.first = i;
            Answer.second = Width;
        }
    }

    std::cout << Answer.first << " " << Answer.second;

    return 0;
}

'코딩테스트 문제 풀이 (C++)' 카테고리의 다른 글

백준 14567 - 선수 과목 (C++)  (1) 2024.06.01
백준 2638 - 치즈 (C++)  (0) 2024.05.31
백준 2294 - 동전 2 (C++)  (0) 2024.05.26
백준 2293 - 동전 1 (C++)  (2) 2024.05.26
백준 1106 - 호텔 (C++)  (0) 2024.05.25

 

 

n개의 동전이 주어졌을 때, n개의 동전으로 k원을 만들 수 있는 경우의 수 중 사용한 동전의 수가 최소인 경우를 구하면 된다.

 

예를 들어, 1원과 5원 동전이 주어진 경우 10원을 만들기 위해 1원을 10개 사용할 수도 있지만 5원을 2개만 사용하여 10원을 만들 수도 있다. 이 때는 2를 출력하면 된다.

 

문제 풀이

해당 문제는 다이나믹 프로그래밍 문제이다.

다이나믹 프로그래밍을 구현하기 위해선, DP배열과 점화식이 필요하다.

 

먼저, DP배열을 만들어보자. 해당 문제는 k원을 만드는데 필요한 동전의 최소 개수를 구하는 문제이다.

즉, DP배열의 k번째 인덱스는 k원을 만들기 위해 필요한 동전의 최소 개수를 저장하는 배열로 하면 좋을 듯 하다.

 

이젠, 점화식을 구성해보자.

 

DP[k] 의 k원을 만들기 위해 필요한 동전의 최소 개수이다.

먼저, (1, 2, 5) 이렇게 3가지 동전이 주어졌다고 해보자.

 

1개의 1원을 추가로 사용해서 k원을 만들고자 한다면 (k - 1)원에 1원을 더해야 할 것이다.

이 때, (k - 1)원을 만드는 경우에 1원을 더해서 k원을 만드는 경우를 구할 수 있다.

 

예를 들어, k가 10이라고 해보자.

그렇다면, 1원을 하나 더해서 10원을 만드는 경우의 수는 9원을 만드는 경우의 수와 같다고 할  수 있다.

9원을 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 로 만들었다면, 그 뒤에 + 1 만 추가해주면 되기 때문이다.

만약, 2 + 2 + 2 + 2 + 1 의 조합으로 9를 만들었다면, 똑같이 뒤에 + 1을 추가해 2 + 2 + 2 + 2 + 1 + 1 의 조합으로 10을 만들 수 있다.

 

그런데, 우리가 구하고자 하는 것은 10원을 만들기 위해 최소로 필요한 동전의 개수이다.

그렇다면, 9원을 만들기 위해 필요한 최소 동전의 개수 + 1을 하면 10원을 만드는데 필요한 최소 동전의 개수를 구할 수 있지 않을까?

 

즉, i원을 하나 더 사용해서 k원을 만드는 경우중 동전의 개수가 최소인 경우는 (k - 1)원을 만드는데 필요한 최소 동전의 개수 + 1인 것이다.

 

하지만, 우리는 동전의 개수가 1가지가 아니다.

2원도 있고 5원도 있다.

 

그러므로, (k - 2)원을 만드는데 필요한 최소 동전의 개수 + 1도 고려해야 하고 (k - 5)원을 만드는데 필요한 최소 동전의 개수도 고려해야 한다.

 

이를 점화식으로 세워보자.

 

주어진 동전이 (A1, A2, A3....An) 이라고 한다면

DP[K] = std::min(DP[K - A1], DP[K - A2], DP[K - A3] ..... , DP[K -An])이 되는 것이다.

이를 코드로 구현하면 답을 구할 수 있게 된다.

 

풀이 코드

int NumCoin = 0;
int TargetMoney = 0;
std::cin >> NumCoin >> TargetMoney;

std::set<int> Coins;
for (int i = 0; i < NumCoin; i++)
{
    int CurCoin = 0;
    std::cin >> CurCoin;

    Coins.insert(CurCoin);
}

입력을 받아주었다.

그런데 보면 동전을 vector에 저장하는 것이 아니라 set에 저장하고 있다.

그 이유는 문제의 조건중에 같은 가치의 동전이 주어질 수도 있다는 이유 때문이다.

 

이 문제는 2원짜리 동전이 하나만 주어져도 그 동전을 여러 번 사용할 수 있는 문제이다.

그런데 자료구조에 2원짜리 동전을 여러개 가지고 있다고 해서 결과가 달라질까?

 

2원짜리 동전이 자료구조에 1개만 있어도 100번 사용할 수 있기 때문에, 동전이 여러개 있는 것은 연산 낭비라고 생각했다. 그래서 set에 저장하여 중복을 제거하였다.

 

std::vector<int> DP(TargetMoney + 1, INT_MAX / 2);
DP[0] = 0;

std::set<int>::iterator CurIter = Coins.begin();
std::set<int>::iterator EndIter = Coins.end();

while (CurIter != EndIter)
{
    int CurCoin = *CurIter;

    for (int j = CurCoin; j <= TargetMoney; j++)
    {
        DP[j] = std::min(DP[j], DP[j - CurCoin] + 1);
    }

    CurIter++;
}

그리고, DP배열을 만들어서 위의 점화식을 코드로 구현해주었다.

모든 동전에 대해 j원을 만드는데 필요한 최소 동전 갯수를 구한 뒤, DP배열을 최소값으로 갱신해주었다.

 

이 때, DP배열은 INT_MAX가 아닌 INT_MAX / 2 로 초기화해주었는데, 그 이유는 DP[j - CurCoin] + 1 부분에서 오버플로우가 발생할 수 있기 때문이다.

 

if (DP[TargetMoney] == INT_MAX / 2)
{
    std::cout << -1;
}
else
{
    std::cout << DP[TargetMoney];
}

return 0;

마지막으로 답을 출력해주면 된다.

 

 

코드 전문

더보기
#include <iostream>
#include <vector>
#include <set>
#include <climits>

void Init()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
}

int main()
{
    Init();

    int NumCoin = 0;
    int TargetMoney = 0;
    std::cin >> NumCoin >> TargetMoney;

    std::set<int> Coins;
    for (int i = 0; i < NumCoin; i++)
    {
        int CurCoin = 0;
        std::cin >> CurCoin;

        Coins.insert(CurCoin);
    }

    std::vector<int> DP(TargetMoney + 1, INT_MAX / 2);
    DP[0] = 0;

    std::set<int>::iterator CurIter = Coins.begin();
    std::set<int>::iterator EndIter = Coins.end();

    while (CurIter != EndIter)
    {
        int CurCoin = *CurIter;

        for (int j = CurCoin; j <= TargetMoney; j++)
        {
            DP[j] = std::min(DP[j], DP[j - CurCoin] + 1);
        }

        CurIter++;
    }

    if (DP[TargetMoney] == INT_MAX / 2)
    {
        std::cout << -1;
    }
    else
    {
    	std::cout << DP[TargetMoney];
    }

    return 0;
}

+ Recent posts