각 아이템 간의 선후 관계를 통해 최종적인 순서를 결정짓는 위상정렬 문제이다.

 

일반적인 위상정렬은 답이 여러개가 나올 수 있다. 왜냐하면 1과 2를 지어야 3을 지을 수 있는 상황에선 (1, 2, 3)도 가능하지만 (2, 1, 3)도 가능하기 때문이다. 하지만 이 문제는 사전순으로 아이템을 구매한다는 조건 때문에 답은 하나밖에 나올 수 없다. 그렇기 때문에 단순한 위상정렬로 끝내는 것이 아니라 각 아이템의 우선순위를 구해서 정렬까지 해 준 뒤에 답을 출력해야 한다.

 

 

예를 들어 예제 1번은 이런 식의 관계도가 나오는데, 가장 처음에 구매할 수 있는 3개의 아이템들 (goredrinker, stridebreaker, riftmaker)는 order가 0이고 galeforce는 order가 1이고 everfrost는 order가 2라고 매길 수 있다.

그렇다면 각 order에 대해 정렬을 수행한 뒤 출력해주면 된다.

 

그리고, 이 문제는답을 구할 수 없는 경우도 있는데 그 경우는 예외 처리를 해주어야 한다.

 

Case 1) Indegree가 0인 아이템이 없는 경우 (무한 순환)

Case 2) 위상정렬을 수행한 아이템의 수보다 실제 아이템의 수가 더 많은 경우 (무한 순환 싸이클이 중간에 포함된 경우)

 

#include <iostream>
#include <vector>
#include <map>
#include <unordered_map>
#include <queue>

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

int main()
{
    Init();

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

    std::unordered_map<std::string, int> InDegree;
    std::map<int, std::vector<std::string>> Order;

    std::unordered_map<std::string, std::vector<std::string>> NextItems;

    for (int i = 0; i < NumRelation; i++)
    {
        std::string Prev = "", Next = "";
        std::cin >> Prev >> Next;

        InDegree.insert({ Prev, 0 });
        InDegree[Next]++;

        NextItems[Prev].push_back(Next);
    }

    std::queue<std::pair<std::string, int>> Queue;
    
    for (auto Iter : InDegree)
    {
        if (Iter.second == 0)
        {
            Queue.push({ Iter.first, 0 });
            Order[0].push_back(Iter.first);
        }
    }

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

    int EraseCount = 0;

    while (Queue.size() > 0)
    {
        std::string ItemName = Queue.front().first;
        int ItemOrder = Queue.front().second;

        Queue.pop();

        EraseCount++;

        for (const std::string& NextItem : NextItems[ItemName])
        {
            int NextInDegree = --InDegree[NextItem];

            if (NextInDegree == 0)
            {
                Queue.push({ NextItem, ItemOrder + 1 });
                Order[ItemOrder + 1].push_back(NextItem);
            }
        }
    }

    if (EraseCount != InDegree.size())
    {
        std::cout << -1;
        return 0;
    }

    for (auto Pair : Order)
    {
        std::sort(Pair.second.begin(), Pair.second.end());

        for (auto Item : Pair.second)
        {
            std::cout << Item << "\n";
        }
    }

    return 0;
}

 

 

이전 문제와 동일하게 가장 긴 증가하는 부분수열을 구하는 문제이다. LIS를 구하는 NlogN의 알고리즘을 그대로 적용해주면 된다.

#include <iostream>
#include <vector>

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

int main()
{
    Init();

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

    std::vector<int> Link(NumPort);
    for (int i = 0; i < NumPort; i++)
    {
        std::cin >> Link[i];
    }
    
    std::vector<int> LIS;
    LIS.reserve(NumPort);

    for (int i = 0; i < Link.size(); i++)
    {
        auto Iter = std::upper_bound(LIS.begin(), LIS.end(), Link[i]);

        if (Iter == LIS.end())
        {
            LIS.push_back(Link[i]);
        }
        else
        {
            *Iter = Link[i];
        }
    }

    std::cout << LIS.size();
    return 0;
}

 

