입력으로 문자열이 주어진다.

해당 문자열에서 1과 0을 절반만큼 제거하였을 때 나올 수 있는 문자열중 사전순으로 가장 빠른 것을 출력하면 된다.

 

예를 들어, 1010이라는 입력이 주어졌다고 가정해보자.

1이 2개이고, 0이 2개이므로 1을 1개 제거하고, 0을 1개 제거하면 된다.

 

경우는 아래와 같이 총 4개의 경우가 존재할 것이다.

 

즉, 나올 수 있는 문자열의 후보는 10, 01, 10, 10이다.

이 중, 사전순으로 가장 빠른 문자열은 01이므로, 01을 출력하면 된다.

 

문제 풀이

 

풀이는 간단하다.

제거해야할 1의 개수과 0의 개수를 세어준 뒤, 앞에서부터 1을 제거하고 뒤에서부터 0을 제거하면 된다.

 

0과 1로 이루어진 문자열중 사전순으로 빠른 문자열이라면, 문자열의 앞쪽에서부터 0이 가장 길게 배치되어있어야 한다.

 

000100, 000010 두 문자열을 비교한다면 첫 문자열은 앞에 0이 3개 배치되어있고 두 번째 문자열은 0이 4개 배치되어있다. 그러므로, 두 번째 문자열이 사전순으로 더 빠르다고 할 수 있을 것이다.

 

하지만, 이러한 경우도 있다.
000011, 000010 두 문자열의 경우엔 앞의 0의 개수는 똑같다. 그렇기 때문에 앞의 0의 개수로는 사전 정렬 순서를 비교할 수가 없다. 이 땐, 뒤에서부터 1이 몇개가 주어지는 지를 확인하면 된다.

 

뒤에서부터 세어봤을 때 1이 더 많다면, 사전순으로 더 느린 문자열이라고 할 수 있을 것이다.

 

그러므로, 어떤 문자열에서 1과 0을 제거해서 사전순으로 가장 빠른 문자열을 만들고 싶다면 앞쪽에 있는 1을 최대한 제거하고, 뒤쪽에 있는 0을 최대한 제거하는 것이 답이될 것이다.

 

 

 위의 문자열을 보자. 1의 총 개수는 4개이고, 0의 총 개수도 4개이다.

그러므로, 1을 2개 제거하고, 0을 2개 제거해야 한다.

 

그렇다면, 아래와 같이 제거하는 것이 가장 사전순으로 빠른 문자열을 만드는 방법일 것이다.

가장 앞에 있는 1을 2개 제거하였고, 가장 뒤에있는 0을 2개 제거하였다.

정답 문자열은 0011이 된다.

 

풀이 코드

std::string InputStr;
std::cin >> InputStr;

int ZeroCount = 0;
int OneCount = 0;

for (int i = 0; i < InputStr.size(); i++)
{
    if (InputStr[i] == '0')
    {
        ZeroCount++;
    }
    else
    {
        OneCount++;
    }
}

ZeroCount /= 2;
OneCount /= 2;

 

먼저, 문자열을 입력받은 뒤, 1의 개수와 0의 개수를 세주었다.

그 다음, 1의 개수와 0의 개수를 절반으로 나눠 제거해야 할 개수를 구해주었다.

std::string Answer;
Answer.reserve(ZeroCount + OneCount);

다음은 정답을 저장할 문자열을 선언하고, reserve를 이용하여 필요한 크기만큼 메모리를 할당해주었다.

for (int i = 0; i < InputStr.size(); i++)
{
    if (OneCount > 0)
    {
        if (InputStr[i] == '1')
        {
            OneCount--;
            InputStr[i] = '2';
        }
    }
    else
    {
        break;
    }
}

 

입력받은 문자열을 앞에서부터 선형으로 탐색하며, 현재 문자가 1이라면 OneCount를 1감소시켰고, 동시에 입력 문자열에서 해당 부분을 2로 바꿔주었다. (제거했다는 의미)

 

그리고, OneCount의 수만큼 모두 제거해주었다면 반복문을 종료해주었다.

 

for (int i = InputStr.size() - 1; i >= 0; i--)
{
    if (ZeroCount > 0)
    {
        if (InputStr[i] == '0')
        {
            ZeroCount--;
            InputStr[i] = '2';
        }
    }
    else
    {
        break;
    }
}

 

다음은 뒤에서부터 탐색하며 동일한 과정을 거쳐주었다.

문자가 0이라면, 문자를 2로 바꿔주고 ZeroCount를 감소시키며 계속 탐색하다 ZeroCount가 0이 되면 반복문을 종료해주었다.

 

for (int i = 0; i < InputStr.size(); i++)
{
    if (InputStr[i] != '2')
    {
        Answer.push_back(InputStr[i]);
    }
}

 

마지막으로 입력 문자열에서 2가 아닌 문자만 추출하여 Answer에 삽입해주었다.

 

std::cout << Answer;

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();

    std::string InputStr;
    std::cin >> InputStr;

    int ZeroCount = 0;
    int OneCount = 0;

    for (int i = 0; i < InputStr.size(); i++)
    {
        if (InputStr[i] == '0')
        {
            ZeroCount++;
        }
        else
        {
            OneCount++;
        }
    }

    ZeroCount /= 2;
    OneCount /= 2;

    std::string Answer;
    Answer.reserve(ZeroCount + OneCount);

    for (int i = 0; i < InputStr.size(); i++)
    {
        if (OneCount > 0)
        {
            if (InputStr[i] == '1')
            {
                OneCount--;
                InputStr[i] = '2';
            }
        }
        else
        {
            break;
        }
    }

    for (int i = InputStr.size() - 1; i >= 0; i--)
    {
        if (ZeroCount > 0)
        {
            if (InputStr[i] == '0')
            {
                ZeroCount--;
                InputStr[i] = '2';
            }
        }
        else
        {
            break;
        }
    }

    for (int i = 0; i < InputStr.size(); i++)
    {
        if (InputStr[i] != '2')
        {
            Answer.push_back(InputStr[i]);
        }
    }

    std::cout << Answer;

    return 0;
}

코드를 보다보니, 윈도우 함수를 초기화하면서 윈도우 크기를 설정을 안해놓은 것을 확인했다..

그래서 먼저 윈도우 크기 설정부터 추가하였다.

 

Init함수는 윈도우 크기 인자를 추가로 받도록 해주었다.

 

Init함수 내부에선 이 크기를 멤버변수에 저장해주었다.

 

이후, CreateWindow할 때, 윈도우의 크기를 세팅해주었다.

 

이제, DirectX를 초기화해보자.

 

먼저, Init함수 내부를 조금 수정해주었다.

BOOL EngineBase::Init(HINSTANCE _hInstance, int _Width, int _Height)
{
    WindowWidth = _Width;
    WindowHeight = _Height;

    if (!WindowInit(_hInstance))
    {
        return FALSE;
    }

    if (!DirectXInit())
    {
        return FALSE;
    }

    return TRUE;
}

 

이렇게, Window초기화와 DirectX초기화를 나누어주었다.

 

BOOL EngineBase::DirectXInit()
{
    if (!CreateDevice())
    {
        std::cout << "CreateDevice() is Failed!" << std::endl;
        return FALSE;
    }

    if (!CreateSwapChain())
    {
        std::cout << "CreateSwapChain() is Failed!" << std::endl;
        return FALSE;
    }

    if (!CreateRasterizerState())
    {
        std::cout << "CreateRasterizerState() is Failed!" << std::endl;
        return FALSE;
    }

    if (!CreateDepthStencil())
    {
        std::cout << "CreateDepthStencil() is Failed!" << std::endl;
        return FALSE;
    }

    SetViewport();

    return TRUE;
}

 

DirectXInit함수 내부에서도 만들어야 하는 것들을 구분해서 만들도록 하였다.

 

BOOL EngineBase::CreateDevice()
{
    //하드웨어 드라이버를 사용할건가 소프트웨어 드라이버를 사용할건가
    const D3D_DRIVER_TYPE DriverType = D3D_DRIVER_TYPE_HARDWARE;

    //디버그 기능을 사용할 것인가
    UINT CreateDeviceFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    CreateDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
#endif

    //다이렉트X의 버전 목록 (만약 컴퓨터에 버전이 없다면, 더 낮은 버전으로 Device 생성을 시도
    const D3D_FEATURE_LEVEL featureLevels[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_9_3 };
    
    //생성된 Device의 다이렉트X 버전을 저장
    D3D_FEATURE_LEVEL featureLevel;

    HRESULT Result = D3D11CreateDevice(
        nullptr,                  // nullptr이면 기본 어댑터를 사용
        DriverType,               // 어떤 드라이버를 사용하여 Device를 만들 것인가
        0,                        // 소프트웨어 드라이버를 사용할 것이라면, 어떤걸 사용할지 선택하는 옵션인듯
        CreateDeviceFlags,        // 플래그
        featureLevels,            // 다이렉트X 버전 목록 배열
        ARRAYSIZE(featureLevels), // 위의 배열의 사이즈
        D3D11_SDK_VERSION,        // 무조건 D3D11_SDK_VERSION 쓰라고 써있네
        &Device,                  // 생성된 디바이스를 저장
        &featureLevel,            // 생성된 디바이스의 DirectX 버전을 저장
        &Context                  // 생성된 디바이스의 Context를 저장
    );

    if (Result != S_OK)
    {
        return FALSE;
    }

    return TRUE;
}

