동전이 여러 종류가 주어졌을 때, 각 동전을 사용해서 K원을 만드는 경우의 수를 구하면 된다.

 

주의할 점은 1 + 1 + 2 로 4원을 만드는 것과 2 + 1 + 1 로 4원을 만드는 것은 같은 경우로 친다.

(순서만 바뀌는 것은 같은 경우이다.)

 

문제 풀이

이 문제는 다이나믹 프로그래밍으로 해결할 수 있는 문제이다.

코드는 간단하지만, 아이디어를 구상하는 것이 다소 어려울 수 있다.

 

먼저, 입력으로 주어진 동전은 (1, 2, 5) 3개이며, K는 10이라고 가정해보자.

 

먼저, 다이나믹 프로그래밍은 무엇을 기준으로 DP배열을 만드느냐가 첫 번째 관문이다.

해당 문제에선 우리가 찾고자 하는 것이 k원을 만드는 대한 경우의 수이다.

그러므로, DP배열의 i번 인덱스에는 i원을 만드는 경우의 수를 저장할 것이다.

 

Ex) DP[5] 는 주어진 동전을 사용해 5원을 만드는 경우의 수

 

그렇다면, 이제 점화식을 세워보자.

 

10원을 만드는 경우의 수를 생각해보자.

 

만약, 1원부터 9원까지 만들 수 있는 경우의 수가 모두 구해진 상태라고 가정을 해보자.

이 때, 우리에게 주어진 동전은 (1, 2, 5) 총 3가지가 있다.

 

그렇다면, 1원을 더해서 10원을 만드는 경우를 생각해보자.

1원을 더해서 10원이 되려면 9원에 1원을 더해야 한다.

 

즉, 1원을 더해서 10원을 만드는 경우의 수는 9원을 만드는 경우의 수라고 할 수 있지 않을까?

 

만약, 2 + 2 + 2 + 1 이라는 조합으로 9를 만들었다고 해보자.

 

여기의 끝에 1을 더해서, 2 + 2 + 2 + 1 + 1 을 하면 10이 된다.

즉, 9에 1을 더해서 10을 만드는 경우의 수는 결국 9를 만드는 경우의 수가 된다.

 

똑같이 2원을 더해서 10원을 만드는 경우의 수는 8원을 만드는 경우의 수가 되고, 5원을 더해서 10원을 만드는 경우의 수는 5원을 만드는 경우의 수가 된다.

 

주어진 입력에 대해 점화식을 세워보면, DP[10] = DP[9] + DP[8] + DP[5] 라고 할 수 있다.

DP[9]도 동일한 방식으로 DP[8] + DP[7] + DP[4]로 표현할 수 있으며 DP[8]과 DP[5]도 마찬가지의 논리가 적용된다.

 

즉 동전의 가치가 (A1, A2, A3 ....  An)이렇게 주어진다면, DP[K] = DP[K - A1] + DP[K - A2] ...... + DP[K - An] 이 된다.

이 점화식을 코드로 구현해보면 답을 구할 수 있다.

 

풀이 코드

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

std::vector<int> Coins(NumCoin);
std::vector<int> DP(TargetMoney + 1);

for (int i = 0; i < NumCoin; i++)
{
    std::cin >> Coins[i];
}

먼저, 입력을 모두 저장해주자.

 

DP[0] = 1;

for (int i = 0; i < NumCoin; i++)
{
    for (int j = Coins[i]; j <= TargetMoney; j++)
    {
        DP[j] += DP[j - Coins[i]];
    }
}

다음은 DP[0]을 1로 초기화해준 뒤, 반복문을 돌려주었다.

0원을 만드는 경우도 1가지 있으니, DP[0] = 1로 초기화 해주었다.

 

이제, 각 동전에 대해 마지막으로 동전을 더해서 j원을 만드는 경우를 계산해 주었다.

동전의 가치보다 작은 금액은 만들 수 없으므로 j는 Coins[i]부터 시작하도록 하였다.

 

Coins[i] 가 2원인 경우, 마지막으로 2원을 더해서 2원을 만드는 경우, 3원을 만드는 경우, 4원을 만드는 경우 ....... 10원을 만드는 경우까지 갱신해주는 것이다.

std::cout << DP[TargetMoney];

return 0;

모두 갱신한 뒤 답을 출력해주면 된다.

 

 

코드 전문

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

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::vector<int> Coins(NumCoin);
    std::vector<int> DP(TargetMoney + 1);

    for (int i = 0; i < NumCoin; i++)
    {
        std::cin >> Coins[i];
    }

    DP[0] = 1;

    for (int i = 0; i < NumCoin; i++)
    {
        for (int j = Coins[i]; j <= TargetMoney; j++)
        {
            DP[j] += DP[j - Coins[i]];
        }
    }

    std::cout << DP[TargetMoney];

    return 0;
}

 

 

도시별로 홍보 비용과 기대 효과가 주어진다.

 

A도시에서 5원을 사용하면, 3명의 고객이 확보된다는 정보가 있고

B도시에서 3원을 사용하면, 1명의 고객이 확보된다는 정보가 있다고 해보자. (정보는 확실하다고 가정한다.)

 

같은 도시의 홍보를 여러 번 할 수 있기 때문에, A도시에서 10원을 사용한다면 6명의 고객을 확보할 수 있다.

 

이 때, 내가 12명의 고객을 늘리고 싶다면, 어떻게 홍보를 선택해야 최소 비용으로 12명 이상의 고객을 늘릴 수 있을까?

 

문제 풀이

다이나믹 프로그래밍을 활용하면, 문제를 해결할 수 있다.

DP배열을 만든 뒤, i번 인덱스에 대해 i원을 사용했을 때 확보할 수 있는 고객의 최대 수를 저장할 것이다.

 

이 때, 내가 모으고 싶은 고객의 수가 12명이라고 한다면 DP배열에서 가장 먼저 12 이상의 수가 나오는 인덱스를 답으로 반환하면 된다.

 

12 2
3 5
1 1

 

위의 입력에 대해 과정을 알아보자.

위의 표는 DP 배열이다. 인덱스는 사용할 가격이며, 원소의 값은 해당 가격을 사용했을 때 확보할 수 있는 고객의 최대 수이다.

 

먼저, 첫번째 입력인 3, 5에 대해 갱신을 해보자.

 

가격은 최소가 3원 단위이기 때문에, (3, 5)의 홍보를 이용해선 1원과 2원을 사용해서는 고객을 확보할 수 없다.

즉, 1원과 2원에 대해서는 위와 같이 갱신될 것이다.

 

다음 3원에 대해서 갱신해보자. 3원의 경우엔 5명의 고객을 확보할 수 있을 것이다.

4원과 5원을 사용하더라도, 역시나 3원 단위로밖에 돈을 사용할 수 없으므로 3원을 사용했을 때와 동일하게 5명을 확보할 수 있다.

위의 표와 같이 값을 갱신할 수 있다.

동일하게 반복하면 표는 아래와 같아진다.

(3, 5)의 광고에 대해서는 이렇게 갱신할 수 있다.

 

이제 다음 입력인 (1, 1)의 광고를 기준으로 배열의 값을 한 번 더 갱신해보자.

먼저, 1원과 2원에 대해서는 기존에 0이었던 값을 1과 2로 갱신할 수 있다.

이번 광고는 가격이 1원단위이기 때문이다.

 

그런데, 3원을 보자. 현재 광고를 기준으로 하면 3원을 사용했을 때 3명밖에 확보하지 못한다.

하지만, DP배열에 저장된 값은 5이다. 그러므로 3원에 대해서는 갱신할 수 없다.

3원에 대해서는 가격이 갱신되지 않고 고정될 것이다.

반면, 4원 5원을 보자.

 

4원의 경우, 3원으로 5명을 확보한 뒤 1원으로 1명을 더 확보할 수 있다.

5원의 경우에도, 3원으로 5명을 확보한 뒤 2원으로 2명을 더 확보할 수 있다.

 

현재 광고의 가격을 Cost, 확보할 수 있는 고객의 수를 man이라고 한다면 

