BOJ(백준)

[BOJ/C++] 백준 1021 회전하는 큐

ruming 2022. 10. 3. 00:31

지민이는 N개의 원소를 포함하고 있는 양방향 순환 큐를 가지고 있다. 지민이는 이 큐에서 몇 개의 원소를 뽑아내려고 한다.

지민이는 이 큐에서 다음과 같은 3가지 연산을 수행할 수 있다.

  1. 첫 번째 원소를 뽑아낸다. 이 연산을 수행하면, 원래 큐의 원소가 a1, ..., ak이었던 것이 a2, ..., ak와 같이 된다.
  2. 왼쪽으로 한 칸 이동시킨다. 이 연산을 수행하면, a1, ..., ak가 a2, ..., ak, a1이 된다.
  3. 오른쪽으로 한 칸 이동시킨다. 이 연산을 수행하면, a1, ..., ak가 ak, a1, ..., ak-1이 된다.

큐에 처음에 포함되어 있던 수 N이 주어진다. 그리고 지민이가 뽑아내려고 하는 원소의 위치가 주어진다. (이 위치는 가장 처음 큐에서의 위치이다.) 이때, 그 원소를 주어진 순서대로 뽑아내는데 드는 2번, 3번 연산의 최솟값을 출력하는 프로그램을 작성하시오.

입력

첫째 줄에 큐의 크기 N과 뽑아내려고 하는 수의 개수 M이 주어진다. N은 50보다 작거나 같은 자연수이고, M은 N보다 작거나 같은 자연수이다. 둘째 줄에는 지민이가 뽑아내려고 하는 수의 위치가 순서대로 주어진다. 위치는 1보다 크거나 같고, N보다 작거나 같은 자연수이다.

출력

첫째 줄에 문제의 정답을 출력한다.

예제 입력 1 

10 3
1 2 3

예제 출력 1 

0

예제 입력 2 

10 3
2 9 5

예제 출력 2 

8

예제 입력 3 

32 6
27 16 30 11 6 23

예제 출력 3 

59

예제 입력 4 

10 10
1 6 3 2 7 9 8 4 10 5

예제 출력 4 

14

 

핵심은 3가지 연산을 잘 구현해내는 것이다.

  1. 첫 번째 원소를 뽑아낸다. 이 연산을 수행하면, 원래 큐의 원소가 a1, ..., ak이었던 것이 a2, ..., ak와 같이 된다.
  2. 왼쪽으로 한 칸 이동시킨다. 이 연산을 수행하면, a1, ..., ak가 a2, ..., ak, a1이 된다.
  3. 오른쪽으로 한 칸 이동시킨다. 이 연산을 수행하면, a1, ..., ak가 ak, a1, ..., ak-1이 된다.

첫 번째 원소를 뽑거나 왼/오른쪽으로 이동시키는 것.

원소가 무슨 값인지는 나오지 않았지만 원소를 뽑는 것이 중요한 게 아니라 몇 번째 원소를 뽑느냐가 중요한 거라 원소는 1부터 n까지 수를 놓고 구현해도 상관 없다.

 


예제 1을 보자.

원소가 10개, 뽑아야 하는 수는 3개, 뽑을 수의 위치는 1 2 3으로 연속되어 있다.

1 2 3 4 5 6 7 8 9 10

 

예제 1은 1부터 연속된 원소를 뽑기 때문에 이동이 필요 없다.

첫번째 원소를 뽑고

2 3 4 5 6 7 8 9 10

두번째 원소를 뽑고

3 4 5 6 7 8 9 10

세번째 원소를 뽑으면 된다.

4 5 6 7 8 9 10


예제 2를 보자.

N = 10, M = 3

2, 9, 5번째 원소를 뽑는다.

 

먼저 2를 뽑으려면 2가 첫번째 자리에 와야 하므로 왼쪽으로 한 칸 이동시켜야 한다. 따라서 2번 연산이 한 번 수행된다.

1 2 3 4 5 6 7 8 9 10

2 3 4 5 6 7 8 9 10 1  => 왼쪽으로 한 칸 이동 (cnt=1)

3 4 5 6 7 8 9 10 1     => 첫번째 원소 제거

 

그 다음 9번째 원소를 제거해야 한다. 양방향으로 이동할 수 있기 때문에 더 적은 수만큼 이동하는 방향을 골라 이동한다. 왼쪽으로 이동하면 6번, 오른쪽으로 이동하면 3번 이동하므로 3번 연산을 세 번 이용한다.

3 4 5 6 7 8 9 10 1

1 3 4 5 6 7 8 9 10   => 오른쪽으로 한 칸 이동 (cnt=2)

10 1 3 4 5 6 7 8 9   => 오른쪽으로 한 칸 이동 (cnt=3)

9 10 1 3 4 5 6 7 8   => 오른쪽으로 한 칸 이동 (cnt=4)

10 1 3 4 5 6 7 8      => 첫번째 원소 제거

 

마지막으로 5번째 원소를 제거한다. 오른쪽이든 왼쪽이든 똑같이 4번 이동해야 한다. 

왼쪽으로 네 번 이동시키겠다.