골드 2 문제인데, 매우 쉬웠다. 이 문제도 알면 쉽고 모르면 어려운 문제인데 LIS(가장 긴 증가하는 부분 수열) 문제이다.

LIS는 구하는 두 가지 방식이 있는데, 이중반복문을 사용해 N^2으로 구하는 방법과 이진탐색을 사용해 NlogN으로 구하는 방법이다.

 

이 문제는 당연히 NlogN의 방식으로 풀어야 시간초과가 안나는데, 이걸 모르는 사람이 스스로 떠올리기는 쉽지 않을 것이다. LIS 알고리즘을 그대로 적용하면 문제를 해결할 수 있다.

 

#include <iostream>
#include <vector>

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

int main()
{
    Init();

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

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

    std::vector<int> LIS;
    LIS.reserve(NumSize);

    for (int i = 0; i < NumSize; i++)
    {
        auto Iter = std::lower_bound(LIS.begin(), LIS.end(), Nums[i]);

        if (Iter != LIS.end())
        {
            *Iter = Nums[i];
        }
        else
        {
            LIS.push_back(Nums[i]);
        }
    }

    std::cout << NumSize - LIS.size();

    return 0;
}

그래픽스에서 오브젝트에 명암을 부여하기 위해 빛을 연산하는 방식은 크게 3가지가 있다.

바로 포워드 렌더링, 디퍼드 렌더링, 포워드+ 렌더링이다.

(참고로 이 셋은 레스터화 렌더링에서 사용하는 것이고 레이트레이싱은 아예 다른 방식으로 렌더링을 수행한다.)

 

포워드 렌더링

포워드 렌더링이란, 오브젝트를 렌더링할 때 빛을 바로바로 적용하는 방식이다. 하나의 오브젝트를 렌더링할 때, 레벨에 30개의 광원이 있다면 모든 광원에 대해 빛을 계산하여 색상을 결정짓게 된다. 가장 단순하면서 기본적인 렌더링 방식이다. 이 렌더링 방식은 구현이 단순하지만, 단점이 하나 있다. 바로 모든 오브젝트의 빛 계산을 수행한다는 것이다.

 

예를 들어, 이런 상황을 보자. 뒤쪽에 사람이 있는데 앞에 있는 건물에 의해 가려진다고 해보자. 이 경우 건물에 대해서도 빛 연산을 수행하고 사람에 대해서도 빛 연산을 수행하지만 실제로 사람의 색상은 모두 버려지게 된다. (깊이 테스트에 의해 건물의 색상만 남는다.)

 

그러므로, 불필요한 빛 연산이 수행된 것이다. 완전히 가려지는 경우라면 오클루전 컬링 등으로 아예 렌더링에서 제외할 수 있겠지만 반만 걸쳐있는 경우라면? 이 경우 사람의 반쪽에 해당하는 픽셀에 대해선 불필요한 연산이 들어갈 수 밖에 없다.

 

이처럼 연산을 했음에도 실제로 렌더링이 되지 않는 경우가 발생하기 문제를 개선하기 위해 디퍼드 렌더링이 탄생하였다.

 

디퍼드 렌더링

디퍼드 렌더링이란 이름 그대로 렌더링을 늦추는 것이다. 오브젝트의 드로우 콜이 발생하면 바로 빛을 적용하고 렌더타겟에 그리는 것이 아니라, 화면에 실제로 그려지는 부분만 추출하여 빛을 적용하는 것이다. 즉, 가려지는 부분에서 연산의 낭비가 발생하지 않는 것이다.

 

그렇다면 어떻게 이런 것을 할까?

출처 : https://www.3dgep.com/forward-plus/

 

이렇게 멀티 렌더타겟 기능을 활용하는 것이다. 빛 연산을 바로바로 적용하는 것이 아니라 각 오브젝트의 렌더링을 수행할 때 DiffuseColor, Normal, Specular, Depth를 각각의 타겟에 기록한다. 

 

