특정 마을에서 파티가 열린다고 했을 때, 모든 학생은 본인이 사는 마을에서 파티가 열리는 마을로 갔다가 다시 원래 마을로 돌아올 것이다.

 

이 때, 사는 마을 -> 파티 마을 -> 사는 마을의 경로를 통해 이동하게 될 것이다.

이 경로를 통해 이동할 때, 가장 큰 시간을 소비하는 학생의 소요 시간을 출력하면 된다.

 

모든 길은 단방향이기 때문에, 사는 마을 -> 파티 마을로 갈 때 소요되는 시간과 파티 마을 -> 사는 마을로 갈 때 소요되는 시간은 서로 다를 수 있다는 것을 주의해야 한다.

 

문제 풀이

먼저, 이 문제는 다익스트라를 이용하면 쉽게 해결할 수 있다.

 

사는 마을 -> 파티 마을로 가는 경로에 대해 다익스트라 알고리즘을 적용하여 최소 비용을 구하고,

파티 마을 -> 사는 마을로 가는 경로에 대해 다익스트라 알고리즘을 적용하여 최소 비용을 구할 수 있다.

 

두 경우의 비용을 더해서 각 학생의 소요 시간을 구할 수 있을 것이다.

 

다만, 입력으로 주어진 값을 평범하게 다익스트라 알고리즘을 적용하게 되면 시간초과가 발생할 수 있다.

 

왜냐하면, 1번 마을을 시작점으로 다익스트라 알고리즘을 수행해야 하고, 2번 마을을 시작점으로 다익스트라 알고리즘을 수행해야 하고, N번 마을까지 모두 다익스트라 알고리즘을 수행해야 한다.

 

파티 마을에서 원래 마을로 돌아올 떄엔 시작점이 파티 마을로 고정이므로 1번의 다익스트라로 해결할 수 있지만, 기존의 마을에서 파티 마을로 가는 경우에는 마을의 개수만큼 다익스트라 알고리즘을 수행해야 할 것이다.

 

하지만, 주어진 입력을 뒤집는다면 한 번의 다익스트라를 통해 답을 구할 수 있게 된다.

 

예를 들어, 5개의 마을이 있고 5번 마을에서 파티가 열린다고 해보자.

 

1번 마을 -> 5번 마을 : 비용 5

2번 마을 -> 5번 마을 : 비용 3

 

이렇게 입력이 주어진다면,

 

5번 마을 -> 1번 마을 : 비용 5

5번 마을 -> 2번 마을 : 비용 3 

 

이렇게 마을의 시작점과 도착점을 뒤집어서 저장하는 것이다.

 

이 문제에선 특정 마을에서 파티 마을을 향해 가는 경로의 방향이 중요한 것이 아니라 비용이 중요한 것이므로 뒤집어서 파티 마을 -> 특정 마을로 해석해도 문제가 발생하지 않을 것이다.

 

그러므로, 입력을 그대로 저장한 뒤 파티 마을을 시작점으로 다익스트라 알고리즘을 1번 수행하고 입력을 반대로 저장한 뒤 파티 마을을 시작점으로 다익스트라 알고리즘을 1번 수행하게 되면 총 2번의 다익스트라 알고리즘으로 답을 구할 수 있게 되는 것이다.

 

코드 풀이

void Dijk(std::vector<int>& _Cost, std::vector<std::vector<std::pair<int, int>>>& _Link, int _StartNode)
{
    std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> Queue;
    
    Queue.push({ 0, _StartNode });

    while (Queue.size() > 0)
    {
        int CurNode = Queue.top().second;
        int CurCost = Queue.top().first;

        Queue.pop();

        if (_Cost[CurNode] < CurCost)
        {
            continue;
        }

        for (int j = 0; j < _Link[CurNode].size(); j++)
        {
            int NextNode = _Link[CurNode][j].second;
            int NextCost = _Link[CurNode][j].first;

            int NextCostSum = CurCost + NextCost;

            if (NextCostSum < _Cost[NextNode])
            {
                _Cost[NextNode] = NextCostSum;
                Queue.push({ NextCostSum, NextNode });
            }
        }
    }
}

다익스트라 알고리즘 구현 코드이다. 우선 순위 큐를 활용하여 구현하였다.

int NumMan = 0;
int NumRoad = 0;
int PartyNode = 0;

std::cin >> NumMan >> NumRoad >> PartyNode;

std::vector<std::vector<std::pair<int, int>>> Link(NumMan);
std::vector<std::vector<std::pair<int, int>>> ReverseLink(NumMan);

for (int i = 0; i < NumRoad; i++)
{
    int Start = 0;
    int End = 0;
    int Cost = 0;
    
    std::cin >> Start >> End >> Cost;

    Link[Start - 1].push_back({ Cost, End - 1});
    ReverseLink[End - 1].push_back({ Cost, Start - 1 });
}

위에서 말했던 것처럼 정방향으로 입력을 저장하고, 역방향으로도 입력을 저장해주었다.

std::vector<int> Costs(NumMan, INT_MAX);
std::vector<int> ReverseCosts(NumMan, INT_MAX);

다익스트라를 사용해 갱신할 비용 배열도 정방향, 역방향 2개를 만들어주었다.

Dijk(Costs, Link, PartyNode - 1);
Dijk(ReverseCosts, ReverseLink, PartyNode - 1);

정방향, 역방향으로 다익스트라를 1번씩 수행해 주었다.

int MaxCost = -1;

Costs[PartyNode - 1] = 0;
ReverseCosts[PartyNode - 1] = 0;

for (int i = 0; i < NumMan; i++)
{
    int CurCost = Costs[i] + ReverseCosts[i];
    MaxCost = std::max(MaxCost, CurCost);
}

std::cout << MaxCost;

return 0;

정방향 비용은 파티 마을 -> 시작 마을의 비용이며 역방향 비용은 시작 마을 -> 파티 마을의 비용이다.

두 비용을 합한 값이 가장 큰 값을 구해주었고, 그 값을 출력해주었다.

 

여기서 중요한 것은 파티가 시작되는 마을의 값을 0으로 초기화해주어야 한다는 것이다.

다익스트라 알고리즘을 수행하게 되면, 파티 마을도 의도치 않게 비용이 갱신될 수 있으므로 반드시 제외해주어야 한다.

 

 

문제 풀이

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

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

void Dijk(std::vector<int>& _Cost, std::vector<std::vector<std::pair<int, int>>>& _Link, int _StartNode)
{
    std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> Queue;
    
    Queue.push({ 0, _StartNode });

    while (Queue.size() > 0)
    {
        int CurNode = Queue.top().second;
        int CurCost = Queue.top().first;

        Queue.pop();

        if (_Cost[CurNode] < CurCost)
        {
            continue;
        }

        for (int j = 0; j < _Link[CurNode].size(); j++)
        {
            int NextNode = _Link[CurNode][j].second;
            int NextCost = _Link[CurNode][j].first;

            int NextCostSum = CurCost + NextCost;

            if (NextCostSum < _Cost[NextNode])
            {
                _Cost[NextNode] = NextCostSum;
                Queue.push({ NextCostSum, NextNode });
            }
        }
    }
}

