BOJ 1697 - 숨바꼭질
- 0<=N, K<=100000일 때
+1
,-1
,*2
세 가지 연산을 사용해서 N에서 K로 가는 최소 연산 횟수를 구하는 문제이다.
문제 풀이
-
+1
,-1
,*2
세 가지 연산을 할 수 있는데 N에서 시작해서 이 세 연산들을 하면서 Dijkstra랑 비슷하게 연산 횟수를 값마다 저장하면서 BFS를 돌리면 된다.(N, K<=10^5이기 때문에 값을 인덱스로 사용 가능하다) -
재귀로 바로 풀어버릴 수도 있다. 재귀함수는 아래와 같이 구현할 수 있다.(chungha님 코드 참고)
1 2 3 4 5 6
int f(int n, int k){ if(n>=k) return n-k; else if(k==1) return 1; else if(k&1) return 1+min(f(n, k-1), f(n, k+1)); else return min(k-n, 1+f(n, k/2)); }
증명
-
우선
+1
,-1
,*2
연산을 통해 N에서 K로 가는 것과,-1
,+1
,/2
연산을 통해 K에서 N으로 가는 것은 동일한 문제이다. 하지만 N->K는{n-k}
,{f(n+1, k), f(n-1, k), f(n*2, k)}
로 케이스가 잘 나눠지지 않는데 비해 K->N은{n-k}
,{f(n, k-1), f(n, k+1)}
,{k-n, f(n, k/2)}
로 케이스가 잘 나눠져 비교적 재귀호출이 적다.
(+ N->K를 재귀호출로 구현하면 DFS로 푸는 꼴인데 이 문제는 최소 연산 횟수를 구하는 것이기 때문에 BFS로 탐색하면서 N이 K가 되었을 때 탐색을 종료하면 시간이 훨씬 줄어든다. 근데 N->K 재귀로 구현해도 통과가 되긴한다.) -
N>=K일 때는 K-N 이외에 다른 연산을 할 수 없기 때문에 K-N을 반환한다.
-
N<K일 때,
- K==1이면 N=0, K=1인데, Edge case로 처리해준다.
- K가 홀수면
+1
,-1
연산을 할 수 있다. 따라서1+min(f(N, K-1), f(N, K+1))
을 반환한다. - K가 짝수면
/2
연산을 하거나-1
연산을 K-N번 해서 바로 N으로 갈 수 있다.(최적의 해인 최소 연산 횟수를 구하는 것이기 때문에+1
연산을 하는 경우 최적의 해가 아니게 된다. 즉 동전 dp문제에서 동전 가격이 1, 2, 4, 8과 같이 배수일 때와 비슷하게 그리디를 적용할 수 있다.)
따라서 위와 같이 재귀함수를 구현할 수 있다.
+1
, -1
, *3
으로 나눌 경우 k%3!=0
일 때 위/아래 3의 배수와 그 사이 K 이외의 수를 모두 포함해야 한다. (ex. K=4일 경우 3, 6, 5) 따라서 아래와 같이 재귀함수를 구현할 수 있다.
1
2
3
4
5
6
int f(int n, int k){
if(n>=k) return n-k;
else if(k==1) return 1;
else if(k%3) return min(min(k%3+f(n, k-k%3), 3-k%3+f(n, k+3-k%3)), 1-2*(k&1)+f(n, k+1-2*(k&1)));
else return min(k-n, 1+f(n, k/3));
}
소스코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// // BFS
// #include<bits/stdc++.h>
// using namespace std;
// typedef pair<int, int> pii;
// int main()
// {
// int n, k, ccnt, cur, cnt;
// int v[100001];
// fill(v, v+100001, 100001);
// cin>>n>>k;
// ccnt=abs(k-n);
// queue<pii> q;
// q.push({n, 0});
// cur=n, cnt=0;
// while(cur!=k && cnt<ccnt){
// cur=q.front().first, cnt=q.front().second;
// q.pop();
// if(cur-1>-1 && v[cur-1]>cnt+1){
// q.push({cur-1, cnt+1});
// v[cur-1]=cnt+1;
// }
// if(cur+1<100001 && v[cur+1]>cnt+1){
// q.push({cur+1, cnt+1});
// v[cur+1]=cnt+1;
// }
// if(cur*2<100001 && v[cur*2]>cnt+1){
// q.push({cur*2, cnt+1});
// v[cur*2]=cnt+1;
// }
// }
// cout<<cnt;
// }
// Divide and conquer
#include<bits/stdc++.h>
using namespace std;
int f(int n, int k){ //K->N
if(n>=k) return n-k;
else if(k==1) return 1;
else if(k&1) return 1+min(f(n, k-1), f(n, k+1));
else return min(k-n, 1+f(n, k/2));
}
int g(int n, int k){ //N->K
if(n>=k) return n-k;
else if(n>0) return 1+min(f(n+1, k), min(f(n-1, k), f(n*2, k)));
else return 1+min(f(n+1, k), f(n*2, k));
}
int main()
{
int n, k;
cin>>n>>k;
cout<</*f(n, k)<<' '<<*/g(n, k);
}
풀고나서
- 문제 풀이 전략 중 거꾸로 생각해보기의 좋은 예시이다. 이 문제의 경우 N->K는 K 이상으로 더 갈수 있지만, K->N의 경우 K가 1로 수렴하기 때문에 N<K, K=1의 Edge case만 처리해주면 되기 때문에 재귀관계를 정의하기 더 편리하다. 근데 아무리 그래도 저렇게 깔끔하게 정리하는 것은 경험이 많이 필요한 것 같다.
Leave a comment