모든 오브젝트가 다 기록되면, 마지막에 4개의 타겟을 이용해 하나로 합치는 것이다. DiffuseColor에 Specular, Normal, Depth를 활용해 빛을 적용하여 최종 렌더타겟에 출력하는 방식으로 말이다.

 

포워드 렌더링의 경우 드로우 콜이 발생한 오브젝트가 그려지는 픽셀 개수의 합이 Sum(P)이고 N개이고 광원이 M개라면 Sum(P) * N * M 만큼의 빛 연산이 발생한다.

 

하지만, 디퍼드 렌더링의 경우 오브젝트가 몇개든 화면 픽셀 개수 * M 만큼의 빛 연산만 발생하게 된다. 오브젝트 개수와 관계 없이 빛 연산이 수행되는 것이다. 

 

이러한 특징으로 인해, 디퍼드 렌더링은 한 화면에 렌더링되는 오브젝트가 많을수록 포워드 렌더링에 비해 현저히 빠른 렌더링 속도를 보여준다. 하지만, 반대로 말하면 렌더링되는 오브젝트가 적다면 포워드 렌더링이 더 빠를 확률이 높다.

 

또한, 디퍼드 렌더링은 한 번의 렌더링에서 여러개의 렌더타겟을 사용하는 만큼 GPU와 CPU 사이의 높은 대역폭을 필요로 한다. 현대의 하드웨어 성능에서 사실 문제되는 부분은 아니지만 이게 모바일 환경에서는 조금 얘기가 다르다. 본인도 하드웨어를 잘 아는 건 아니지만 높은 대역폭을 요구할수록 발열이 심해지고 이로 인해 전력 효율을 크게 감소시킨다고 한다. 그래서 모바일 환경에선 디퍼드 렌더링을 잘 사용하지 않는다고 한다.

 

이 외에도 디퍼드 렌더링은 치명적인 문제가 하나 있는데, 반투명 물체를 처리하는 것이 매우 까다롭다는 것이다. 포워드 렌더링은 반투명 물체를 렌더링할 때 기존의 색상과 블렌드해주면 그만이지만 디퍼드 렌더링은 깊이만을 사용해 물체를 기록하기 때문에 반투명한 물체가 제대로 기록되지 않는다. 

 

이러한 문제를 해결하기 위해 본인은 과거 프로젝트에서 렌더링 패스를 활용했었다. 불투명한 물체의 포워드 렌더링을 먼저 수행하고, 불투명한 물체의 디퍼드 렌더링을 수행하고, 마지막에 반투명 물체를 렌더링하는 것이다. (디퍼드 렌더링 자체에서 반투명을 해결하는 것이 매우 힘들다고 판단하여, 아예 반투명 물체는 별도로 렌더링한 것이다.)

 

반투명 물체를 먼저 렌더링하게 되면 디퍼드 렌더링 과정에서 그 뒤에 그려져야 하는 대상을 클리핑하기 때문에 반투명 물체를 가장 마지막에 렌더링해주었다.

 

포워드+ 렌더링

포워드 렌더링과 디퍼드 렌더링을 보면 둘 다 명확한 장단점이 있다.

 

포워드 렌더링은 오브젝트와 광원의 수가 많아질수록 빛 연산의 부담이 엄청 커지지만, 반투명 물체 등을 정확하게 렌더링할 수 있다. 반면 디퍼드 렌더링은 빛 연산이 오브젝트의 개수에 독립적으로 수행되기 때문에 상황에 따라 빛 연산을 크게 줄일 수 있지만 반투명 물체를 제대로 렌더링하지 못하는 등의 문제가 발생할 수 있다.

 

그렇다면, 포워드 렌더링에서 빛 연산을 최대한 줄일 수 있다면, 가장 좋은 결과가 아닌가? 하는 아이디어에서 나온 것이 포워드+ 렌더링이다. 

 

