728x90
 

1547번: 공

첫째 줄에 컵의 위치를 바꾼 횟수 M이 주어지며, M은 50보다 작거나 같은 자연수이다. 둘째 줄부터 M개의 줄에는 컵의 위치를 바꾼 방법 X와 Y가 주어지며, X번 컵과 Y번 컵의 위치를 서로 바꾸는 것을 의미한다. 컵을 이동시키는 중에 공이 컵에서 빠져나오는 경우는 없다. X와 Y의 값은 3보다 작거나 같고, X와 Y가 같을 수도 있다.

www.acmicpc.net

#include <iostream>
#include <string>
#include <map>
using namespace std;


int main() {
	int m;
	cin >> m;
	int cup[3] = { 1, 2, 3 };
	map<int, int> map;
	map[1] = 0;
	map[2] = 1;
	map[3] = 2;
	for (int i = 0; i < m; i++) {
		int from, to;
		cin >> from >> to;
		swap(cup[map[from]], cup[map[to]]);
		swap(map[from], map[to]);
	}
	cout << cup[0];
	return 0;
}
728x90
728x90

map은 데이터를 key-value 형태로 저장하는 자료구조이다.

http://www.cplusplus.com/reference/map/map/

 

map - C++ Reference

difference_typea signed integral type, identical to: iterator_traits ::difference_type usually the same as ptrdiff_t

www.cplusplus.com

 

map을 선언하는 방법은 다음과 같다.

map<int, int> m;

map은 템플릿 클래스이기 때문에 선언할 때 <> 사이에 key 값의 타입, value 값의 타입을 순서대로 적어줘야 한다.

key 타입과 value 타입이 같지 않아도 된다.

map<int, string> m;

이렇게 타입이 달라도 상관없다.

 

이렇게 선언된 map에 데이터를 저장하고 싶다면

m[1] = "one";

이렇게 하면 된다.

크기가 확보된 것도 아닌데 빈 map 객체인데도 이렇게 인덱스로 저장이 가능하다.

벡터의 경우에 데이터를 추가하고 싶다면 push_back() 함수를 써야 하고 set의 경우에는 insert() 함수를 써서 데이터를 추가할 수 있었는데 map은 원래 1이라는 인덱스가 있는 것처럼 저렇게 key 값을 [ ] 사이에 적고 그 키 값에 해당하는 value 값을 대입해주면 된다.

다만 주의할 것은 map이라는 자료구조는 key를 통해서 원하는 값을 꺼내오는 것이기 때문에 key 값은 유일한 값이어야 한다. 즉 key 값들 끼리는 중복이 없어야 한다. 다음 코드를 보자.

#include <iostream>
#include <string>
#include <map>
using namespace std;


int main() {
	map<string, int> m;
	m["김철수"] = 15;
	m["김영희"] = 23;
	cout << "철수의 나이: " << m["김철수"] << endl;
	cout << "영희의 나이: " << m["김영희"] << endl;
	return 0;
}

이런 코드가 있을 때 이 코드를 실행하면

이렇게 철수와 영희의 나이가 제대로 출력된 것을 볼 수 있다.

 

하지만 10살짜리 김철수라는 동명이인이 있어서 map에 이 김철수의 데이터를 추가하면 어떻게 될까?

#include <iostream>
#include <string>
#include <map>
using namespace std;


int main() {
	map<string, int> m;
	m["김철수"] = 15;
	m["김영희"] = 23;
	m["김철수"] = 10; //새로 추가된 10살짜리 김철수
	cout << "철수의 나이: " << m["김철수"] << endl;
	cout << "영희의 나이: " << m["김영희"] << endl;
	cout << "철수의 나이: " << m["김철수"] << endl;
	return 0;
}

 

새로운 김철수가 추가된 것이 아니라 원래 있던 15짜리 철수의 나이가 10살로 바뀐 것을 볼 수 있다.

따라서 map을 사용할 때 key는 중복될 수도 있는 데이터가 아니라 unique한 데이터를 key 값으로 하는 것이 좋다. 만약 이 예제처럼 했다면 나도 모르게 이런 실수를 하고는 이상한 결과가 나온다고 코드를 붙잡고 끙끙댈 수가 있다.

 

이제 map 객체 선언 방법과 map에 데이터를 추가하는 방법을 알았으니 유용한 함수들을 알아보자.

+size(): size_type map에 저장된 쌍의 개수를 반환한다.
+empty(): bool map이 비어있는지 아닌지 반환한다.
+find(k: key_type): iterator 키값인 key가 있으면 그 iterator 반환, 없으면 end() 반환
+count(k: key_type): size_type k라는 키값을 가진 key의 개수를 반환한다. (0또는 1 반환)
+insert(pair<key_type, value_type>) pair의 first 값을 key로, second 값을 value로 가지는 쌍을 map 객체에 추가
+erase(k: key_type): size_type 해당 키 값을 가진 데이터를 삭제한다.
+erase(position: iterator): iterator 해당 iterator 위치에 있는 원소를 삭제한다.
+begin(): iterator map 객체의 시작 주소를 반환한다.
+end(): iterator map 객체의 가장 끝 원소의 다음 주소를 반환한다.