D(N) = D(N - Cost) + Man 이라는 점화식을 세울 수 있다.

 

현재 광고를 사용한다고 가정했을 때 최대값은 당연히 광고 가격을 뺀 가격에서 최대한으로 확보할 수 있는 고객의 수에 현재 광고를 통해 확보할 수 있는 고객의 수를 더하는 것이 최대 기대값일 것이다.

 

하지만, 그 값이 기존 값보다 항상 크다고 할 수는 없으므로 D(N) = std::max(D(N), D(N - Cost) + Man)이 된다.

 

이 점화식을 통해 배열을 갱신하면, 아래와 같이 갱신된다.

즉, 12명을 확보하기 위해서 최소값은 12 이상의 값이 처음으로 나오는 인덱스의 값인 8이 된다.

 

코드 풀이

int TargetValue = 0;
int NumCity = 0;

std::cin >> TargetValue >> NumCity;

std::vector<std::pair<int, int>> ADs(NumCity);
for (int i = 0; i < NumCity; i++)
{
    std::cin >> ADs[i].first;
    std::cin >> ADs[i].second;
}

먼저, 입력을 모두 저장해준다.

std::vector<int> DP(100001);
for (int i = 0; i < NumCity; i++)
{
    for (int j = 1; j <= 100000; j++)
    {
        int Cost = ADs[i].first;
        int Mans = ADs[i].second;

        if (j - Cost >= 0)
        {
            DP[j] = std::max(DP[j], DP[j - Cost] + Mans);
        }
    }
}

위에서 말했던 점화식 그대로 계속 배열을 갱신해주면 된다.

모든 광고에 대해 갱신을 계속하였다.

 

여기서, DP을 왜 100001로 resize하였나 의문이 들 수 있다.

입력조건 범위를 보면, 확보하고 싶은 고객의 최대 수는 1000명이고, 광고의 최대 비용은 100원이다.

 

이 조건으로 인해, 100원을 들여서 1명을 확보하는 경우가 가장 비싼 경우일 것이다.

이 경우에 1000명을 확보하기 위해선, 10만원이 필요하기 때문에 DP를 가능한 최대 가격인 100000으로 갱신해주었다.
(100000 + 1 인 이유는 인덱스를 직관적으로 사용하기 위해)

 

물론, 입력받은 값을 기준으로 1인당 비용의 최대값을 구한 뒤, 그 값을 기준으로 DP를 갱신할 수도 있지만 실제로 프로그램을 만드는 것이 아니니 이런 사소한 부분은 넘어가고 최대한 편하게 구현하는 것이 좋다고 생각한다.

 

int Answer = std::lower_bound(DP.begin(), DP.end(), TargetValue) - DP.begin();
std::cout << Answer;

return 0;

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

 

lower_bound는 해당 값보다 크거나 같은 원소 중 가장 앞에 있는 원소의 이터레이터를 반환해준다.

이터레이터의 포인터 연산으로 인해, Lower_bound가 반환해주는 값 - begin을 하면 해당 원소의 인덱스를 구할 수 있다.

 

그렇게 구한 인덱스를 답으로 출력해주면 된다.

 

코드 전문

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

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

int main()
{
    Init();

    int TargetValue = 0;
    int NumCity = 0;

    std::cin >> TargetValue >> NumCity;

    std::vector<std::pair<int, int>> ADs(NumCity);
    for (int i = 0; i < NumCity; i++)
    {
        std::cin >> ADs[i].first;
        std::cin >> ADs[i].second;
    }

    std::vector<int> DP(100001);
    for (int i = 0; i < NumCity; i++)
    {
        for (int j = 1; j <= 100000; j++)
        {
            int Cost = ADs[i].first;
            int Mans = ADs[i].second;

            if (j - Cost >= 0)
            {
                DP[j] = std::max(DP[j], DP[j - Cost] + Mans);
            }
        }
    }

    int Answer = std::lower_bound(DP.begin(), DP.end(), TargetValue) - DP.begin();
    std::cout << Answer;

    return 0;
}

 

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

백준 2294 - 동전 2 (C++)  (0) 2024.05.26
백준 2293 - 동전 1 (C++)  (2) 2024.05.26
백준 15486 - 퇴사 2 (C++)  (0) 2024.05.25
백준 2011 - 암호코드 (C++)  (0) 2024.05.25
백준 2615 - 오목 (C++)  (0) 2024.05.22

 

 

백준이가 퇴사하기 전까지 N일동안 최대의 이익을 얻는 것이 목표이다.

각 날짜별로 상담이 잡혀있으며, 각 상담별로 상이하게 소요 시간과 상담료가 설정되어있다.

이 때, 최고 효율로 상담을 선택하여 총 이익을 최대값을 구하면 된다.

 

문제 풀이

본인은 다이나믹 프로그래밍을 활용하여 문제를 해결하였다.

 

DP배열을 만들어서, 각 인덱스에는 인덱스에 해당하는 날짜까지 얻을 수 있는 총 이익의 최대값을 저장할 것이다.

이 때, 가장 기본적으로 성립하는 것은 당연히 DP[i] >= DP[i - 1]이라는 것이다.

 

3일동안 벌 수 있는 총 이익이 2일동안 벌 수 있는 총 이익과 같을 수는 있어도 더 적을 수는 없기 때문이다.

 

그러므로, DP[i] = std::max(DP[i], DP[i - 1])의 점화식이 먼저 성립하게 된다.

 

만약, 6일부터 10일까지 상담을 진행함으로써 7일 8일 9일 10일엔 상담을 새로 받을 수 없으므로 7~10일의 값은 DP[6]의 값을 그대로 가져갈 것이다.

 

DP[i] = std::max(DP[i], DP[i - 1])이라는 점화식은 특정 날짜에 상담을 새로 배정할 수 없다면, 현재 맡고 있는 상담의 시작날짜의 값을 그대로 가져가겠다는 의미로 해석할 수도 있다. (DP배열의 초기값은 0이기 때문에)

 

그리고, 한가지 점화식을 더 만들어야 한다.

 

i일에 시작해서 n일이 걸리는 상담을 맡았다고 해보자.

그렇다면, 해당 상담의 종료 일자는 (i + n - 1)일이 될 것이다.

((i + n - 1)일까지 상담이므로, (i + n - 1)의 상담을 맡을 수는 없음)

 

해당 상담의 상담료가 p원이라고 한다면, (i + n - 1)에는 p원의 이익이 추가될 것이다.

DP[i + n - 1] = DP[i  - 1] + p; 라고 할 수 있다.

(i일 전까지 얻었던 총 이익 + i일에 시작하는 상담의 상담료)

 

하지만, 다른 날짜에서 (i + n - 1)과 동일한 날에 종료하는 상담을 계산함으로써 DP[i + n - 1]이 이미 0이 아닌 값으로 갱신되어 있을 수도 있다.

 

예를 들면, 3일에 시작해서 2일이 걸리는 상담도 5일에 끝이나며 1일에 시작해서 5일이 걸리는 상담도 5일에 끝이난다.

이런 경우엔 두 상담의 이익을 비교해서 더 큰 쪽으로 값을 갱신해주어야 한다.

 

그러므로 DP[i + n - 1] = std::max(DP[i + n - 1], DP[i - 1] + p); 라는 점화식을 세울 수 있다.

 

1일차부터 퇴사하는 날까지 모든 날에 대해 위의 두개의 점화식으로 DP배열을 갱신하게 되면, DP배열의 마지막 원소가 정답이 된다.

 

코드 풀이

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

std::vector<std::pair<int, int>> Task(Days + 1);
for (int i = 1; i <= Days; i++)
{
    std::cin >> Task[i].first;
    std::cin >> Task[i].second;
}

먼저, 모든 입력을 받아서 저장해주었다.

std::vector<int> DP(Days + 1);
for (int i = 1; i <= Days; i++)
{
    int EndDay = Task[i].first + i - 1;

    if (EndDay <= Days)
    {
        DP[EndDay] = std::max(DP[EndDay], DP[i - 1] + Task[i].second);
    }

    DP[i] = std::max(DP[i], DP[i - 1]);
}