먼저, 포워드 렌더링을 수행할 때 광원을 무식하게 모두 렌더링하는 것이 아니라 라이트 컬링을 한 뒤에 렌더링을 수행한다. 렌더링 전에 오브젝트에 실제로 적용이 될 광원만 걸러내 쉐이더에 전달하는 것이다. 이를 통해 불필요한 빛 연산을 줄일 수 있게 된다. 하지만, 하나의 오브젝트에서도 위쪽에는 광원이 20개가 적용되는데 아래쪽에는 10개만 적용되는 등의 상황을 생각해보면 불필요한 빛연산이 모두 사라지는 것은 아니다.

 

이를 위해 화면을 여러개의 타일로 쪼개고, 각 타일별로 적용될 광원을 걸러내는 것이 포워드+ 렌더링이다.

출처 : https://www.3dgep.com/forward-plus/

 위의 그림처럼 화면을 타일로 쪼갠 뒤, 각 타일별로 적용되는 광원을 추출하고 해당 광원에 대해서만 연산을 수행하는 것이다. 이를 통해 오브젝트에서 수행하는 빛 연산을 최소한으로 줄일 수 있게 되는 것이다. 

 

이 방식도 물론 단점은 존재하는데, 타일을 너무 크게 쪼개면 불필요한 빛 연산이 포함되는 수가 많아지고, 타일을 너무 작게 쪼개면 메모리 사용량이나 타일 처리 과정에서 발생하는 연산 횟수가 증가한다는 것이다.

 

즉, 너무 잘게 쪼개면 감소한 빛 연산보다 오히려 타일 관리 부분에서 더 큰 오버헤드가 발생할 수도 있다는 것이다. 그래서 적절한 타일 크기를 구성하는 것이 중요하다고 한다.

 

모바일 게임에서 높은 퀄리티의 그래픽 품질을 구현하려면 연산 부담을 최소화 해야 하는데, 디퍼드 렌더링의 경우 빛 연산이 감소하더라도 높은 대역폭 요구치로 인한 문제가 더 크게 발생할 수 있어 포워드 렌더링이 거의 강제되는 상황이었는데 Forward+ 렌더링을 잘 활용하면 훨씬 높은 수준의 최적화를 달성할 수 있어서 모바일 게임에서 종종 사용된다고 한다.

 

 

숫자를 지워서 크게 만들려면, 앞자리의 수를 가장 크게 만들어야 한다.

즉 숫자를 앞자리부터 가능한 내림차순의 형태로 만드는 것이 가장 큰 숫자를 만드는 것이다.

 

예를 들자면, 129378 에서 2개의 숫자를 지운다면 1, 2를 지워 9378의 형태로 만드는 것이다. 여기서 1개를 더 지운다면 3을 지워 978의 형태로 만드는 것이 최대고, 하나를 더지운다면 7을 지워 98의 형태로 만드는 것이 최대값이다.

 

즉, 숫자를 가장 앞자리부터 하나씩 순회하며 앞의 숫자가 현재 숫자보다 작다면 앞의 숫자를 제거하는 것이다. 이를 위해 stack을 활용하였다.

 

문자열에서 그대로 삭제해버리면 시간 복잡도가 너무 커진다. 그러므로 문자열의 크기를 직접 변경하며 앞의 숫자를 제거하는 것이 아니라, stack에 숫자를 차례대로 삽입하고 삭제하며 바로 앞의 숫자를 빠르게 구하는 것이다.

 

그래서, 문자열에선 삭제된 문자를 'X'로 변경하고, 직전의 숫자는 stack을 통해 확인하도록 하였다. 문자를 하나 지울 때마다 카운팅을 하며, 지울 수 있는 최대 개수만큼 다 지웠다면 반복문을 종료해준다. 하지만, 최대 개수만큼 다 지우지 못하고 반복문이 종료되는 경우도 있다.

 

이러한 경우 마지막에 남은 삭제 개수만큼 숫자의 뒤에서부터 제거해준다. 왜냐하면, 반복문을 수행하며 지울수 있는 만큼 다 지웠다면 숫자는 이미 완벽한 내림차순의 형태일 것이다. (987653, 999999 이런 형태)

 