CreateDevice함수는 위와 같다.

Device가 무사히 생성되었다면, TRUE를 리턴할 것이다.

 

다음은 SwapChain을 만들어보자.

BOOL EngineBase::CreateSwapChain()
{
    HRESULT Result = S_OK;

    Microsoft::WRL::ComPtr<IDXGIDevice> DXGIDevice;
    Result = Device.As(&DXGIDevice);

    Microsoft::WRL::ComPtr<IDXGIAdapter> DXGIAdapter;
    Result = DXGIDevice->GetAdapter(&DXGIAdapter);

    Microsoft::WRL::ComPtr<IDXGIFactory> DXGIFactory;
    Result = DXGIAdapter->GetParent(IID_PPV_ARGS(&DXGIFactory));

    SwapChain.Reset();

    //멀티샘플링 안티에일리어싱 (MSAA)
    UINT numQualityLevels;
    Device->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM, 4, &numQualityLevels);
    if (numQualityLevels <= 0) 
    {
        std::cout << "MSAA not supported!" << std::endl;
    }

    DXGI_SWAP_CHAIN_DESC SD;
    ZeroMemory(&SD, sizeof(SD));
    SD.BufferDesc.Width = (UINT)WindowWidth;                 // 백버퍼 사이즈 (너비)
    SD.BufferDesc.Height = (UINT)WindowHeight;               // 백버퍼 사이즈 (높이)
    SD.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 색상 포맷
    SD.BufferCount = 2;                                // 백버퍼 개수
    SD.BufferDesc.RefreshRate.Numerator = 60;          // 갱신률 (분자)
    SD.BufferDesc.RefreshRate.Denominator = 1;         // 갱신률 (분모)
    SD.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;  // 스왑체인을 어떻게 쓸 것인가
    SD.OutputWindow = hWnd;                            // 스왑체인이 사용될 윈도우
    SD.Windowed = TRUE;                                // 창모드, 전체모드 
    SD.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // 창모드, 전체모드 전환을 허용할 것인가
    SD.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;  

    SD.SampleDesc.Count = 4;
    SD.SampleDesc.Quality = numQualityLevels - 1;

    Result = DXGIFactory->CreateSwapChain(Device.Get(), &SD, SwapChain.GetAddressOf());

    if (Result != S_OK)
    {
        return FALSE;
    }

    //백버퍼의 렌더타겟 뷰 생성
    ID3D11Texture2D* BackBuffer;
    SwapChain->GetBuffer(0, IID_PPV_ARGS(&BackBuffer));

    if (BackBuffer) 
    {
        Device->CreateRenderTargetView(BackBuffer, NULL, &RenderTargetView);
        BackBuffer->Release();
    }
    else 
    {
        std::cout << "CreateRenderTargetView() failed!" << std::endl;
        return FALSE;
    }

    return TRUE;
}

 

스왑체인을 만들면서, 멀티샘플링 설정도 해주었다.

뭔가 길고 복잡해보이지만, 그냥 원하는대로 설정하면 된다.

 

안에 보면, BackBuffer의 경우엔 Release를 호출해주고 있는데, BackBuffer는 Comptr을 사용해서 선언한 것이 아니라 직접 Release를 호출해주어야 한다.

 

Comptr로 선언한 애들은 프로세스가 종료될 때 알아서 해제해준다.

BOOL EngineBase::CreateRasterizerState()
{
    D3D11_RASTERIZER_DESC RD;
    ZeroMemory(&RD, sizeof(D3D11_RASTERIZER_DESC));
    RD.FillMode = D3D11_FILL_MODE::D3D11_FILL_SOLID;
    RD.CullMode = D3D11_CULL_MODE::D3D11_CULL_BACK;
    RD.FrontCounterClockwise = false;

    HRESULT Result = Device->CreateRasterizerState(&RD, &RasterizerState);
    if (Result != S_OK)
    {
        return FALSE;
    }
    
    return TRUE;
}

 

RasterizerState도 만들어주었다.

후면 컬링도 설정해주었다.

BOOL EngineBase::CreateDepthStencil()
{
    D3D11_TEXTURE2D_DESC DepthStencilBufferDesc;

    DepthStencilBufferDesc.Width = WindowWidth;
    DepthStencilBufferDesc.Height = WindowHeight;
    DepthStencilBufferDesc.MipLevels = 1;
    DepthStencilBufferDesc.ArraySize = 1;

    DepthStencilBufferDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
   
    if (NumQualityLevels > 0) 
    {
        DepthStencilBufferDesc.SampleDesc.Count = 4;
        DepthStencilBufferDesc.SampleDesc.Quality = NumQualityLevels - 1;
    }
    else 
    {
        DepthStencilBufferDesc.SampleDesc.Count = 1; 
        DepthStencilBufferDesc.SampleDesc.Quality = 0;
    }

    DepthStencilBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    DepthStencilBufferDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
    DepthStencilBufferDesc.CPUAccessFlags = 0;
    DepthStencilBufferDesc.MiscFlags = 0;

    HRESULT Result = Device->CreateTexture2D(&DepthStencilBufferDesc, 0, DepthStencilBuffer.GetAddressOf());

    if (Result != S_OK)
    {
        return FALSE;
    }

    Result = Device->CreateDepthStencilView(DepthStencilBuffer.Get(), 0, &DepthStencilView);
    
    if (Result != S_OK)
    {
        return FALSE;
    }

    D3D11_DEPTH_STENCIL_DESC DepthStencilDesc;
    ZeroMemory(&DepthStencilDesc, sizeof(D3D11_DEPTH_STENCIL_DESC));
    DepthStencilDesc.DepthEnable = true;
    DepthStencilDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK::D3D11_DEPTH_WRITE_MASK_ALL;
    DepthStencilDesc.DepthFunc = D3D11_COMPARISON_FUNC::D3D11_COMPARISON_LESS_EQUAL;

    Result = Device->CreateDepthStencilState(&DepthStencilDesc, DepthStencilState.GetAddressOf());

    if (Result != S_OK)
    {
        return FALSE;
    }

    return TRUE;
}

 

뎁스스텐실 버퍼와 뎁스스텐실 뷰도 생성해보았다.

void EngineBase::SetViewport()
{
    ZeroMemory(&ScreenViewPort, sizeof(D3D11_VIEWPORT));
    ScreenViewPort.TopLeftX = 0;
    ScreenViewPort.TopLeftY = 0;
    ScreenViewPort.Width = float(WindowWidth);
    ScreenViewPort.Height = float(WindowHeight);
    ScreenViewPort.MinDepth = 0.0f;
    ScreenViewPort.MaxDepth = 1.0f;
    
    Context->RSSetViewports(1, &ScreenViewPort);
}

 

마지막으로, 뷰포트 세팅까지 해주면서, 기본적인 초기화는 얼추 끝났다..

 

 

홍정모 강사님의 그래픽스 강의를 듣다가 갑자기 프로젝트를 하나 만들고 싶었다.

 

해당 강의는 아무래도 다이렉트 X 자체보다는 그래픽스에 더 집중하는 부분이 있었다. 물론 이 부분은 교육에 정말 도움이 되고 긍정적으로 생각하고 있다. 어느 그래픽스 라이브러리를 사용하더라도, 쉽게 적응할 수 있도록 이론적인 부분에 더 초점을 두신 것 같다.

 

그렇게, 큰 틀은 대부분 직접 구현하신 상태로 그래픽스에 관한 부분만 비워서 직접 작성해보며 테스트 할 수 있게 예제 프로젝트를 제공해주시는데, 갑자기 왠지 모를 욕심이 생겨서 나만의 프로젝트를 만들어서 초기화하며 교육 내용을 직접 적용해보고 싶다는 생각이 들었다.

 

그런 이유로, DirectX를 이용한 그래픽스 프로젝트를 시작하기로 했다...

프로젝트라고 하지만 거창한 무언가를 할 계획은 아니고, 강의를 들으며 배운 내용을 나의 프로젝트에 적용해보며 구현해볼 생각이다. 

 

먼저 Windows API기반으로 프로젝트를 생성한 뒤, DirectXTK를 프로젝트에 추가해주었다.

(홍정모 강사님의 강의에서 SimpleMath를 사용하셔서, 추가하게 되었다.)

 

그리고, Main함수에 있는 윈도우 초기화 함수들을 정리해주었다.

#include "framework.h"
#include "GraphicsProject.h"
#include "EngineBase.h"

#include <iostream>
#include <d3d11.h>

#pragma comment (lib, "d3d11.lib")
#pragma comment(linker, "/entry:wWinMainCRTStartup /subsystem:console")

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    EngineBase NewEngineBase;

    if (!NewEngineBase.Init(hInstance))
    {
        std::cout << "Init Failed!" << std::endl;
        return -1;
    }

    NewEngineBase.Loop();

    WPARAM EndParam = NewEngineBase.End();

    return (int)EndParam;
}

 

먼저, EngineBase라는 클래스를 하나 정의하였고, 해당 함수에 Init, Loop, End 함수를 만들었다.

