백준

백준 9466 - 텀 프로젝트

황태건 2023. 9. 12. 14:42

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

 

9466번: 텀 프로젝트

이번 가을학기에 '문제 해결' 강의를 신청한 학생들은 텀 프로젝트를 수행해야 한다. 프로젝트 팀원 수에는 제한이 없다. 심지어 모든 학생들이 동일한 팀의 팀원인 경우와 같이 한 팀만 있을

www.acmicpc.net

간단해보여서 접근했는데.. 분기 조건을 찾는 데도 좀 헤맸고 구현에도 애를 좀 먹었다. 낮은 난이도 문제부터 다시 풀면서 깔끔한 구현을 연습해야겠음

0.

모든 정점의 사이클 포함 여부를 파악해야 하는 문제이다. 단순하게 모든 정점에서 출발하는 DFS로는 시간 초과를 해결할 수 없다. 시간 단축의 핵심은, 매 DFS마다 한 번의 DFS를 통해 도달할 수 있는 모든 정점의 사이클 포함 여부를 파악해 저장하는 것이다. 사이클에 포함되든 안되든 포함 여부가 결정되기만 하면 된다. 그게 가능한 이유는 다음 장을 보자.

1.

각 정점에서 DFS를 출발했을 때 마주하게 되는 경우를 모두 따져보자.

 

- 시작 정점의 간선이 자기 자신을 가리킬 경우

- 시작 정점에서부터 간선을 따라 쭉 이동해 시작 정점에 도달하는 경우

개꿀이다. 위 연결 그래프의 어디서 출발해도 자신이 싸이클에 속한다는 걸 확인할 수 있다.

 

- 탐색하는 도중에 이미 존재하는 사이클을 발견할 경우

 

 

불쌍한 5는 사이클에 포함되지 않는다.

그런데 여기서, 5가 2 - 3 - 4를 훑지 않아도 이 사실을 깨달을 수 있다면, 5는 조금이라도 덜 비참해지지 않을까?

 

2.

이번 DFS에서는 기본 DFS를 위한 visited 배열 외에 cycle 배열을 사용한다. cycle 배열의 원소는 사이클에 포함됨/포함되지 않음/ 아직 포함여부를 모름 의 3가지 상태를 가질 수 있다. 그 값을 순서대로 1, -1, 0이라고 하자.

 

위 그림을 다시 보자.

2, 3, 4는 이미 자기들끼리 사이클을 형성했다. 여기서 5가 손을 뻗어봤자 5를 포함한 사이클이 형성될 수 있을까? 불가능하다. 이들의 손은 하나뿐이고, 5에게 건넬 손은 더이상 남지 않았다.

 

그럼 이 경우는 어떨까?

- 탐색하는 도중에 사이클에 포함되지 않는 정점을 발견할 경우

 

다음 그림을 보자.

아무것도 모르는 6은 이미 팽 당한 5에게 다가간다. 하지만 이미 2-3-4 서클에 마음을 빼앗긴 5가 6의 제안을 받아들일 리 만무하다. 6에게 돌아올 답신 따위는 없다.

 

이 슬픈 이야기의 교훈은, 현재 노드 i가 가리키는 정점 j의 사이클 포함 여부가 정해져있기만 하면, i는 반드시 사이클에 포함되지 않는다는 것이다.

3.

이런 문제도 있다. 개인적으로 이번 이야기가 제일 슬프다.

 

- 탐색 중 경로 내부에서 새로운 사이클을 발견하는 경우

안도의 한숨을 내쉬던 1은 5의 배신으로 인해 사이클에서 낙오되었다. 눈물이 앞을 가린다....

 

어쨌든 우리는 할 일을 해야한다. DFS 경로 상의 모든 정점이 사이클 여부가 결정되지 않았다. 하지만 마지막 정점은 시작 정점을 가리키지 않는다. 이 경우 우리는 내부에 사이클이 생겼다고 판단할 수밖에 없다. 따라서 경로를 역추적하며 마지막 정점이 가리키는 정점을 찾고, 이 구간은 cycle 값을 1로, 나머지 구간은 전부 -1로 갱신한다.

 

이제 앞전의 내용을 종합하면, 매 DFS마다 우리는 3가지 결과 중 하나를 만날 수 있으며, 그때마다 다음과 같은 액션을 취하면 된다.


1) DFS 도중에 중단되는 경우

→ 사이클 확인한 정점 j를 만났다는 뜻이므로 j 앞까지 모든 정점에 사이클이 없다고 표시한다.

 

2) DFS가 중단되지 않았는데, DFS 마지막 정점이 시작 정점을 가리키는 경우

경로 전체가 사이클이므로 모든 정점에 사이클이 있다고 표시한다.

 

3) DFS가 중단되지 않았는데, DFS 마지막 정점이 시작 정점을 가리키지 않는 경우

내부에 사이클이 존재하므로, 경로를 역추적하며 사이클을 찾아 1을 표시하고 나머지 정점은 -1을 표시한다


 

말이 참 길었는데... 결론적으로 말하면 매 DFS마다 3가지 분기에 따라 path를 이용해 사이클 포함 여부를 표시해주면 된다. 참고로 cycle 배열은 DFS가 끝날 때까지 갱신되지 않으므로 DFS를 위해서는 별도의 방문 처리를 해줘야 한다.

 

소스 코드

더보기
//0 : 모름, 1 : 있음, -1 : 없음
int cycle[100001];

void solve() {
	//매 TC마다 초기화
	memset(cycle, 0, sizeof(cycle));
	
    //입력
	int N; cin >> N;
	vector<int> arr(N + 1);
	for (int i = 1; i <= N; i++)
		cin >> arr[i];
	
	for (int i = 1; i <= N; i++) {
		stack<int> path;
        
        //매번 visited 초기화하는 것보다 빠름
		set<int> visited;

		//이미 확인했을 수도 있음
		if (cycle[i]) continue;
		
		int nxt = i;
		bool flag = 0;
		
		//DFS
		while (visited.find(nxt) == visited.end()) {
			if (cycle[nxt]) {
				flag = 1;
				break;
			}
			visited.insert(nxt);
			path.push(nxt);
			nxt = arr[nxt];
		}

		//도중에 중단
		if (flag) {
			while (!path.empty()) {
				cycle[path.top()] = -1;
				path.pop();
			}
		}

		//경로 전체가 사이클
		else if (nxt == i) {
			while (!path.empty()) {
				cycle[path.top()] = 1;
				path.pop();
			}
		}

		//경로 내부에 사이클
		else {
			while (!path.empty()) {
				int top = path.top();
				cycle[top] = 1;
				path.pop();

				if (top == nxt)
					break;
			}

			while (!path.empty()) {
				cycle[path.top()] = -1;
				path.pop();
			}
				
		}
		
	}

	cout << count(cycle + 1, cycle + N + 1, -1) << endl;
}