그렇다면, 앞의 숫자를 지우는 것보다 뒤의 숫자를 지우는 것이 더 큰 값을 만들기 때문에 뒤의 숫자를 지워야한다. 그렇게 모두 지운 다음, 문자열을 다시 순회하며 'X'가 아닌 숫자만 출력해주면 된다.

 

#include <iostream>
#include <vector>
#include <stack>

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

int main()
{
    Init();

    int NumLength = 0, DeleteCount = 0;
    std::cin >> NumLength >> DeleteCount;
    
    std::string Number = "";
    std::cin >> Number;
    
    std::stack<int> Indexs;

    for (int i = 0; i < NumLength; i++)
    {
        if (Indexs.size() == 0)
        {
            Indexs.push(i);
        }
        else
        {
            while(Indexs.size() > 0 && Number[Indexs.top()] < Number[i])
            {
                Number[Indexs.top()] = 'X';
                Indexs.pop();

                DeleteCount--;

                if (DeleteCount == 0)
                {
                    break;
                }
            }

            if (DeleteCount == 0)
            {
                break;
            }

            Indexs.push(i);
        }
    }

    while(Number.size() > 0 && DeleteCount > 0)
    {
        if (Number.back() != 'X')
        {
            DeleteCount--;
        }

        Number.pop_back();
    }

    for (int i = 0; i < Number.size(); i++)
    {
        if (Number[i] != 'X')
        {
            std::cout << Number[i];
        }
    }

    return 0;
}

 

이 문제는 알면 쉬운 문제이지만 모르면 어려운 그런 문제인 듯 하다. 항상 문제가 비슷비슷하게 나오지만, 모르면 풀기 어려운..

 

먼저, 각 선분의 시작점과 끝점을 모두 벡터에 넣어준다. pair로 하나의 벡터에 넣어주고, 다른 벡터에는 점을 하나씩 따로 넣어준다.

 

따로 넣어줄 때, 그냥 점의 위치만 넣는 것이 아니라 {점의 위치, int}의 형태의 페어로 넣어주어야 판다.

second인 int은 해당 점이 선의 시작점이냐 끝점이냐를 표시하는 것이다. 본인은 second가 0이면 시작점, 1이면 끝점으로 표시하였다.

 

그리고, 점을 넣은 벡터를 오름차순으로 sort해준다. 이후, 차례대로 점을 훑으며 second가 0이면 Count를 ++하고, second가 1이면 Count를 --해준다.

 

그러면서, Count의 최대값을 찾는 것이다. Count의 값이 현재 탐색하는 위치가 걸쳐있는 선분의 개수를 의미하기 때문이다. Count의 갱신을 조금이라도 최적화하기 위해, second가 0일 때에만 Count를 갱신해주어도 된다. 

 

이렇게 구한 Count가 출력해야 할 클리크의 크기 s가 된다. 하지만 우리는 크기만 구하는 것이 아니라 클리크의 정점을 모두 출력해야 한다.

 

이룰 위해, Count를 갱신할 때 추가적으로 Position을 구해줄 것이다. Position은 Count가 갱신되는 지점의 점 위치이다.

Count와 Positon을 구한 뒤에, 반복문을 한 번 더 돌아주면 되는데 이번엔 점을 넣은 벡터가 아니라 처음에 선분의 시작점, 끝점을 pair로 넣었던 벡터를 순회할 것이다.

 