count() 함수는 사실 find() 함수로 대체할 수 있다. count() 함수는 해당 키 값을 가진 key의 개수를 반환한다고는 하지만 key 값들은 서로 중복이 없는 유일한 값이기 때문에 많아봤자 최대 1을 반환한다.

#include <iostream>
#include <string>
#include <map>
#include <tuple>
using namespace std;


int main() {
	map<string, int> m;
	cout << "m의 크기: " << m.size() << endl; // 0 출력
	cout << "m은 비어있습니까? " << (m.empty() ? "true" : "false") << endl; //true 출력
	m["김철수"] = 15;
	m["김영희"] = 23;
	cout << "m의 크기: " << m.size() << endl; //2 출력
	m.insert(make_pair("홍길동", 40));
	cout << "홍길동의 나이: " << m["홍길동"] << endl; //40 출력
	cout << ((m.find("김철수") != m.end()) ? "m에 김철수 있습니다." : "m에 김철수 없습니다.") << endl;
	map<string, int>::iterator iter = m.find("김철수");
	if (iter != m.end()) cout << "김철수의 나이: " << iter->second << endl; // 주소는 -> 연산자를 통해 접근. (*iter).second도 가능
	cout << ((m.find("김민지") != m.end()) ? "m에 김민지 있습니다." : "m에 김민지 없습니다.") << endl;
	cout << "삭제 전: " << ((m.find("김철수") != m.end()) ? "m에 김철수 있습니다." : "m에 김철수 없습니다.") << endl;
	m.erase(iter); //iterator를 이용해 삭제
	cout << "삭제 후: " << ((m.find("김철수") != m.end()) ? "m에 김철수 있습니다." : "m에 김철수 없습니다.") << endl;
	cout << "삭제 전: " << ((m.find("홍길동") != m.end()) ? "m에 홍길동 있습니다." : "m에 홍길동 없습니다.") << endl;
	m.erase("홍길동"); //key 값으로 삭제
	cout << "삭제 후: " << ((m.find("홍길동") != m.end()) ? "m에 홍길동 있습니다." : "m에 홍길동 없습니다.") << endl;
	return 0;
}

이 코드를 실행하면 다음과 같은 결과가 나온다.

 

함수들의 사용법은 매우 간단하기 때문에 이 예제 코드와 실행 결과만 봐도 알 수 있을 것이라 생각된다.

 

그럼 이제 map에 저장된 모든 데이터 쌍을 순회하는 법을 알아보자.

배열이나 벡터 같은 경우는 for(int i = 0; i < n; i++) 이 문장으로 모든 것이 가능했다. 이렇게 하고 for문 안에서 i를 인덱스로 해서 그 인덱스에 해당하는 원소에 접근할 수 있었다. 하지만 map은 인덱스로 접근이 불가능하고 key 값으로 접근이 가능한 자료구조이기 때문에 순회를 하려면 iterator를 사용해야 한다.

#include <iostream>
#include <string>
#include <map>
#include <tuple>
using namespace std;


int main() {
	map<string, int> m;
	m["김철수"] = 15;
	m["김영희"] = 23;
	m["홍길동"] = 40;
	for (map<string, int>::iterator iter = m.begin(); iter != m.end(); iter++) {
		cout << "이름: " << iter->first << ", 나이: " << iter->second << endl;
	}
	return 0;
}

for(int i = 0; i < n; i++) 대신에 for(map<string, int>::iterator iter = m.begin(); iter != m.end(); iter++)를 쓰는데

for(map<string, int>::iterator iter = m.begin(); iter != m.end(); iter++)

이 코드 꼭 알아두자.

iterator는 주소이기 때문에 iter.first가 아니라 iter->first 또는 (*iter).first로 접근이 가능하다.

 

예를 들어 하나의 원소가 8바이트인 map 객체가 있는데 3개의 쌍이 들어있고 map의 시작 주소는 1000이라고 하자.

그러면 첫 번째 원소는 1000번지~1007번지 이렇게 8바이트에 걸쳐서 저장되어 있다.

두 번째 원소는 1008번지~1015번지에, 세 번째 원소 즉 마지막 원소는 1016번지~1023번지에 저장되어 있을 것이다.

iterator 변수인 iter를 iter++를 하면 iter가 가지고 있는 주소는 8바이트씩 증가한다.

즉 처음에 iter=m.begin() 이렇게 선언되었을 때 iter는 1000, for문을 한 번 돌고 나면 iter++에 의해 1008이 되고, 그 다음 for문을 돌면서 1016이 된다. 그리고 iter++에 의해서 1024가 된다. 이 1024가 바로 m.end()이다.