다음은 위에서 말했던 것과 같이 DP배열을 갱신해주었다.

 

std::cout << DP[Days];

return 0;

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

 

 

DP문제는 코드가 길거나 구현하기 까다로운 경우는 많지 않다.

대부분 아이디어를 구상하는 것이 어렵기 때문에, 문제를 자주 풀어보고 생각해보는 것이 중요한 것 같다.

 

코드 전문

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

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

int main()
{
    Init();
    
    int Days = 0;
    std::cin >> Days;

    std::vector<std::pair<int, int>> Task(Days + 1);
    for (int i = 1; i <= Days; i++)
    {
        std::cin >> Task[i].first;
        std::cin >> Task[i].second;
    }
    
    std::vector<int> DP(Days + 1);
    for (int i = 1; i <= Days; i++)
    {
        int EndDay = Task[i].first + i - 1;

        if (EndDay <= Days)
        {
            DP[EndDay] = std::max(DP[EndDay], DP[i - 1] + Task[i].second);
        }

        DP[i] = std::max(DP[i], DP[i - 1]);
    }

    std::cout << DP[Days];

    return 0;
}

 

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

백준 2293 - 동전 1 (C++)  (2) 2024.05.26
백준 1106 - 호텔 (C++)  (0) 2024.05.25
백준 2011 - 암호코드 (C++)  (0) 2024.05.25
백준 2615 - 오목 (C++)  (0) 2024.05.22
백준 2436 - 공약수 (C++)  (0) 2024.05.21

 

 

A = 1, B = 2, C = 3 ... Z = 26 등 알파벳을 각 순서와 동일한 숫자로 치환하여 새로운 숫자 문자열을 만든다.

해당 숫자 문자열을 다시 기존의 알파벳 문자열로 복호화하게 되면, 경우의 수가 하나가 아니라 여럿 존재할 수 있다.

 

예를 들어, 19의 경우 1 + 9 => AI 로 표현할 수도 있지만, 19 -> S 로 표현할 수도 있다.

이처럼, 숫자 문자열이 주어졌을 때 해석 가능한 알파벳 문자열의 경우의 수를 출력하면 된다.

 

주의할 점은 경우의 수가 매우 많으므로 1000000으로 나눠서 저장하도록 하자.

 

문제 풀이

DP를 이용하여 풀 수 있는 문제이다.

 

입력된 문자열을 순차적으로 탐색하며, 현재 가리키는 숫자가 1자리 숫자로 해석될 수 있는 경우2자리 숫자의 1의 자리로 해석될 수 있는 경우를 구해주면 된다.

 

이렇게 길이가 5인 문자열 "12345"가 주어졌다고 해보자.

"12345"의 부분 문자열인 "123"에 대해 경우를 생각해보자.

123은 위와 같이 3개의 경우를 가지며, 3개의 경우는 2개의 종류로 나누어 볼 수 있다.

1. 마지막 숫자가 한 자리수인 경우

2. 마지막 숫자가 두 자리수의 1의 자리인 경우

 

1번 경우를 살펴보자.

1번 경우는 마지막 3을 제외한 앞의 숫자들을 보면 규칙이 보인다.

12를 해석하는 경우의 수이다.

 

즉, 길이가 N인 문자열을 해석할 때, 마지막 숫자를 한자리 숫자라고 고려한다면

0 ~ (N - 1) 의 문자열을 해석하는 경우의 수와 동일하다는 것이다

 

2번 경우도 살펴보자.

2번 경우는 마지막 숫자가 두 자리수의 1의 자리라고 했기 때문에, 그 앞의 숫자는 두자리 숫자의 10의 자리가 된다.

1번 경우를 살필 때와 동일하지만, 이 번엔 두자리 숫자이므로 0 ~ (N - 2) 길이의 문자열을 해석하는 경우의 수와 동일하게 된다.

 

즉 DP(N)을 길이가 N인 문자열을 해석하는 경우의 수라고 한다면, DP(N) = DP(N - 1) + DP(N - 2)가 된다.

 

풀이 코드

std::string Encryption;
std::cin >> Encryption;

if (Encryption[0] == '0')
{
    std::cout << 0;
    return 0;
}

if (Encryption.size() == 1)
{
    std::cout << 1;
    return 0;
}

먼저 입력을 받아주었다.

이후, 맨 앞이 0으로 시작하거나 길이가 1인 경우에 대해 예외처리를 해주었다.

std::vector<int> DP(Encryption.size());
DP[0] = 1;

int FrontTwoNum = std::stoi(Encryption.substr(0, 2));
if (FrontTwoNum >= 10 && FrontTwoNum <= 26)
{
	DP[1] += 1;
}

if(Encryption[1] - '0' != 0) 
{
	DP[1] += 1;
}

다음은 DP배열을 선언해주었다.
DP[0]은 한자리 숫자로 해석되는 1가지 경우밖에 없기 때문에 1로 초기화할 수 있다.

DP[1]의 경우 두 가지를 고려할 수 있다.

 

12처럼 1 + 2, 12 이렇게 두가지로 해석되는 경우가 있고

27처럼 2 + 7로만 해석되는 경우가 있다.

 

그러므로, 앞자리 숫자와 현재숫자를 고려하여 두가지 경우로 해석될 수 있는 경우엔 DP[1]  = 2 로 초기화하였고, 아닌 경우엔 1로 초기화하였다.

for (int i = 2; i < Encryption.size(); i++)
{
    int CurNum = Encryption[i] - '0';
    int PrevNum = Encryption[i - 1] - '0';

    if ((PrevNum == 1 && CurNum <= 9) || (PrevNum == 2 && CurNum <=6))
    {
        DP[i] += DP[i - 2];
    }

    if (CurNum != 0)
    {
        DP[i] += DP[i - 1];
    }

    DP[i] %= 1000000;
}

그 다음은 반복문을 돌며 DP배열을 갱신해주었다.

 

만약, 현재 숫자와 앞의 숫자를 합한 두자리 숫자가 10~26 사이의 숫자로 해석될 수 있다면 DP[i -2]를 DP[i]에 더해주었으며, 1~9로 해석될 수 있는 경우에는 DP[i]를 더해주었다.

 

만약, 현재 숫자가 0이라면 한자리 숫자로는 해석될 수 없다.

 

경우의 수가 커지는 것을 방지하여 문제의 조건에 맞게 1000000으로 나누어서 저장해주자.

std::cout << DP[DP.size() - 1];

return 0;

 

DP의 마지막 원소를 출력해주면 문제 해결이다.

 

코드 전문

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

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

int main()
{
    Init();

    std::string Encryption;
    std::cin >> Encryption;

    if (Encryption[0] == '0')
    {
        std::cout << 0;
        return 0;
    }

    if (Encryption.size() == 1)
    {
        std::cout << 1;
        return 0;
    }

    std::vector<int> DP(Encryption.size());
    DP[0] = 1;

    int FrontTwoNum = std::stoi(Encryption.substr(0, 2));
    if (FrontTwoNum >= 10 && FrontTwoNum <= 26)
    {
        DP[1] += 1;
    }
    
    if(Encryption[1] - '0' != 0) 
    {
        DP[1] += 1;
    }

    for (int i = 2; i < Encryption.size(); i++)
    {
        int CurNum = Encryption[i] - '0';
        int PrevNum = Encryption[i - 1] - '0';

        if ((PrevNum == 1 && CurNum <= 9) || (PrevNum == 2 && CurNum <=6))
        {
            DP[i] += DP[i - 2];
        }

        if (CurNum != 0)
        {
            DP[i] += DP[i - 1];
        }

        DP[i] %= 1000000;
    }

    std::cout << DP[DP.size() - 1];

    return 0;
}

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

백준 1106 - 호텔 (C++)  (0) 2024.05.25
백준 15486 - 퇴사 2 (C++)  (0) 2024.05.25
백준 2615 - 오목 (C++)  (0) 2024.05.22
백준 2436 - 공약수 (C++)  (0) 2024.05.21
백준 16678 - 모독 (C++)  (0) 2024.05.19

 