int main()
{
    Init();

    int NumMan = 0;
    int NumRoad = 0;
    int PartyNode = 0;
    
    std::cin >> NumMan >> NumRoad >> PartyNode;
    
    std::vector<std::vector<std::pair<int, int>>> Link(NumMan);
    std::vector<std::vector<std::pair<int, int>>> ReverseLink(NumMan);

    for (int i = 0; i < NumRoad; i++)
    {
        int Start = 0;
        int End = 0;
        int Cost = 0;
        
        std::cin >> Start >> End >> Cost;

        Link[Start - 1].push_back({ Cost, End - 1});
        ReverseLink[End - 1].push_back({ Cost, Start - 1 });
    }

    std::vector<int> Costs(NumMan, INT_MAX);
    std::vector<int> ReverseCosts(NumMan, INT_MAX);

    Dijk(Costs, Link, PartyNode - 1);
    Dijk(ReverseCosts, ReverseLink, PartyNode - 1);

    int MaxCost = -1;

    Costs[PartyNode - 1] = 0;
    ReverseCosts[PartyNode - 1] = 0;

    for (int i = 0; i < NumMan; i++)
    {
        int CurCost = Costs[i] + ReverseCosts[i];
        MaxCost = std::max(MaxCost, CurCost);
    }

    std::cout << MaxCost;

    return 0;
}

 

 

 

규칙에 맞게 게임을 했을 때, 플레이어의 점수를 예측하는 문제이다.

A의 카드 숫자가 B의 카드 숫자로 나누어진다면 A의 패배, B의 승리이며 B의 카드 숫자가 A의 카드 숫자로 나누어진다면 A의 승리, B의 패배이다.

 

문제 풀이

한 쪽의 숫자가 다른 한 쪽의 숫자로 나누어진다면 승패가 결정나는 게임이기 때문에, 배수관계를 통해 승패를 파악할 수 있다. 하지만, 최대 10만명의 플레이어가 참가한 상황에서 모든 플레이어간의 대결 경우를 계산하게 되면 50억번의 경우를 계산해야한다. 

 

이를 해결하기 위해, 플레이어가 가진 숫자에 접근하여 계산하는 것이 아니라 숫자 자체의 존재 여부를 파악하도록 하였다. 무슨 말이냐면, 입력받은 플레이어의 카드 숫자 배열 외에 100만개의 boolean 배열을 만들어 인덱스에 해당하는 숫자가 있는지를 저장하는 배열을 하나 만들었다는 의미이다.

 

예를 들어, (2, 4, 6, 8)의 카드가 현재 플레이어들이 보유하고 있는 카드라면, boolean 배열 A에서 A[2] = true, A[4] = true, A[6] = true, A[8] = true 인 것이다. 이를 제외한 나머지 모든 값은 false이다.

 

이렇게 값을 별도로 저장한 이유는 연산 횟수를 줄이기 위해서이다. 플레이어 간의 대결을 이중 반복문으로 구현하여 모든 대결을 하게 되면, N^2의 시간복잡도를 가진 연산을 해야한다. 반면, 위의 boolean 배열을 통해 대결을 하면 NlogN의 시간복잡도를 보유하게 된다. 뭐가 다른지 아래 식을 통해 보자.

//플레이어간의 대결을 하는 경우
for(int i = 0; i < 플레이어의 수; i++)
{
    for(int j = i + 1; j < 플레이어의 수; j++)
    {
        //대결
    }
}

//boolean배열을 통해 숫자끼리 대결을 하는 경우
std::vector<bool> Array(1000001);

for(int i = 0; i < 플레이어의 수; i++)
{
    int Num = 플레이어의 카드 숫자;
    
    for(int j = Num * 2; j < 1000001; j += Num)
    {
        if(Array[j] == true)
        {
            //대결
        }
    }
}

 

두 방식의 차이는 내부에 있는 반복문의 차이이다.

 

위에선 모든 플레이어에 대해 반복문을 돌고 있다. j를 1씩 증가하면 반복문을 돌기 때문에 10만명의 플레이어가 있다고 했을 땐 99999 + 99998 + 99997 + 99996 .... + 1 번의 반복문을 돌게 될 것이다. 대략 50억번의 반복문을 돌게 된다.

 

반면, 아래의 방식은 j를 Num씩 증가시키며 배수에 대해서만 체크하고 있기 때문에, 1000000 + 500000 + 333333 + 250000 + 200000 + 133333 ..... + 1 번의 반복문을 돌게 된다. 대략 1400만번의 반복문을 돌게 된다.

 

이를 통해, 숫자간의 대결을 진행한 뒤, 점수를 저장해야 하는데 플레이어의 카드 숫자 배열에선 해당 숫자로 접근할 수가 없다. (다시 순차탐색 과정을 거쳐야 하는데, 그렇게 하면 결국 시간초과가 발생할 수 밖에 없게 된다.)

 

그러므로, 점수를 저장할 배열을 하나 더 만들어 주어야 한다.

boolean 배열과 동일하게 1000000개의 원소를 저장하도록 배열을 만들어서 각 인덱스에 맞는 숫자의 점수를 저장하면 된다.

 

코드 풀이

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

std::vector<int> PlayerNums(PlayerSize);
std::vector<bool> isExist(1000001);
std::vector<int> Score(1000001);

for (int i = 0; i < PlayerSize; i++)
{
    std::cin >> PlayerNums[i];
    isExist[PlayerNums[i]] = true;
}

입력과 초기화이다.

 

먼저, Player의 수를 입력받은 뒤 그 수에 맞게 PlayerNum을 생성하였고, 각 플레이어의 카드 숫자를 입력받아주었다. 

isExist는 해당 숫자가 있는가를 저장하는 배열이며, Score는 점수를 저장하는 배열이다.

isExist는 플레이어의 점수를 저장한 뒤, 바로 값을 갱신해주었다.

for (int i = 0; i < PlayerSize; i++)
{
    int CurNum = PlayerNums[i];

    for (int i = CurNum * 2; i < 1000001; i += CurNum)
    {
        if (isExist[i] == true)
        {
            Score[CurNum]++;
            Score[i]--;
        }
    }
}

탐색과정이다.

먼저, 현재 플레이어의 숫자를 기준으로 그 배수에 대해 모두 탐색하였다.

CurNum의 배수가 존재하는 숫자라면, 대결을 실행하였다.

대결은 항상 나의 점수 + 1, 상대방의 점수 -1로 끝난다. (상대방의 숫자가 나의 숫자의 배수이기 때문에)

for (int i = 0; i < PlayerSize; i++)
{
    std::cout << Score[PlayerNums[i]] << " ";
}