Init에선 여러 초기화를 실행하도록 정리하였고, Loop에선 메세지 루프가 실행되도록 하였다.

End는 프로그램이 종료될 때 처리해야 할 것들을 구현하기 위해서 정의하였다.

 

EngineBase에는 기존에 작성되어 있던 윈도우 초기화 함수들을 정리해서 넣어주었다.

#pragma once
#include "Windows.h"

class EngineBase
{

public:

    EngineBase();
    ~EngineBase();

    EngineBase(const EngineBase& _Other) = delete;
    EngineBase(EngineBase&& _Other) noexcept = delete;
    EngineBase& operator=(const EngineBase& _Other) = delete;
    EngineBase& operator=(EngineBase&& _Other) noexcept = delete;

    ATOM MyRegisterClass(HINSTANCE hInstance);
    static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
    BOOL InitInstance(HINSTANCE hInstance, int nCmdShow);
    
    BOOL Init(HINSTANCE _hInstance);
    void Loop();
    WPARAM End();

protected:

private:
   HINSTANCE hInstance;
   WNDCLASSEXW wcex;
   MSG msg;
};

 

먼저, HINSTANCE나 MSG같은 중요한 변수들은 멤버변수에 저장해주었다.

 

멤버함수들의 구현부를 보면 아래와 같다.

BOOL EngineBase::Init(HINSTANCE _hInstance)
{
    hInstance = _hInstance;

    MyRegisterClass(hInstance);

    if (!InitInstance(hInstance, SW_SHOW))
    {
        return FALSE;
    }

    return TRUE;
}

 

Init함수에선 MyRegisterClass함수를 먼저 실행해주었다.

해당 함수는 윈도우를 생성하기 전에, 어떻게 생성할지에 관한 여러 정보를 세팅하는 함수이다.

 

InitInstance함수는 윈도우를 생성하는 함수이다. 만약 윈도우 생성이 실패했다면, init함수는 FALSE를 반환하도록 하였다.

main함수에서 Init이 FALSE를 반환할 경우, 콘솔창에 메세지를 출력하도록 함으로써 디버깅을 좀 더 수월하게 하였다.

ATOM EngineBase::MyRegisterClass(HINSTANCE _hInstance)
{
    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = &EngineBase::WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = _hInstance;
    wcex.hIcon = nullptr; //LoadIcon(_hInstance, MAKEINTRESOURCE(IDI_GRAPHICSPROJECT));
    wcex.hCursor = nullptr; //LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = nullptr; //MAKEINTRESOURCEW(IDC_GRAPHICSPROJECT);
    wcex.lpszClassName = L"WindowDefault";
    wcex.hIconSm = nullptr; //LoadIcon(wcex._hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&wcex);
}

 

myRegisterClass함수 내부이다.

윈도우의 모양이나 색상, 이름 등을 정의할 수 있는데, 지금 당장 필요하다고 생각하는 것들이 딱히 없어서 대부분 nullptr로 설정해주었다.

 

wcex.lpfnwndproc를 보면, 메세지 함수를 콜백 형식으로 저장할 수 있는데 본인은 멤버함수를 사용하기 위해, WndProc함수를 static으로 선언해주었다.

 