그렇게 for문을 돌면서 순회하다가 for문에서 조건 체크를 한다. iter != m.end()이어야 for문의 body 부분으로 들어갈 수 있는데 1024는 m.end()이다. 따라서 for문을 빠져나가게 되는 것이다.

포인터에 대해 익숙치 않은 분이라면 https://breakcoding.tistory.com/81

 

[C++] 포인터

포인터를 어려워하는 분들이 많은 것 같다. 물론 나도 겁먹었었다. 하지만 전혀 어렵지 않다는 것을 알게 되었고 다른 사람들에게도 포인터가 어렵지 않다는 것을 알려주고 싶다. 일반적으로 변수는 정수, 실수,..

breakcoding.tistory.com

이 글을 먼저 보고 오시는 것을 추천드린다.

 

그래서 아까 find() 함수를 사용할 때에도

if(m.find("홍길동") != m.end())

이런 식으로 사용했던 것이다. find() 함수는 iterator가 map을 순회하면서 find()의 매개변수로 들어온 그 key 값을 가진 원소가 있는지 찾는 것인데 m.end()까지 탐색해보고 없으면 멈추는 것이기 때문이다.

(선형 탐색을 하는 것처럼 말했지만 사실 map은 항상 정렬되어 있기 때문에 선형탐색이 아니라 이진탐색을 해서 find() 함수의 시간 복잡도는 로그 복잡도를 가진다.)

 

STL 라이브러리를 알아두면 굉장히 유용하므로 자주 쓰이는 함수, 순회하는 방법은 꼭 알아두자.

더 많은 함수를 알고 싶다면 http://www.cplusplus.com/reference/map/map/

 

map - C++ Reference

difference_typea signed integral type, identical to: iterator_traits ::difference_type usually the same as ptrdiff_t

www.cplusplus.com

이 사이트에 직접 들어가서 보는 것을 추천한다.

728x90
728x90
 

코딩테스트 연습 - 오픈채팅방 | 프로그래머스

오픈채팅방 카카오톡 오픈채팅방에서는 친구가 아닌 사람들과 대화를 할 수 있는데, 본래 닉네임이 아닌 가상의 닉네임을 사용하여 채팅방에 들어갈 수 있다. 신입사원인 김크루는 카카오톡 오픈 채팅방을 개설한 사람을 위해, 다양한 사람들이 들어오고, 나가는 것을 지켜볼 수 있는 관리자창을 만들기로 했다. 채팅방에 누군가 들어오면 다음 메시지가 출력된다. [닉네임]님이 들어왔습니다. 채팅방에서 누군가 나가면 다음 메시지가 출력된다. [닉네임]님이 나갔습니다. 채팅

www.welcomekakao.com

 

2019 카카오 신입 공채 1차 코딩 테스트 문제 해설

작년에 이어 올해도 블라인드 전형으로 카카오 개발 신입 공채가 시작되었습니다! 그 첫 번째 관문으로 1차 온라인 코딩 테스트가 지난 9월 15일(토) 오후 2시부터 7시까지 5시간 동안 치러졌는데요. 지원자분들 만큼이나 준비위원들도 테스트가 문제없이, 공정하게 치러질 수 있도록 많은 준비를 했고 두근 거리는 마음으로 끝까지 온라인 테스트를 모니터링했답니다. 문제는 작년과 비슷하게 구현 문제 위주로 쉬운 난이도에서 어려운 […]

tech.kakao.com

#include <string>
#include <vector>
#include <tuple>
#include <map>
using namespace std;

vector<string> solution(vector<string> record) {
    vector<string> answer;
    map<string, int> m;
    vector<string> ids;
    vector<string> nicknames;
    vector<pair<int, string>> ment;
    for (int i = 0; i < record.size(); i++) {
        if (record[i][0] == 'E') {
            int j = 6;
            while (record[i][j] != ' ') {
                j++;
            }
            string id = record[i].substr(6, j - 6);
            string nickname = record[i].substr(j + 1);
            //answer.push_back(nickname + "님이 들어왔습니다.");
            if (m.find(id) != m.end()) {
                nicknames[m[id]] = nickname;
            }
            else {
                ids.push_back(id);
                nicknames.push_back(nickname);
                m[id] = ids.size() - 1;
            }
            ment.push_back(make_pair(m[id], "님이 들어왔습니다."));
        }
        else if (record[i][0] == 'L') {
            string id = record[i].substr(6);
            ment.push_back(make_pair(m[id], "님이 나갔습니다."));
        }
        else if (record[i][0] == 'C') {
            int j = 7;
            while (record[i][j] != ' ') {
                j++;
            }
            string id = record[i].substr(7, j - 7);
            string nickname = record[i].substr(j + 1);
            nicknames[m[id]] = nickname;
        }
    }
    for (pair<int, string> t : ment) {
        string temp = nicknames[t.first] + t.second;
        answer.push_back(temp);
    }
    return answer;
}

 