return 0;

모든 대결이 끝난 뒤, 각 플레이어의 카드 숫자에 해당하는 Score의 값을 출력해주면 된다.

 

코드 전문

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

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

int main()
{
    Init();

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

    std::vector<int> PlayerNums(PlayerSize);
    std::vector<bool> isExist(1000001);
    std::vector<int> Score(1000001);
    
    for (int i = 0; i < PlayerSize; i++)
    {
        std::cin >> PlayerNums[i];
        isExist[PlayerNums[i]] = true;
    }

    for (int i = 0; i < PlayerSize; i++)
    {
        int CurNum = PlayerNums[i];

        for (int i = CurNum * 2; i < 1000001; i += CurNum)
        {
            if (isExist[i] == true)
            {
                Score[CurNum]++;
                Score[i]--;
            }
        }
    }

    for (int i = 0; i < PlayerSize; i++)
    {
        std::cout << Score[PlayerNums[i]] << " ";
    }

    return 0;
}

 

 

각 학생들이 싫어하는 사람의 목록이 주어졌을 때, 모든 학생이 싫어하는 사람과 같은 팀이 되지 않도록 팀을 배분하면 된다.

5
1 3
1 5
2 1 4
1 3
1 2

 

위와 같이 입력이 주어진다면 아래와 같이 여러개의 정답이 나올 수 있다.

 

백팀 : 1, 2, 4 / 청팀 : 3, 5

백팀 : 3, 5 / 청팀 : 1, 2, 4

백팀 : 1, 4, 5 / 청팀 : 2, 3

백팀 : 2, 3 / 청팀 : 1, 4, 5

(실제로는 더 있을 수도 있다.)

 

이 중, 하나의 경우를 구한 뒤 출력하면 된다.

 

문제 풀이

각 학생들이 싫어하는 학생의 목록을 확인한 뒤, 하나씩 팀에 배분해주면 된다.

위에서 보여주었던 입력에 대해 생각해보자.

 

1번 학생은 3번 학생을 싫어한다.

=> 청팀 : 1 / 백팀 : 3

 

2번 학생은 5번 학생을 싫어한다.

=> 청팀 : 1, 2 / 백팀 : 3, 5 

 

3번 학생은 1번 학생, 4번 학생을 싫어한다.

=> 청팀 : 1, 2, 4 / 백팀 : 3, 5 

 

4번 학생은 3번 학생을 싫어한다.

=> 청팀 : 1, 2, 4 / 백팀 : 3, 5 (이미 팀 배분이 되어 있어, 변화가 없다.)

 

5번 학생은 2번 학생을 싫어한다.

=> 청팀 : 1, 2, 4 / 백팀 : 3, 5 (이미 팀 배분이 되어 있어, 변화가 없다.)

 

이렇게, 선택된 학생이 싫어하는 학생의 목록을 기반으로 팀에 차례대로 배분해주면 된다.

 

풀이 코드

std::vector<std::vector<int>> Hates;

std::vector<bool> isDevided;

//-1이면 청, 1이면 백
std::vector<int> DevidedTeam;

std::vector<int> BlueTeam;
std::vector<int> WhiteTeam;

 

먼저, 이렇게 5개의 벡터를 선언해주었다.

 

Hates는 인덱스에 해당하는 학생이 싫어하는 학생들의 목록을 저장한 벡터이다.

isDevided는 인덱스에 해당하는 학생이 싫어하는 학생들을 팀에 배분했는가를 담고 있는 벡터이다.

DevidedTeam은 학생들의 현재 팀을 저장한 벡터이다. 0이면 아직 팀 배분이 되지 않은 것이고, -1이면 청팀 1이면 백팀이다. 

BlueTeam과 WhiteTeam은 각각 팀에 포함된 학생들의 목록이다.

void Recursive(int _CurIndex)
{
    if (isDevided[_CurIndex] == true)
    {
        return;
    }

    isDevided[_CurIndex] = true;
    
    if (DevidedTeam[_CurIndex] == 0)
    {
        DevidedTeam[_CurIndex] = -1;
    }

    for (int i = 0; i < Hates[_CurIndex].size(); i++)
    {
        int CurHateIndex = Hates[_CurIndex][i];

        if (DevidedTeam[CurHateIndex] != 0)
        {
        	continue;
        }

        DevidedTeam[CurHateIndex] = -DevidedTeam[_CurIndex];
        
        Recursive(CurHateIndex);
    }
}

학생들이 현재 싫어하는 목록을 학생의 반대 팀에 배치한 뒤, 배치된 학생을 기준을 기준으로 함수를 재귀적으로 호출하여 다시 해당 학생이 싫어하는 학생들을 모두 반대 팀에 배치하도록 하였다.

싫어하는 학생을 배치한 학생의 인덱스에 대해 isDevided 를 true로 만들어, 같은 학생에 대해 여러 번 실행되지 않도록 하였다.

isDevided.resize(NumMan, false);
DevidedTeam.resize(NumMan, 0);

for (int i = 0; i < NumMan; i++)
{
    Recursive(i);
}

위의 함수를 모든 학생에 대해 실행하였다.

BlueTeam.reserve(NumMan);
WhiteTeam.reserve(NumMan);

for (int i = 0; i < NumMan; i++)
{
    if (DevidedTeam[i] == -1)
    {
        BlueTeam.push_back(i + 1);
    }
    else if (DevidedTeam[i] == 1)
    {
        WhiteTeam.push_back(i + 1);
    }
}

std::cout << BlueTeam.size() << "\n";

for (int i = 0; i < BlueTeam.size(); i++)
{
    std::cout << BlueTeam[i] << " ";
}

std::cout << "\n";
std::cout << WhiteTeam.size() << "\n";

for (int i = 0; i < WhiteTeam.size(); i++)
{
    std::cout << WhiteTeam[i] << " ";
}

학생들이 배치된 팀에 따라, 각 벡터에 삽입하였고, 해당 벡터의 사이즈와 원소를 모두 출력해주었다.

 

코드 전문

#include <iostream>
#include <vector>

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

std::vector<std::vector<int>> Hates;

std::vector<bool> isDevided;

//-1이면 청, 1이면 백
std::vector<int> DevidedTeam;

std::vector<int> BlueTeam;
std::vector<int> WhiteTeam;

void Recursive(int _CurIndex)
{
    if (isDevided[_CurIndex] == true)
    {
        return;
    }

    isDevided[_CurIndex] = true;

    if (DevidedTeam[_CurIndex] == 0)
    {
        DevidedTeam[_CurIndex] = -1;
    }

    for (int i = 0; i < Hates[_CurIndex].size(); i++)
    {
        int CurHateIndex = Hates[_CurIndex][i];

        if (DevidedTeam[CurHateIndex] != 0)
        {
            continue;
        }

        DevidedTeam[CurHateIndex] = -DevidedTeam[_CurIndex];

        Recursive(CurHateIndex);
    }
}