우리가 구한 position이 걸쳐있는 선분의 인덱스를 구해서 출력할 것이기 때문이다. 벡터의 원소를 모두 순회하며 first <= position && position <= second 인 모든 인덱스를 출력해주면 된다.

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

    std::vector<std::pair<int, int>> Lines(NumLine);
    std::vector<std::pair<int, int>> Position(NumLine * 2);

    for (int i = 0; i < NumLine * 2; i +=2)
    {
        int Start = 0, End = 0;
        std::cin >> Start >> End;

        Lines[i / 2] = { Start, End };

        Position[i] = { Start, 0 };
        Position[i + 1] = { End, 1 };
    }

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

    int Answer = 0;
    int AnswerPosition = 0;

    int StartCount = 0;

    for (int i = 0; i < Position.size(); i++)
    {
        if (Position[i].second == 0)
        {
            StartCount++;

            if (StartCount > Answer)
            {
                AnswerPosition = Position[i].first;
                Answer = StartCount;
            }
        }
        else
        {
            StartCount--;
        }
    }

    std::cout << Answer << "\n";

    for (int i = 0; i < Lines.size(); i++)
    {
        if (Lines[i].first <= AnswerPosition && Lines[i].second >= AnswerPosition)
        {
            std::cout << i + 1 << " ";
        }
    }

    return 0;
}

 

이 문제의 아이디어를 찾는 것을 조금 헤맸는데, 본인은 이분매칭을 활용하였다. 이분 매칭이란, N개의 원소가 속한 그룹 A와 M개의 원소가 속한 그룹 B의 원소들을 1:1로 매칭시킬 수 있는 최대의 수를 구하는 알고리즘이다. 

 

이분 매칭을 활용해서, 조건에 맞도록 사람과 책을 매칭해주면 된다. 이분 매칭은 인접한 노드가 필요한데, 본인은 인접한 노드를 원하는 책의 범위(1~5면 1, 2, 3, 4, 5)로 설정하여 알고리즘을 수행하였다.

 

그런데, 속도가 조금 느렸다. 굉장히 빠르게 푼 다른 사람의 코드를 보았는데 이 사람은 정렬을 활용하였다.

원하는 책의 범위를 (x, y)라고 한다면 (x, y) 페어를 자료구조에 모두 넣어 정렬한 뒤, x가 같은 그룹만 추출하였고, 해당 그룹의 반복문을 돌며 (x~y) 순서대로 책을 분배하였다.

 

본인은 어느 순간부터 어떤 알고리즘을 사용해야 하는지만 고민하게 되었는데, 이런 창의적인 풀이도 떠올릴 줄 아는 여유가 필요할 듯 하다.

 

#include <iostream>
#include <vector>

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

bool Match(const std::vector<std::vector<int>>& _Link, std::vector<int>& _MatchedMan, std::vector<bool>& _IsVisit, int _CurIndex)
{
    _IsVisit[_CurIndex] = true;

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

        if (_MatchedMan[LinkNode] == -1 || (_IsVisit[_MatchedMan[LinkNode]] == false && Match(_Link, _MatchedMan, _IsVisit, _MatchedMan[LinkNode])))
        {
            _MatchedMan[LinkNode] = _CurIndex;
            return true;
        }
    }

    return false;
}

int main()
{
    Init();

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

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

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

        std::vector<std::vector<int>> Link(NumMan);
        
        for (int i = 0; i < NumMan; i++)
        {
            int Start = 0, End = 0;
            std::cin >> Start >> End;


            for (int j = Start; j <= End; j++)
            {
                Link[i].push_back(j - 1);
            }
        }

        std::vector<int> MatchedMan(NumBook, -1);
        std::vector<bool> isVisit(NumMan, false);

        int Answer = 0; 

        for (int i = 0; i < NumMan; i++)
        {
            std::fill(isVisit.begin(), isVisit.end(), false);

            if (Match(Link, MatchedMan, isVisit, i) == true)
            {
                Answer++;
            }
        }

        std::cout << Answer << "\n";
    } 

    return 0;
}

 

제목 그대로 최대 유량을 구하는 문제이다.

잘 알려진 최대 유량(네트워크 플로우) 알고리즘인 에드몬드-카프 알고리즘을 사용했다.

 

문제는 일반적인 에드몬드-카프 알고리즘을 사용하면 바로 풀리는 기본적인 문제이다. 다만, 양방향 파이프이기 때문에 동일한 경로의 파이프가 여러개있다면 최대 용량 값을 누적해주어야 한다는 점을 주의해야 한다.

 