728x90

'알고리즘 문제' 카테고리의 다른 글

[백준] 1120번 문자열  (0) 2020.02.09
[백준] 1547번 공  (0) 2020.02.09
[백준] 3613번 Java vs C++  (0) 2020.02.08
[백준] 10808번 알파벳 개수  (0) 2020.02.08
[백준] 11403번 경로 찾기  (0) 2020.02.07
728x90
 

3613번: Java vs C++

문제 Java 예찬론자 김동규와 C++ 옹호가 김동혁은 서로 어떤 프로그래밍 언어가 최고인지 몇 시간동안 토론을 하곤 했다. 동규는 Java가 명확하고 에러가 적은 프로그램을 만든다고 주장했고, 동혁이는 Java는 프로그램이 느리고, 긴 소스 코드를 갖는 점과 제네릭 배열의 인스턴스화의 무능력을 비웃었다. 또, 김동규와 김동혁은 변수 이름을 짓는 방식도 서로 달랐다. Java에서는 변수의 이름이 여러 단어로 이루어져있을 때, 다음과 같은 방법으로 변수명을

www.acmicpc.net

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


int main() {
	string s;
	cin >> s;
	if (s[0] < 97) {
		cout << "Error!"; //첫 글자가 대문자면 에러
		return 0;
	}
	vector<int> underscoreIndex;
	vector<int> capitalLetterIndex;
	for (int i = 1; i < s.size(); i++) {
		if (s[i] == '_')
			underscoreIndex.push_back(i);
		else if (char c = s[i] < 97)
			capitalLetterIndex.push_back(i);
	}
	if (underscoreIndex.size() == 0 && capitalLetterIndex.size() == 0) {
		cout << s; //언더바도 안 나오고 대문자도 안 나오면 그대로 출력
		return 0;
	}
	else if (underscoreIndex.size() != 0 && capitalLetterIndex.size() != 0) {
		cout << "Error!"; //언더바도 나오고 대문자도 나오면 에러
		return 0;
	}
	else if (s[s.size() - 1] == '_' || s[0] == '_') {
		cout << "Error!";//첫글자나 마지막 글자가 언더바면 에러
		return 0;
	}
	else if (underscoreIndex.size() != 0) {// 대문자가 안 나오는 C++ 형식의 경우
		int previous = underscoreIndex[0];
		for (int i = 1; i < underscoreIndex.size(); i++) {
			if (underscoreIndex[i] - previous == 1) {//언더바가 연속으로 두개 나오면 에러
				cout << "Error!";
				return 0;
			}
			previous = i;
		}
		int count = 0;
		for (int i : underscoreIndex) {
			s.erase(i - count, 1);
			s.replace(i - count, 1, 1, s[i - count] - 32);
			count++;
		}
	}
	else if (capitalLetterIndex.size() != 0) {//언더바가 안 나오는 Java 형식의 경우
		int count = 0;
		for (int i : capitalLetterIndex) {
			s.replace(i + count, 1, 1, s[i + count] + 32);
			s.insert(i + count, 1, '_');
			count++;
		}
	}
	cout << s;
	return 0;
}
728x90
728x90
 

10808번: 알파벳 개수

단어에 포함되어 있는 a의 개수, b의 개수, …, z의 개수를 공백으로 구분해서 출력한다.

www.acmicpc.net

a의 아스키코드는 97, b의 아스키코드는 98, c의 아스키코드는 99 이런식으로 가기 때문에 char형으로 바꾸고 char에는 아스키코드인 정수가 저장되어있으니 97을 뺀 그 인덱스의 count를 늘려주었다.

#include <iostream>
#include <string>
using namespace std;


int main() {
	string s;
	cin >> s;
	int alphabet[26];
	for (int i = 0; i < 26; i++) {
		alphabet[i] = 0;
	}
	for (int i = 0; i < s.size(); i++) {
		char c = s[i];
		alphabet[c - 97]++;
	}
	for (int i = 0; i < 26; i++) {
		cout << alphabet[i] << " ";
	}
	return 0;
}
728x90
728x90

최단경로 알고리즘은 플로이드 알고리즘과 다익스트라 알고리즘이 가장 대표적인 두 알고리즘이다.

다익스트라 알고리즘은 출발지가 주어졌을 때 그 출발지(one)로부터 모든 지점(all)으로 가는 최단 경로를 구하는 one-to-all 알고리즘이고 시간복잡도는 O(n^2)이다.

반면 플로이드 알고리즘은 그래프 상의 정점들의 모든 쌍에 대해서 그 두 점 사이의 최단경로 값을 다 구하는 알고리즘이다. 따라서 모든 출발지에서 모든 도착지로의 최단 경로를 구하는 all-to-all 알고리즘이고 시간복잡도는 O(n^3)이다.