int main()
{
    Init();

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

    Hates.resize(NumMan);

    for (int i = 0; i < NumMan; i++)
    {
        int NumHate = 0;
        std::cin >> NumHate;

        Hates[i].reserve(NumHate);

        for (int j = 0; j < NumHate; j++)
        {
            int HateMan = 0;
            std::cin >> HateMan;

            Hates[i].push_back(HateMan - 1);
        }
    }

    isDevided.resize(NumMan, false);
    DevidedTeam.resize(NumMan, 0);

    for (int i = 0; i < NumMan; i++)
    {
        Recursive(i);
    }

    BlueTeam.reserve(NumMan);
    WhiteTeam.reserve(NumMan);

    for (int i = 0; i < NumMan; i++)
    {
        if (DevidedTeam[i] == -1)
        {
            BlueTeam.push_back(i + 1);
        }
        else if (DevidedTeam[i] == 1)
        {
            WhiteTeam.push_back(i + 1);
        }
    }

    std::cout << BlueTeam.size() << "\n";

    for (int i = 0; i < BlueTeam.size(); i++)
    {
        std::cout << BlueTeam[i] << " ";
    }

    std::cout << "\n";
    std::cout << WhiteTeam.size() << "\n";

    for (int i = 0; i < WhiteTeam.size(); i++)
    {
        std::cout << WhiteTeam[i] << " ";
    }

    return 0;
}

 

 

 

N개의 앱이 켜있다고 해보자.

 

각 앱은 m_1, m_2, m_3....m_n의 메모리를 사용하고 있으며, 각 앱을 비활성화하게 되면 각 c_1, c_2, c_3, c_4...c_n의 비용이 발생하게 된다.

 

M바이트를 확보하기 위해, 임의의 앱을 비활성화 했을 때 발생하는 비용의 최소값을 구하자.

 

문제 풀이

문제 자체는 간단한 배낭 문제이다. 다만, 일반적인 배낭 문제와는 다르게 최소값을 구하는 문제이기 때문에 떠올리는 것이나 구현하는 것이 다소 어려울 수도 있었을 것 같다.

 

배낭문제는 보통 개수와 수치(무게, 비용 등)을 이용해서 2차원 배열을 구성하게 된다. 위의 문제에서 개수, 메모리를 이용해서 2차원 배열을 구성하게 되면 최대 1000000000개의 원소를 가지는 배열을 만들어야 하므로 메모리 초과가 발생할 수 밖에 없다. 그러므로 2차원 배열은 (개수, 비용)을 이용해서 구성할 것이다.

 

그렇다면, 배열의 원소는 어떤 값을 저장해야 할까?

문제에서 구하는 것은 M바이트를 확보하기 위한 최소 비용이다.

 

반대로, N의 비용을 사용했을 때 최대한으로 확보할 수 있는 메모리를 구하면 어떨까?

문제에서 주어진 최대 비용은 10000이다. (100개의 앱 * 100의 비용)

 

그러므로, N의 비용을 사용했을 때 최대한으로 확보할 수 있는 메모리를 배열에 저장한 뒤 0 ~ 10000 까지의 비용 중 확보할 수 있는 메모리가 입력값보다 큰 비용을 구한 뒤, 그 값의 최소를 출력하면 되는 것이다.

 

배열의 원소를 갱신하는 것은 일반 배낭 문제와 동일하게 하면 된다.

 

풀이 코드

int NumApp = 0;
int TargetMem = 0;

std::cin >> NumApp >> TargetMem;

std::vector<int> AppMemories(NumApp + 1, 0);
std::vector<int> AppCosts(NumApp + 1, 0);

for (int i = 1; i <= NumApp; i++)
{
    std::cin >> AppMemories[i];
}

for (int i = 1; i <= NumApp; i++)
{
    std::cin >> AppCosts[i];
}

먼저, 입력을 모두 받아주자.

앱의 개수와 확보할 메모리를 입력받은 뒤, 각 앱의 메모리 사용량과 비활성화시 발생하는 비용을 모두 저장해주었다.

std::vector<std::vector<int>> DP(NumApp + 1, std::vector<int>(10001, 0));

다음으로 배낭 문제 해결에 사용할 DP 배열을 만들어주었다.

발생할 수 있는 최대 비용은 10000이므로, 배열의 size를 위와 같이 만들어주었다.

for (int i = 1; i <= NumApp; i++)
{
    for (int j = 0; j <= 10000; j++)
    {
        if (j >= AppCosts[i])
        {
            DP[i][j] = std::max(DP[i - 1][j], DP[i - 1][j - AppCosts[i]] + AppMemories[i]);
        }
        else
        {
            DP[i][j] = DP[i - 1][j];
        }
    }
}

일반적인 배낭문제와 동일하게 반복문을 돌려주면 된다.

각 원소의 값은 i개의 앱을 비활성화하여 j의 비용이 발생했을 때 확보할 수 있는 최대 메모리양이다.

int Answer = 0;

for (int i = 0; i <= 10000; i++)
{
    if (DP[NumApp][i] >= TargetMem)
    {
        Answer = i;
        break;
    }
}

이후, i를 0부터 10000까지 반복문을 돌며 TargetMem을 확보할 수 있는 최소 비용을 구해주면 된다.

std::cout << Answer;

답을 출력해주면 된다.

 

 

코드 전문

더보기
#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 NumApp = 0;
    int TargetMem = 0;
    
    std::cin >> NumApp >> TargetMem;

    std::vector<int> AppMemories(NumApp + 1, 0);
    std::vector<int> AppCosts(NumApp + 1, 0);

    for (int i = 1; i <= NumApp; i++)
    {
        std::cin >> AppMemories[i];
    }

    for (int i = 1; i <= NumApp; i++)
    {
        std::cin >> AppCosts[i];
    }

    std::vector<std::vector<int>> DP(NumApp + 1, std::vector<int>(10001, 0));

    for (int i = 1; i <= NumApp; i++)
    {
        for (int j = 0; j <= 10000; j++)
        {
            if (j >= AppCosts[i])
            {
                DP[i][j] = std::max(DP[i - 1][j], DP[i - 1][j - AppCosts[i]] + AppMemories[i]);
            }
            else
            {
                DP[i][j] = DP[i - 1][j];
            }
        }
    }

    int Answer = 0;

    for (int i = 0; i <= 10000; i++)
    {
        if (DP[NumApp][i] >= TargetMem)
        {
            Answer = i;
            break;
        }
    }

    std::cout << Answer;

    return 0;
}

 

 

.과 #으로 이루어진 2차원의 배열이 주어진다. 행의 길이는 n이며, 열의 길이는 m이다.

좌측상단의 좌표는 (1,1)이며, 우측 하단의 좌표는 (n, m)이다.

 