현재, 오목에서 승리한 사람이 있는지 탐색하는 문제이다.

오목의 규칙 그대로, 5개의 연속된 바둑알을 놓은 사람이 있는지 판단하고 있다면 이긴 사람을 출력하고, 없다면 0을 출력하면 된다.

문제 풀이

바둑판 위에서 바둑알이 놓여있는 모든 위치에 대해,  5개의 바둑알이 연결되어 있는지를 검사하였다.

 

기본적으로는 이렇게, 8방향에 대해 오목이 5개가 연결되어 있는지를 검사해야 할 것이다.

 

하지만, 꼭 8방향을 다 검사할 필요는 없다.

 

왜냐하면, 모듬 점에 대해서 탐색을 진행하기 때문에, 기준점에서 오른쪽 아래로 검사하는 것은 다른 기준점에서 왼쪽 위를 검사하는 것과 같다. 

 

예를 들어, (1,1) 에서 오른쪽 아래 방향을 검사해서 오목이 나오지 않았다면, (1,1) (2,2) (3,3) (4,4) (5,5)는 연속으로 돌이 놓여있지 않은 상황이다.

 

이 때, 만약 (1,1) (2,2) (3,3)은 검은돌이고, (4,4)는 하얀돌이며 (5,5)는 검은 돌이라고 해보자.

이 때, (5,5)에서 왼쪽 위를 검사하는 것이 의미가 있을까?

 

(5,5)에서 왼쪽 위 방향으로 오목이 만들어져 있다면, 이미 (1,1)에서 탐지한 뒤 검사를 종료했을 것이다.

그러므로 모든 점을 기준으로 오른쪽 아래만 검사하면 된다.

 

동일한 이유로 모든 점에선 우측 상단, 우측, 우측 하단, 하단 이렇게 4방향에 대해서만 검사하면 된다.

 

이 4방향으로만 연속된 바둑알 5개가 놓여있는지 검사하였다.

 

풀이 코드

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

먼저 바둑판의 상태를 입력받았다.

 

for (int i = 0; i < 19; i++)
{
    for (int j = 0; j < 19; j++)
    {
        if (Board[i][j] == 0)
        {
            continue;
        }

        if (Check(Board, j, i) == true)
        {
            std::cout << Board[i][j] << "\n";
            std::cout << i + 1 << " " << j + 1;

            return 0;
        }
    }
}

std::cout << 0;

return 0;

이후, 모든 점에 대해 검사를 하였다. 

바둑알이 놓여있지 않은 점에 대해서는 continue해주었다.

 

만약, 해당 위치에서 오목이 발견되었다면, 해당 위치의 바둑알의 생상과 위치를 출력한 뒤 프로세스를 종료하였으며, 발견되지 않고 반복문들 탈출하였다면 0을 출력하도록 해주었다.

 

bool Check(const std::vector<std::vector<int>>& _Board, int _X, int _Y)
{
    int CurStone = _Board[_Y][_X];

    //우상
    if (!(_X - 1 >= 0 && _Y + 1 < 19 && _Board[_Y + 1][_X - 1] == CurStone))
    {
        int Count = 0;

        for (int i = 0; i < 19; i++)
        {
            int NextX = _X + i;
            int NextY = _Y - i;

            if (NextX >= 19 || NextY < 0)
            {
                break;
            }

            if (_Board[NextY][NextX] == CurStone)
            {
                Count++;
            }
            else
            {
                break;
            }
        }

        if (Count == 5)
        {
            return true;
        }
    }

    //우
    if (!(_X - 1 >= 0 && _Board[_Y][_X - 1] == CurStone))
    {
        int Count = 0;

        for (int i = 0; i < 19; i++)
        {
            int NextX = _X + i;

            if (NextX >= 19)
            {
                break;
            }

            if (_Board[_Y][NextX] == CurStone)
            {
                Count++;
            }
            else
            {
                break;
            }
        }

        if (Count == 5)
        {
            return true;
        }
    }

    //우하
    if (!(_X - 1 >= 0 && _Y - 1 >= 0 && _Board[_Y - 1][_X - 1] == CurStone))
    {
        int Count = 0;

        for (int i = 0; i < 19; i++)
        {
            int NextX = _X + i;
            int NextY = _Y + i;

            if (NextX >= 19 || NextY >= 19)
            {
                break;
            }

            if (_Board[NextY][NextX] == CurStone)
            {
                Count++;
            }
            else
            {
                break;
            }
        }

        if (Count == 5)
        {
            return true;
        }
    }

    //하
    if (!(_Y - 1 >= 0 && _Board[_Y - 1][_X] == CurStone))
    {
        int Count = 0;

        for (int i = 0; i < 19; i++)
        {
            int NextY = _Y + i;

            if (NextY >= 19)
            {
                break;
            }

            if (_Board[NextY][_X] == CurStone)
            {
                Count++;
            }
            else
            {
                break;
            }
        }

        if (Count == 5)
        {
            return true;
        }
    }

    return false;
}

 

Check함수 내부이다.

무식하게 4방향에 대해 모드 반복문을 돌아주었다.

 

코드 전문

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

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

bool Check(const std::vector<std::vector<int>>& _Board, int _X, int _Y)
{
    int CurStone = _Board[_Y][_X];

    //우상
    if (!(_X - 1 >= 0 && _Y + 1 < 19 && _Board[_Y + 1][_X - 1] == CurStone))
    {
        int Count = 0;

        for (int i = 0; i < 19; i++)
        {
            int NextX = _X + i;
            int NextY = _Y - i;

            if (NextX >= 19 || NextY < 0)
            {
                break;
            }

            if (_Board[NextY][NextX] == CurStone)
            {
                Count++;
            }
            else
            {
                break;
            }
        }

        if (Count == 5)
        {
            return true;
        }
    }

    //우
    if (!(_X - 1 >= 0 && _Board[_Y][_X - 1] == CurStone))
    {
        int Count = 0;

        for (int i = 0; i < 19; i++)
        {
            int NextX = _X + i;

            if (NextX >= 19)
            {
                break;
            }

            if (_Board[_Y][NextX] == CurStone)
            {
                Count++;
            }
            else
            {
                break;
            }
        }

        if (Count == 5)
        {
        return true;
        }
    }

    //우하
    if (!(_X - 1 >= 0 && _Y - 1 >= 0 && _Board[_Y - 1][_X - 1] == CurStone))
    {
        int Count = 0;

        for (int i = 0; i < 19; i++)
        {
            int NextX = _X + i;
            int NextY = _Y + i;

            if (NextX >= 19 || NextY >= 19)
            {
                break;
            }

            if (_Board[NextY][NextX] == CurStone)
            {
                Count++;
            }
            else
            {
                break;
            }
        }

        if (Count == 5)
        {
            return true;
        }
    }

    //하
    if (!(_Y - 1 >= 0 && _Board[_Y - 1][_X] == CurStone))
    {
        int Count = 0;

        for (int i = 0; i < 19; i++)
        {
            int NextY = _Y + i;

            if (NextY >= 19)
            {
                break;
            }

            if (_Board[NextY][_X] == CurStone)
            {
                Count++;
            }
            else
            {
                break;
            }
        }

        if (Count == 5)
        {
            return true;
        }
    }

    return false;
}

int main()
{
    Init();

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

    for (int i = 0; i < 19; i++)
    {
        for (int j = 0; j < 19; j++)
        {
            if (Board[i][j] == 0)
            {
                continue;
            }

            if (Check(Board, j, i) == true)
            {
                std::cout << Board[i][j] << "\n";
                std::cout << i + 1 << " " << j + 1;

                return 0;
            }
        }
    }

    std::cout << 0;

    return 0;
}

 

 

일반적인 공약수를 구하는 문제를 반대로 제시한 문제이다.

 

A와 B가 주어졌을 때 GCD와 LCM을 구하는 것이 아니라, GCD와 LCM이 주어졌을 때 A와 B를 구하는 문제이다.

