https://www.acmicpc.net/problem/17141
얻어갈 게 많은 BFS 문제. 시간초과에 어떻게 대응할지 고민하는 시간이었다.
문제에서 추출할 수 있는 정보는 다음과 같다.
추가적인 조건을 확인할 수 있다.
이 문제는 크게 2가지를 처리해야 한다.
첫번째 문제로 시간초과가 발생할 가능성이 매우 높았다.
이때 반환해야 하는 값은 "빈칸을 모두 감염시키는데 성공한 case 중 가장 적게 걸린 시간"이다. 여기서 맵에서 가장 큰 숫자가 마지막으로 전염시킨 시간이 되므로, 성공 case의 맵 값 중 가장 큰 값을 저장한 뒤, 이 큰 값 중 가장 적은 값을 반환하면 된다. 말이 좀 어렵다.
그렇다면 실패한 case는 무엇일까? 앞서 언급했다시피 실패 조건은 0인데 아직 방문하지 않은 map 좌표가 존재할 때이다. 이 경우 -1을 반환해야 한다.
이를 반영한 BFS 코드는 위와 같다.
그렇다면 이제 어떻게 nCm을 구현할 수 있을까? 먼저 백트래킹으로 구현한 나의 첫번째 코드는 아래와 같다.
for문을 살펴보자. 저 for문의 문제점은, 예를 들어 m=3일 때 (1번 좌표, 2번 좌표, 3번 좌표)를 처음에 선택했을 때, i가 0부터 시작하기 때문에 (3번 좌표, 1번 좌표, 2번 좌표)나 (2번 좌표, 1번 좌표, 3번 좌표)와 같은 case가 중복되어 계산된다. nCm을 원하는데, 의도치 않게 nPm이 된 것이다.
실제로 디버깅을 해보면 이 코드의 문제점을 알 수 있다.
예제 1의 디버깅 결과이다.
0 0 1 5 4 3
1 5 0 0 4 3 이 보이는가?
이미 선택했던 결과를 선택 순서만 바꾸어서 반복하고 있다.
수정된 코드는 위와 같다. 이제 디버깅 결과를 살펴보자.
중복되는 케이스가 소멸한 것을 알 수 있다.
nCm과 nPm의 구현 차이에 대해서 제대로 알아야 한다는 교훈을 안겨준 문제였다.
#include <iostream>
#include <queue>
#include <algorithm>
#include <cstring>
using namespace std;
int n, m;
vector<pair<int, int> > v; // 바이러스 배치 후보 명단
int map[51][51]; // 사용될 배열
int origin[51][51]; // 원본 배열
bool visited[51][51]; // 방문 여부
vector<pair<int, int> > vv; // 선택한 바이러스 위치
bool visit_v[11]; // 바이러스 위치 선택 여부
int ans = 999;
int dx[4] = {-1, 0, 1, 0};
int dy[4] = {0, 1, 0, -1};
int BFS(vector<pair<int, int> > pos){
queue<pair<int, int> > q;
int cnt = 0;
for(int i=0; i<pos.size(); i++){ // 최초 바이러스 위치 방문처리하고 큐에 담기
q.push(make_pair(pos[i].first, pos[i].second));
visited[pos[i].first][pos[i].second] = true;
}
while(!q.empty()){ // BFS
int x = q.front().first;
int y = q.front().second;
q.pop();
if(map[x][y] > cnt){ // 가장 큰 맵의 숫자 -> 마지막으로 전염시킨 위치가 된다.
cnt = map[x][y];
}
for(int i=0; i<4; i++){
int nx = x + dx[i];
int ny = y + dy[i];
if(nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
if(visited[nx][ny] || map[nx][ny] == -1) continue;
if(!visited[nx][ny] && map[nx][ny] == 0){
map[nx][ny] = map[x][y] + 1;
q.push(make_pair(nx, ny));
visited[nx][ny] = true;
}
}
}
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
if(map[i][j] == 0 && !visited[i][j]){ // 실패 조건
return -1;
}
}
}
return cnt;
}
void copys(){ // 원본 배열로 초기화
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
map[i][j] = origin[i][j];
}
}
}
void DFS(int idx, int cnt){
if(cnt == m){ // m개 선택했다면
memset(visited, false, sizeof(visited));
copys(); // 각 선택의 경우마다 맵과 방문여부를 초기화해야한다.
int cnt = BFS(vv);
if(cnt != -1){ // -1이면 실패이므로 배제한다.
ans = min(ans, cnt);
}
return;
}
for(int i=idx; i<v.size(); i++){ // 백트래킹 방식으로 nCm을 구현한다.
if(visit_v[i]) continue;
visit_v[i] = true;
vv.push_back(v[i]);
DFS(i + 1, cnt + 1);
visit_v[i] = false;
vv.pop_back();
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
cin >> origin[i][j];
if(origin[i][j] == 2){
origin[i][j] = 0;
v.push_back(make_pair(i, j)); // 바이러스 후보군에 담는다.
} else if(origin[i][j] == 1){
origin[i][j] = -1;
}
}
}
DFS(0, 0);
if(ans == 999) ans = -1; // 모든 case가 실패했다면 초기값인 999이므로
cout << ans;
return 0;
}
정말 잘 읽었습니다, 고맙습니다!