이 그리드 안에는 맨해튼 원이 존재한다. 맨해튼 원의, 중심을 (h, k)라고 하자.

이 때, (a, b)의 좌표를 지닌 점이, |h - a| + |k - b| < r 을 만족한다면 (a, b)는 맨해튼 원의 내부에 있는 것이다.

 

맨해튼 원의 내부에 속한 점을 #로 표시한 2차원 배열이 주어졌을 때, 맨해튼 원의 중심 좌표를 구하여 출력하면 된다.

 

문제 풀이

문제 해결은 간단하다.

가장 좌측의 점과 우측의 점 혹은 가장 상단의 점과 하단의 점을 구하면 된다.

 

맨해튼 원을 그려보면, 항상 마름모의 꼴을 나타내고 해당 마름모는 중심점을 기준으로 대칭을 이룬다.

또한, 원의 중심을 지나며 x축과 수평인 직선에 대해서도 대칭이며, 원의 중심을 지나며 y축과 수평인 직선에 대해서도 대칭을 이룬다.

 

맨해튼 원 내부의 점중 가장 좌측에 있는 점과 가장 우측에 있는 점 또한 원의 중심점을 기준으로 대칭을 이룬다.

그렇다면, 맨해튼 원 내부의 점중 가장 좌측에 있는 점과 우측에 있는 점의 중심점이 원의 중심이 될 것이다. 

 

풀이 코드

int Height = 0;
int Width = 0;

std::cin >> Height >> Width;

먼저, 배열의 행과 열의 크기를 입력받아주자.

std::pair<int, int> LeftPos = { 9999999,9999999 };
std::pair<int, int> RightPos = { -9999999, -9999999 };

맨해튼 원 내부의 점중 가장 좌측에 있는 점과 우측에 있는 점을 구하기 위해 선언해주자.

for (int i = 0; i < Height; i++)
{
    for (int j = 0; j < Width; j++)
    {
        char Input = 0;
        std::cin >> Input;

        if (Input == '#')
        {
            if (LeftPos.second > j)
            {
                LeftPos = { i, j };
            }

            if(RightPos.second < j)
            {
                RightPos = { i, j };
            }
        }
    }
}

입력을 받아 주면서, 현재 좌표에 대해 LeftPos와 RightPos를 계속 갱신해주자.

std::pair<int, int> Answer = { LeftPos.first + RightPos.first + 2, LeftPos.second + RightPos.second + 2};

Answer.first /= 2;
Answer.second /= 2;

최종적으로 구해진 leftPos와 RightPos의 중심점을 구해주자.

 

(첫번쨰 줄을 보면, LeftPos.first + RightPos.first에 2를 추가로 더해주고 있는데, 정답으로 출력될 좌표는 (1,1)을 시작으로 하기 때문에 각 좌표에 1씩 더해준 것이다.)

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

구한 정답을 출력해주면 된다.

 

코드 전문

#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 NumCase = 0;
    std::cin >> NumCase;

    for (int Case = 0; Case < NumCase; Case++)
    {
        int Height = 0;
        int Width = 0;

        std::cin >> Height >> Width;

        std::pair<int, int> LeftPos = { 9999999,9999999 };
        std::pair<int, int> RightPos = { -9999999, -9999999 };

        for (int i = 0; i < Height; i++)
        {
            for (int j = 0; j < Width; j++)
            {
                char Input = 0;
                std::cin >> Input;

                if (Input == '#')
                {
                    if (LeftPos.second > j)
                    {
                        LeftPos = { i, j };
                    }

                    if(RightPos.second < j)
                    {
                        RightPos = { i, j };
                    }
                }
            }
        }

        std::pair<int, int> Answer = { LeftPos.first + RightPos.first + 2, LeftPos.second + RightPos.second + 2};
        
        Answer.first /= 2;
        Answer.second /= 2;

        std::cout << Answer.first << " " << Answer.second << "\n";
    }


    return 0;
}

 

 

{A1, A2, A3, .... An} 처럼 n개의 원소를 가진 배열이 있다고 해보자.

특정 원소를 Ak를 제외한 나머지 원소를 모두 더한 값이 Ak가 되는 Ak가 존재한다면, 해당 Array는 Good이라고 한다.

예를 들어, {1, 6, 3, 2}에서 6을 제외한 나머지 원소의 합은 6이 된다. 그러므로, 이 배열은 Good이라고 할 수 있다.

 

{A1, A2, A3, ..., An}과 같이 n개의 원소로 이루어진 배열이 있다고 해보자.

{A1}, {A1, A2}, {A1, A2, A3} ... 등 배열의 가장 앞에서부터 잘라낸 배열을 Prefix라고 할 것이다.

An에 대한 모든 Prefix중 Good인 Array가 몇 개인지를 구하여 출력하자.

 

문제 풀이

배열에서 특정 원소 Ak를 제외한 나머지 원소의 총 합이 Ak가 된다면, 해당 배열은 Good이라고 할 수 있다.

그렇다면, 해당 배열에서 Ak가 될 수 있는 후보는 어떤 숫자가 있을까?

 

Ak가 될 수 있는 값은, 해당 배열의 원소중 최대값이다.

 

{1, 2, 3, 6}이라는 배열이 있다고 해보자. 여기서, 최대값은 6이다. 이 배열에서 만약 Ak를 최대값이 아닌 다른 값으로 잡는다면, (Ak = 최대값 + 다른 원소) 의 공식이 성립해야 한다. 즉, Ak는 배열의 원소중 최대값보다 더 큰 값이어야 한다는 것인데 이는 모순되는 말이므로 Ak는 반드시 배열의 원소중 최대값이어야 한다.

 

즉, (배열의 최대값 = 나머지 원소의 총합)이 성립한다면 해당 배열은 Good이라고 할 수 있으며, 성립하지 않는다면 Good이 아니라고 할 수 있다.

 

그렇다면, 주어진 배열의 모든 Prefix 에 대해 Good을 판단할 수 있게 된다.

std::vector<int> Array;

for(int i = 0; i < Array.size(); i++)
{    
    int Max = 0;
    int Sum = 0;
    
    for(int j = 0; j <= i; j++)
    {
        Max = std::max(Max, Array[j]);
        Sum += Array[j];
    }
    
    if(Max - Sum == Sum)
    {
        //Array is Good!
    }
}

위와 같이 이중 반복문을 사용해 모든 Prefix 배열에 대해 Good을 판별할 수 있다.

하지만, 이렇게 무식하게 판별을 하게 되면 시간초과가 발생할 것이다.

그러므로, 판별 과정을 조금 더 단순하게 만들 필요가 있다.

 

결국 배열이 Good인지 판단하기 위해 중요한 것은 배열의 원소의 총 합과 최대값이다.

(배열의 원소의 총 합 - 최대값 = 최대값) 을 만족한다면 배열은 Good이라고 할 수 있기 때문이다.

 