BOOL EngineBase::InitInstance(HINSTANCE _hInstance, int nCmdShow)
{
    hInstance = _hInstance;

    HWND hWnd = CreateWindowW(L"WindowDefault", L"GraphicsProject", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

    if (!hWnd)
    {
        return FALSE;
    }

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    return TRUE;
}

 

InitInstance함수 내부도 심플하다. 윈도우를 생성해주었고, 실패시 FALSE를 반환하도록 하였다.

 

다음은 Loop함수를 보자.

void EngineBase::Loop()
{
    while(WM_QUIT != msg.message)
    {
        if ((PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
}

 

기본적인 메세지 루프만 담고있다.

While문이나 if문의 조건문을 조금 수정해주었다.

 

WM_QUIT 메세지가 전달되기 전까지 반복문을 계속 돌도록 하였으며, 메세지를 전달받으면 해당 메세지를 처리하도록 해주었다.

 

 

다음은 End이다.

WPARAM EngineBase::End()
{
    return msg.wParam;
}

 

아직은 뭐 추가한 기능이 하나도 없어서, End에서 처리할 것이 따로 없다.

기존에 메인함수에서 msg.wparam을 return하도록 되어있길래, 동일하게 이 것을 리턴해주며 main함수에서는 반환된 값을 저장하여 return하도록 해주었다.

 

WindowAPI의 기초적인 함수만 정리하는데도 생각보다 시간이 좀 소요되었다.

쓸모없는 기능은 쳐내고, 필요한 기능은 남겨두면서 정리를 하려고 했는데, 뭐가 쓸모없는 기능인지 찾는 것도 쉽지 않았다.

 

일단, 간단한 틀은 완성되었고 이 틀에 계속 살을 붙혀서 원하고자 하는 것을 렌더링 할 수 있을 때까지 가보도록 하겠다.

 

 

주어진 문자열에서, 두 문자를 바꿔가며 a가 연속된 문자열을 만들면 된다.

예를 들어, abbababa라는 문자열이 있다고 치자.

 

이 문자열에서 4번째 문자와 7번째 문자를 바꿔보자.

abbbbaaa가 된다.

 

일반적인 문자열이라면, a가 연속되지 않는다고 생각할 수 있다.

하지만, 이 문자열은 원형으로 이어져 있다고 가정되어 있으므로 4개의 a는 모두 이어져 있다고 볼 수 있다.

 

두 문자를 바꾸는 '교환'을 최소 몇 번 해야 a가 모두 연속된 문자열이 될 수 있는지 탐색하여 최소 교환 횟수를 출력하면 된다.

 

문제 풀이

 

이 문제는 아이디어를 구하는 것이 다소 어려운 문제였다.

본인은 슬라이딩 윈도우 기법을 활용하여 문제를 해결하였다.

 

먼저, 아래 그림을 보자.

 

이렇게 문자열이 주어졌다고 가정해보자.

이 때, 이 문자열에는 a가 총 3개, b가 총 5개 포함되어 있다.

 

그렇다면, 이렇게 상황을 생각해 볼 수 있지 않을까?

문자열에서 위처럼 a개수와 동일한 3개의 공간을 확보한 뒤에, 거기에 a를 가득 채우면 나머지 공간은 b로 가득 찰 것이고, 결국 a가 연속된 문자열이 완성되는게 아닐까?

이렇게, 어떤 위치를 잡든 a개수만큼 공간을 할당한 뒤 a로 가득채우면 나머지 공간은 b로 가득 찰 것이고, 결국 a가 연속된 형태의 문자열을 만들 수 있을 것이다.

 

그런데, 굳이 새로운 문자열을 만들 필요도 없고, 아래와 같이 만들 수 있을 것이다.

기존 문자열에서 a개수만큼의 공간을 탐색한 뒤, 안에 있는 b를 전부 밖의 a랑 바꿔버린다면?

이렇게 a가 연속된 형태의 문자열이 완성된다.

다른 위치라고 해도, 마찬가지로 a가 연속된 문자열을 만들 수 있다.

 

이를 토대로 생각해본다면, a로 모두 바꿔버리기로 마음 먹은 구간에 b가 2개있다면, 총 2번의 교환을 통해 a가 연속된 문자열을 만들 수 있다는 뜻일 것이다.

 

그렇다면, 문자열 내에서 a개수만큼의 범위를 모두 탐색하여 b가 가장 적게 들어있는 구간을 찾는다면?

해당 구간에 속한 b의 개수가 최소 교환 횟수가 될 것이다.

 

아래 과정을 보자.

처음에 3칸의 구간을 탐색해보면,b가 2개 속해있다. 이 때, 교환횟수는 2회가 된다.

이 구간엔 b가 3개 속해있으므로, 교환 횟수는 3회가 된다.

이 구간 역시 마찬가지로 교환 횟수는 3회가 된다.

 

쭉 가서, 마지막 구간을 보자.

이 구간에선, b가 1개밖에 존재하지 않는다. 즉 교환 횟수는 1회가 된다.

그러므로, 이 문자열에서 연속된 a를 만들기 위해선 최소 교환횟수는 1회라고 할 수 있다.

 

하지만, 이렇게만 탐색하면 한가지 문제가 있다.

위에서 구한 방식대로, 위의 문자열에서 최소 교환 횟수를 탐색해보면 1회가 나온다.

a의 개수와 동일한 5칸으로 이루어진 구간중에 b의 최소 개수는 1개이기 때문이다.

 

그런데, 문제를 생각해보면 해당 문자열은 단 1번도 바꾸지 않아도 이미 a가 연속된 문자열이다.

그렇기 때문에, 우리는 한가지 경우를 더 생각해야 한다.

 

구간에 a를 몰아넣을 경우가 아니라, 구간에 b를 몰아넣는 경우이다.

b의 개수만큼의 구간을 기준으로, 구간에 b를 몰아넣는다고 가정하고 탐색하면 위의 그림처럼 b로 가득찬 구간을 탐지할 수 있다. 이 때, 해당 영역에 속한 a의 개수는 0개이므로 교환 횟수는 0회가 나온다.

 

즉, 구간에 a를 몰아넣는 경우의 최소 교환 횟수와 구간에 b를 몰아넣는 경우의 최소 교환 횟수를 둘 다 구한 뒤에

둘 중에서 최소값을 정답으로 출력해야 하는 것이다.

 

풀이 코드

 

std::string InputStr;
std::cin >> InputStr;

int NumOfA = 0;
int NumOfB = 0;

for (int i = 0; i < InputStr.size(); i++)
{
    if (InputStr[i] == 'a')
    {
        NumOfA++;
    }
    else
    {
        NumOfB++;
    }
}

문자열을 입력받은 뒤, a의 개수와 b의 개수를 각각 세어주고 있다.

 

int Answer1 = 0;
int Answer2 = 0;

이후, 구간에 a를 몰아넣는 경우의 최소 교환횟수를 저장할 Answer1과, 구간에 b를 몰아넣는 경우의 최소 교환횟수를 저장할 Answer2를 선언해주었다.

 

{
    int WindowSize = NumOfA;
    int BCount = 0;

    for (int i = 0; i < WindowSize; i++)
    {
        if (InputStr[i] == 'b')
        {
            BCount++;
        }
    }

    int MinBCount = BCount;

    for (int i = 1; i < InputStr.size() - WindowSize; i++)
    {
        if (InputStr[i - 1] == 'b')
        {
            BCount--;
        }

        if (InputStr[i + WindowSize - 1] == 'b')
        {
            BCount++;
        }

        MinBCount = std::min(MinBCount, BCount);
    }

    Answer1 = MinBCount;
}

 

먼저, A의 개수만큼 WindowSize를 설정해주었다.

(탐색할 구간의 크기이다.)

 

최초에는 0~ WindowsSize - 1까지 b가 몇개 있나 탐색해주었고, 이후 구간을 우측으로 1씩 움직이며, 기존 구간에서 b가 빠졌다면 b의 개수를 빼주고, 새롭게 b가 구간에 추가되었다면 b의 개수를 더해주면서 BCount를 갱신해주었다.

{
    int WindowSize = NumOfB;
    int ACount = 0;

    for (int i = 0; i < WindowSize; i++)
    {
        if (InputStr[i] == 'a')
        {
            ACount++;
        }
    }

    int MinACount = ACount;

    for (int i = 1; i < InputStr.size() - WindowSize; i++)
    {
        if (InputStr[i - 1] == 'a')
        {
            ACount--;
        }

        if (InputStr[i + WindowSize - 1] == 'a')
        {
            ACount++;
        }

        MinACount = std::min(MinACount, ACount);
    }

    Answer2 = MinACount;
}

 

다음은 완전히 동일하게 구간에 b를 몰아넣는 경우의 최소 교환 횟수를 구해주었다.

 

int Answer = std::min(Answer1, Answer2);

std::cout << Answer;

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();

    std::string InputStr;
    std::cin >> InputStr;

    int NumOfA = 0;
    int NumOfB = 0;

    for (int i = 0; i < InputStr.size(); i++)
    {
        if (InputStr[i] == 'a')
        {
            NumOfA++;
        }
        else
        {
            NumOfB++;
        }
    }

    int Answer1 = 0;
    int Answer2 = 0;

    {
        int WindowSize = NumOfA;
        int BCount = 0;

        for (int i = 0; i < WindowSize; i++)
        {
            if (InputStr[i] == 'b')
            {
                BCount++;
            }
        }

        int MinBCount = BCount;

        for (int i = 1; i < InputStr.size() - WindowSize; i++)
        {
            if (InputStr[i - 1] == 'b')
            {
                BCount--;
            }

            if (InputStr[i + WindowSize - 1] == 'b')
            {
                BCount++;
            }

            MinBCount = std::min(MinBCount, BCount);
        }

        Answer1 = MinBCount;      
        
        int WindowSize = NumOfB;
        int ACount = 0;

        for (int i = 0; i < WindowSize; i++)
        {
            if (InputStr[i] == 'a')
            {
                ACount++;
            }
        }

        int MinACount = ACount;

        for (int i = 1; i < InputStr.size() - WindowSize; i++)
        {
            if (InputStr[i - 1] == 'a')
            {
                ACount--;
            }

            if (InputStr[i + WindowSize - 1] == 'a')
            {
                ACount++;
            }

            MinACount = std::min(MinACount, ACount);
        }

        Answer2 = MinACount;
    }

    int Answer = std::min(Answer1, Answer2);

    std::cout << Answer;

    return 0;
}

 

 

주차장에는 주차 공간이 N개만큼 있다.

이 주차장에는 오늘 하루동안 M개의 자동차가 주차와 출차를 반복할 예정이다.

 

각 주차 공간에는 번호가 부여되어 있으며, 주차 공간 별로 요금이 상이하다.

요금의 기준은 자동차의 무게이며, 단위 무게당 받아야 하는 금액이 주차 공간별로 책정되어 있다.

 

예를 들어, 1번 주차공간이 단위 무게당 3원의 요금을 받는다면 200의 무게를 가진 자동차는 1번 주차공간에 주차하게 되면 200 * 3인 600원의 요금을 지불해야 하는 것이다.

 

주차를 하고자 하는 자동차는 비어있는 주차 공간중 가장 번호가 낮은 공간에 우선적으로 주차를 해야한다.

1, 3, 5번의 공간이 비어있다면 자동차는 무조건 1번에 주차를 해야하는 것이다.

 

하지만, 주차공간이 모두 꽉차있는 경우도 있을 수 있는데 이 경우엔 별도의 대기구역에 자동차가 들어온 순서대로 대기를 하게 되며 기존의 자동차가 출차하여 주차 공간이 새로 생길 경우, 대기구역에 먼저 들어온 자동차가 우선적으로 새로운 주차공간에 주차를 하게 된다.

 

이 규칙으로 주차장을 운영했을 때, 하루동안 주차장이 벌어들일 총 수익을 출력하면 되는 문제이다.

 

문제 풀이

문제를 이해하는 것이 다소 헷갈리지만, 이해만 한다면 그대로 구현하면 되는 문제이다.

본인은 해당 주차장을 구현하기 위해, 1개의 우선순위 큐, 2개의 큐, 2개의 벡터를 선언하였다.

 

우선순위 큐는 현재 비어있는 주차공간의 번호와 해당 주차공간의 단위무게당 요금을 pair로 담고있다.

두개의 큐는 대기공간에 들어온 차량의 순서를 저장하는 큐 한개와 주차, 출차기록을 담고있는 큐 한개이다.

두개의 벡터는 차량 번호별 무게를 담고있는 벡터 한개와, 현재 차량이 주차하고 있는 주차공간의 정보를 담고있는 벡터이다.

 

주차, 출차 과정은 아래와 같다.

 

1.먼저 주차,출차 기록을 담은 큐에서 가장 위의 원소를 꺼내어 확인해준다.

 

2 - 1.해당 값이 양수라면 비어있는 주차공간을 담은 우선순위 큐에서 가장 위의 원소를 꺼내어, 해당 공간에 차량을 주차한다. (만약, 비어있는 주차공간이 없다면 대기공간 큐에 차량을 삽입하여준다.)

 

2 - 2. 해당 값이 음수라면, 차량이 주차되어있는 주차 공간의 정보를 참조하여 요금을 계산한 뒤 총 수익에 더해준다.

(이 때, 대기공간 큐에 차량이 1개이상 있다면, 가장 위의 차량을 꺼내어 해당 주차공간에 주차해준다.)

 

3. 주차, 출차 기록을 담은 큐의 원소가 사라질때까지 반복한다.

 

4. 총 수익을 출력해준다.

 

위의 과정을 거치면, 문제는 간단하게 해결된다.

 

풀이 코드

int NumOfSpace = 0;
int NumOfCar = 0;
std::cin >> NumOfSpace >> NumOfCar;

  먼저, 주차 공간의 수와 차량의 수를 입력받아 준다.

 

std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> Spaces;
for (int i = 0; i < NumOfSpace; i++)
{
    int Price = 0;
    std::cin >> Price;

    Spaces.push({ i + 1, Price });
}

이후, 우선순위 큐를 하나 선언하여 주차공간의 번호와 가격을 저장해준다.

이 때, 우선순위 큐는 주차 공간의 번호가 작은 원소가 더 앞에 있도록 정렬되어야 한다.

 

std::vector<int> CarWeights(NumOfCar + 1);
for (int i = 1; i <= NumOfCar; i++)
{
    std::cin >> CarWeights[i];
}

std::queue<int> InAndOut;
for (int i = 0; i < 2 * NumOfCar; i++)
{
    int Record = 0;
    std::cin >> Record;
    InAndOut.push(Record);
}

다음은 자동차의 무게와 주차,출차기록을 입력받아서 저장해준다.

 

std::vector<std::pair<int, int>> CarStopSpace(NumOfCar + 1);
std::queue<int> WaitQueue;

자동차가 현재 주차되어 있는 공간의 정보를 저장하는 벡터와 대기 차량을 담을 큐를 선언해주었다.

 

int Revenue = 0;
while (InAndOut.size() > 0)
{
    int CurRecord = InAndOut.front();
    InAndOut.pop();

    if (CurRecord > 0)
    {
        if (Spaces.size() == 0)
        {
            WaitQueue.push(CurRecord);
        }
        else
        {
            CarStopSpace[CurRecord] = Spaces.top();
            Spaces.pop();
        }
    }
    else
    {
        int CurCar = abs(CurRecord);
        Spaces.push(CarStopSpace[CurCar]);

        Revenue += CarStopSpace[CurCar].second * CarWeights[CurCar];

        if (WaitQueue.size() > 0)
        {
            CarStopSpace[WaitQueue.front()] = Spaces.top();
            Spaces.pop();
            WaitQueue.pop();
        }
    }
}

 

위에서 말한 과정을 반복문으로 구현하였다.

먼저, 총 수익을 저장할 Revenue 변수를 선언하였다.

 

이후 주차, 출차 기록이 담긴 큐에서 원소를 하나씩 빼며 검사하도록 하였으며 해당 큐의 원소가 모두 사라지면 반복문을 종료하도록 하였다.

 

반복문 내에서는 먼저 현재 주차, 출차 기록이 양수인지 음수인지를 확인하여 주차인지 출차인지를 판별하였다.

 

만약 주차라면, 비어있는 주차공간이 있나 없나를 판단한 뒤, 주차공간이 있다면 해당 영역에 차량을 주차해주었고, 비어있는 공간이 없다면 대기차량에 차량을 삽입해주었다.

 

차량을 주차했기 때문에, 비어있는 주차공간을 담고 있는 Space의 가장 위의 원소를 pop하였으며, 차량이 주차된 공간의 정보를 담은 CarStopSpace의 값을 해당 주차공간의 값으로 갱신해주었다.

 

반대로, 현재 기록이 출차라면 차량의 무게와 주차공간의 요금을 참조하여 총 요금을 계산한 뒤 Revenue에 더해주었다.

그리고 이 때, 주차공간이 하나 비게 되므로 대기하고 있는 차량이 있다면 해당 차량을 주차해주도록 하였다.

 

std::cout << Revenue;

return 0;

 

모두 끝나고, Revenue를 출력해주면 문제 해결이다.

 

 

코드 전문

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

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

int main()
{
    Init();

    int NumOfSpace = 0;
    int NumOfCar = 0;
    std::cin >> NumOfSpace >> NumOfCar;
    
    std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> Spaces;
    for (int i = 0; i < NumOfSpace; i++)
    {
        int Price = 0;
        std::cin >> Price;

        Spaces.push({ i + 1, Price });
    }
    
    std::vector<int> CarWeights(NumOfCar + 1);
    for (int i = 1; i <= NumOfCar; i++)
    {
        std::cin >> CarWeights[i];
    }

    std::queue<int> InAndOut;
    for (int i = 0; i < 2 * NumOfCar; i++)
    {
        int Record = 0;
        std::cin >> Record;
        InAndOut.push(Record);
    }

    std::vector<std::pair<int, int>> CarStopSpace(NumOfCar + 1);
    std::queue<int> WaitQueue;

    int Revenue = 0;
    while (InAndOut.size() > 0)
    {
        int CurRecord = InAndOut.front();
        InAndOut.pop();

        if (CurRecord > 0)
        {
            if (Spaces.size() == 0)
            {
                WaitQueue.push(CurRecord);
            }
            else
            {
                CarStopSpace[CurRecord] = Spaces.top();
                Spaces.pop();
            }
        }
        else
        {
            int CurCar = abs(CurRecord);
            Spaces.push(CarStopSpace[CurCar]);

            Revenue += CarStopSpace[CurCar].second * CarWeights[CurCar];

            if (WaitQueue.size() > 0)
            {
                CarStopSpace[WaitQueue.front()] = Spaces.top();
                Spaces.pop();
                WaitQueue.pop();
            }
        }
    }

    std::cout << Revenue;

    return 0;
}

 

게임 프로그래밍에서 벡터는 너무나도 많이 사용되고 중요한 존재이다.

그런 벡터를 사용한 여러 연산 중 특히나 자주 쓰이고 중요하게 사용되는 두 가지의 연산이 있다.

바로 내적과 외적이다.

 

이번 게시글에선 내적을 알아볼 것이다.

 

벡터의 내적

벡터의 내적은 아래와 같이 점 기호로 표현한다.

$\vec{a} \cdot \vec{b}$

 

내적은 기본적으로, 두 벡터가 가지고 있는 각 원소의 곱의 합이다.

두 벡터를 아래와 같이 정의해보자.

$\vec{a} = \left \{ a_1, b_1, c_1 \right \}, \vec{b} = \left \{ a_2, b_2, c_2 \right \}$

 

두 벡터를 내적하게 되면 아래와 같다.

$\vec{a} \cdot \vec{b} = a_1 \times a_2\, + \,b_1 \times b_2 \,+\, c_1 \times c_2$

 

내적의 특징은 결과값이 스칼라로 나온다는 것이다.

더보기

벡터 : 방향, 크기를 모두 가지고 있다.

ex) {1, 3} , {2, 5, 7}, {5, -1} ....

 

스칼라 : 크기만 가지고 있다.

ex) 3, 6, 8.4, 1.6 .... 

내적 연산에는 몇 가지 성질이 있다.

 

1. 교환법칙이 성립한다.

$\vec{a} \cdot \vec{b} = \vec{b} \cdot \vec{a}$

 

2.분배법칙이 성립한다.

$\vec{a} \cdot \left ( \vec{b}\,+\, \vec{c} \right ) = \vec{a}\cdot \vec{b} + \vec{a}\cdot \vec{c}$

 

3. 영벡터와의 내적은 결과값이 반드시 0이다.

$\vec{0}\cdot \vec{a} = 0$

 

4. 같은 벡터를 내적하면, 해당 벡터 길이를 제곱한 값이 나온다.

$\vec{a}\cdot \vec{a} = \left | a \right |^2$

 

이 외에도 여러 성질이 있지만, 일단 여기까지만 알아보도록 하자.

 

내적에서 중요한 것은 아래의 공식이다.

$\vec{a}\cdot \vec{b} = | \vec{a} |\times| \vec{b}|  \times cos(\theta)$

 

두 벡터의 내적은 두 벡터의 길이와 두 벡터의 사잇각(theta)의 cos값을 곱한 것과 같다.

 

아래는 증명과정이다. 

더보기

 

제 2 코사인 법칙을 활용하면, 아래 공식을 유도할 수 있다.

$\vec{a} - \vec{b}|^2 = \left | \vec{a} \right |^{2} + |\vec{b}|^{2} - 2|a||b|cos(\theta )$

 

위에서 설명한 내적의 성질을 활용하면, 좌항에 대해 아래와 같은 식을 유도할 수 있다.

$|\vec{a} - \vec{b}|^2 = |\vec{a} - \vec{b}|\cdot |\vec{a} - \vec{b}|$

$|\vec{a} - \vec{b}|\cdot |\vec{a} - \vec{b}| = |\vec{a}|^2 -2\times \vec{a}\cdot \vec{b} + |\vec{b}|^2$

 

이제, 두 식을 합쳐보자.

$ |\vec{a}|^2 -2\times \vec{a}\cdot \vec{b} + |\vec{b}|^2 = \left | \vec{a} \right |^{2} + |\vec{b}|^{2} - 2|a||b|cos(\theta )$

 

식을 정리하면 아래와 같다.

$\vec{a}\cdot \vec{b}= |a||b|cos(\theta )$

위의 공식을 활용하면 두 벡터의 내적을 이용해, 두 벡터 사이의 각도를 구할 수 있다는 것이다.

게임 수학에서 두 벡터 사이의 각도를 구해야하는 일은 상당히 많다.

 

예를 들면, 빛의 방향벡터와 물체의 법선벡터의 사잇각을 이용해 굴절, 반사 등의 효과를 적용하기도 하고, 플레이어와 적의 위치관계를 알아내는데에도 사용된다.

 

 

 

4개의 배열이 주어지고, 동일한 개수의 숫자가 각 배열에 할당된다.

이 때, 각 배열에서 1개씩 선택하여 나온 4개의 숫자를 더한 값이 0이되는 경우의 수를 구하면 된다.

 

문제 풀이

 

처음엔 메모리나 시간초과를 생각하며, 이 방법이 진짜 맞나? 이런 생각이 들었지만 결론적으로는 그냥 생각나는대로 풀면 된다.

 

먼저, A,B,C,D 이렇게 주어진 4개의 배열을 (AB) (CD)이렇게 2개로 줄여주었다.

 

A와 B의 원소를 합해서 나올 수 있는 모든 경우의 수를 하나의 배열에 저장해주었고, C와 D도 동일하게 합해서 나올 수 있는 모든 경우의 수를 하나의 배열에 저장해주었다.

 

이렇게 합치고 나면, 두개의 배열만 남게 되는데 두 배열에서 각 원소를 합했을 때 0이되는 경우의 수를 구해주면 된다. 

 

4개의 배열에서 합했을 때 0이되는 경우의 수는 구하기 매우 까다롭고 경우가 많지만, 2개의 배열이라면 정렬해준 뒤에 투 포인터나 이분탐색으로 쉽게 답을 구할 수 있다. 본인은 이분탐색을 활용하였다.

 

엄밀히 말하면 이분탐색을 사용하는 std함수인 std::lower_bound와 std::upper_bound를 사용하였다.

더보기

std::lower_bound : 배열에서 특정 값보다 크거나 같은 원소가 처음으로 나온 위치를 반환해준다.

ex) {1, 2, 3, 4, 5} 에서 3을 lower_bound에 대입한다면, 3이 처음으로 나온 3번째 원소를 가리키는 이터레이터를 반환해준다.

 

