백준 1432 그래프 수정

백준 1432
위상 정렬로 풀면 된다. 그런데 특이한 점은 보통의 위상정렬(in degree 0인 정점부터 제거)가 아닌, 리버스(?) 위상 정렬로 풀어야 한다.(out degree 0인 정점부터 제거)
처음엔 이 둘의 결과가 차이난다는 것을(단순히 순서가 뒤집히는 정도가 아니다) 납득하지 못해 직접 테스트 케이스를 생성해 결과까지 대조해봤다.

처음 소스(보통의 위상 정렬)

#include <iostream>
#include <functional>
#include <string>
#include <queue>
#include <vector>
using namespace std;

vector<int> adj[301];
int ind[301];
int ans[301];

int main() {
	int N;
	cin >> N;
	for (int i = 1; i <= N; i++) {
		string cur;
		cin >> cur;
		for (int k = 1; k <= N; k++) if (cur[k - 1] == '1') {
			adj[i].push_back(k);
			ind[k]++;
		}
	}

	priority_queue<int, vector<int>, greater<int>> pq;

	for (int i = 1; i <= N; i++) if (ind[i] == 0) {
		pq.push(i);
	}
	
	int m = 0;
	while (!(pq.empty())) {
		int cur = pq.top(); pq.pop();
		ans[cur] = ++m;
		
		for (int next : adj[cur]) {
			ind[next]--;
			if (ind[next] == 0) {
				pq.push(next);
			}
		}
	}
	
	if (m != N) {
		cout << -1;
		return 0;
	}

	for (int i = 1; i <= N; i++) {
		cout << ans[i] << ' ';
	}

	return 0;
}

도저히 맞는 것 같은데 CA가 안뜨길레
정답이라도 알아야 겠다 싶어서 인터넷 찬스를 써봤다.
https://fatc.club/2017/03/01/397

위 링크를 참조한 수정 코드(CA가 떴다)
차이점은, 처음 그래프를 만들 때 간선 방향을 모두 반대로 해서 넣어줬다.(그렇게 하면 indegree가 outdegree가 된다.) 그리고 우선순위 큐도 큰 값을 먼저 뱉는다.
그리고 그 상태로 위상 정렬을 하면 리버스(?) 위상 정렬이 되는데, 이 때도 순열 값을(M1~MN) 올림차순(++m)이 아니라 내림차순(m–)으로 넣어줬다.
한마디로 위상 정렬도 거꾸로 하는 대신 순열 값도 거꾸로 내림차순으로 넣어줬다.
처음에는 이 둘의 결과가 같을 줄 알았다. 근데 아니었다….

#include <iostream>
#include <functional>
#include <string>
#include <queue>
#include <vector>
using namespace std;

vector<int> adj[301];
int ind[301];
int ans[301];

int main() {
	int N;
	cin >> N;
	for (int i = 1; i <= N; i++) {
		string cur;
		cin >> cur;
		for (int k = 1; k <= N; k++) if (cur[k - 1] == '1') {
			adj[k].push_back(i);
			ind[i]++;
		}
	}

	priority_queue<int> pq;

	for (int i = 1; i <= N; i++) if (ind[i] == 0) {
		pq.push(i);
	}

	int m = N;
	while (!(pq.empty())) {
		int cur = pq.top(); pq.pop();
		ans[cur] = m--;

		for (int next : adj[cur]) {
			ind[next]--;
			if (ind[next] == 0) {
				pq.push(next);
			}
		}
	}

	if (m != 0) {
		cout << -1;
		return 0;
	}

	for (int i = 1; i <= N; i++) {
		cout << ans[i] << ' ';
	}

	/*for (int i = 1; i <= N; i++) {
	for (int k = 1; k <= N; k++) {
	cout << adj[i][k] << ' ';
	}
	cout << endl;
	}*/

	return 0;
}

너무나도 이해가 안간 나머지 고민을 많이 했는데, 이럴 바에야 몸이 고생해서 직접 브루트 포스로 확인해보자 싶었다. 따라서, 테스트 케이스를 랜덤으로 생성해서 두 알고리즘에 나란히 적용하고, 두 알고리즘의 결과가 다른 케이스를 발견할 때까지 무작정 반복. 오래 걸릴 줄 알았는데, 이외로 정점 4개짜리 간단한 그래프를 발견했다. 일단 아래는 테스트를 위한 소스코드. 개념을 이해하지 못해 몸으로 때운 흔적이다(…)