물론, A와 B의 쌍은 여럿 존재할 수 있기 때문에 그 중에서 A와 B의 합이 최소인 쌍을 출력해주면 된다.

 

문제 풀이

 

먼저, 두 수 A,B에 최대 공약수를 G, 최소공배수를 L이라고 한다면 A와 B는 반드시 GCD의 배수이다.

 

이를 이용하여, L보다 작은 G의 배수를 모두 구한 뒤, 그 중 2개를 뽑아 두 수의 최소 공배수와 최대 공약수가  G, L과 동일한지를 검사하여 조건을 만족하는 (A, B)쌍을 모두 구할 수 있을 것이다.

 

하지만, 문제의 조건을 보면 숫자의 범위가 2~100,000,000이다.

위와 같은 방식으로 답을 구하려고 했다간 시간초과가 발생할 수 밖에 없다.

 

그렇기 때문에, 위에서 주어진 풀이 방법을 기반으로 범위를 좁히는 것이 필요하다.

두 수를 구하기 위해 과연 G보다 작은 L의 배수를 모두 구해야 할까?

 

먼저 한 가지 수식을 보자.

 

$$GCD = \frac{A * B}{LCM}$$

 

유클리드 호제법에 따르면, 위 공식이 성립한다.

여기서 변환을 하나 해보자.

 

$$GCD * LCM = A * B$$

 

이렇게도 표현할 수 있을 것이다.

 

A와 B의 곱이 GCD * LCM 이라고 한다면, A와 B 둘 중 하나는 반드시 GCD * LCM의 제곱근보다 작거나 같아야 한다.

더보기

M = A * B 이고, R을 M의 제곱근이라고 했을 때, 4가지 경우

 

1.  A = R 인 경우

=> R * B = R * R 이므로, B또한 R이다.

=> A = R, B = R

 

2. A > R 인 경우

=> B또한 R보다 크다면, A * B는 (R + a) * (R + b)이 되므로, R * R 보다 큰 값을 가지게 됨. (a, b는 양의 정수)

A * B = R * R 을 만족하려면, B는 R보다 작아야 함.

 

3.A < R 인 경우

=> 위 조건과 마찬가지의 논리로, B가 R보다 커야함.

 

그러므로, A와 B중 하나는 반드시 R보다 작거나 같아야 한다.

즉, LCM보다 작은 GCD의 배수를 모두 구하는 것이 아니라, GCD * LCM의 제곱근보다 작은 GCD의 배수에서만 값을 구하면 된다.

 

또한, GCD * LCD = A * B 이므로, A를 결정하였다면, GCD * LCD를 A로 나눠서 바로 B를 구할 수도 있기 때문에, GCD의 배수를 모두 구할 필요가 없게 된다.

 

풀이 코드

long long GCD = 0;
long long LCM = 0;

std::cin >> GCD >> LCM;

GCD와 LCM을 입력받았다.

long long Mul = GCD * LCM;
long long MulSqrt = std::sqrt(Mul);

두 수의 곱의 제곱근도 구해주었다.

long long MinSum = LLONG_MAX;
std::pair<int, int> Answer;

for (int i = GCD; i <= MulSqrt; i += GCD)
{
    long long  Cur = i;
    long long  Other = Mul / Cur;

    int  CurGCD = GetGCD(Cur, Other);
    long long  CurLCM = Cur * Other / CurGCD;

    if (CurGCD == GCD && CurLCM == LCM)
    {
        if (Cur + Other < MinSum)
        {
            MinSum = Cur + Other;
            Answer = { Cur, Other };
        }
    }
}

이후, 반복문을 돌며 위에서 말한 논리로 두 숫자 쌍을 구해주었다.

GCD의 배수에 대해서만 검사를 하도록 하였으며, 숫자 1개를 구했다면 GCD * LCM을 해당 숫자로 나누어 다른 숫자도 구해주었다.

 

그리고 두 숫자의 최소 공배수와 최대 공약수가 입력받은 값과 동일한지를 검사하여 조건을 만족하는 두 숫자인지 검사한 뒤, 두 수의 합을 기준으로 Answer를 갱신해주었다.

long long GetGCD(int _Left, int _Right)
{
	int Remain = _Left % _Right;

	while (Remain > 0)
	{
		Remain = _Left % _Right;
		_Left = _Right;
		_Right = Remain;
	}

	return _Left;
}

최대 공약수는 위와 같이 유클리드 호제법으로 구해주었다.

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

return 0;

마지막에 답을 출력해주면 끝이다.

 

코드 전문

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

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

long long GetGCD(int _Left, int _Right)
{
    int Remain = _Left % _Right;

    while (Remain > 0)
    {
        Remain = _Left % _Right;
        _Left = _Right;
        _Right = Remain;
    }

    return _Left;
}

int main()
{
    Init();

    long long GCD = 0;
    long long LCM = 0;

    std::cin >> GCD >> LCM;

    long long Mul = GCD * LCM;
    long long MulSqrt = std::sqrt(Mul);

    long long MinSum = LLONG_MAX;
    std::pair<int, int> Answer;

    for (int i = GCD; i <= MulSqrt; i += GCD)
    {
        long long  Cur = i;
        long long  Other = Mul / Cur;

        int  CurGCD = GetGCD(Cur, Other);
        long long  CurLCM = Cur * Other / CurGCD;

        if (CurGCD == GCD && CurLCM == LCM)
        {
            if (Cur + Other < MinSum)
            {
                MinSum = Cur + Other;
                Answer = { Cur, Other };
            }
        }
    }

    std::cout << Answer.first << " " << Answer.second;
    	
    return 0;
}

 

 

문제 이해가 처음에 다소 헷갈리는 부분이 있었다.

 

먼저, Defile 프로젝트는 위의 설명과 같다.

모든 국회의원의 명예를 1씩 감소시키고, 0이 된 국회의원이 있다면 해당 국회의원을 파면한다.

이 과정을 반복하며 모든 국회의원을 박탈하는 것이 목표이다.

 

다만, 해커를 이용한 명예 실추가 본인은 Defile프로젝트의 1번 과정과 관련이 있는 건가 싶었는데, 이는 일종의 사전작업이라고 생각하면 될 듯하다.

 

경우에 따라 Defile 프로젝트를 실행하더라도, 모든 국회의원을 파면할 수 없을 수도 있다. 이런 상황을 방지하기 위해, 해커를 이용해 명예를 미리 감소시켜놓는 것이다.

 

즉, Defile 프로젝트를 성공시키기 위해 해커를 이용해 사전 작업을 한다고 가정했을 때, 해커를 최소 몇 명을 고용해야 하는가? 를 구하는 문제이다.

 

문제 풀이

먼저, Defile프로젝트를 성공시키기 위한 조건을 생각해보자.

 

조건을 보면, 모든 국회의원의 명예를 1씩 실추시킨 뒤, 파면되는 국회의원이 없다면 해당 프로젝트는 종료된다.

즉, 첫 시도에서 적어도 1명 이상의 국회의원이 파면되어야 한다는 것이다.

=> 명예가 1인 국회의원이 적어도 1명 있어야 한다.

 

또한, 첫 번째 시도만 성공해야 하는 것이 아니라 매 시도마다 파면되는 국회의원이 반드시 1명은 있어야 한다.

(1명도 없는 순간에는 프로젝트가 종료되기 때문에)

 

생각해보면, 모든 국회의원의 명예를 1씩 실추하게 되면, 1인 국회의원은 명예가 0이 되므로 파면될 것이다.

그렇다면 그 다음 시도에서 파면될 국회의원은? 1이 실추된 이후에 남은 명예가 1인 국회의원일 것이다.

 

즉, 처음 명예가 2인 국회의원이 두 번째 시도에서 파면될 것이다.

 

이 논리를 반복적으로 이어가보면, 명예가 1인 국회의원이 처음 파면되고 그 다음엔 명예가 2인 국회의원이 파면되며 그 다음엔 명예가 3인 국회의원이 파면되고 이 과정이 반복될 것이다.

 