std::upper_bound : 배열에서 특정 값을 초과하는 원소가 처음으로 나온 위치를 반환해준다.

ex) {1, 2, 2, 4, 5} 에서 2를 upper_bound에 대입한다면, 2보다 큰 값이 처음으로 나온 4번째 원소를 가리키는 이터레이터를 반환해준다.

lower_bound와 upper_bound를 사용한 이유는 배열 내에 동일한 값이 여러개 있을 경우를 고려하였기 때문이다.

 

예를 들어, AB와 CD의 원소가 각각 (-3, -4) (4, 4) 라고 한다면, 두 원소를 합해서 0이되는 경우는 2가지이다.

이 때, AB의 원소 -4를 기준으로 했을 때, CD에 존재하는 4의 개수가 -4와 합해서 0이되는 경우의 수가 될 것이다.

 

그러므로, (upper_bound를 통해 나온 인덱스 - lower_bound를 통해 나온 인덱스)의 값이 해당 원소와 합했을 때 0이되는 원소의 개수라고 판단할 수 있다.

 

AB를 기준으로, 원소를 하나씩 순회하면서 AB의 원소와 절대값은 같지만 부호가 다른 값이 CD내부에 몇개씩 있는지 탐색하면서 나온 값을 ZeroCount라는 변수에 저장해주었고 최종적으로 ZeroCount를 출력하여주었다.

 