생각해보면, Prefix 배열의 원소 총 합과 최대값을 구하기 위해서 매번 Prefix 배열의 사이즈만큼 반복문을 돌 필요는 없다.

모든 Prefix배열은 입력으로 주어진 가장 앞에서부터 시작하며, size가 K인 Prefix배열은 size가 (k - 1)인 배열의 가장 뒤에 Ak를 추가한 형태일 뿐이기 때문이다.

 

즉, size가 k인 배열의 원소 총 합은 size가 (k - 1)인 배열의 원소 총 합에 Ak를 더한 것이다.

또한 size가 k인 배열의 원소중 최대값은 size가 (k - 1)인 배열의 원소중 최대값과 Ak중 더 큰 값이 된다.

이를 코드로 구현하면 아래와 같이 간단하게 한 번의 반복문으로 표현할 수 있게 된다.

std::vector<int> Array;

int Sum = 0;
int Max = 0;

for(int i = 0; i < Array.size(); i++)
{    
    Max = std::max(Max, Array[i]);
    Sum += Array[i];
    
    if(Max - Sum == Sum)
    {
        //Prefix Array is Good!
    }
}

 

풀이 코드

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

std::vector<long long> Array(ArraySize);
for (int i = 0; i < ArraySize; i++)
{
    std::cin >> Array[i];
}

먼저 배열을 입력받아 준다.

int GoodCount = 0;

long long MaxNum = 0;
long long Sum = 0;

다음은 값을 저장할 변수들을 선언해주었다.

for (int i = 0; i < ArraySize; i++)
{
    Sum += Array[i];
    MaxNum = std::max(Array[i], MaxNum);

    if (MaxNum - Sum == Sum)
    {
        GoodCount++;
    }
}

앞에서 설명한 것과 같이 반복문을 돌며 모든 prefix에 대해 Good 여부를 판별한 뒤, Good이라면 GoodCount를 증가시켜주었다.

 

모든 케이스에 대해 GoodCount를 구한 뒤 출력해주면 된다.

 

코드 전문

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

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

int main()
{
    Init();

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

    std::vector<int> Answers(NumCase);

    for (int Case = 0; Case < NumCase; Case++)
    {
        int ArraySize = 0;
        std::cin >> ArraySize;

        std::vector<long long> Array(ArraySize);
        for (int i = 0; i < ArraySize; i++)
        {
            std::cin >> Array[i];
        }

        int GoodCount = 0;

        long long MaxNum = 0;
        long long Sum = 0;

        for (int i = 0; i < ArraySize; i++)
        {
            Sum += Array[i];
            MaxNum = std::max(Array[i], MaxNum);

            if (MaxNum * 2 == Sum)
            {
                GoodCount++;
            }
        }

        Answers[Case] = GoodCount;
    }

    for (int i = 0; i < NumCase; i++)
    {
        std::cout << Answers[i] << "\n";
    }

    return 0;
}

 

 

밥은 N개의 빵을 판매할 것이다.

 

밥은 0 <= K <= min(n, b) 범위를 만족하는 K를 하나 고를 것이다.

K개의 빵을 판매할 때, i번째로 판매되는 빵의 가격은 (b - i + 1)원으로 판매할 것이다.

K개의 빵을 모두 판매한 뒤에 남은 빵은 모두 a원의 고정 가격으로 판매할 것이다.

 

이 때, K값에 따라 총 매출은 달라지는데 가능한 경우중 총 매출의 최대값을 구하여 출력하면 된다.

 

문제 풀이

이 문제는 특정할 알고리즘을 사용하기 보단 아이디어를 떠올려 해결하는 문제이다.

두 가지의 경우를 생각해보자.

 

1. b >= a 인 경우

K개의 빵에 대해, 판매되는 빵의 가격은 b원, b - 1원, b - 2원.....b - k + 1원이 된다.

이 때, 이 가격이 모두 a원보다 크거나 같은 값이 되어야 매출은 최대가 될 것이다.

(k가 너무 커서 b - k + 1이 a원보다 작게 되면, a원으로 판매할 때보다 손해를 보기 때문에)

 

즉, b - k + 1 = a 를 만족하는 k에 대해, 총 매출이 최대가 될 것이다.

 

2. b < a 인 경우

이 경우엔, K개의 빵에 대해 모두 a원보다 낮은 가격으로 판매하게 된다.

차라리 모든 빵을 a원으로 판매하는 것이 이득일 것이다.

그러므로, k가 0일 때 총 매출이 최대가 될 것이다.

 

위의 두 경우를 고려하여 답을 구하여 출력하면 된다.

 

풀이 코드

테스트케이스의 수를 입력받는 것은 제외하고 코드를 설명할 것이다.

long long N = 0;
long long A = 0;
long long B = 0;

std::cin >> N >> A >> B;

먼저 값을 입력받아 준다.

long long K = std::max((long long)0, std::min(N, B - A));

K값을 위와 같이 세팅해준다.

 

B가 A보다 크거나 같다면, B - A개만큼 A원보다 크거나 같은 값으로 판매할 수 있다. 반면, B가 A보다 작은 경우엔 1개도 A원보다 비싸게 판매할 수 없다. B가 A보다 작게 되면, B - A가 음수가 되므로 K값은 0이 된다.

 

또한 K의 범위는 0 <= K <= min(n, b) 이므로 B-A가 N보다 큰 경우 N으로 값을 조정해주었다.

long long Answer = GetProfit(K, NumOfBun, BasicPrice, FirstModifiedPrice);

GetProfit이라는 함수는 총 이익을 계산해주는 함수이다. 

long long GetProfit(long long _K, long long _N, long long _A, long long _B)
{
    long long SumProfit = 0;

    SumProfit += _B * (_B + 1) / 2;
    SumProfit -= (_B - _K) * (_B - _K + 1) / 2;
    SumProfit += _A * (_N - _K);

    return SumProfit;
}

등차수열의 합 공식을 사용하여 간단하게 총 이익을 구해주었다.

함수가 반환한 값을 출력해주면 된다.

 

 

코드 전문

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

long long GetProfit(long long _K, long long _N, long long _A, long long _B)
{
    long long SumProfit = 0;

    SumProfit += _B * (_B + 1) / 2;
    SumProfit -= (_B - _K) * (_B - _K + 1) / 2;
    SumProfit += _A * (_N - _K);

    return SumProfit;
}

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

    std::vector<long long> Answers(NumCase);

    for (long long Case = 0; Case < NumCase; Case++)
    {
        long long N = 0;
        long long A = 0;
        long long B = 0;

        std::cin >> N >> A >> B;

        long long K = std::max((long long)0, std::min(N, B - A));
        long long Answer = GetProfit(K, N, A, B);

        Answers[Case] = Answer;
    }

    for (int i = 0; i < NumCase; i++)
    {
        std::cout << Answers[i] << "\n";
    }

    return 0;
}

 

 