즉, 모든 국회의원을 명예 순으로 정렬했을 때 이전 국회의원의 명예와 2이상 차이가 나는 국회의원이 없어야 한다는 것이다.

(한 국회의원을 파면한 이후에 파면되는 국회의원은 파면된 국회의원의 명예보다 1만큼 큰 명예를 가진 국회의원이므로)

 

그러므로, 해커를 이용해 맞추어야할 사전 작업은 아래와 같다.

 

1. 모든 국회의원중 1의 명예를 가진 국회의원이 적어도 1명 존재해야 한다.

2. 모든 국회의원을 명예 기준으로 오름차순 정렬했을 때, 어떤 국회의원의 명예가 앞의 국회의원과 2이상 차이가 나서는 안된다.

 

하나의 예시를 보자.

7 3 6 2 4

 

국회의원의 명예가 위와 같이 입력되었다고 해보자.

이를 오름차순으로 정렬하면, 2 3 4 6 7 이 된다.

 

반드시, 명예가 1인 국회의원이 1명은 존재해야 하므로 해커를 이용해 맨 앞의 국회의원의 명예를 1을 실추시켜야 한다.

-> Hacker 1명 고용 

-> 명예 : 2 3 4 6 7 -> 1 3 4 6 7 로 변경

 

모든 국회의원은 앞의 국회의원의 명예보다 2이상 큰 명예를 지녀선 안된다.

두 번째 국회의원부터 명예를 실추해보자.

 

첫 번째 국회의원의 명예가 1이므로, 두 번째 국회의원의 명예는 최대 2까지 보유할 수 있다.

->Hacker 1명 고용

-> 명예 : 2 3 4 6 7 -> 1 2 4 6 7 로 변경

 

두 번째 국회의원의 명예가 2이므로 세번째 국회의원의 명예는 최대 3까지 보유할 수 있다.

->Hacker 1명 고용

-> 명예 : 1 2 4 6 7 -> 1 2 3 6 7 로 변경

 

세 번째 국회의원의 명예가 3이므로 네 번째 국회의원의 명예는 최대 4까지 보유할 수 있다.

->Hacker 2명 고용

-> 명예 : 1 2 3 6 7 -> 1 2 3 4 7 로 변경

 

네 번째 국회의원의 명예가 4이므로 다섯 번째 국회의원의 명예는 최대 5까지 보유할 수 있다.

->Hacker 2명 고용

-> 명예 : 1 2 3 6 7 -> 1 2 3 4 5 로 변경

 

총 7명의 해커를 고용하여 모든 조건은 만족시키도록 하였다.

이 상태에서 defile 프로젝트를 실행하게 되면 아래와 같은 과정을 거치며 모든 국회의원의 명예를 실추할 수 있다.

 

첫 명예 : 1 2 3 4 5

1번째 시도 : 0 1 2 3 4

2번째 시도 : 0 0 1 2 3

3번째 시도 : 0 0 0 1 2

4번째 시도 : 0 0 0 0 1

5번째 시도 : 0 0 0 0 0

 

이처럼 7명의 해커를 이용해 미리 명예를 실추해놓으면, 1번의 Defile 프로젝트를 통해 모든 국회의원을 파면할 수 있다.

 

코드 풀이

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

std::vector<int> Honors(NumOfMan, 0);
for (int i = 0; i < NumOfMan; i++)
{
    std::cin >> Honors[i];
}

 

먼저, 국회의원의 수와 명예를 입력받는다.

std::sort(Honors.begin(), Honors.end());

 

이후, 명예를 오름차순으로 정렬한다.

long long HackerCount = Honors[0] - 1;
Honors[0] = 1;

 

 

해커를 고용하여 가장 작은 명예를 1로 맞춰준다.

참고로, 국회의원은 최대 10만명이고 명예는 최대 10만이므로 HackerCount의 최대값은 int범위를 초과한다.

그러므로 long long 자료형으로 선언해주어야한다.

for (int i = 1; i < NumOfMan; i++)
{
    int CurHonor = Honors[i - 1] + 1;

    if (Honors[i] > CurHonor)
    {
        int Gap = Honors[i] - CurHonor;
        HackerCount += Gap;
        Honors[i] = CurHonor;
    }
}

 

반복문을 돌며, 이전 인덱스의 명예보다 2 이상 차이나지 않도록 갱신해주었다.

그 과정에서 명예를 깎는 만큼 HackerCount를 증가시켜주었다.

std::cout << HackerCount;

return 0;

 

마지막으로 HackerCount를 출력해주면 끝이다.

 

코드 전문

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

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

int main()
{
    Init();

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

    std::vector<int> Honors(NumOfMan, 0);
    for (int i = 0; i < NumOfMan; i++)
    {
        std::cin >> Honors[i];
    }

    std::sort(Honors.begin(), Honors.end());

    long long HackerCount = Honors[0] - 1;
    Honors[0] = 1;

    for (int i = 1; i < NumOfMan; i++)
    {
        int CurHonor = Honors[i - 1] + 1;
        
        if (Honors[i] > CurHonor)
        {
            int Gap = Honors[i] - CurHonor;
            HackerCount += Gap;
            Honors[i] = CurHonor;
        }
    }

    std::cout << HackerCount;

    return 0;
}

 

 

 

dobarz

adatak 

 

두 개의 문자열이 입력되었다고 치자.

이 때, 각 열을 준으로 문자열을 새로 만들게 되면, "da", "od", "ba", "at", "ra", "zk" 총 6개의 문자열이 만들어진다.

첫 번째 행의 문자열을 제거한 뒤, 동일한 방법으로 문자열을 만들게 되면, "a", "d", "a", "t", "a", "k" 총 6개의 문자열이 만들어진다.

 

처음에 만들었던 "da", "od", "ba", "at", "ra", "zk" 간에는 중복이 존재하지 않는다.

두 번째로 만든  "a", "d", "a", "t", "a", "k" 에는 "a"가 총 3번 중복되고 있다. 

 

즉, 기존의 문자열을 1개 제거했더니 열 문자열에서 중복되는 문자열이 발생한 것이다.

 

이처럼, 입력으로 주어진 문자열에서 몇 개를 지우면 열 문자열에서 중복되는 문자열이 발생하는지를 구하면 된다.

위의 예시에선 1을 답으로 출력하면 된다.

 

문제 풀이

아주 단순하게 접근하였다.

 

주어진 문자열을 기반으로 열 문자열을 만들어서 자료구조에 저장한 뒤, 맨 앞의 문자를 하나씩 지워가며 중복 검사를 진행한 것이다.

 

하지만, 문자열의 경우 맨 앞의 문자를 제거하면 뒤의 모든 문자를 앞으로 땡겨오는 연산을 해야하는 문제가 있다.

그러므로, 열 문자열을 저장할 때 앞뒤를 뒤집은 상태로 저장하고 맨 뒤의 문자를 제거하면서 중복 검사를 하였다.

 

중복검사는 unordered_set을 이용해서 하였다. unordered_set에 모든 문자열을 삽입한 뒤, 문자의 개수와 unordered_set의 size가 동일한지를 테스트하여 중복 여부를 검사하였다.

 

풀이 코드

int StrSize = 0;
int VecSize = 0;
std::cin >> StrSize >> VecSize;

std::vector<std::string> Vec(VecSize, std::string(StrSize, '0'));
for (int i = 0; i < StrSize; i++)
{
    for (int j = 0; j < VecSize; j++)
    {
        std::cin >> Vec[j][StrSize - 1 - i];
    }
}

 

벡터에 열 문자열을 모두 저장해주고 있다.

앞에서 말했던 것처럼, 열 문자열을 저장할 때 뒤집어서 저장을 해주고 있다.

 

int Count = 0;

for(int j = 0; j < StrSize; j++)
{
    for (int i = 0; i < VecSize; i++)
    {
        Vec[i].pop_back();
    }

    std::unordered_set<std::string> Strs;

    for (int i = 0; i < VecSize; i++)
    {
        Strs.insert(Vec[i]);
    }

    if (Strs.size() == VecSize)
    {
        Count++;
    }
    else
    {
        break;
    }
}

 