풀이 코드

 

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

std::vector<int> A(SizeOfSequence);
std::vector<int> B(SizeOfSequence);
std::vector<int> C(SizeOfSequence);
std::vector<int> D(SizeOfSequence);

for (int i = 0; i < SizeOfSequence; i++)
{
    std::cin >> A[i] >> B[i] >> C[i] >> D[i];
}

 

먼저, 모든 입력을 받아 각 배열에 저장해주었다.

 

std::vector<int> AB(SizeOfSequence * SizeOfSequence);
std::vector<int> CD(SizeOfSequence * SizeOfSequence);

for (int i = 0; i < SizeOfSequence; i++)
{
    for (int j = 0; j < SizeOfSequence; j++)
    {
        AB[j + SizeOfSequence * i] = A[i] + B[j];
        CD[j + SizeOfSequence * i] = C[i] + D[j];
    }
}

 

이후, A와 B를 AB라는 하나의 배열에 합쳤고, C와 D를 CD라는 하나의 배열에 합쳐주었다.

AB의 원소는 A의 원소와 B의 원소를 더했을 때 나올 수 있는 모든 경우의 값이다.

CD의 원소는 C의 원소와 D의 원소를 더했을 때 나올 수 있는 모든 경우의 값이다.

 

std::sort(AB.begin(), AB.end());
std::sort(CD.begin(), CD.end());

다음으로 두 배열을 정렬해주었다. 이분탐색을 활용할 때엔 정렬이 필수적이다.

(upperbound와 lowerbound는 이분탐색을 활용하기 때문에 정렬해주어야 한다.)

 

int MaxIndex = SizeOfSequence * SizeOfSequence - 1;
long long ZeroCount = 0;

for (int i = 0; i <= MaxIndex; i++)
{
    int CurValue = AB[i];

    int LowerBound = std::lower_bound(CD.begin(), CD.end(), -CurValue) - CD.begin();
    int UpperBound = std::upper_bound(CD.begin(), CD.end(), -CurValue) - CD.begin();

    ZeroCount += UpperBound - LowerBound;
}

std::cout << ZeroCount;

return 0;

 

다음으로, 반복문을 돌면서 위에서 설명했던 것과 같이 UpperBound와 LowerBound를 활용하여 원소의 개수를 탐색하고 있다.

더보기

만약, lower_bound에서 특정 값을 찾지 못한다면 어떻게 될까?

lower_bound는 주어진 값과 동일한 원소를 찾는 것이 아니라, 주어진 값보다 크거나 같은 값을 탐색한다. 그러므로 원하는 값을 찾지 못했다면, 그 값보다 큰 값중에서 가장 앞에 있는 원소의 위치를 반환할 것이다.

 

upper_bound는 주어진 값을 초과하는 원소를 찾는 함수이기 때문에, 만약 원하는 값이 배열에 없다면, lower_bound와 upper_bound는 같은 값을 반환할 것이다. 그러므로 UpperBound - LowerBound는 0이 된다.

 

이렇게 ZeroCount에 값을 계속 더해준 뒤, 반복문이 끝나고 출력해주면 끝이다.

 

코드 전문

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

    std::vector<int> A(SizeOfSequence);
    std::vector<int> B(SizeOfSequence);
    std::vector<int> C(SizeOfSequence);
    std::vector<int> D(SizeOfSequence);

    for (int i = 0; i < SizeOfSequence; i++)
    {
        std::cin >> A[i] >> B[i] >> C[i] >> D[i];
    }

    std::vector<int> AB(SizeOfSequence * SizeOfSequence);
    std::vector<int> CD(SizeOfSequence * SizeOfSequence);

    for (int i = 0; i < SizeOfSequence; i++)
    {
        for (int j = 0; j < SizeOfSequence; j++)
        {
            AB[j + SizeOfSequence * i] = A[i] + B[j];
            CD[j + SizeOfSequence * i] = C[i] + D[j];
        }
    }

    std::sort(AB.begin(), AB.end());
    std::sort(CD.begin(), CD.end());

    int MaxIndex = SizeOfSequence * SizeOfSequence - 1;
    long long ZeroCount = 0;

    for (int i = 0; i <= MaxIndex; i++)
    {
        int CurValue = AB[i];

        int LowerBound = std::lower_bound(CD.begin(), CD.end(), -CurValue) - CD.begin();
        int UpperBound = std::upper_bound(CD.begin(), CD.end(), -CurValue) - CD.begin();

        ZeroCount += UpperBound - LowerBound;
    }

    std::cout << ZeroCount;

    return 0;
}

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

백준 1522 - 문자열 교환 (C++)  (0) 2024.04.29
백준 5464 - 주차장 (C++)  (1) 2024.04.28
백준 17609 - 회문 (C++)  (1) 2024.04.26
백준 2493 - 탑 (C++)  (0) 2024.04.26
백준 12904 - A와 B (C++)  (0) 2024.04.24

 

 

주어지는 문자열이 회문인지, 유사회문인지 혹은 아무것도 아닌지 판별하는 문제이다.

 

aabbaa 처럼, 뒤집어도 기존과 같은 문자열이라면 회문이다.

aabbcaa 처럼, 그냥은 회문이 아니지만 문자 하나(c)를 제거한다면 회문이 되는 경우는 유사 회문이다.

abcde 처럼, 회문도 유사회문도 아닌 경우는 아무것도 아니다.

 

회문은 0, 유사회문은 1, 아무것도 아니라면 2를 출력하면 되는 문제이다.

 

문제 풀이

 

본인은 재귀함수와 투포인터를 사용하여 풀었다.

문자열을 뒤집어도 기존과 같다는 의미는, 맨 앞으로부터 일정 거리에 있는 문자와 맨 끝으로부터 일정 거리에 있는 문자가 동일하다는 뜻이다.

 

그림처럼, 회문이라면 초록색 화살표가 가리키는 것처럼 양 끝의 문자가 같아야 하고 양 끝으로부터 1씩 떨어진 주황색 화살표가 가리키는 문자도 동일해야 한다.

 

이를 검사하기 위해 투포인터를 사용하였다.

Left는 1씩 증가시키고, Right는 1씩 감소시키면서 Left와 Right가 가리키는 문자가 동일한지를 검사하여 회문 여부를 검사하였다.

 

하지만, 유사 회문을 검사하기 위해선 위의 방법만으로는 검사할 수 없다.

유사 회문의 경우엔 이상한 문자가 하나 중간에 끼어있기 때문이다.

 

하지만, 유사 회문에서도 문자 하나를 제거한다면 회문이 되기 때문에 이를 이용하요 동일한 방식에 조건 하나만 추가해주었다.

맨 처음에, 주황색 화살표는 각각 c와 b를 가리키고 있다.

서로 다른 문자를 가리키고 있는 상황이다.

이 때, Left + 1은 Right가 가리키는 문자와 같은가를 검사하였다.

유사회문이라면 c를 제거하였을 때, 회문이 되어야 한다.

그러므로 c의 다음 문자는 Right가 가리키는 문자와 동일해야 한다.

Left+1 과 Right가 가리키는 문자가 동일하다면 DeleteCount를 1 증가시켰다.

 

문자열의 처음과 끝을 모두 탐색할 때까지 DeleteCount가 1이라면 유사회문이라고 판정하였다.

(문자를 2개 이상 제거해야 회문이 된다면 유사 회문이 아니기 때문이다.)

 

반대로, Right쪽에 제거해야할 문자가 있을 수도 있기 때문에 Left와 Right - 1에 대해서도 동일한 검사를 해주었다.

 