정점이 n개인 그래프에서 다익스트라 알고리즘은 출발점이 한 개라면 플로이드 알고리즘은 출발점이 n개이다.

그렇기 때문에 당연히 플로이드 알고리즘이 시간복잡도가 높을 수밖에 없고 현실에서 플로이드 알고리즘보다 다익스트라 알고리즘을 더 많이 쓰는 이유가 바로 그것이다. 현실에서 필요할 것 같은 일 보다도 과하게 한꺼번에 일을 많이 하는 셈이다.

 

플로이드 알고리즘은 동적계획법으로 풀 수 있다.

다음 그래프를 가지고 풀어보자.

방법은 다음과 같다.

 

1. 어떠한 정점도 경유를 허용하지 않고 Vi에서 Vj로 가는 최단거리를 구해서 D0에 저장한다.

D0이라는 것은 아무 정점도 허용하지 않는다는 뜻이다.

이 그래프를 표현한 자료구조인 인접행렬은 정점 v1에서 v2 사이에 이음선이 있으면 그 가중치가 써있고 아니라면 무한대가 써있다. 즉, 어떠한 정점도 경유를 허용하지 않고 v1에서 v2로 가는 최단경로이다.

따라서 D0은 인접행렬 W와 같다. (Vi와 Vj 사이에 이음선이 없는 경우 INF(무한대)로 표기했다.)

W[i][j]  1   2   3   4   5
   1     0   1  INF  1   5
   2     9   0   3   2  INF
   3    INF INF  0   4  INF              = D0
   4    INF INF  2   0   3
   5     3  INF INF INF  0

 

2. V1만 경유할 수 있도록 허용했을 때 Vi에서 Vj로 가는 최단거리를 구해서 D1에 저장한다.

D1에서 1이란 V1까지 경유 가능하다는 뜻이다.

이전 단계에서는 아무 정점도 거치지 않고 Vi에서 Vj로 가는 최단경로였는데 이제 경유할 수 있는 정점이 한 개가 더 허용된 것이다.

이제 동적계획법의 특징인 이전 단계에서 구해놓은 것을 이용해야 한다.

허용된 정점이 하나 더 늘어난 것인데 그 추가로 허용된 정점을 Vk라고 하자.

이 경우 두 가지 경우가 발생한다.

첫번째 경우는 정점 k가 추가로 허용되었지만  Vk를 거치지 않고 이전단계에서처럼 Vi에서 Vj로 바로 가는 것이 더 이득인 경우이고

두 번째 경우는 Vk를 거쳐서 가는 덕분에 더 가중치가 적은 경로(최단경로)가 새로 발견되는 경우이다.

만약 Vi와 Vj를 바로 잇는 이음선이 없어서 Vi에서 Vj로 가는 경로가 존재하지 않아 D0에서 D0[i][j]는 무한대(INF)였는데 k를 경우하는 덕분에 Vi에서 Vj로 가는 경로가 생기는 경우는 두 번째 경우이다.

V5에서 V2로 가는 경우를 생각해보자.

V5와 V2를 바로 잇는 이음선이 없기 때문에 D0[5][2]는 무한대(INF)였다. 하지만 D1에서 V1을 경유하는 것을 허용하면 V5에서 V1을 경유해서 V2로 갈 수가 있다. 새로운 최단 경로가 발견되는 것이다.

따라서 D1[5][2]는 D0[5][1]+D[1][2] = 3+1=4가 된다.

첫 번째 경우는 D0[i][j]으로, 두 번째 경우는 D0[i][k] + D0[k][j]으로 표현할 수 있다.

D1[i][j] = minimum(D0[i][j], D0[i][1]+D0[1][j])이다.

D1[i][j]  1   2   3   4   5
   1      0   1  INF  1   5
   2      9   0   3   2   14
   3     INF INF  0   4  INF
   4     INF INF  2   0   3
   5      3   4  INF  4   0

 

3. V1, V2만 경유할 수 있도록 허용했을 때 (V2를 추가로 허용했을 때) Vi에서 Vj로 가는 최단거리를 구해서 D2에 저장한다.

D2에서 1이란 V2까지 경유 가능하다는 뜻이다.

이전 단계보다 경유할 수 있는 점이 V2 한 개가 더 허용된 것이다.

V2가 허용됨으로서 V1에서 V3으로 갈 수 있는 경로가 없었는데 4의 비용으로 갈 수 있게 되었고 V5에서 V3도 경로가 없었는데 7의 비용으로 갈 수 있는 경로도 생겼다.

이번에도 D2[i][j] = minimum(D1[i][j], D1[i][2]+D1[2][j])을 이용해 D2 배열을 채운다.

D2[i][j]  1   2   3   4   5
   1      0   1   4   1   5
   2      9   0   3   2   14
   3     INF INF  0   4  INF
   4     INF INF  2   0   3
   5      3   4   7   4   0

 

