Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

4-YIM2UL2ET #12

Merged
merged 1 commit into from
Feb 24, 2024
Merged

4-YIM2UL2ET #12

merged 1 commit into from
Feb 24, 2024

Conversation

YIM2UL2ET
Copy link
Collaborator

@YIM2UL2ET YIM2UL2ET commented Feb 21, 2024

🔗 문제 링크

1021 - 회전하는 큐

✔️ 소요된 시간

30m + α

✨ 수도 코드

1. 문제 설명

이 문제는 1부터 n까지의 숫자 원소를 가지고 있는 queue에서 m개의 수를 차례대로 입력 받아 3가지 연산을 이용하여 순서대로 queue에서 빼내고, 그중에서 2번, 3번의 연산 횟수의 최솟값을 구하는 문제입니다. 다음은 큐의 초기 상태와 각 3가지 연산입니다.

KakaoTalk_20240221_232928020_01

초기 큐의 상태는 이렇습니다.

KakaoTalk_20240221_232928020_02

1번 연산은 첫번째로 가리키는 원소를 빼내는 연산입니다.

KakaoTalk_20240221_232928020_03

2번 연산은 첫번째로 가리키는 원소를 왼쪽으로 한칸씩 옮기는 연산입니다.

KakaoTalk_20240221_232928020_04

3번 연산은 첫번째로 가리키는 원소를 오른쪽으로 한칸씩 옮기는 연산입니다.

2. 문제 분석

1. 큐를 이용한 연산 구현 시도

보자마자 바로 원형 큐가 생각나시죠? 하지만 조금은 다릅니다.
우선 그림을 보면서 3가지 연산에 대한 구현을 생각을 해봅시다. (코드 구현은 나중에)

1번 연산은 queue에서 첫번째로 가리키는 원소를 빼는 작업입니다.
KakaoTalk_20240221_232928020_05

간단하게 첫번째 원소를 pop해주면 되겠지요.

2번 연산은 queue에서 원소들을 한칸씩 왼쪽으로 옮기는 작업입니다.
KakaoTalk_20240221_232928020_06

우리는 큐를 쓰기 때문에 queue에서 첫번째 원소를 뒤쪽으로 옮겨주면 되겠지요?
간단하게 맨 앞에 있는 원소를 임시 변수에 저장해 둔 후에 pop 하고, 이를 다시 뒤에서 push하면 됩니다.

3번 연산은 queue에서 원소들을 한칸씩 오른쪽으로 옮기는 작업입니다.
KakaoTalk_20240221_232928020_07

이 때는 2번 연산처럼 맨 뒤에 있는 원소를 임시 변수에 저장해 둔 후에 pop 하고, 이를 앞에서 push 해야 합니다.

그러나 여기서 문제가 생깁니다. 큐에서는 push를 뒤쪽에만 할 수 있고, pop은 앞쪽에서만 가능하기 때문입니다.
따라서 이 문제는 큐를 써야 할 것이 아니라 덱을 사용하여 앞과 뒤 양쪽에서 값을 pop 하고 push할 수 있도록 해야 합니다.

2. 덱을 이용한 연산 구현

그렇다면 덱을 이용하여 3가지 연산을 코드로 구현해 보도록 하겠습니다.
dq는 임의로 1 ~ n까지 숫자를 차례대로 원소로 가지고 있다고 가정합시다.

int temp;
std::deque <int> dq(n); 

// 1번 연산
dq.pop_front();

// 2번 연산
temp = dq.front();
dq.pop_front();
dq.push_back(temp);

// 3번 연산
int temp = dq.back();
dq.pop_back();
dq.push_front(temp);

큐가 아닌 덱을 사용하면 1번과 2번 연산은 물론이고 3번 연산까지도 깔끔하게 구현 할 수 있음을 확인할 수 있습니다.

3. result값 구하기