그런데, 이 상황에서도 예외가 한가지 있다.

보면, Left와 Right가 다른 값임을 알 수 있다.

이 상황에서 Left를 제거하였다고 가정했을 때 Left + 1과 Right가 동일하기 때문에 Left를 제거하게 될 것이다.

하지만, Left를 제거한다면 yyyxy가 되어 회문이 되지 않는다. 왜일까?

 

Right또한 제거할 수 있는 경우이기 때문이다.

Right를 제거하였다고 가정했을 때 Left와 Right - 1 또한 동일하기 때문에 Right또한 제거할 수 있다.

 

즉, Left + 1 == Right 이면서 동시에 Left == Right - 1인 경우엔 두 경우에 대해 모두 검사를 해보아야 하는 것이다.

이 때문에 본인은 재귀함수를 이용하여 문제를 해결하였다.

 

코드 풀이

 

 

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

std::vector<int> Answer(NumOfCase, 0);

 

먼저, 테스트케이스의 수를 입력받았고 그 크기만큼 정답을 저장해줄 배열을 Resize해주었다.

 

for (int i = 0; i < NumOfCase; i++)
{
    std::string CurStr;
    std::cin >> CurStr;

    int Left = 0;
    int Right = CurStr.size() - 1;

    int DeleteCount = Solution(CurStr, Left, Right, 0);

    Answer[i] = std::min(DeleteCount, 2);
}

 

이후, 입력받는 문자열에 대해 Solution이라는 함수를 실행해주었고, 결과값을 Answer에 저장해주었다.

Solution함수 내부를 보자.

 

int Solution(const std::string& _Str, int _Left, int _Right, int _DeleteCount)
{
    if (_Left > _Right)
    {
        return _DeleteCount;
    }

    if (_DeleteCount >= 2)
    {
        return 2;
    }

    if (_Str[_Left] != _Str[_Right])
    {
        if (_Str[_Left + 1] == _Str[_Right] && _Str[_Left] == _Str[_Right - 1])
        {
            int Answer_1 = Solution(_Str, _Left + 1, _Right, _DeleteCount + 1);
            int Answer_2 = Solution(_Str, _Left, _Right - 1, _DeleteCount + 1);

            return std::min(Answer_1, Answer_2);
        }
        else if (_Str[_Left + 1] == _Str[_Right] && _Str[_Left] != _Str[_Right - 1])
        {
            return Solution(_Str, _Left + 1, _Right, _DeleteCount + 1);
        }
        else if (_Str[_Left + 1] != _Str[_Right] && _Str[_Left] == _Str[_Right - 1])
        {
            return Solution(_Str, _Left, _Right - 1, _DeleteCount + 1);
        }
    }
    else
    {
        return Solution(_Str, _Left + 1, _Right - 1, _DeleteCount);
    }
}

 

먼저, 투포인터 방식이기 때문에 _left가 _Right보다 커질 때 재귀함수를 return하도록 하였다.

또한, _DeleteCount가 2이상이라면 어차피 회문도 유사회문도 아니므로 더 검사할 필요 없이 바로 종료해주었다.

 

그 다음을 보면, Left와 _Right가 가리키는 값이 같다면, Left + 1, _Right - 1에 대해 검사해주고 있다.

만약, 다르다면 왼쪽과 오른쪽중 제거할 수 있는 문자를 판단한 뒤, 해당 문자를 제거해주었다.
(실제로 제거한 것은 아니고, 투 포인터가 가리키는 위치만 바꿔서 제거한 것과 유사한 느낌을 구현하였다.)

 

 만약, 왼쪽 문자와 오른쪽 문자 모두 제거할 수 있는 문자라면 양쪽에 대해 DeleteCount를 모두 검사해준 뒤, 둘 중 더 작은 쪽으로 반환해주었다.

 

이 함수가 끝나면, 문자열을 돌면서 문자를 몇개 제거했는가를 반환하게 된다.

만약 0이라면 회문이고, 1이라면 유사회문이며, 2라면 아무것도 아닌 것이다.

 

모든 테스트케이스에 대해 Solution 함수를 호출한 뒤, 마지막으로 답을 출력해주면 끝이다.

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

 

 

코드 전문

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

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

int Solution(const std::string& _Str, int _Left, int _Right, int _DeleteCount)
{
    if (_Left > _Right)
    {
        return _DeleteCount;
    }

    if (_DeleteCount >= 2)
    {
        return 2;
    }

    if (_Str[_Left] != _Str[_Right])
    {
        if (_Str[_Left + 1] == _Str[_Right] && _Str[_Left] == _Str[_Right - 1])
        {
            int Answer_1 = Solution(_Str, _Left + 1, _Right, _DeleteCount + 1);
            int Answer_2 = Solution(_Str, _Left, _Right - 1, _DeleteCount + 1);

            return std::min(Answer_1, Answer_2);
        }
        else if (_Str[_Left + 1] == _Str[_Right] && _Str[_Left] != _Str[_Right - 1])
        {
            return Solution(_Str, _Left + 1, _Right, _DeleteCount + 1);
        }
        else if (_Str[_Left + 1] != _Str[_Right] && _Str[_Left] == _Str[_Right - 1])
        {
            return Solution(_Str, _Left, _Right - 1, _DeleteCount + 1);
        }
    }
    else
    {
        return Solution(_Str, _Left + 1, _Right - 1, _DeleteCount);
    }
}

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

    std::vector<int> Answer(NumOfCase, 0);
    for (int i = 0; i < NumOfCase; i++)
    {    
        std::string CurStr;
        std::cin >> CurStr;

        int Left = 0;
        int Right = CurStr.size() - 1;

        int DeleteCount = Solution(CurStr, Left, Right, 0);

        Answer[i] = std::min(DeleteCount, 2);
    }

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

    return 0;
}

 

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

백준 5464 - 주차장 (C++)  (1) 2024.04.28
백준 7453 - 합이 0인 네 정수 (C++)  (0) 2024.04.27
백준 2493 - 탑 (C++)  (0) 2024.04.26
백준 12904 - A와 B (C++)  (0) 2024.04.24
백준 2437 - 저울 (C++)  (0) 2024.04.24

 

 

입력에 대해, 그림과 같은 높이의 송신탑을 그려볼 수 있을 것이다.

이 때, 각 송신 레이더에서 왼쪽으로 레이더를 쏘았을 때 가장 먼저 신호를 수신하는 탑의 인덱스를 출력하면 된다.

그림을 보면, 첫번째와 두번째 송신탑은 신호를 수신할 탑이 왼쪽에 존재하지 않기 때문에 0을 출력하면 된다.

세번째 탑의 경우 두번째 탑이 신호를 수신하고 있으며, 네번째 탑도 두번째 탑이 신호를 수신하고 있다.

마지막 탑의 경우는 네번째 탑이 신호를 수신해주고 있다.

 

그러므로, (0, 0, 2, 2, 4)를 출력하면 되는 문제이다.

 

문제 풀이

 

해당 문제는 전형적인 단조 스택 문제이다. 단조 스택을 잘 모른다면, 여기를 클릭하여 알아보고 다시 오도록 하자,

단조스택을 사용하여 PGE를 구하면 된다.

 

입력에 대해 과정을 한 번씩 훑어보자.

먼저, 맨 앞의 6을 검사할 때, Stack의 Size가 0이었으므로 Answer을 0으로 갱신한 뒤 6을 stack에 push해주었다.

9를 검사할 때엔, Stack의 Top이 현재 값보다 작으므로, 해당 값을 pop해주었다.

stack의 size가 0이 될 때까지 PGE를 찾지 못했으므로, PGE가 없다고 간주하고 Answer을 0으로 갱신한 뒤 9를 스택에 삽입하였다.

5를 검사할 때엔, Stack의 Top이 현재 값보다 크므로, 해당 값에 대한 인덱스로 Answer의 값을 갱신하였다.

이후, Stack에 본인을 삽입하였다.

 

7을 검사할 때엔, 5를 pop하였고 PGE인 9를 찾았으므로 Answer를 9의 인덱스로 갱신하였다.

Stack에 본인도 삽입해주었다.

마지막 4를 검사할 때엔, Stack의 top인 7이 PGE이므로 7의 인덱스인 4로 Answer을 갱신해주었다.

 

단조스택 알고리즘을 그대로 활용하면 되는 문제이다.

단, 이 문제는 값이 아닌 인덱스를 저장하고 출력하는 문제이기 때문에 Stack에 데이터를 삽입할 때, {Data, Index}형태의 pair로 삽입해서 검사해야 한다.

 

코드 풀이

 

먼저, 값을 입력받고 초기화해주었다.

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

std::vector<int> TowerArray(NumOfTower, 0);
std::vector<int> Answer(NumOfTower, 0);

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

std::stack<std::pair<int, int>> MonotonicStack;

 

배열을 초기화한 뒤, 사용할 Stack또한 선언해주었다.

 

다음은 PGE를 구하는 반복문이다.