4. V1, V2, V3만 경유할 수 있도록 허용했을 때 (V3을 추가로 허용했을 때)Vi에서 Vj로 가는 최단거리를 구해서 D3에 저장한다.

안타깝게도 이번 경우는 V3을 추가한다고 더 짧은 경로가 발견되는 경우는 없었다.

따라서 D3[i][j] = minimum(D2[i][j], D2[i][3]+D2[3][j]) 이긴 하지만 모두 첫 번째 경우에 해당하므로

이번 경우는 D3[i][j] = D2[i][j]가 된다. 

D3[i][j]  1   2   3   4   5
   1      0   1   4   1   5
   2      9   0   3   2   14
   3     INF INF  0   4  INF
   4     INF INF  2   0   3
   5      3   4   7   4   0

 

5. V1, V2, V3, V4만 경유할 수 있도록 허용했을 때 (V4를 추가로 허용했을 때) Vi에서 Vj로 가는 최단거리를 구해서 D4에 저장한다.

이번에는 V4를 추가함으로서 새로 발견되는 최단 경로가 많다.

이전 단계에서는 V2에서 V5로 가는 경로는 14의 비용이 들었는데 V4를 거침으로서 5로 줄어들었고

V5에서 V3으로 가는 경로의 비용도 7에서 6으로 줄어들었다.

그 계산은 각각 D4[2][5]=D3[2][4]+D3[4][5],  D4[5][3]=D3[5][4]+D3[4][3] 이렇게 한 것이다.

이외에도 최단경로가 많이 갱신되었다.

갱신 방법은 D4[i][j] = minimum(D3[i][j], D3[i][4]+D3[4][j])

D4[i][j]  1   2   3   4   5
   1      0   1   3   1   4
   2      9   0   3   2   5
   3     INF INF  0   4   7
   4     INF INF  2   0   3
   5      3   4   6   4   0

 

6. V1, V2, V3, V4, V5 즉, 모든 정점의 경유를 허용했을 때 (V5를 추가로 허용했을 때) Vi에서 Vj로 가는 최단거리를 구해서 D5에 저장한다. (이것이 우리가 구하고자 하는 궁극적인 정답인 배열 D이다.)

V5를 추가로 허용했을 때의 경로의 가중치를 D5에 저장한다.

D5[i][j] = minimum(D4[i][j], D[i][5]+D[5][j])

D5[i][j]  1   2   3   4   5
   1      0   1   3   1   4
   2      8   0   3   2   5
   3      10  11  0   4   7
   4      6   7   2   0   3
   5      3   4   6   4   0

이것이 플로이드 알고리즘의 답이다. 모든 정점에 대해서 Vi에서 Vj로 가는 경로의 길이(또는 비용)가 구해진 것이다.

D5[i][j]에 저장된 값은 정점 Vi에서 정점 Vj로 가는 최단 경로의 거리(비용)이다.

정답을 구하는 과정을 보면 이전 단계에서 구해놓은 것들의 단순 비교(대소비교)만으로 이번 단계의 해답을 구할 수 있었다.

Dk[i][j] = min(Dk-1[i][j], Dk-1[i][k]+Dk-1[k][j]) 이렇게 k번째 단계의 해답은 k-1번째에서 구해놓은 것들의 비교로 구했다.

이 그림에서 지금까지 발견된 경로인 Vi에서 Vj로 가는 경로와 새로 발견되는 경로인 Vi에서 Vk로 가는 경로+Vk에서 Vj로 가는 경로 중 어느 경로로 가야 비용이 적은지를 매 단계마다 선택하는 것이다.

여기에서 주목할 것은 두 개의 부분 경로 즉, Vi에서 Vk로 가는 경로, Vk에서 Vj로 가는 경로는

V1에서 Vk-1까지의 정점만 경유를 허용했을 때의 최단경로이다.

이것은 다 구하고 나서 전체를 보니 이런 사실들이 보이는 것이고 우리는 이것을 동적 계획법을 이용해서 아주 작은 문제부터 시작해서 큰 문제를 해결하는 상향식으로 푼 것이다.

 

그럼 이제 코드로 구현해보자.

#include <iostream>
#include <algorithm>
using namespace std;


int main() {
	const int INF = 1000000; //이음선의 가중치로 나올 수 없는 아주 큰 값
	int d[5][5] = { {0, 1, INF, 1, 5},
				{9, 0, 3, 2, INF}, 
				{INF, INF, 0, 4, INF}, 
				{INF, INF, 2, 0, 3}, 
				{3, INF, INF, INF, 0} };

	for (int k = 0; k < 5; k++) {
		for (int i = 0; i < 5; i++) {
			for (int j = 0; j < 5; j++) {
				d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
			}
		}
	}
	for (int i = 0; i < 5; i++) {
		for (int j = 0; j < 5; j++) {
			cout << d[i][j] << " ";
		}
		cout << endl;
	}
	return 0;
}

3중 for문이 있는 것으로 봐서 시간복잡도가 O(n^3)이 될 것이라는 것을 추측해볼 수 있다.