이후, 모든 문자열의 가장 마지막 원소(실제로는 열 문자열의 가장 앞 문자)를 제거해주었다.

그리고 unordered_set에 모든 문자열을 삽입하였고, unordered_set의 size와 기존 문자열의 개수와 동일한지를 검사하여 Count를 증가시키거나 반복문을 탈출하도록 분기를 두었다.

 

std::cout << Count;

return 0;

 

반복문이 끝나고 Count를 출력하면 끝이다.

 

코드 전문

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

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

int main()
{
    Init();

    int StrSize = 0;
    int VecSize = 0;
    std::cin >> StrSize >> VecSize;

    std::vector<std::string> Vec(VecSize, std::string(StrSize, '0'));
    for (int i = 0; i < StrSize; i++)
    {
        for (int j = 0; j < VecSize; j++)
        {
            std::cin >> Vec[j][StrSize - 1 - i];
        }
    }

    int Count = 0;
    
    for(int j = 0; j < StrSize; j++)
    {
        for (int i = 0; i < VecSize; i++)
        {
            Vec[i].pop_back();
        }

        std::unordered_set<std::string> Strs;

        for (int i = 0; i < VecSize; i++)
        {
            Strs.insert(Vec[i]);
        }

        if (Strs.size() == VecSize)
        {
            Count++;
        }
        else
        {
            break;
        }
    }

    std::cout << Count;

    return 0;
}

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

백준 2436 - 공약수 (C++)  (0) 2024.05.21
백준 16678 - 모독 (C++)  (0) 2024.05.19
백준 2665 - 미로만들기 (C++)  (0) 2024.05.17
백준 20207 - 달력 (C++)  (0) 2024.05.17
백준 5397 - 키로거 (C++)  (0) 2024.05.16

 

1행 1열에서 시작해서, n행 n열까지 도달하기 위해선, 몇개의 검은 방을 하얀 방으로 바꾸어야 하는 가를 구하는 문제이다.

 

가는 길이 검은방으로 막혀있다면, 검은 방을 하얀 방으로 바꿔 지나갈 수 있는 길로 만들 수 있다. 이렇게, 방을 바꿔가며 목적지까지 이동할 때, 방의 색을 바꾸는 최소의 수를 구하면 된다.

 

문제 풀이

 

본인은 BFS를 활용하여 해결하였다. 행렬의 최대 크기가 50인만큼, 완전탐색을 하여도 부담이 없기 때문이다.

 

또한, 본인이 푼 방식은 50개의 칸만 탐색하는 완전탐색이 아니라, 이미 거쳤던 곳도 다시 방문하여 탐색하는 방식인데 행렬의 크기가 커질수록 연산 부담이 커지겠지만, 문제에서 주어진 최대 행렬 크기가 50 x 50 이기 때문에 사용할 수 있었다.

 

먼저 2개의 이차원 배열을 준비하였다.

 

하나는 맵 배열이고, 하나는 방을 바꾼 횟수를 저장한 배열이다.

맵 배열을 Map, 방을 바꾼 횟수를 저장한 배열을 ChangeCount라고 하겠다.

 

ChangeCount에서 (i, j)가 의미하는 것은 (1, 1)에서 (i, j)로 갈 때, 검은 방을 하얀 방으로 바꾸는 최소 횟수이다.

 

탐색 방식은 아래와 같다.

 

먼저, 첫 번째 칸 (1, 1)을 큐에 삽입한다.

이후, 큐의 size가 0이 될 때까지 반복문을 돌린다.

 

반복문 내부에선, 큐의 front를 기준으로 4방향을 탐색한다.

 

다음에 나아갈 방향이 검은 방이라면, 현재 칸의 ChangeCount + 1과 다음 칸의 ChangeCount를 비교한다.

만약, 다음 칸의 ChangeCount 값이 현재 칸의 ChangeCount + 1 보다 같거나 작다면, 다음 칸으로 나아가지 않는다.

 (ChangeCount는 최소값을 저장하는 배열이기 때문에, 최소 값을 갱신할 수 없는 상황이라면 나아가지 않는다.)

 

다음에 나아갈 방향이 하얀 방이라면, 현재 칸의 ChangeCount와 다음 칸의 ChangeCount를 비교한다.

만약, 다음 칸의 ChangeCount 값이 현재 칸의 ChangeCount보다 같거나 작다면, 다음 칸으로 나아가지 않는다.

 

다음에 나아갈 방향이 검은 방일 때와 하얀 방일 때 모두 처리하는 과정은 똑같다. 다만, 검은 방인 경우 해당 방의 색을 바꿔주어야 하기 때문에, ChangeCount에 1을 더해서 비교해주고 있다.

 

이 과정을 모두 마치고 나면, ChangeCount의 (n, n)에는 1,1에서 시작해서 n,n에 도착할 때 까지 방의 색을 바꾼 최소 횟수가 저장되어 있을 것이다.

 

풀이 코드

 

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

std::vector<std::vector<int>> Map(Size, std::vector<int>(Size));
for (int i = 0; i < Size; i++)
{
    std::string Input;
    std::cin >> Input;

    for (int j = 0; j < Size; j++)
    {
        Map[i][j] = Input[j] - '0';
    }
}

 

먼저 입력을 받아주었다.

int로 받으면 한 줄이 하나의 원소에 저장되어 버려서, string으로 받은 뒤 하나씩 분배해주었다.

 

struct Node
{
    int Y = 0;
    int X = 0;
};

 

위치 정보를 저장할 구조체를 선언하였다. std::pair을 사용해도 되지만, 직접 정의한 구조체를 이용해 조금 더 가독성을 높혀보았다.

 

std::vector<std::vector<int>> ChangeCount(Size, std::vector<int>(Size, 10000000));

ChangeCount[0][0] = 1 - Map[0][0];

std::queue<Node> BFS;
BFS.push({ 0, 0});

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

 

ChangeCount는 위에서 말한 것과 같이 해당 칸으로 이동하는동안 방을 바꾸는 최소 횟수를 저장한 배열이다.

최소 값을 갱신하는 배열이기 때문에 초기값을 충분히 큰 값으로 잡아주었다.

 

다음은 BFS를 하기 위한 queue를 선언하였고, 4방향을 탐색하기 위한 Dir 배열도 선언해주었다.

while (BFS.size() > 0)
{
    Node CurNode = BFS.front();
    BFS.pop();

    for (int i = 0; i < 4; i++)
    {
        int NextX = CurNode.X + DirX[i];
        int NextY = CurNode.Y + DirY[i];

        if (NextX < 0 || NextY < 0 || NextX >= Size || NextY >= Size)
        {
            continue;
        }

        if (Map[NextY][NextX] == 0)
        {
            if (ChangeCount[CurNode.Y][CurNode.X] + 1 >= ChangeCount[NextY][NextX])
            {
                continue;
            }
            else
            {
                ChangeCount[NextY][NextX] = ChangeCount[CurNode.Y][CurNode.X] + 1;
                BFS.push({ NextY, NextX });
            }
        }
        else
        {
            if (ChangeCount[CurNode.Y][CurNode.X] >= ChangeCount[NextY][NextX])
            {
                continue;
            }
            else
            {
                ChangeCount[NextY][NextX] = ChangeCount[CurNode.Y][CurNode.X];
                BFS.push({ NextY, NextX });
            }
        }
    }
}

 

큐의 최상위 원소를 뽑아서, 4방향을 탐색해주었다.

 

위에서 설명한 것과 같이, 다음에 나아갈 곳이 검은 방이라면 ChangeCount + 1 을 기준으로 대소비교를 하고있으며 하얀 방이라면 ChangeCount를 기준으로 대소비교를 하고 있다.

 

최소값을 갱신할 수 있다면, 해당 방향으로 나아가 계속 최소값을 갱신해주었다.

std::cout << ChangeCount[Size - 1][Size - 1];

return 0;

 

BFS가 끝난 뒤 목적지의 값을 출력해주면 끝이다.

 

코드 전문

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

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