N개의 책과 각 책이 보유한 페이지 수가 주어진다.

각 책은 입력된 순서대로 1~N의 번호가 부여되어 있다.

 

이 때, N개의 책을 두 그룹으로 나눌 것이다. (각 그룹에는 최소한 1개의 책이 존재한다.)

Alice는 각 그룹에서 가장 번호가 높은 책을 하나씩 읽을 것이다.

 

가능한 경우 중에서, Alice가 읽을 수 있는 페이지 수의 총합의 최대값은 얼마인가?

 

문제 풀이

특정 알고리즘을 사용하기 보단, 아이디어를 떠올려 푸는 문제이다.

 

먼저, 모든 책을 두 그룹으로 나눈다면, 두 그룹 중 하나에는 반드시 N번째의 책이 포함되어 있을 것이다.

해당 그룹에선, 무슨 일이 있어도 N번째의 책을 읽을 수 밖에 없다. (N보다 큰 번호를 부여받은 책은 없으므로)

 

즉, 어떠한 경우에든 N번째의 책은 반드시 읽힌다. 그렇다면, N번째 책을 제외한 나머지 하나의 책의 페이지 수가 최대일 때의 앨리스가 읽은 페이지 수의 총합이 최대값이 될 것이다. 

 

즉 N번째 책을 제외한, 나머지 책 중 페이지수가 가장 많은 책이 해당 그룹에서 가장 높은 번호를 가지도록 그룹을 나누면 된다. (만약, M번째의 책이 N번째 책을 제외한 책중 페이지 수가 가장 많다면, 1~M, M+1~N으로 두 그룹을 나누어 M번째책과 N번째 책을 읽도록 할 수 있다.)

 

아이디어만 떠올리면 아주 간단하게 코드로 작성할 수 있다.

 

풀이 코드

테스트 케이스의 개수에 대한 입력은 제외하고, 코드를 설명할 것이다.

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

std::vector<int> Pages(NumBook);
for (int i = 0; i < NumBook; i++)
{
	std::cin >> Pages[i];
}

먼저, 입력을 받아 저장해주자.

책의 수와 각 책의 페이지 수를 저장해 주었다.

int Answer = Pages[NumBook - 1];

이후, N번째 책의 페이지 수를 Answer에 더해주자. (N번째의 책은 어떠한 경우에든 반드시 읽으므로)

int MaxPage = 0;
for (int i = 0; i < NumBook - 1; i++)
{
    MaxPage = std::max(MaxPage, Pages[i]);
}

N번째 책을 제외한 나머지 책들 중 페이지 수가 가장 많은 책을 탐색해주자.

Answer += MaxPage;

위에서구한 값을 Answer에 더해주면 끝이다.

Answer을 출력해주면 된다.

  

코드 전문

#include <iostream>
#include <vector>
#include <algorithm>

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

    std::vector<int> Answers(NumTestCase);

    for (int Case = 0; Case < NumTestCase; Case++)
    {
        int NumBook = 0;
        std::cin >> NumBook;

        std::vector<int> Pages(NumBook);
        for (int i = 0; i < NumBook; i++)
        {
            std::cin >> Pages[i];
        }

        int Answer = Pages[NumBook - 1];

        int MaxPage = 0;
        for (int i = 0; i < NumBook - 1; i++)
        {
            MaxPage = std::max(MaxPage, Pages[i]);
        }

        Answer += MaxPage;
        Answers[Case] = Answer;
    }

    for (int i = 0; i < NumTestCase; i++)
    {
        std::cout << Answers[i] << std::endl;
    }

    return 0;
}

 

 

로봇이 같은 곳을 한 번보다 많이 이동하지 않는다는 의미가 다소 헷갈렸는데, 방문했던 곳을 다시 방문하지 않는다는 의미였다. 즉, 로봇이 한 번 방문했던 곳을 방문하지 않고 이동하는 경우가 이동경로가 단순한 것이다.

 

로봇이 동, 서, 남, 북으로 이동할 수 있는 확률이 주어질 때 단순하게 이동하는 경우의 확률을 출력하는 문제이다.

 

문제 풀이

풀이는 간단하다. 단순하게 이동하는 모든 경우의 수에 대해 확률을 구한 뒤, 그 확률을 모두 더한 값이 정답이 된다.

모든 경우의 수를 어떻게 구할까?

 

DFS를 사용하면 쉽게 구할 수 있다. 먼저, 로봇이 방문한 거리를 체크하기 위한 이차원 배열을 하나 만들어줄 것이다.

이차원 배열의 크기를 설정할 때, 최소의 크기는 (N * 2 + 1) * (N * 2 + 1) 이 되어야 한다.

 

배열의 중앙에서 로봇이 이동을 시작한다고 했을 때, 상하좌우 중 한쪽으로만 쭉 이동한 경우 배열의 범위를 초과하지 않으려면 배열의 중앙을 기준으로 양 끝까지 최소 N의 거리가 필요하기 때문이다.

(왼쪽 N + 중앙 1칸 + 오른쪽 N) -> (2 * N + 1)

 

본인은 N에 따라 가변적으로 변하도록 구현하지는 않았고, 편하게 30 * 30의 배열을 선언하여 사용하였다.

(메모리를 최적화 하고 싶다면  (N * 2 + 1) * (N * 2 + 1) 의 크기로 만드는 것이 당연히 유리하다.)

 

로봇이 배열의 중앙에서 출발하도록 한 뒤, DFS를 사용하여 이동할 수 있는 모든 경로를 탐색하면 된다.

N번 이동을 마친 뒤, 해당 경로의 확률을 계산한 뒤 그 값을 하나의 변수에 누적해서 더해주면 된다.

 

풀이 코드

int MoveCount = 0;
double ProbSum = 0;

std::vector<double> Probs;
std::vector<std::vector<bool>> isVisit;

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

먼저, 위와 같이 전역변수를 선언하였다.

MoveCount는 N을 저장할 변수이며, ProbSum은 출력할 정답을 저장할 곳이다.

Probs는 상하좌우로 이동할 확률을 저장하고 있으며, isVisit는 위에서 설명했던 방문체크 배열이다.

DirX, DirY는 DFS에서 사용되는 상하좌우 이동에 사용될 배열이다.

Probs.resize(4);

std::cin >> MoveCount;

for (int i = 0; i < 4; i++)
{
	std::cin >> Probs[i];
	Probs[i] /= 100.0l;
}

위와 같이 입력을 받아준 뒤, Probs[i]의 모든 원소를 100.0l로 나누어준다.

(25로 입력이 들어오면, 0.25로 변환하여 확률 계산을 편하게 하기 위해서)

isVisit.resize(30, std::vector<bool>(30, 0));

위에서 설명했던 것과 같이 방문 배열을 30 * 30으로 resize해주었다.

DFS(15, 15, 0, 1.0l);