for (int i = 0; i < TowerArray.size(); i++)
{
    int CurNum = TowerArray[i];

    while (MonotonicStack.size() > 0)
    {
        if (MonotonicStack.top().first > CurNum)
        {
            break;
        }

        MonotonicStack.pop();
    }

    if (MonotonicStack.size() == 0)
    {
        Answer[i] = 0;
    }
    else
    {
        Answer[i] = MonotonicStack.top().second + 1;
    }

    MonotonicStack.push({ CurNum, i});
}

 

 

Stack의 Top이 현재 값보다 큰 값이 나올때까지 반복문을 돌려주었다.

반복문이 끝난 뒤, Stack의 size가 0이라면 PGE를 찾지 못한것이므로 Answer을 0으로 갱신해주었다.

만약, PGE를 찾았다면, 해당 PGE의 인덱스 + 1로 Answer을 갱신해주었다.

(배열의 인덱스는 0부터 시작이지만, 문제에서 송신탑의 번호는 1부터 시작이므로 1을 더해주어야 한다.)

 

마지막으로 Stack에 본인을 삽입해주었다.

 

이렇게 반복문을 모두 돌린 뒤, Answer의 원소를 모두 출력해주면 끝이다.

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

 

 

 

참고로, 본인은 문제를 풀 때 Answer을 따로 사용하지 않고 TowerArray의 값을 갱신하면서 풀어보았는데, 동일하게 잘 풀리는 것을 확인하였다. 사용 메모리도 2000kb가량 감소하였다. 다만, 약간의 예외처리가 필요한데 이 것이 설명을 방해할까봐 정석의 방식으로 문제를 해결하였다.

 

코드 전문

 

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

    std::vector<int> TowerArray(NumOfTower, 0);
    std::vector<int> Answer(NumOfTower, 0);

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

    std::stack<std::pair<int, int>> MonotonicStack;

    for (int i = 0; i < TowerArray.size(); i++)
    {
        int CurNum = TowerArray[i];

        while (MonotonicStack.size() > 0)
        {
            if (MonotonicStack.top().first > CurNum)
            {
            	break;
            }

            MonotonicStack.pop();
        }

        if (MonotonicStack.size() == 0)
        {
            Answer[i] = 0;
        }
        else
        {
            Answer[i] = MonotonicStack.top().second + 1;
        }

        MonotonicStack.push({ CurNum, i});
    }

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

    return 0;
}

 

단조 스택 (Monotonic Stack)

단조 스택이란, Stack을 활용한 알고리즘중 하나이다.

수학에서 단조란, 우상향 혹은 우하향을 계속 유지하는 형태를 의미한다.

 

단조 스택도 이와 유사하다.

스택 내부의 원소가 오름차순 혹은 내림차순으로 정렬된 형태를 유지하는 것을 의미한다.

 

그렇다면, 단조 스택을 어떻게 활용할까?

단조 스택은 보통 NGE 혹은 PGE를 구하기 위해 사용한다.

 

NGE, PGE

NGE는 Next Greater Element의 약자로, 다음의 수중 가장 가까운 더 큰 수를 의미한다.

PGE는 Previous Greater Element의 약자로, 이전의 수중 가장 가까운 더 큰 수를 의미한다.

 

말로만 보면 이해가 잘 안된다.

아래 예시를 보자.

 

만약, 배열에 {1, 7, 5, 3, 6, 10} 이 있다고 해보자.

 

5 기준으로 보았을 때, 뒤의 원소는 3, 6, 10이 있다.

이 중 5보다 더 큰 원소는 6과 10이 있다.

6이 5와 가장 가깝기 때문에, 6이 5의 NGE가 된다.

 

3을 기준으로 보았을 때, 앞의 원소는 1, 7, 5가 있다.

이 중 3보다 더 큰 원소는 7과 5가 있다.

5가 3과 가장 가깝기 때문에, 5가 3의 PGE가 된다.

 

구현 방법

먼저, 기대값을 보고 가자.

 

1,7,5,3,6,10 이라는 배열이 처음에 주어진다면, NGE와 PGE는 그림과 같을 것이다.

X표시된 부분은 해당 원소에 대해 NGE혹은 PGE가 존재하지 않는다는 뜻이다.

 

본인보다 더 큰 수가 범위내에 없다면, X를 표시한 것이다.

 

이제, 차례대로 배열 원소들의 NGE를 구해본 뒤, 기대값과 동일한지 확인해보자.

 

먼저, 사용할 스택을 하나 선언해주었고, NGE를 저장할 Answer배열도 선언해주었다.

먼저, 배열의 마지막 원소를 Stack에 넣어보자.

Stack에는 10이라는 값이 삽입되었고, 해당 원소는 배열의 마지막 원소라 NGE가 존재하지 않으므로 Answer엔 -1을 저장해주었다.

 

다음은, 6에 대한 NGE를 검사해야 한다.

 

먼저, Stack의 Top을 보자. Top의 값은 현재 10이다.

이 Top의 값이 현재 원소의 NGE가 된다.

즉, 6의 NGE는 10이 된다.

 

이렇게, NGE를 갱신해준 뒤 6또한 다시 Stack에 삽입해주었다. 

 

다음 3에 대해서도 똑같은 과정을 거치면 된다.

현재 stack 의 top은 6이다. 6은 3보다 큰 값이기 때문에, 6은 3의 NGE라고 할 수 있다.

그러므로 아래와 같이 Answer 배열을 갱신해주면 된다.

Answer배열을 갱신해주고, Stack에 3을 삽입하였다.

5에 대해서 동일한 과정을 거치려고 하니까, 한가지 문제가 발견되었다.

Stack의 Top이 5보다 작기 때문에, Stack의 Top을 NGE라고 할 수가 없어져버린 것이다.

이럴 땐, Stack의 Top이 5보다 커질때까지, Stack에서 pop을 하면 된다.

이렇게 스택에서 3을 pop했더니, top이 5보다 큰 값이 되었다.

이 값이 5의 NGE가 되는 것이다.

그러므로, 배열을 아래와 같이 갱신해주면 된다.

스택에 본인을 삽입해주는 것도 잊지 말자.

 

7에 대해서도 동일한 과정을 거치면 된다.

Stack에서 5를 pop하고, 6을 pop하면 top은 10이 된다.

그러므로, 10을 7의 NGE로 저장한 뒤, 7을 다시 Stack에 push해주면 된다.

 

마지막 1에 대해서도 동일한 과정을 거치면 최종적으로 아래와 같아진다.

처음에 보았던 기댓값과 Answer배열의 값이 완전히 동일한 것을 볼 수 있다.

 

PGE를 구하는 방법도 동일하다. 다만, 배열의 뒤에서부터 진행하는 것이 아니라 앞에서 부터 진행하면 된다.

이렇게 NGE를 구하는 과정에서 Stack의 원소를 보면, 항상 오름차순 형태를 유지하고 있다. 이러한 형태때문에 단조 스택(Monotonic Stack)이라는 이름이 붙은 것 같다.

 

코드 구현

std::vector<int> Array = {1, 7, 5, 3, 6, 10};
std::vector<int> Answer(Array.size(), 0);

std::stack<int> MonotonicStack;

 

먼저 이렇게 입력을 받고 사용할 스택도 선언해주었다.

 

다음은 배열을 순회하며 NGE를 구해줄 것이다.

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

    while (MonotonicStack.size() > 0)
    {
        if (MonotonicStack.top() > CurNum)
        {
            break;
        }

        MonotonicStack.pop();
    }

    if (MonotonicStack.size() == 0)
    {
        Answer[i] = -1;
    }
    else
    {
        Answer[i] = MonotonicStack.top();
    }

    MonotonicStack.push(CurNum);
}

 

for문의 조건을 보면, 배열의 마지막원소부터 순회하는 것을 볼 수 있다.

스택의 top이 현재 값보다 큰 값이 될 때까지, 계속 pop을 해주고 있다. 만약 stack의 모든 원소를 pop했음에도 큰 값을 찾지 못했다면 NGE가 없는 것으로 간주하고 Answer을 -1로 갱신해주었다.

반대로, NGE를 찾았다면 그 값으로 Answer을 갱신하였다.

 

마지막으로, MonotonicStack에 자기 자신을 push해주면 된다.

 

반복문이 모두 끝나면, 아래와 같이 Answer 배열의 값은 갱신되어 있을 것이다.

위에서 보았던 기댓값과 완전히 일치하는 것을 알 수 있다.

 

만약 PGE를 구하고 싶다면, 동일한 코드에서 for문의 조건만 아래와 같이 수정하면 된다.

이렇게, 앞에서부터 탐색하게 되면 Answer의 값은 아래와 같이 갱신된다.

위에서 보았던 PGE의 기댓값과 동일하다.

 

코드 전문

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

int main()
{
    std::vector<int> Array = { 1, 7, 5, 3, 6, 10 };
    std::vector<int> Answer(Array.size(), 0);

    std::stack<int> MonotonicStack;

    for (int i = Array.size() - 1; i >= 0; i--)
        
        int CurNum = Array[i];

        while (MonotonicStack.size() > 0)
        {
            if (MonotonicStack.top() > CurNum)
            {
                break;
            }

            MonotonicStack.pop();
        }

        if (MonotonicStack.size() == 0)
        {
            Answer[i] = -1;
        }
        else
        {
            Answer[i] = MonotonicStack.top();
        }

        MonotonicStack.push(CurNum);
    }

    return 0;
}

+ Recent posts