10 1 3 4 5 6 7 8

1 3 4 5 6 7 8 10   => 왼쪽으로 한 칸 이동 (cnt=5)

3 4 5 6 7 8 10 1   => 왼쪽으로 한 칸 이동 (cnt=6)

4 5 6 7 8 10 1 3   => 왼쪽으로 한 칸 이동 (cnt=7)

5 6 7 8 10 1 3 4   => 왼쪽으로 한 칸 이동 (cnt=8)

6 7 8 10 1 3 4      => 첫번째 원소 제거

 

이렇게 총 8번 이동해야 하므로 출력은 8이 된다.


즉, 왼쪽이나 오른쪽 중 어느 쪽이 더 적게 이동하는지 판별한 후 원소를 제거하면 되는데, 

필자는 양방향으로 push, pop이 가능한 덱을 이용해 해결했다.

 

먼저 덱에 1부터 N까지의 수를 push했다. 

첫번째 원소를 제거: d.pop_front()

왼쪽으로 이동: 맨 앞 원소를 맨 뒤에 추가 후에 맨 앞 원소 제거 → d.push_back(d.front()), d.pop_front()

오른쪽으로 이동: 맨 뒤 원소를 맨 앞에 추가 후에 맨 뒤 원소 제거 → d.push_front(d.back()), d.pop_back()

 

mid 변수를 사용해 수열의 가운데에서 왼쪽이 가까운지 오른쪽이 가까운지 판별한다.

mid = n/2;

idx 변수는 뽑아야 하는 수가 몇 번째 인덱스인지 찾아준다.

인덱스와 mid를 비교해야 하기 때문에 필요하다. find 함수를 이용했다. 앞부분을 제거하기 때문에 begin()만큼 빼준다.

while문은 뽑아야 하는 수가 맨 처음에 오기 전까지 반복해서 수열을 이동시켜 준다.

#include <bits/stdc++.h>
using namespace std;
int main(void){
    ios::sync_with_stdio(0);
    cin.tie(0);
    deque<int> D;
    int n, m;
    cin >> n >> m;
    for(int i=1; i<=n; i++)	D.push_back(i);
    int cnt = 0, mid;
    
    while(m--){
    	int x;
    	cin >> x;
    	if(x == D.front()){
    		if(!D.empty())
	    		D.pop_front();
		}else{
			int idx = find(D.begin(), D.end(), x) - D.begin();
			mid = n/2;
			if(idx <= mid){	//2번 연산 
	    		while(x != D.front()){
	    			D.push_back(D.front());
	    			D.pop_front();
	    			cnt++;
				}	
			}else{		//3번 연산  
				while(x != D.front()){
					D.push_front(D.back());
					D.pop_back();
					cnt++;
				}
			}
			D.pop_front();
		}
		n--;
	}
	cout << cnt;
    return 0;
}

풀면서 막혔던 게 mid 부분이다.

n이 감소함에 따라 mid를 계속 구해야 하는데 처음에 mid를 구하고 다시 구하지 않아서 조금 헤맸다.

 

 

 

자 그럼 이제 바킹독 코드를 분석해보자.

https://github.com/encrypted-def/basic-algo-lecture/blob/master/0x07/solutions/1021.cpp

#include <bits/stdc++.h>
using namespace std;

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);
  int n, m;
  cin >> n >> m;
  deque<int> DQ;
  for (int i = 1; i <= n; i++) DQ.push_back(i);
  int ans = 0;
  while (m--) {
    int t;
    cin >> t;
    int idx = find(DQ.begin(), DQ.end(), t) - DQ.begin(); // idx : t가 있는 위치
    while (DQ.front() != t) {
      if (idx < DQ.size() - idx) { 
        DQ.push_back(DQ.front());
        DQ.pop_front();
      }
      else {
        DQ.push_front(DQ.back());
        DQ.pop_back();
      }
      ans++;
    }
    DQ.pop_front();
  }
  cout << ans;
}
/*
20번째 줄에서, 지금은 idx가 항상 DQ.size()보다 작아서 문제가 없지만
DQ.size()는 unsigned int(or unsigned long long)이기
때문에 만약 idx가 DQ.size()보다 컸다면 overflow가 발생해
의도한대로 동작하지 않을 수 있음을 인지해야 함. 그래서 size()로
받아온 값에 대해서는 안전하게 (int)DQ.size() 로 항상 형변환을
하는 것도 괜찮음.
*/

while문 조건에서 t가 덱의 맨 앞에 있지 않으면 오른쪽이나 왼쪽으로 이동시킨다.

idx < DQ.size() - idx에서

2idx < DQ.size(),

idx < DQ.size()/2 로 바꿀 수 있으므로 결국 왼쪽인지 오른쪽인지 확인하는 계산은 비슷하다.

t가 맨 앞으로 오면 그 수를 pop한다.

 

어차피 뽑는 수를 맨 앞으로 이동시키고 뽑아야 하므로 내 코드처럼 굳이 if문으로 한번 더 확인해 줄 필요가 없었다. 더 간결하게 짤 수 있는 코드였다.