알고리즘 문제해결 전략 Chapter 28 - WORDCHAIN(C++)

포타토·2020년 2월 2일
0

알고리즘

목록 보기
35/76

예제: 단어 제한 끝말잇기

끝말잇기는 참가자들이 원을 그리고 앉은 뒤, 시계 방향으로 돌아가면서 단어를 말하는 게임입니다. 이 때 각 사람이 말하는 단어의 첫 글자는 이전 사람이 말한 단어의 마지막 글자와 같아야 합니다. 단어 제한 끝말잇기는 일반적인 끝말잇기와 달리 사용할 수 있는 단어의 종류가 게임 시작 전에 미리 정해져 있으며, 한 단어를 두 번 사용할 수 없습니다. 단어 제한 끝말잇기에서 사용할 수 있는 단어들의 목록이 주어질 때, 단어들을 전부 사용하고 게임이 끝날 수 있는지, 그럴 수 있다면 어떤 순서로 단어를 사용해야 하는지를 계산하는 프로그램을 작성하세요.

입력
입력의 첫 줄에는 테스트 케이스의 수 C (1 <= C <= 50) 가 주어집니다. 각 테스트 케이스의 첫 줄에는 게임에서 사용할 수 있는 단어의 수 N (1 <= N <= 100) 이 주어집니다. 그 후 N 줄에 하나씩 게임에서 사용할 수 있는 단어가 주어집니다. 각 단어는 알파벳 소문자로 구성되어 있으며, 길이는 2 이상 10 이하입니다. 중복으로 출현하는 단어는 없습니다.

출력
각 테스트 케이스마다 한 줄을 출력합니다. 만약 모든 단어를 사용하고 게임이 끝나는 방법이 없다면 "IMPOSSIBLE" 을 출력하고 (따옴표 제외), 방법이 있다면 사용할 단어들을 빈 칸 하나씩을 사이에 두고 순서대로 출력합니다. 방법이 여러 개 있다면 그 중 아무 것이나 출력해도 좋습니다.

예제 입력

3
4
dog
god
dragon
need
3
aa
ab
bb
2
ab
cd

예제 출력

need dog god dragon
aa ab bb
IMPOSSIBLE

풀이

처음 이 문제를 접했을 때, 난이도 '하' 라고 적혀있길래 지난번에 풀었던 DICTIONARY(고대어 사전) 문제에 오일러 서킷과 트레일 개념이 더해진 것이겠거니, 했다.

하지만, 그 오일러 서킷과 트레일 개념을 제대로 이해 못해서 아주 고생한 문제이다.
그래프의 깊이 우선 탐색과 유사하지만, 오일러 개념을 모른다면 매우 어려운 문제가 될 것이다.

소스코드를 일일이 분석하기는 그렇고, 필자가 해맸던 개념을 알아보자.

  1. 오일러 서킷/트레일
    1) 오일러 서킷 : 쉽게 말해, 한붓 그리기가 가능한 그래프(ex: a -> b -> c -> a)
    2) 오일러 트레일 : 쉽게 말해, 한붓 그리기가 가능한 그래프에서 지작점과 끝점의 edge를 뺀 것(ex: a -> b-> c)

  2. 오일러 서킷/트레일 판별법
    1) 각 vertex에 들어오는 edge의 개수(이하 in), 나가는 edge(이하 out)의 개수를 센다.
    2) 오일러 서킷이라면, 각 vertex의 in과 out의 개수가 같아야 한다(in == out)
    3) 오일러 트레일이라면, 시작점과 끝점을 제외하고 vertex의 in과 out의 개수가 같아야 한다. 또한, 시작점과 끝점의 in과 out의 차는 1이다.(시작점 in + 1 = 끝점 in, 시작점 out = 끝점 out + 1)

  3. 구현시 주의점
    1) 오일러 서킷은 어느 vertex에서 시작하나 답을 구할 수 있고, reverse도 할 필요가 없다. 말했듯이 한붓그리기가 가능하기 때문에.
    2) 오일러 트레일은 시작점에서 시작해야하만 답을 구할 수 있다. 또한, 끝에 reverse를 해 주어야 한다.

이들을 잘 조합해서 구현해주어야 하고, 행렬 또는 벡터를 charater와 맵핑하여 가지고 놀기 때문에 오타 등에 매우 주의해 주어야 한다.

전체적인 소스코드는 아래와 같다.

소스 코드

