12 min to read
PS 풀이 - BOJ 17070 파이프 옮기기 1
백준 17070번 문제 풀이 포스팅
문제 소개
BOJ 17070 파이프 옮기기 1 썸네일에 나와있듯, 골드 5 문제이며, solved.ac 기준으로, 클래스 4에 수록되어 있는 문제입니다. 저번 게시글에서 다룬 17144번 문제와는 다르게 두가지 방법의 풀이방법을 다룰것인데, “머리가 나쁘면 몸이 고생한다” 라는 교훈을 일깨워준 좋은 문제였던것 같습니다.
문제의 상황
- 최소 3 \(\times\) 3 에서, 최대 16 \(\times\) 16 크기의 정사각형 배열이 주어진다는 사실
- 파이프가 2개의 연속된 칸을 차지하는 크기이며, 방향은 가로, 세로, 우측아래 대각선이 가능
- 파이프를 옮길 수 있는 방법, 그리고 옮길 때, 비어있어야 하는 칸에 대한 정보. (아래 그림에서 색칠된 칸이 비어 있어야 하는 칸들을 나타낸다) 가로로 놓인 파이프를 옮길 수 있는 방법 세로로 놓인 파이프를 옮길 수 있는 방법 대각선으로 놓인 파이프를 옮길 수 있는 방법
- 파이프는 빈칸만을 차지하고 있어야 한다는 조건
- 파이프의 시작 위치는 (1,1), (1,2)이고, 파이프의 한쪽 끝을 오른쪽 구석으로 옮겨야 한다.
문제의 조건(입력, 제한시간)
방의 가로, 세로 길이 N이 주어지고, 그 밑의 N 줄에는 방의 상태가 주어진다. 0은 빈칸, 1은 빈칸이 아닌 칸이다. \($(3 \le N \le 16)\)$ 또, (1,1), (1,2)는 항상 빈칸이다
제한시간은 1초, 메모리 제한은 512MB 이다.
맨처음 한 구현
우선 전의 포스팅에서 언급한 것처럼, 단순 구현으로 풀 수 있는 문제인가를 먼저 생각해 보았다.
최악의 상황을 가정하자면, 방의 크기가 16 \(\times\) 16일 때, 파이프의 한쪽 끝이 있을 방법은 16 \(\times\) 16 \(=\) 256 보다 적을것이고(가장자리 쪽에 있는 경우의 수는 몇가지가 불가능하다. 아예 칸 밖으로 나갈수가 없으니), 위와 비슷한 이유로, 가능한 방향도 3가지이긴 한데, 3가지가 다 안되는 경우도 많을 것이다. 또 위의 문제 상황에서 DFS나 BFS 등으로 탐색하면서, 예전에 있던 상태공간으로 다시 돌아올 일도 없다. 파이프가 움직이는 방향은, 오른쪽, 아래, 오른쪽 아래 대각선 뿐이기 때문이다. \(256 \times 3 = 768\)이니, 1초안에 연산은 다 되겠다 하여, 위의 문제 상황을 시뮬레이팅 하듯이 그대로 코드로 옮겨보았다.
#include<iostream>
#include<utility>
#include<queue>
using namespace std;
std::pair<int, int> operator +(const std::pair<int, int>& x, const std::pair<int, int>& y) {
return std::make_pair(x.first + y.first, x.second + y.second);
}
typedef struct{
int direction;
pair<int,int> first, second;
}position;
pair<int,int> wayToGo[3][3][2];
vector<pair<int,int> >wayToClean[3][3];
int N;
int state[16][16];
position pipe{0,make_pair(0,0),make_pair(0,1)};
vector<pair<int,int> > diag{make_pair(0,1),make_pair(1,0),make_pair(1,1)};
queue<position> q;
pair<int,int> goal;
void preset();
bool isEmpty(pair<int,int> p);
bool canMove(position p, int way);
position movePipe(position p, int way);
int main(void){//좌표 순서는 row,col이다.
int ret = 0;
preset();
cin >> N;
goal = make_pair(N-1,N-1);
for(int i = 0; i < N;++i)
for(int j = 0; j < N;++j)
cin >> state[i][j];
q.push(pipe);
while(!q.empty()){
position p = q.front();
q.pop();
if(p.first == goal || p.second == goal){
ret++;
continue;
}
if(p.direction == 2){
for(int i = 0; i < 3; ++i){
if(canMove(p,i))
q.push(movePipe(p,i));
}
}
else{
for(int i = 0; i < 2; ++i){
if(canMove(p,i))
q.push(movePipe(p,i));
}
}
}
cout << ret;
return 0;
}
void preset(){
pipe.first = make_pair(0,0); pipe.second = make_pair(0,1);
wayToGo[0][0][0] = wayToGo[0][0][1] = make_pair(0,1);
wayToClean[0][0].push_back(make_pair(0,1));
wayToGo[0][1][0] = make_pair(0,1);
wayToGo[0][1][1] = make_pair(1,1);
wayToClean[0][1] = diag;
wayToGo[1][0][0] = wayToGo[1][0][1] = make_pair(1,0);
wayToClean[1][0].push_back(make_pair(1,0));
wayToGo[1][1][0] = make_pair(1,0);
wayToGo[1][1][1] = make_pair(1,1);
wayToClean[1][1] = diag;
wayToGo[2][0][0] = make_pair(1,1);
wayToGo[2][0][1] = make_pair(0,1);
wayToClean[2][0].push_back(make_pair(0,1));
wayToGo[2][1][0] = make_pair(1,1);
wayToGo[2][1][1] = make_pair(1,0);
wayToClean[2][1].push_back(make_pair(1,0));
wayToGo[2][2][0] = wayToGo[2][2][1] = make_pair(1,1);
wayToClean[2][2] = diag;
}
bool isEmpty(pair<int,int> p){
int row = p.first;
int col = p.second;
if(row < 0 || row >= N || col < 0 || col >= N) return false;
return state[row][col] == 0;
}
bool canMove(position p, int way){
for(int i = 0 ;i < wayToClean[p.direction][way].size();++i){
if(!isEmpty(p.second+wayToClean[p.direction][way][i])) return false;
}
return isEmpty(p.first+wayToGo[p.direction][way][0])
&& isEmpty(p.second+wayToGo[p.direction][way][1]);
}
position movePipe(position p, int way){
position ret{p.direction,p.first+wayToGo[p.direction][way][0],
p.second+wayToGo[p.direction][way][1]};
if(p.direction < 2 && way == 1) ret.direction = 2;
else if(p.direction == 2) ret.direction = way;
return ret;
}
파이프의 위치를 나타내는 구조체를 만들고, 파이프의 움직임을 제약하는 사항들을 모두 변수 등으로 기록하여서 움직이게 하였다. 이 방법은 코드가 길어지기 때문에 상당한 노력을 필요로 하였고, 1초라는 제한시간에 만족하였지만, 위 문제와 상황은 같은데, 방 크기 제한이 32까지로 늘어난 파이프 옮기기 2라는 문제에서는 도저히 시간 내에 풀 수 없었다.
옆동네에서 머리를 탁 치며 가져온 DP
구현 문제를 풀었다는 뿌듯함에 평소처럼 다른 사람들의 코드를 읽어봤던 본인은 다른 사람들의 코드 길이에 경악을 금치 않을 수 없었다. 나는 140줄에 짰는데, 숏코딩으로 가독성을 포기한 코드가 아닌데도, 20줄 가량의 코드가 정답 판정을, 그것도 더 빠르게 받는 것이었다.
타인의 코드를 찬찬히 읽어보고 깨달았다. 이 문제가 고등학교때 확률과 통계 과목에서 다루었던, 길찾기 문제(위 글에서 덧셈을 이용해 푸는 방법의 핵심 아이디어)와 크게 다를 바 없다는 것을. 그냥 몇몇가지 조건을 반영 잘 시키면 된다는 것을.
#include<iostream>
int N, map[17][17] = {}, dp[3][17][17] = {};
using namespace std;
bool check(int x, int y) {
if (1 <= x && x <= N && 1 <= y && y <= N && map[x][y] == 0)
return true;
return false;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> N;
for (int i = 1; i <= N; i++)
for (int j = 1; j <= N; j++)
cin >> map[i][j];
dp[0][1][2] = 1;
for (int i = 1; i <= N; i++){
for (int j = 1; j <= N; j++) {
if (check(i, j - 1) && check(i, j))
dp[0][i][j] += dp[0][i][j - 1] + dp[2][i][j - 1];
if (check(i - 1, j) && check(i, j))
dp[1][i][j] += dp[1][i - 1][j] + dp[2][i - 1][j];
if (check(i - 1, j - 1) && check(i - 1, j) && check(i, j - 1) && check(i, j))
dp[2][i][j] += dp[0][i - 1][j - 1] + dp[1][i - 1][j - 1] + dp[2][i - 1][j - 1];
}
}
cout << dp[0][N][N] + dp[1][N][N] + dp[2][N][N];
return 0;
}
출처 : https://travelbeeee.tistory.com/277
위 코드에서는 dp라는 배열을 추가로 사용하는데, 인덱스의 의미는 다음과 같다 [파이프의 방향] [파이프 끝의 행 좌표] [파이프 끝의 열 좌표]
(파이프의 방향 : 가로 0, 세로 1, 대각 2)
그러므로 시작할 때, 파이프의 한쪽 끝이 (1,2)에 있으니 이걸 배열에 반영시켜 주고, 각 칸을 순회하면서, 이동하기에 충분한 칸이 비어 있으면, 위의 길찾기 문제 풀듯이 인접한 칸으로 갈 수 있는 경우의 수를 더해가며 문제를 해결한다.