위 코드의 실행 결과

 

우리는 D0(인접행렬 W)부터 D5까지 6개의 배열을 사용했는데 코드를 보니 이차원 배열 D 하나를 썼다. 심지어 인접행렬 W와 D 이렇게 두 개를 쓰지도 않았다. D는 인접행렬이었는데 거기에 덮어쓰면서 배열 한 개로 해결하고 있다.

이렇게 덮어써도 아무런 문제가 없을까?

그렇다.

왜냐하면 Dk를 구할 때에는 k행 k열은 변하지 않기 때문이다.

직접 해보는 것이 이해하는 데에 가장 확실한 방법이라고 생각한다. 나도 처음에 플로이드 알고리즘 자체를 이해하는 것은 어렵지 않았지만 D 배열 하나에 계속 덮어쓰는 것이 과연 괜찮을까? 하는 의문이 계속 들었었다.

교수님께서 꼭 손으로 따라가보라고 하셔서 직접 공책에 그래프를 그리고 D0부터 D5까지의 행렬을 직접 손으로 써서 구해보니 왜 D 배열 하나만으로 해결가능한지 확실히 이해가 잘 되었고 다시는 플로이드 알고리즘을 잊지 않을 것 같다.

이해가 가지 않는다면 직접 손으로 배열을 하나하나 따라가면서 구해보는 것을 추천한다.

이 그림에서 지금까지 발견된 경로인 Vi에서 Vj로 가는 경로와 새로 발견되는 경로인 Vi에서 Vk로 가는 경로+Vk에서 Vj로 가는 경로 중 어느 경로로 가야 비용이 적은지를 매 단계마다 선택하는 것이다.

여기에서 주목할 것은 두 개의 부분 경로 즉, Vi에서 Vk로 가는 경로, Vk에서 Vj로 가는 경로는

V1에서 Vk-1까지의 정점만 경유를 허용했을 때의 최단경로이다.

이것은 다 구하고 나서 전체를 보니 이런 사실들이 보이는 것이고 우리는 이것을 동적 계획법을 이용해서 아주 작은 문제부터 시작해서 큰 문제를 해결하는 상향식으로 푼 것이다.

 

 

 

※이렇게 작업할 때에는 화질이 선명한데 저장만 하면 왜 이렇게 화질이 깨지는지 모르겠다...

 

728x90

'알고리즘' 카테고리의 다른 글

[알고리즘] 조합 (Combination)  (0) 2020.01.06
[알고리즘] 순열 (Permutation)  (0) 2020.01.06
728x90
 

11403번: 경로 찾기

가중치 없는 방향 그래프 G가 주어졌을 때, 모든 정점 (i, j)에 대해서, i에서 j로 가는 경로가 있는지 없는지 구하는 프로그램을 작성하시오.

www.acmicpc.net

DFS나 BFS로 풀려다가 코드 짜기가 너무 복잡할 것 같아서 간단하게 플로이드로 풀었다.

#include <iostream>
#include <tuple>
#include <vector>
#include <algorithm>
using namespace std;


int main() {
	cin.tie(NULL);
	ios_base::sync_with_stdio(false);
	int n;
	cin >> n;
	const int INF = 1000;
	vector<vector<int>> d(n, vector<int>(n, INF));
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			int temp;
			cin >> temp;
			if (temp != 0) {
				d[i][j] = temp;
			}
		}
	}
	for (int k = 0; k < n; k++) {
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
			}
		}
	}
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			if (d[i][j] == INF) cout << 0 << " ";
			else cout << 1 << " ";
		}
		cout << endl;
	}
	return 0;
}
728x90

'알고리즘 문제' 카테고리의 다른 글

[백준] 3613번 Java vs C++  (0) 2020.02.08
[백준] 10808번 알파벳 개수  (0) 2020.02.08
[백준] 11404번 플로이드  (0) 2020.02.07
[백준] 1977번 완전제곱수  (0) 2020.02.07
[백준] 2163번 초콜릿 자르기  (0) 2020.02.07
728x90

그 유명한 플로이드(Floyd) 알고리즘이다.

 

11404번: 플로이드

첫째 줄에 도시의 개수 n(1 ≤ n ≤ 100)이 주어지고 둘째 줄에는 버스의 개수 m(1 ≤ m ≤ 100,000)이 주어진다. 그리고 셋째 줄부터 m+2줄까지 다음과 같은 버스의 정보가 주어진다. 먼저 처음에는 그 버스의 출발 도시의 번호가 주어진다. 버스의 정보는 버스의 시작 도시 a, 도착 도시 b, 한 번 타는데 필요한 비용 c로 이루어져 있다. 시작 도시와 도착 도시가 같은 경우는 없다. 비용은 100,000보다 작거나 같은 자연수이다. 시작

www.acmicpc.net