struct Node
{
    int Y = 0;
    int X = 0;
};

int main()
{
    Init();

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

    std::vector<std::vector<int>> Map(Size, std::vector<int>(Size));
    for (int i = 0; i < Size; i++)
    {
        std::string Input;
        std::cin >> Input;

        for (int j = 0; j < Size; j++)
        {
            Map[i][j] = Input[j] - '0';
        }
    }

    std::vector<std::vector<int>> ChangeCount(Size, std::vector<int>(Size, 10000000));

    ChangeCount[0][0] = 1 - Map[0][0];

    std::queue<Node> BFS;
    BFS.push({ 0, 0});

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

    while (BFS.size() > 0)
    {
        Node CurNode = BFS.front();
        BFS.pop();

        for (int i = 0; i < 4; i++)
        {
            int NextX = CurNode.X + DirX[i];
            int NextY = CurNode.Y + DirY[i];

            if (NextX < 0 || NextY < 0 || NextX >= Size || NextY >= Size)
            {
                continue;
            }
            
            if (Map[NextY][NextX] == 0)
            {
                if (ChangeCount[CurNode.Y][CurNode.X] + 1 >= ChangeCount[NextY][NextX])
                {
                	continue;
                }
                else
                {
                	ChangeCount[NextY][NextX] = ChangeCount[CurNode.Y][CurNode.X] + 1;
                	BFS.push({ NextY, NextX });
                }
            }
            else
            {
                if (ChangeCount[CurNode.Y][CurNode.X] >= ChangeCount[NextY][NextX])
                {
                    continue;
                }
                else
                {
                    ChangeCount[NextY][NextX] = ChangeCount[CurNode.Y][CurNode.X];
                    BFS.push({ NextY, NextX });
                }
            }
        }
    }

    std::cout << ChangeCount[Size - 1][Size - 1];

    return 0;
}

 

일정을 규칙에 따라 차례대로 달력에 기록한다.

일정이 겹치면, 해당 일정은 아랫줄에 추가로 기록한다.

 

모든 일정을 기록한 뒤, 직사각형의 코팅지를 일정 위에 붙인다고 했을 때, 최소로 필요한 코팅지의 면적을 구하면 된다.

 

문제 풀이

 

본인은 365일을 기록하는 벡터를 만든 뒤, 해당 벡터에 일정을 모두 기록하여 문제를 해결하였다.

7
2 4
4 5
5 6
5 7
7 9
11 12
12 12

 

입력이 위와 같이 주어졌다고 해보자.

이렇게, 모든 날의 일정을 기록할 벡터를 선언하였다.

이후, 일정을 차례대로 입력할 것이다.

 

문제 예시를 보면, 겹치는 일정이 하나가 생길 때마다 높이가 1씩 증가한다.

그러므로, 한 날짜에 일정이 부여될 때마다 ++을 해줄 것이다.

 

(만약, 5일에 5개의 일정이 있다면, 5번 인덱스의 값은 5가 된다.)

 

이제 차례대로 입력을 처리해보자.

첫 번째 입력은 2 4 이다.

이렇게 2~4에 1을 더해주었다.

다음 입력은 4~5이다.

이번엔, 4와 5에 1을 더해서 위 표와 같은 값으로 갱신되었다.

다음 입력은 5~6이다.

위 표와 같이 갱신되었다.

다음 입력은 5~7이다.

이렇게 갱신되었다.

동일한 방식으로 나머지 모든 입력을 처리해주면 아래와 같아질 것이다.

 

이렇게, 일정을 모두 기록하였다면 코팅지의 최소 크기를 구하면 된다.

먼저, 일정이 없는 날에는 코팅지를 붙일 필요가 없다.

그러므로 일정이 있는 날에 대해서만 코팅지의 크기를 계산해줄 것이다.

 

 

먼저, 이 두개의 섹션에 대해서만 코팅지를 붙이면 된다.

각 섹션의 길이가 코팅지의 가로 길이일 것이다.

 

코팅지의 세로 길이는 그 안에 있는 모든 일정을 담아야 하기 때문에, 섹션에 포함된 값들 중 가장 큰 값이 코팅지의 세로 길이가 될 것이다.

 

그러므로, 왼쪽 섹션의 코팅지의 세로 길이는 3이 될 것이며 오른쪽 섹션의 코팅지의 세로 길이는 2가 될 것이다.

 

왼쪽 섹션의 코팅지의 넓이 : 가로 길이 * 세로 길이 = 8 * 3 = 24

오른쪽 섹션의 코팅지의 넓이 : 가로 길이 * 세로 길이 = 2 * 2 = 4

 

정답 = 24 + 4 = 28 이 된다.

 

풀이 코드

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

std::vector<std::pair<int, int>> Schedules(InputSize);

for (int i = 0; i < InputSize; i++)
{
    std::cin >> Schedules[i].first >> Schedules[i].second;
}

 

먼저, 입력되는 일정 정보를 vector에 저장해주었다.

 

std::vector<int> Days(367);

for (int i = 0; i < InputSize; i++)
{
    for (int j = Schedules[i].first; j <= Schedules[i].second; j++)
    {
        Days[j]++;
    }
}

 

이후, 일정을 기록할 Days 배열을 선언한 뒤, 모든 일정을 기록해주었다.

일정이 하나 겹칠 때마다 1씩 값을 증가시켜주었다.

 

여기서 Days의 길이가 365가 아니라, 367인데 이유는 아래에서 확인해보자.

int Start = 0;
int MaxY = 0;
int Sum = 0;

for (int i = 1; i <= 366; i++)
{
    if (Days[i - 1] != 0 && Days[i] == 0)
    {
        Sum += MaxY * (i - Start);
        MaxY = -1;
    }
    else if (Days[i - 1] == 0 && Days[i] != 0)
    {
        Start = i;
    }

    if (Days[i] != 0)
    {
        MaxY = std::max(MaxY, Days[i]);
    }
}

 

먼저, 시작 인덱스와 섹션의 최대 값을 저장할 Start와 MaxY를 선언해주었고, 정답을 저장할 Sum도 선언해주었다.

 

여기서, 각 섹션의 시작을 값이 0인 인덱스에서 0이 아닌 인덱스로 변화하는 시점으로 잡아주었다. 또한, 섹션의 끝은 값이 0이 아닌 인덱스에서 값이 0으로 변하는 시점으로 잡아주었다.

 

이 때, 시작부터 끝까지 모든 섹션을 동일하게 처리하기 위해, 배열을 367칸으로 선언하여 0번 인덱스와 366번 인덱스를 활용하였다.

 

만약 0~364로 선언된 배열이었을 때, 마지막 일정이 365일에 끝난다면 해당 일정이 속한 섹션을 반복문 내에서 처리할 수 없기 때문에 추가적으로 코드를 작성해야 한다.

 

그렇게 각 섹션의 길이와 최대값을 구한 뒤, 섹션을 탈출할 때 두 값을 곱해 코팅지의 넓이를 구하고 Sum에 더해주었다.

 

std::cout << Sum;

return 0;

 

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

 

코드 전문

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

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

int main()
{
    Init();

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

    std::vector<std::pair<int, int>> Schedules(InputSize);

    for (int i = 0; i < InputSize; i++)
    {
        std::cin >> Schedules[i].first >> Schedules[i].second;
    }

    std::vector<int> Days(367);

    for (int i = 0; i < InputSize; i++)
    {
        for (int j = Schedules[i].first; j <= Schedules[i].second; j++)
        {
            Days[j]++;
        }
    }

    int Start = 0;
    int MaxY = 0;
    int Sum = 0;

    for (int i = 1; i <= 366; i++)
    {
        if (Days[i - 1] != 0 && Days[i] == 0)
        {
            Sum += MaxY * (i - Start);
            MaxY = -1;
        }
        else if (Days[i - 1] == 0 && Days[i] != 0)
        {
            Start = i;
        }

        if (Days[i] != 0)
        {
            MaxY = std::max(MaxY, Days[i]);
        }
    }

    std::cout << Sum;

    return 0;
}

+ Recent posts