그럼 이제 문제의 본질로 돌아와 봅시다. 문제가 원하는 값은 2번과 3번 연산을 가능한 최소로 사용한 횟수입니다.

어떻게 구할까 머릿속으로 생각하지 말고 그림을 보고 편하게 생각해봅시다.

KakaoTalk_20240221_232928020_08

만약 덱의 크기가 8이라고 할 때 첫 번째로 가리키는 원소가 a1이고, a7을 꺼내고 싶으면 어떻게 하면 좋을까요?
2번 연산을 6회 사용한 후에 1번 연산을 하는 것과, 3번 연산을 2회 사용한 후에 1번 연산을 하는 것이 있겠지요?
또한 result값에는 더 작은 값을 더해주어야 하므로 2를 더해주어야 할 것입니다.

KakaoTalk_20240221_232928020_09

이를 선형으로 봐봅시다.

KakaoTalk_20240221_232928020_10

a1a7의 거리 차는 2번 연산을 활용하면 6의 거리를 갖게 되고, 3번 연산을 활용하면 2의 거리를 갖게 됩니다.
이 때 각각의 거리 (= 연산 횟수)는 a7의 인덱스와 덱의 크기 간의 관계식으로 나타낼 수 있습니다.
이 그림에서는 2번 연산 횟수 = a7의 인덱스 이고, 3번 연산 횟수 = 덱의 크기 - a7의 인덱스 로 나타낼 수 있지요.

이를 일반화 하면 꺼내고 싶은 원소의 위치를 idx, 덱의 사이즈를 size라고 할 때 다음과 같이 정리할 수 있습니다.

연산 종류 연산 횟수
2번 연산 idx
3번 연산 size - idx

이를 총 m번의 입력 값에 따라 if문을 활용하여 더 적은 연산 횟수를 구하여 그에 따른 연산 횟수를 result값에 더하고, 연산하는 것을 반복해 나가면 될 것입니다. 이제 수도코드를 짜봅시다.

3. 수도코드

int형 변수 n, m, k, idx, result 선언
int형 덱 dq 선언

n, m 입력받기
for (i = 0 부터 n-1 까지) {
    dq에  i+1을 push
}

result 0으로 초기화