주의할 점은 n에서 m으로 가는 이음선이 여러 개가 있을 수 있다는 것이다. 따라서 가중치를 입력받을 때 작은 값만 배열에 저장하도록 해야 한다.

플로이드 알고리즘은 아래 글에서 자세히 설명해놓았다.

https://breakcoding.tistory.com/148

 

[알고리즘] 플로이드 (Floyd) 알고리즘

최단경로 알고리즘은 플로이드 알고리즘과 다익스트라 알고리즘이 가장 대표적인 두 알고리즘이다. 다익스트라 알고리즘은 출발지가 주어졌을 때 그 출발지(one)로부터 모든 지점(all)으로 가는

breakcoding.tistory.com

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


int main() {
	cin.tie(NULL);
	ios_base::sync_with_stdio(false);
	int n, e;
	cin >> n >> e;
	const int INF = 100001;
	vector<vector<int>> d(n + 1, vector<int>(n + 1, INF));
	for (int i = 1; i <= n; i++) {
		d[i][i] = 0;
	}
	for (int i = 0; i < e; i++) {
		int from, to, weight;
		cin >> from >> to >> weight;
		if(d[from][to] == 0 || d[from][to] == INF)
			d[from][to] = weight;
		else d[from][to] = min(weight, d[from][to]);
	}
	for (int k = 1; k <= n; k++) {
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= n; j++) {
				d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
			}
		}
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			if (d[i][j] == INF) cout << 0 << " ";
			else cout << d[i][j] << " ";
		}
		cout << '\n';
	}
	return 0;
}
728x90

'알고리즘 문제' 카테고리의 다른 글

[백준] 10808번 알파벳 개수  (0) 2020.02.08
[백준] 11403번 경로 찾기  (0) 2020.02.07
[백준] 1977번 완전제곱수  (0) 2020.02.07
[백준] 2163번 초콜릿 자르기  (0) 2020.02.07
[백준] 1037번 약수  (0) 2020.02.07
728x90
 

1977번: 완전제곱수

M과 N이 주어질 때 M이상 N이하의 자연수 중 완전제곱수인 것을 모두 골라 그 합을 구하고 그 중 최솟값을 찾는 프로그램을 작성하시오. 예를 들어 M=60, N=100인 경우 60이상 100이하의 자연수 중 완전제곱수는 64,  81,  100 이렇게 총 3개가 있으므로 그 합은 245가 되고 이 중 최솟값은 64가 된다.

www.acmicpc.net

#include <iostream>
#include <tuple>
#include <vector>
#include <algorithm>
using namespace std;


int main() {
	int arr[100];
	int m, n;
	cin >> m >> n;
	for (int i = 0; i < 100; i++) {
		arr[i] = (i + 1) * (i + 1);
	}
	int count = 0;
	long long sum = 0;
	int minNum;
	for (int i = 0; i < 100; i++) {
		if (arr[i] >= m && arr[i] <= n) {
			count++;
			sum += arr[i];
			if (count == 1) {
				minNum = arr[i];
			}
		}
	}
	if (count == 0) cout << -1;
	else {
		cout << sum << endl;
		cout << minNum;
	}
	return 0;
}​
728x90

'알고리즘 문제' 카테고리의 다른 글

[백준] 11403번 경로 찾기  (0) 2020.02.07
[백준] 11404번 플로이드  (0) 2020.02.07
[백준] 2163번 초콜릿 자르기  (0) 2020.02.07
[백준] 1037번 약수  (0) 2020.02.07
[백준] 1094번 막대기  (0) 2020.02.07
728x90
 

2163번: 초콜릿 자르기

정화는 N×M 크기의 초콜릿을 하나 가지고 있다. 초콜릿은 금이 가 있는 모양을 하고 있으며, 그 금에 의해 N×M개의 조각으로 나눠질 수 있다. 초콜릿의 크기가 너무 크다고 생각한 그녀는 초콜릿을 친구들과 나눠 먹기로 했다. 이를 위해서 정화는 초콜릿을 계속 쪼개서 총 N×M개의 조각으로 쪼개려고 한다. 초콜릿을 쪼갤 때에는 초콜릿 조각을 하나 들고, 적당한 위치에서 초콜릿을 쪼갠다. 초콜릿을 쪼갤 때에는 금이 가 있는 위치에서만 쪼갤 수 있다. 이와

www.acmicpc.net

#include <iostream>
using namespace std;


int main() {
	int n, m;
	cin >> n >> m;
	cout << (n - 1) + (m - 1) * n;
	return 0;
}
728x90

'알고리즘 문제' 카테고리의 다른 글

[백준] 11404번 플로이드  (0) 2020.02.07
[백준] 1977번 완전제곱수  (0) 2020.02.07
[백준] 1037번 약수  (0) 2020.02.07
[백준] 1094번 막대기  (0) 2020.02.07
[백준] 1010번 다리 놓기  (0) 2020.02.07

+ Recent posts