또한, 입력이 대문자와 소문자로 주어진다는 점을 주의해야 한다... (이거때문에 한시간 헤맴;; 문제를 잘 읽자)

#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <map>

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

int main()
{
    Init();

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

    std::map<int, std::vector<int>> Link;
    std::vector<std::vector<int>> CurFlow(52, std::vector<int>(52, 0));
    std::vector<std::vector<int>> MaxFlow(52, std::vector<int>(52, 0));

    for (int i = 0; i < NumEdge; i++)
    {
        char Start = 0, End = 0;
        int Flow = 0;

        std::cin >> Start >> End >> Flow;

        if (Start >= 'A' && Start <= 'Z')
        {
            Start -= 'A';
        }
        else
        {
            Start -= 'a';
            Start += 26;
        }

        if (End >= 'A' && End <= 'Z')
        {
            End -= 'A';
        }
        else
        {
            End -= 'a';
            End += 26;
        }

        Link[End].push_back(Start);
        Link[Start].push_back(End);
        
        MaxFlow[Start][End] += Flow;
        MaxFlow[End][Start] += Flow;
    }

    std::vector<int> Prev(52, -1);
    int Answer = 0;

    while (true)
    {
        std::fill(Prev.begin(), Prev.end(), -1);

        std::queue<int> Queue;
        Queue.push(0);

        while (Queue.size() > 0)
        {
            int CurNode = Queue.front();
            Queue.pop();

            for (int i = 0; i < Link[CurNode].size(); i++)
            {
                int NextNode = Link[CurNode][i];

                if (CurFlow[CurNode][NextNode] < MaxFlow[CurNode][NextNode] && Prev[NextNode] == -1)
                {
                    Prev[NextNode] = CurNode;
                    Queue.push(NextNode);

                    if (NextNode == 'Z' - 'A')
                    {
                        break;
                    }
                }
            }
        }

        if (Prev['Z' - 'A'] == -1)
        {
            break;
        }

        int BTNode = 'Z' - 'A';
        int MinCapacity = INT_MAX;

        while (BTNode != 0)
        {
            MinCapacity = std::min(MinCapacity, MaxFlow[Prev[BTNode]][BTNode] - CurFlow[Prev[BTNode]][BTNode]);
            BTNode = Prev[BTNode];
        }

        BTNode = 'Z' - 'A';

        while (BTNode != 0)
        {
            CurFlow[Prev[BTNode]][BTNode] += MinCapacity;
            CurFlow[BTNode][Prev[BTNode]] -= MinCapacity;

            BTNode = Prev[BTNode];
        }

        Answer += MinCapacity;
    }

    std::cout << Answer << "\n";

    return 0;
}

  

 

중앙값이란, 정렬했을 때 중앙에 있는 값이다. 이 값을 추적하기 위해서 두 개의 우선순위 큐를 활용하였다.

하나의 큐는 기준 값보다 작거나 같은 값을 넣는 왼쪽 큐이고, 다른 하나의 큐는 기준값보다 큰 값을 넣는 오른쪽 큐이다.

왼쪽 큐는 탑이 최대값이 되도록, 오른쪽 큐는 탑이 최솟값이 되도록 하여 중앙에 있는 숫자를 확인할 수 있도록 했다.

 

첫 번째 원소를 먼저 왼쪽 큐에 넣고, 다음 원소부터 배열을 순회하며 왼쪽 큐의 top보다 작거나 같다면 왼쪽 큐에 원소를 삽입하고 그렇지 않다면 오른쪽 큐에 삽입하였다.

 

삽입 이후엔 왼쪽 큐와 오른쪽 큐의 size를 맞춰주었다. (항상 왼쪽 큐의 top이나 오른쪽 큐의 top이 중앙값이 되도록)

원소의 총 개수 (왼쪽 큐의 size + 오른쪽 큐의 size)가 짝수라면 left와 right는 size가 같아야 하고 홀수라면 두 큐의 차이는 1이어야 한다.

 