for (i = 0 부터 m-1 까지) {
    k 입력 받기
    idx = k가 값으로 있는 dq의 index
    
    if (idx < dq의 크기 - idx) {
        result += idx
        for (j = 0 부터 idx-1 까지) {
            2번 연산 (맨 앞 원소를 맨 뒤로 옮기기)
        }
    else {
        result += dq의 크기 - idx
        for (j = 0 부터 idx-1 까지) {
            3번 연산 (맨 뒤 원소를 맨 앞으로 옮기기)
        }
    1번 연산 (맨 앞 원소 제거)
}

result 출력
프로그램 종료

4. 최종 코드

#include <iostream>
#include <algorithm>
#include <deque>

int main(void)
{
    int n, m, k, idx, result;
    std::deque <int> dq;

    std::cin >> n >> m;
    for (int i = 0; i < n;) {
        dq.push_back(++i);
    }

    result = 0;
    for (int i = 0; i < m; i++) {
        std::cin >> k;

        std::deque <int> :: iterator iter;
        iter = std::find(dq.begin(), dq.end(), k);
        idx = std::distance(dq.begin(), iter);

        if (idx < dq.size() - idx) {
            result += idx;
            for (int j = 0; j < idx; j++) {
                int temp = dq.front();
                dq.pop_front();
                dq.push_back(temp);
            }
        }
        else {
            result += dq.size() - idx;
            for (int j = 0; j < dq.size() - idx; j++) {
                int temp = dq.back();
                dq.pop_back();
                dq.push_front(temp);
            }
        }

        dq.pop_front();
    }
    
    std::cout << result;
    return 0;   
}

📚 새롭게 알게된 내용

스택에 이어서 큐를 공부해 보았습니다. 생각하는 과정을 쓰는 것은 여전히 쉽지가 않네요. (문제 푸는데 30분, PR 쓰는데 4시간..)

다음은 도움을 받은 자료들 입니다.
deque container 정리
find 함수 사용법

Copy link
Collaborator

@rivkms rivkms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✔️ 흠잡을 곳 없는 좋은 코드라고 생각합니다.

✔️ 2번과 3번 기능에 대하여 main함수에 바로 작성해주셨지만, 따로 함수를 빼서 작성해보시는 것도 추천합니다!!
더 코드가 직관적이고 보기 좋아질 수도 있을 것 같아요!!

수고하셨습니다.😁

Comment on lines +11 to +13
for (int i = 0; i < n;) {
dq.push_back(++i);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for문에서 증감식을 쓰지 않는 형태는 되게 오랜만에 보는 것 같습니다 ㅎㅎ
우와!! 하면서 오랜만에 눈이 뜨였던 코드인거 같네요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for (int i = 0; i < n; i++) {
        dq.push_back(i+1);
    }

이렇게 적어도 무방하지만 이게 더 가독성이 더 좋지 않을까 싶어 적어보았습니다. 둘 중에 어떤게 더 나으신 것 같나요?

Copy link
Collaborator

@mong3125 mong3125 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 좋은 코드인거 같아요!

❓ 문제 설명에서 보면 덱이 아니라 배열로 구현을 하고 시작 인덱스만 옮겨다니는 방법으로도 가능할 것 같은데 어떻게 생각하시는지 궁금합니다!

@mong3125
Copy link
Collaborator

전 문제 해석을 잘못해서 입력 순서대로가 아니라 한번에 입력받고 원하는 위치의 원소를 모두 얻는 최단 방법을 구하느라 삽질했네요 ㅎㅎ..

@YIM2UL2ET
Copy link
Collaborator Author

YIM2UL2ET commented Feb 24, 2024

✅ 좋은 코드인거 같아요!

❓ 문제 설명에서 보면 덱이 아니라 배열로 구현을 하고 시작 인덱스만 옮겨다니는 방법으로도 가능할 것 같은데 어떻게 생각하시는지 궁금합니다!

저도 문제를 풀고 조금 생각해 보니 이 방법이 조금 더 좋겠다고 생각했었습니다.

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

int main(void)
{
    int n, m, k, front, result;
    std::vector <int> vec;

    std::cin >> n >> m;
    for (int i = 0; i < n;) vec.push_back(++i);

    result = 0, front = 0;
    for (int i = 0; i < m; i++) {
        std::cin >> k;

        int idx = std::distance(vec.begin(), std::find(vec.begin(), vec.end(), k));
        int distance = abs(front - idx);

        if (distance < vec.size() - distance) result += distance;
        else result += vec.size() - distance;

        vec.erase(vec.begin()+idx);
        if (vec.size() > 0) front = idx % vec.size();
    }
    
    std::cout << result;
    return 0;   
}

덱을 벡터로 바꾸고, 지울 원소의 인덱스인 idx와, 맨 앞 원소를 가리키는 인덱스인 front와의 거리 차이를 distance 라는 변수를 활용하여 코드를 수정해보았습니다.

distance < vector.size() - distance 라는 조건문을 활용하여 distance가 더 작으면 3번 연산을, 그게 아니면 2번 연산을 한다고 가정하고 연산 횟수를 result에 일괄적으로 반영 후에 index의 원소를 삭제 후에 front의 값을 idx % vector.size() 로 초기화 합니다. (idx의 원소를 삭제하면 vector.size()가 1만큼 작아지므로 자동으로 front가 다음 인덱스로 옮겨짐)

추가적으로

vec.erase(vec.begin()+idx);
if (vec.size() > 0) front = idx % vec.size();

이 부분을

vec.erase(vec.begin()+idx);
front = idx;

이렇게 바꿔도 값은 같게 나오지만, front가 작아진 vector.size()의 값이 될 수도 있기 때문에 앞선 distance < vector.size() - distance 조건문에서 2번 연산과 3번 연산 중에 어떤 연산으로 result값이 더해지는지는 알 수 없습니다.
(if문이 하나 없어졌기 때문에 시간은 이게 조금 더 적게 걸리겠네요.)

@YIM2UL2ET
Copy link
Collaborator Author

전 문제 해석을 잘못해서 입력 순서대로가 아니라 한번에 입력받고 원하는 위치의 원소를 모두 얻는 최단 방법을 구하느라 삽질했네요 ㅎㅎ..

저도 항상 문제 풀 때마다 문제 이해를 잘 못 할 때가 많아서 공감이 됩니다..
문제를 많이 풀면 실력이 느는 만큼 문제를 많이 읽으면 좀 나아지겠죠..?

@YIM2UL2ET
Copy link
Collaborator Author

✔️ 흠잡을 곳 없는 좋은 코드라고 생각합니다.

✔️ 2번과 3번 기능에 대하여 main함수에 바로 작성해주셨지만, 따로 함수를 빼서 작성해보시는 것도 추천합니다!! 더 코드가 직관적이고 보기 좋아질 수도 있을 것 같아요!!

수고하셨습니다.😁

#include <iostream>
#include <algorithm>
#include <deque>

void op_2(std::deque <int> * dq);
void op_3(std::deque <int> * dq);

int main(void)
{
    int n, m, k, idx, result;
    std::deque <int> dq;

    std::cin >> n >> m;
    for (int i = 0; i < n;) dq.push_back(++i);

    result = 0;
    for (int i = 0; i < m; i++) {
        std::cin >> k;
        
        int idx = std::distance(dq.begin(), std::find(dq.begin(), dq.end(), k));

        if (idx < dq.size() - idx) {
            result += idx;
            for (int j = 0; j < idx; j++) op_2(&dq);
        }
        else {
            result += dq.size() - idx;
            for (int j = 0; j < dq.size() - idx; j++) op_3(&dq);
        }

        dq.pop_front();
    }
    
    std::cout << result;
    return 0;   
}

void op_2(std::deque <int> * dq)
{
    int temp = dq->front();
    dq->pop_front();
    dq->push_back(temp);
}

void op_3(std::deque <int> * dq)
{
    int temp = dq->back();
    dq->pop_back();
    dq->push_front(temp);
}

조언 해주신 대로 2번 연산과 3번 연산을 함수로 짜서 코드를 수정해보았습니다.
코드 자체의 길이는 늘어날지 몰라도 확실히 main함수의 가독성은 훨씬 좋아졌네요!

Copy link
Member

@kjs254 kjs254 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이름처럼 선형 큐가 바로 떠오르는 문제네요.

문제에서 큐의 길이를 먼저 지정해주기 때문에 포인터를 움직이면서 접근하는게 좋을 것 같다고 생각했는데 이미 위에서 코드구현까지 다 하셨네요 😄

연산방법과 인덱스접근 각각 깔끔하게 잘 설명하시고 풀이하셔서 이해하기 매우 쉬웠습니다. 수고하셨습니다. 👍 👍

Comment on lines +26 to +28
int temp = dq.front();
dq.pop_front();
dq.push_back(temp);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 코드에서 front()pop_front()를 각각 사용하셨는데 temp=dq.pop_front()로 삭제와 대입을 동시에 하면 안되는 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cpp에서 pop에 관련된 멤버 함수는 return값이 없는 void형 함수밖에 없는 것으로 알고 있습니다.
실제로 저 두 줄을 지우고 temp = dq.pop_front() 를 적으면 오류가 납니다.
무조건 먼저 값을 가져온 후에 pop을 해줘야 합니다. (STL 공부를 안 해서 틀릴 수도 있어요..)

@YIM2UL2ET YIM2UL2ET merged commit cd93df9 into main Feb 24, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants