백준 3850 Jumping monkey 풀이

7 분 소요

문제

https://www.acmicpc.net/problem/3850

&Title
3850번 - Jumping monkey

&Question
You are a hunter chasing a monkey in 
the forest, trying to shoot it down with your 
all-powerful automatic machine gun. The monkey is hiding somewhere 
behind the branches of one of the trees, out 
of your sight. You can aim at one of 
the trees and shoot; your bullets are capable of 
going through the branches and killing the monkey instantly 
if it happens to be in that tree. If 
it isn’t, the monkey takes advantage of the time 
it takes you to reload and takes a leap 
into a neighbouring tree without you noticing. It never 
stays in the same place after a shot. You 
would like to find out whether there is an 
strategy that allows you to capture the monkey for 
sure, irrespective of its initial location and subsequent jumps. 
If so, you need to determine the shortest sequence 
of shots guaranteeing this.Figure 2As an example, consider the 
situation in which there are only two neighboring trees 
in the forest (left hand side of Figure 2). 
It is then possible to make sure you capture 
the monkey by shooting twice at the same tree. 
Your first shot succeeds if the monkey happened to 
be there in the first place. Otherwise, the monkey 
was behind the other tree and it will necessarily 
have moved when you shoot for the second time.However, 
depending on the shape of the forest it may 
not possible for you to ensure victory. One example 
of this is if there are three trees, all 
connected to one another (right hand side of Figure 
2). No matter where you aim at, there are 
always two possible locations for the monkey at any 
given moment. (Note that here we are concerned with 
the worst-case scenario where the monkey may consistently guess 
your next target tree). 

&Input
The input consists of several test cases, separated 
by single blank lines. Each test case begins with 
a line containing two integers n and m (1 
≤ n ≤ 21); n is the number of 
trees in the forest, and m is the number 
of adjacency relations between trees. Each of the following 
m lines contains two distinct integers between 0 and 
n − 1 (inclusive), the identifiers of the trees 
in an adjacent pair. The order of both trees 
within a pair carries no meaning, and no pair 
appears more than once. You may further assume that 
no tree is adjacent to itself, and there is 
always a path between any two trees in the 
forest.The test cases will finish with a line containing 
only two zeros (also preceded with a blank line). 


&Output
Print a line for each test case. The 
line should contain the single word Impossible if the 
task is impossible. Otherwise, it must contain the shortest 
sequence of shots with the required property, in the 
format L: V1V2...VL, where L is the length of 
the sequence, and V1, V2, ... , VL are 
space-separated integers containing the identifiers of the trees to 
shoot at in the right order. If several shortest 
sequences exist, print the lexicographically smallest one. (A sequence 
is smaller than another in lexicographic order if the 
first element on which they differ is smaller in 
the first one). 

&Example
-input
2 1
0 1

3 3
0 1
1 2
2 0

4 3
0 1
2 3
1 3

0 0

-output
2: 0 0
Impossible
4: 1 3 3 1

요약

  • 사냥꾼은 원숭이를 총으로 쏴서 잡으려 한다.
  • 원숭이는 매턴마다 무조건 연결된 이웃한 나무로 움직인다.
  • 사냥꾼은 조준을 그대로 하거나 다른 나무로 옮길 수도 있다.
  • 원숭이의 시작 위치가 어디든간에 쏴서 죽일 수 있는 방법을 출력하라.

접근

  • 원숭이의 위치가 어디있는지 알면 쉬울텐데 모든 위치를 고려한다는것, 원숭이를 궁지에 몰아넣어 가만히 있어도 내 쪽으로 오게하여야 잡을 수 있다는것을 먼저 알 수 있다.(leaf 노드로 몰아야함)
  • n이 작기 때문에 비트마스킹으로 모든나무에 원숭이가 있다고 가정하고 시작해야함
  • bfs로 원숭이가 있는 위치에 총을 쏘고, 나머지 원숭이는 다른 갈 수 있는 나무로 옮김
  • 이미 고려된 경우의수(-1이 아닌)의 경우 무시함
  • 백트래킹으로 총을 쐈던 경로를 출력할 수 있도록 저장함