이후, 출력할 때엔 왼쪽 큐와 오른쪽 큐중 사이즈가 더 큰놈의 top을 출력해주었다.

 

이 문제의 최종보스는 출력 형식인데, 개행이나 띄어쓰기 때문인지 자꾸 틀렸다고 나와서 몇 번 고쳤더니 맞음처리되었다.

 

#include <iostream>
#include <string>
#include <vector>
#include <queue>

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<std::vector<std::string>> Answers(NumCase);

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

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

        Answers[Case].reserve(NumSize / 2 + 1);

        std::priority_queue<int> LeftNums;
        std::priority_queue<int, std::vector<int>, std::greater<>> RightNums;

        for (int i = 0; i < NumSize; i++)
        {
            if (LeftNums.size() == 0)
            {
                LeftNums.push(Nums[i]);
            }
            else
            {
                if (LeftNums.top() >= Nums[i])
                {
                    LeftNums.push(Nums[i]);
                }
                else if (LeftNums.top() < Nums[i])
                {
                    RightNums.push(Nums[i]);
                }
            }

            if (i % 2 != 0)
            {
                if (LeftNums.size() > RightNums.size())
                {
                    RightNums.push(LeftNums.top());
                    LeftNums.pop();
                }
                else if (LeftNums.size() < RightNums.size())
                {
                    LeftNums.push(RightNums.top());
                    RightNums.pop();
                }
            }
            else
            {
                if (LeftNums.size() < RightNums.size())
                {
                    Answers[Case].push_back(std::to_string(RightNums.top()));
                }
                else
                {
                    Answers[Case].push_back(std::to_string(LeftNums.top()));
                }
            }
        }
    }

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

        for (int j = 0; j < Answers[i].size(); j++)
        {
            std::cout << Answers[i][j];

            if ((j + 1) % 10 == 0 || j == Answers[i].size() - 1)
            {
                std::cout << "\n";
            }
            else
            {
                std::cout << " ";
            }
        }
    }

    return 0;
}

 

 

 

병사의 수가 100개밖에 되지 않기 때문에 가능한 모든 조합을 구해도 시간초과가 발생하지 않는다.

단순하게 3중반복문을 통해 모든 힘, 민첩, 지능의 조합을 구한 뒤, 그 조합으로 이길 수 있는 병사의 수가 몇명이 되는지 개수를 세어 K를 넘는지 확인하면 된다. (K를 넘을 시, 합의 최소값을 갱신)

 

총 4중 반복문을 무식하게 돌려주면 되고, 최악의 경우에 1억번의 연산이 발생하지만 다행히 시간 초과는 발생하지 않는다.  

 

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

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

struct Stat
{
    int Str = 0;
    int Dex = 0;
    int Int = 0;
};

std::vector<Stat> SoldierStats;

int main()
{
    Init();

    int NumSoldier = 0, NeedWin = 0;
    std::cin >> NumSoldier >> NeedWin;

    SoldierStats.resize(NumSoldier);

    for (int i = 0; i < NumSoldier; i++)
    {
        std::cin >> SoldierStats[i].Str >> SoldierStats[i].Dex >> SoldierStats[i].Int;
    }

    int Answer = 100000000;

    for (int i = 0; i < NumSoldier; i++)
    {
        for (int j = 0; j < NumSoldier; j++)
        {
            for (int k = 0; k < NumSoldier; k++)
            {
                int CurStr = SoldierStats[i].Str;
                int CurDex = SoldierStats[j].Dex;
                int CurInt = SoldierStats[k].Int;

                int Count = 0;

                for (int l = 0; l < NumSoldier; l++)
                {
                    if (SoldierStats[l].Str <= CurStr && SoldierStats[l].Dex <= CurDex && SoldierStats[l].Int <= CurInt)
                    {
                        Count++;
                    }

                    if (Count >= NeedWin)
                    {
                        Answer = std::min(Answer, CurStr + CurDex + CurInt);
                        break;
                    }
            	}
            }
        }
    }

    std::cout << Answer;

    return 0;
}

+ Recent posts