그에 앞서 발견한 반례를 공개한다.

4
0011
0000
0000
0100

기존 알고리즘의 결과 : 1 4 2 3
참고해 고친 알고리즘의 결과(정답) : 1 3 4 2

간선 3개짜리 간단한 그래프이나, 두 알고리즘의 차이점을 알 수 있었다.
기존 알고리즘은 1정점을 먼저 취하고, 그로 인해 indegree=0인 정점 두개(3,4)가 남았을 때 greedy하게 3을 택하는 바람에 2정점의 순서가 뒤로 밀려 버렸다. 사전 순서로 첫번째인 순열을 출력했어야 하는데 이렇듯 순서가 뒤로 밀려버리니….
그에 반해 고친 알고리즘은 outdegree=0인 정점(2,3)부터 취하는데, 이 때 큰 값부터 우선순위 큐가 뱉어주므로 3정점을 취한다. 그리고 3정점은 가장 큰 번호를 배정해준다.(N=4) 그 다음 outdegree=0인 정점은 2정점 밖에 없으므로 그 다음 값인 3을 배정해준다. 비록 두번째로 큰 번호를 배정받긴 했지만 최적값임에 놀랐다. 그 다음 차례대로 4정점 1정점 순으로 점점 작은 번호를 배정해준다.

참 동일한 알고리즘이라 생각했는데 그게 아니었다는 게 신기했다..

#include <iostream>
#include <functional>
#include <string>
#include <queue>
#include <vector>
#include <cstdlib>
#include <ctime>
using namespace std;

vector<int> adj[301], adj2[301];
int ind[301], ind2[301];
int ans[301], ans2[301];

bool f(int N) {
	fill(adj, adj + N, vector<int>());
	fill(ind, ind + N, 0);
	fill(ans, ans + N, 0);
	fill(adj2, adj2 + N, vector<int>());
	fill(ind2, ind2 + N, 0);
	fill(ans2, ans2 + N, 0);


	int test[301][301];
	for (int i = 1; i <= N; i++) {
		test[i][i] = 0;
		for (int k = i + 1; k <= N; k++) {
			int tmp = rand() % 2;
			test[i][k] = tmp;
			test[k][i] = (tmp == 0 && rand() % 2 == 1)? 1 : 0;
		}
	}


	for (int i = 1; i <= N; i++) {
		for (int k = 1; k <= N; k++) if(test[i][k] == 1){
			adj[k].push_back(i);
			ind[i]++;

			adj2[i].push_back(k);
			ind2[k]++;
		}
	}

	priority_queue<int> pq;

	for (int i = 1; i <= N; i++) if (ind[i] == 0) {
		pq.push(i);
	}

	int m = N;
	while (!(pq.empty())) {
		int cur = pq.top(); pq.pop();
		ans[cur] = m--;

		for (int next : adj[cur]) {
			ind[next]--;
			if (ind[next] == 0) {
				pq.push(next);
			}
		}
	}


	priority_queue<int, vector<int>, greater<int>> pq2;

	for (int i = 1; i <= N; i++) if (ind2[i] == 0) {
		pq2.push(i);
	}

	int m2 = 0;
	while (!(pq2.empty())) {
		int cur = pq2.top(); pq2.pop();
		ans2[cur] = ++m2;

		for (int next : adj2[cur]) {
			ind2[next]--;
			if (ind2[next] == 0) {
				pq2.push(next);
			}
		}
	}
	bool firstNeg = m != 0, secondNeg = m2 != N;
	if (firstNeg && secondNeg) {
		return true;
	}
	else if (firstNeg) {
		return false;
	}
	else if (secondNeg) {
		return false;
	}
	else {
		for (int i = 1; i <= N; i++) {
			if (ans[i] != ans2[i]) {
				return false;
			}
		}
		return true;
	}
}


int main() {

	srand(time(0));
	int K = 5;
	while(1)
		cout << (f(rand()%K + 1) ? " " : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx");
	

	return 0;
}

출력 기능이 있지만, 실제로는 false값을 리턴하는 지점에 중단점을 설정하고 변수 모니터로 로컬 변수 값(테스트 케이스 값)을 확인하는 방식을 취했다.
순열이 없는 경우(사이클이 있는 경우;-1출력)에서 틀리지 않았으리라고 생각했기 때문에, 적어도 길이 2 이하의 사이클은 배제한 테스트 케이스만 사용했다. (그 정도 조건은 구현이 간단하기도 하고)

댓글 남기기