풀이

  1. 선언부부터 상당히 많다.
    • dist배열은 -1로 초기화하여 해당 경우에 대한 방문체크를 해주고, 이전 경우 + 1을 하여 몇번의 움직임만에 원숭이를 처리할수 있는지 저장한다.
    • pre배열은 이전 상태를 현재 위치에 저장하여 역탐색하게 한다. 즉 백트래킹으로 경로 출력하는 용도
    • shoot배열은 어떤 나무에서 쏘았는지 저장하는 용도있다.(pre배열은 index를 저장하는 담당이다. 내용물은 애가 갖고 있다.)
    • adjacent는 a번 나무와 인접한 나무들을 저장하는 평범한 그래프이다.
    • 현재상태에서 다음상태로 갈때 해당 나무에 원숭이가 방문할 가능한 경우를 센다.
        #define MAXN 21
        #define INF 1e9
      
        int dist[1 << MAXN]; // 해당 경우 방문확인 및 dp
        int pre[1 << MAXN]; // 이전 상태 저장
        int shoot[1 << MAXN]; // 총을 쏜 원숭이 저장
        int n, m;
        vector<int> adjacent[MAXN]; // 인접한 노드 저장
        int counter[MAXN];
      
  2. bfs를 돌려 결과를 확인한다.
    • 큐에 모든 나무에 원숭이가 있는 경우부터 넣고 시작해준다.
        dist[(1 << n) - 1] = 0; // 모든 나무에 원숭이가 있다는 가정하에서 start n=5일때 11111
        q.push((1 << n) - 1);
      
    • 큐가 빌때까지 원소를 꺼내서 반복한다. 카운터를 0으로 초기화해준다.
        while (!q.empty()) {
            int p = q.front();
            cur_state = 0;
            q.pop();
            memset(counter, 0, sizeof(counter));
      
    • 현재 나무에 원숭이가 다음 나무로 옮겨 갈 수 있는 위치에 비트로 visited 체크해준다. 카운트도 해준다.
        // 살아있을 가능성이 있는 원숭이들이 다음에 있을 수 있는 위치
        for (i = 0; i < n; ++i) {
            if (p & (1 << i)) { // 해당 위치에 원숭이가 살아 있는가?
                for (int next : adjacent[i]) { // 해당 원숭이가 갈 수 있는 곳 cur_state에 마킹
                    counter[next]++;
                    cur_state |= (1 << next); // 해당 비트 채워줌
                }
            }
        }
      
    • 현재 i번째 나무에 원숭이가 있었을때
        for (i = 0; i < n; ++i) {
            if (p & (1 << i)) {
      
      • i번째 나무에 있던 원숭이를 총으로 쏴서 죽일 것이다. 원숭이가 갈 수 있는 인접한 나무의 counter가 1보다 작으면 비트를 비워준다.(그 원숭이가 죽었으므로)
          next_state = cur_state;
              for (int next : adjacent[i]) {
                  if (counter[next] - 1 <= 0) // 해당 위치에 원숭이가 가는 경우가 한번 이하면
                      next_state &= ~(1 << next); // 해당 비트 지워줌, 방문 불가
              }
        
      • 해당 경우가 처음이라면 이전 경우 + 1(dp)값을 담아주고 큐에 next_state를 넣어준다. 그리고 어느 나무에 조준하고 쏘았는지 저장, 모든 원숭이가 죽었다면 dp로 담았던 값을 return 해준다.
          if (dist[next_state] == -1) { // 해당 경우가 처음이라면
              dist[next_state] = dist[p] + 1; // 이전 경우에서 + 1
              q.push(next_state); // 다음 상태 넣어줌
              pre[next_state] = p; // 이전 경우 넣어줌
              shoot[next_state] = i; // 어디를 쏘고있는지 저장
              if (next_state == 0)return dist[next_state]; // 모든 원숭이가 죽었다면 finish
          }
        
  3. 결과를 출력해주는데 INF인경우 모든 원숭이를 잡을 수 없기 때문에 Impossible을 출력, 모두 잡은 경우 경로를 백트래킹으로 출력해준다.
     void print(int st) {
         if (st == (1 << n) - 1)return;
         print(pre[st]);
         cout << ' ' << shoot[st];
     }
    
     int main(){
    
         res = bfs();
    
         if (res == INF) {
             cout << "Impossible\n";
             continue;
         }
         cout << res << ':';
         print(0);
         cout << '\n';
     }
    

틀렸던 이유

  • 비트마스킹과 bfs개념이 합쳐지니 문제가 상당히 어려웠다… 비트마스킹 개념을 쓰지 않으면 시간초과는 당연지사인 문제였다.

소스

#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;

#define MAXN 21
#define INF 1e9

int dist[1 << MAXN]; // 해당 경우 방문확인 및 dp
int pre[1 << MAXN]; // 이전 상태 저장
int shoot[1 << MAXN]; // 총을 쏜 원숭이 저장
int n, m;
vector<int> adjacent[MAXN]; // 인접한 노드 저장
int counter[MAXN];

int bfs() {
	int cur_state, next_state, i, j;
	queue<int> q;
	memset(dist, -1, sizeof(dist)); // 해당 경우가 이미 있었는지 확인하기 위해 -1 초기화
	dist[(1 << n) - 1] = 0; // 모든 나무에 원숭이가 있다는 가정하에서 start n=5일때 11111
	q.push((1 << n) - 1);
	while (!q.empty()) {
		int p = q.front();
		cur_state = 0;
		q.pop();
		memset(counter, 0, sizeof(counter));
		// 살아있을 가능성이 있는 원숭이들이 다음에 있을 수 있는 위치
		for (i = 0; i < n; ++i) {
			if (p & (1 << i)) { // 해당 위치에 원숭이가 살아 있는가?
				for (int next : adjacent[i]) { // 해당 원숭이가 갈 수 있는 곳 cur_state에 마킹
					counter[next]++;
					cur_state |= (1 << next); // 해당 비트 채워줌
				}
			}
		}
		for (i = 0; i < n; ++i) {
			if (p & (1 << i)) {
				next_state = cur_state;
				for (int next : adjacent[i]) {
					if (counter[next] - 1 <= 0) // 해당 위치에 원숭이가 가는 경우가 한번 이하면
						next_state &= ~(1 << next); // 해당 비트 지워줌, 방문 불가
				}
				if (dist[next_state] == -1) { // 해당 경우가 처음이라면
					dist[next_state] = dist[p] + 1; // 이전 경우에서 + 1
					q.push(next_state); // 다음 상태 넣어줌
					pre[next_state] = p; // 이전 경우 넣어줌
					shoot[next_state] = i; // 어디를 쏘고있는지 저장
					if (next_state == 0)return dist[next_state]; // 모든 원숭이가 죽었다면 finish
				}
			}
		}
	}
	return INF; // 원숭이를 모두 죽이지 못한채 queue가 비었다면 실패
}


void print(int st) {
	if (st == (1 << n) - 1)return;
	print(pre[st]);
	cout << ' ' << shoot[st];
}

int main() {
	int res;
	while (true) {
		cin >> n >> m;
		if (n == 0 && m == 0)break;
		for (int i = 0; i < n; ++i)
			adjacent[i].clear();
		int a, b;
		for (int i = 0; i < m; ++i) {
			cin >> a >> b;
			adjacent[a].push_back(b);
			adjacent[b].push_back(a);
		}

		res = bfs();

		if (res == INF) {
			cout << "Impossible\n";
			continue;
		}
		cout << res << ':';
		print(0);
		cout << '\n';
	}
	return 0;
}

댓글남기기