다음은 DFS 함수를 호출하였다. 첫번째와 두번째 인자는 시작 위치이며, 세번째 인자는 움직인 횟수이다.

4번째 인자는 누적확률이다.

함수 내부 코드를 보자.

void DFS(int _X, int _Y, int _CurMoveCount, double _ProbSum)
{
    if (_CurMoveCount >= MoveCount)
    {
        ProbSum += _ProbSum;
        return;
    }

    isVisit[_Y][_X] = true;

    for (int i = 0; i < 4; i++)
    {
        int NextX = _X + DirX[i];
        int NextY = _Y + DirY[i];

        if (Probs[i] == 0)
        {
            continue;
        }

        if (isVisit[NextY][NextX] == false)
        {
            DFS(NextX, NextY, _CurMoveCount + 1, _ProbSum * Probs[i]);
        }
    }

    isVisit[_Y][_X] = false;
}

가장 위의 조건문을 보면, 이동 횟수가 최대 이동 횟수보다 같거나 크게 된 경우 확률을 ProbSum에 누적해준 뒤 return해주었다.

 

그 다음, isVisit에 현재 위치에 대한 방문 체크를 해주었다.

다음은 4방향에 대해 아직 방문하지 않은 곳이라면 이동하도록 하도록 하였다.

이동할 때, 현재 방향에 대한 확률을 _ProbSum에 곱하여 확률을 갱신하도록 하였다.

(Probs 배열의 인덱스에 대해 0, 1, 2, 3을 동, 서, 남, 북으로 설정하였다.)

printf("%.10lf", ProbSum);

return 0;

DFS가 모두 끝난 뒤, 정답을 출력해주었다.

(문제의 조건이 최대 10^(-9)까지 오차를 인정하고 있기 때문에 소수점 10자리까지 줄력하도록 하였다.)

 

 

코드 전문

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

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

int MoveCount = 0;
double ProbSum = 0;

std::vector<double> Probs;
std::vector<std::vector<bool>> isVisit;

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

void DFS(int _X, int _Y, int _CurMoveCount, double _ProbSum)
{
    if (_CurMoveCount >= MoveCount)
    {
        ProbSum += _ProbSum;
        return;
    }

    isVisit[_Y][_X] = true;

    for (int i = 0; i < 4; i++)
    {
        int NextX = _X + DirX[i];
        int NextY = _Y + DirY[i];

        if (Probs[i] == 0)
        {
            continue;
        }

        if (isVisit[NextY][NextX] == false)
        {
            DFS(NextX, NextY, _CurMoveCount + 1, _ProbSum * Probs[i]);
        }
    }

    isVisit[_Y][_X] = false;
}

int main()
{
    Init();
    Probs.resize(4);

    std::cin >> MoveCount;
    
    for (int i = 0; i < 4; i++)
    {
        std::cin >> Probs[i];
        Probs[i] /= 100.0l;
    }

    isVisit.resize(30, std::vector<bool>(30, 0));
    
    DFS(15, 15, 0, 1.0l);

    printf("%.10lf", ProbSum);

    return 0;
}

 

 

N이라는 숫자를 2의 멱수 (2의 제곱수)의 합으로 표현하는 경우의 수를 구하는 문제이다.

 

문제 풀이

위의 문제는 DP를 사용하여 해결할 수 있다.

DP를 사용하기 위해선, 먼저 점화식을 세워야 한다.

 

DP배열의 i번째 인덱스의 원소는 i를 2의 멱수의 합으로 표현할 수 있는 경우의 수라고 하자.

그렇다면, DP[i]에 대한 점화식은 어떻게 될까?

 

먼저, N을 2의 멱수의 합으로 표현하기 위한 한 가지 간단한 경우가 있다.

(N - 1)을 2의 멱수의 합으로 표현한 것 뒤에 + 1을 더해주는 것이다.

 

예를 들어, 3을 2의 멱수의 합으로 표현하는 방법은 (1 + 1 + 1) 과 (1 + 2)가 있다.

이 식에 + 1만 추가해준다면, (1 + 1 + 1 + 1), (1 + 1 + 2)로 4를 2의 멱수로 표현하는 경우가 된다.

 

즉, DP[i - 1]의 값은 그대로 DP[i]에 포함된다고 할 수 있다. (모든 경우에 1만 더하면 i를 만들 수 있으므로)

 

그렇다면, DP[i] = DP[i -1]; 일까?

 

한 가지 경우를 더 고려해야 한다.

 

멱수의 합에 1이 포함되지 않은 경우이다.

2 + 4, 2 + 8, 4 + 4 등의 경우이다.

 

멱수의 합에 1이 포함되지 않았다면, 식을 이루는 모든 수가 짝수라는 의미가 된다.

즉 2로 묶을 수가 있게 된다. 2 + 4 = 2 * (1 + 2) , 2 + 8 = 2 * (1 + 4) 등등..

 

즉 N을 1을 제외한 2의 멱수의 합으로 표현하는 것은 2 * (N / 2를 멱수의 합으로 표현한 것) 이라고 할 수 있으며 이를 통해 DP[i / 2] 또한 DP[i]에 포함된다고 할 수 있다.

 

그렇다면 최종적으로 DP[i] = DP[i - 1] + DP[i / 2]라고 할 수 있다.

하지만, 이 것은 틀렸다.

 

1이 아닌 2의 멱수로만 합을 표현하는 경우 짝수의 합으로만 N을 구성하기 때문에 반드시 N은 짝수여야 한다.

즉 N이 홀수라면 DP[N / 2]를 더해선 안된다는 것이다.

 

그러므로 N이 홀수일 땐 DP[N] = DP[N -1]이 되며, N이 짝수일 땐 DP[N] = DP[N - 1] + DP[N / 2]라는 점화식이 성립한다.

 

풀이 코드

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

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

 

먼저, 목표 숫자를 입력받은 뒤 해당 숫자 + 1만큼 DP배열을 resize해준다.

그리고, DP[1]과 DP[2]를 1과 2로 초기화해준다.

 

for (int i = 3; i <= TargetNumber; i++)
{
    if (i % 2 == 0)
    {
        DP[i] = DP[i - 1] + DP[i / 2];
    }
    else
    {
        DP[i] = DP[i - 1];
    }

    DP[i] %= 1000000000;
}

다음은 위에 말했던 점화식을 그대로 사용하면 된다.

 

std::cout << DP[TargetNumber];
return 0;

마지막으로 목표 숫자에 해당하는 인덱스의 값을 출력해주면 된다.

 

 

코드 전문

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

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

int main()
{
    Init();

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

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

    for (int i = 3; i <= TargetNumber; i++)
    {
        if (i % 2 == 0)
        {
            DP[i] = DP[i - 1] + DP[i / 2];
        }
        else
        {
            DP[i] = DP[i - 1];
        }

        DP[i] %= 1000000000;
    }

    std::cout << DP[TargetNumber];
    return 0;
}

+ Recent posts