#include<iostream>
#include<vector>
#include<string>
#include<cstring>
#include<algorithm>

using namespace std;

int N;
vector<string> words;
int adj[26][26];
int outEdge[26], inEdge[26];
vector<string> wordsMap[26][26];
vector<int> order;

void init();
void makeAdj();
int isEuler();
void getEulerCircuitOrTrail(int);
void getEuler(int);
void dfsAll();

int main() {
	
	int testCase;
	cin >> testCase;
	for (int tc = 0; tc < testCase; tc++) {
		cin >> N;

		init();
		for (int i = 0; i < N; i++) {
			cin >> words[i];
		}

		dfsAll();
	}

	return 0;
}

void init() {
	words = vector<string>(N);
	memset(adj, 0, sizeof(adj));
	memset(outEdge, 0, sizeof(outEdge));
	memset(inEdge, 0, sizeof(inEdge));
	for (int y = 0; y < 26; y++) {
		for (int x = 0; x < 26; x++) {
			wordsMap[y][x].clear();
		}
	}
	order.clear();
}

void makeAdj() {
	for (int i = 0; i < words.size(); i++) {
		int start = words[i][0] - 'a';
		int end = words[i][words[i].size() - 1] - 'a';
		//adj
		adj[start][end]++;
		//in.out
		outEdge[start]++;
		inEdge[end]++;
		//word
		wordsMap[start][end].push_back(words[i]);
	}
}

int isEuler() {
	//차이나는 정점은 둘 다 존재하거나 없음
	//차이나는 정점은 처음과 끝 두개만 존재
	//두 개의 차는 1
	int diffOut = 0, diffIn = 0;

	for (int i = 0; i < 26; i++) {
		int diff = outEdge[i] - inEdge[i];
		if (diff < -1 && diff > 1)
			return 0;
		if (diff == 1) diffOut++;
		if (diff == 1) diffIn++;
	}

	//Euler Circuit이면 1, trail이면 2를 리턴
	if (diffOut == 0 && diffIn == 0)
		return 1;
	else if (diffOut == 1 && diffIn == 1)
		return 2;

	return 0;
}

void getEuler(int here) {
	for (int next = 0; next < 26; next++) {
		while (adj[here][next] > 0) {
			adj[here][next]--;
			getEuler(next);
		}
	}

	order.push_back(here);
}

void getEulerCircuitOrTrail(int type) {
	//Euler Circuit인 경우
	if (type == 1) {
		for (int i = 0; i < 26; i++) {
			if (outEdge[i] > 0) {
				getEuler(i);
				return;
			}
		}
	}
	//Euler Trail인 경우
	else {
		for (int i = 0; i < 26; i++) {
			if (outEdge[i] - inEdge[i] == 1) {
				getEuler(i);
				return;
			}
		}
	}
}

void dfsAll() {
	makeAdj();

	int type = isEuler();
	if (!type) {
		cout << "IMPOSSIBLE" << endl;
		return;
	}

	getEulerCircuitOrTrail(type);

	//모든 정점을 방문하지 않았다면 false;
	//정점간 단어가 1개이므로, order 개수가 단어 개수보다 1 많다.
	if (order.size() != words.size() + 1) {
		cout << "IMPOSSIBLE" << endl;
		return;
	}

	//Euler circuit이면 reverse 할 필요 없으나, trail이면 해야 함
	reverse(order.begin(), order.end());

	for (int i = 0; i < order.size() - 1; i++) {
		cout << wordsMap[order[i]][order[i + 1]].back() << " ";
		wordsMap[order[i]][order[i + 1]].pop_back();
	}
	cout << endl;
}

풀이 후기

하.. 필자에겐 너무도 어려운 문제였다.
이런 문제를 풀때마다 스스로에 대한 실망감이 커지는 느낌이다. 난이도가 하라고 써져있어서 더 그런지 모르겠다. 그래도 다음번에 비슷한 유형의 문제를 풀 때는 훨씬 빨리 풀 수 있겠지..
필자는 변수를 선언해서 메모리 할당을 많이 안하려는 습성이 있다. 물론 좋은 것이겠지만, 위 풀이에서 wordsMap으로 단어들을 저장해서 하지 않고 order를 사용해서 단어를 찾아내려다 성공도 못하고 엄청난 시간을 소모했다. 과감히 메모리 할당을 해보는것도 때로는 좋지 않을까.. 하는 생각이 든다.

profile
개발자 성장일기

0개의 댓글

관련 채용 정보