백준 풀이 알고리즘 템플릿 - cpp
Intro..
참고
-
현재 Java로 알고리즘을 풀고 있어서 “알고리즘 템플릿 - JAVA” 가 훨씬 잘 정리되어있다.
-
아래는 C++ 기준으로 작성되었으며 옛날에 작성했다보니 내용이 많이 부실하다. JAVA 꺼 보는걸 추천
사용할 알고리즘들의 템플릿들을 정리하겠다.
물론 약간의 알고리즘 해석만을 동반하며, 자세한 해석은 아래 링크를 달아두겠다.
- 참고 링크 : 바킹독, 구글링 등등
- 코드들만 기록할랬는데,,, 정리하다보니 해석을 좀 많이했네,,
공부하면서 바킹독님의 유튜브, 블로그의 도움을 많이 받았습니다!!
문자열 - string
주의사항
- c++에서 string은 문자열을 이어붙일때 잘못 사용하면 O(N^2) 복잡도를 발생시킬 수 있다.
-
s+'a'
라는 새 문자열 객체를 만든후s
에 더하기 때문에s+'a'
만큼 매번 추가로 길이가 필요하기 때문이다.
-
-
따라서 반드시 아래 방식으로 사용해야한다 - O(N)
-
'a'
만 생성되어서s
에 더하기 때문에a
만큼 길이만 추가로 필요하기 때문에 빠르다. - 물론 char[]을 써도 되지만 string이 매우 편리
-
-
문자열이라기보단 memset, fill 메소드 설명
- 배열이나 이런 숫자를 하나로 초기화하는 로직 작성은 하다보면 귀찮다.
- 이를 쉽게 해주는게 memset 함수와 fill 함수이며 fill 함수는 algorithm 선언이 필요하다.
- fill 함수를 사용하면 된다. memset은 0, -1 초기화에만 주로 사용한다. (정확히는 구글링참고)
int main() {
string s;
for(int i = 0 ; i<100000; i++) {
// 1. O(N^2) 복잡도 코드
s=s+'a';
s=s+"ab";
// 2. O(N) 복잡도 코드
s+='a';
s+="ab";
}
}
아래 문법들은 기본적으로 암기
int main() {
string s = "hello";
s += " BKD!"; // hello BKD!
cout << s.size() << '\n'; // 10
cout << s.substr(2, 3) << '\n'; // llo
cout << s[1] << '\n'; // e
s.replace(6, 4, "guys"); // hello guys
cout << s << '\n';
int it = s.find("guys"); // 6
s.replace(it, 4, "everyone"); // hello everyone
cout << s << '\n';
s.erase(7, 6); // hello ee
cout << s << '\n';
s[6] = 'm'; // hello me
cout << s << '\n';
s.insert(0, "say "); // say hello me
cout << s << '\n';
if (s.find("to") == string::npos) // string::npos == -1
cout << "'to' is not in string 's'\n";
}
c++ string에서 split
함수는 지원하지 않으므로 직 접 구 현
// vector, string 등등은 반드시 주소로 넘겨야 값 복사하는 복잡도 안생김
vector<string> split(string& inStr, string& sep) {
vector<string> ret;
int cur = 0;
while (cur < inStr.size()) {
int nxt = inStr.find(sep, cur); // cur 부터 검색 시작
if (nxt == -1) nxt = inStr.size(); // end
if (nxt - cur > 0) // 없어도 잘 동작하지만, substr로 나눌 문자열이 길이가 0이면 안되긴 하니까
ret.push_back(inStr.substr(cur, nxt - cur));
cur = nxt + sep.size(); // sep(구분자)만큼 추가로 이동
}
return ret;
}
BFS, DFS
BFS(넢이우선검색), DFS(깊이우선검색)은 정말 단골 문제들이다.
- BFS를 우선으로 사용하며, DFS는 DFS로만 풀리는 문제에 사용하면 된다.
- BFS는 큐 를 활용하고, DFS는 스택 또는 재귀(필자는 주로 재귀)를 활용한다.
- 주의할점은 BFS에서 큐에 집어넣는 시점에 방문기록을 남긴다.
BFS 기본형태 4방향
// 입력이 좌상을 0,0 으로 좌표계 형성하게끔 들어왔다 가정
int dx[4] = { 0, 1, 0, -1 }; // 행 : 우,하,좌,상
int dy[4] = { 1, 0, -1, 0 }; // 열 : 우,하,좌,상
void bfs(int x, int y) {
queue<pair<int, int>> qu;
visited[x][y] = true; // 방문 표시 (x,y는 처음 BFS 순회할 좌표)
qu.push({ x, y });
while (!qu.empty()) {
int cx = qu.front().first;
int cy = qu.front().second;
qu.pop();
for (int i = 0; i < 4; i++) {
int nx = cx + dx[i];
int ny = cy + dy[i];
if (nx<0 || ny<0 || nx>N-1 || ny > M-1) continue; // 범위 내 체크
if (visited[nx][ny]) continue; // 방문 여부 체크
if (inArr[nx][ny] == 1) {
// 땅이 있는 경우(=1) 스택에 추가한다.
visited[nx][ny] = true; // 방문 표시
qu.push({ nx,ny });
}
}
}
}
BFS 시작점이 여러 개
- BFS를 동시에 동작하듯이 해야하는 경우에 해당된다.
- 같이 시작할 시작점들을 while문 돌리기전 큐에 전부 삽입후 평소처럼 BFS를 진행한다.
- 큐의 특성상 들어온 순서대로 진행할거기 때문에 가능한 것이다.
BFS 거리측정 + 시작점이 두 종류
-
BOJ 4179번 : 불!
문제를 예시로 들겠다.- 위처럼 시작점 여러개를 적용을 하는 부분이 초기에 진행된다.
- 그리고 두 종류로 BFS를 따로 동작하는 형식이다.
// BOJ 4179번 : 불!
void bfs() {
queue<pair<int, int>> qu1; // J (지훈)
queue<pair<int, int>> qu2; // F (불)
// J, F 위치 찾아서 push
for (i = 0; i < N; i++) {
for (j = 0; j < M; j++) {
if (inArr[i][j] == 'J') {
qu1.push({ i,j });
dist1[i][j] = 0; // 지훈이 거리 기록(분)
}
else if (inArr[i][j] == 'F') {
qu2.push({ i,j });
dist2[i][j] = 0; // 불 거리 기록(분)
}
}
}
// F를 BFS
while (!qu2.empty()) {
int cx = qu2.front().first;
int cy = qu2.front().second;
qu2.pop();
for (int i = 0; i < 4; i++) {
int nx = cx + dx[i];
int ny = cy + dy[i];
if (nx<0 || ny<0 || nx>N - 1 || ny > M - 1) continue; // 범위 내 체크
if (inArr[nx][ny] == '#') continue; // 벽 체크
if (dist2[nx][ny] == -1) {
dist2[nx][ny] = dist2[cx][cy] + 1; // 거리(분) 기록
qu2.push({ nx,ny });
}
}
}
// 불이 언제 어디서 퍼지는지 다 알고난 후 J를 BFS 한다.
// 이때 핵심은 J가 범위를 벗어난 경우 탈출 성공이라 했었다.
// => 불이 퍼지기 전에 탈출인지만 마지막에 체크하면 됨.
while (!qu1.empty()) {
int cx = qu1.front().first;
int cy = qu1.front().second;
qu1.pop();
for (int i = 0; i < 4; i++) {
int nx = cx + dx[i];
int ny = cy + dy[i];
// 범위 내 체크
if (nx<0 || ny<0 || nx>N - 1 || ny > M - 1) {
// 불보다 먼저인지 체크
// dist2[cx][cy] == -1 의 의미는 F(불)이 사방에 갇혀서 못나온 경우다.
if (dist1[cx][cy] < dist2[cx][cy] || dist2[cx][cy] == -1) {
cout << dist1[cx][cy]+1;
return;
}
}
if (inArr[nx][ny] == '#') continue; // 벽 체크
if (dist1[nx][ny] == -1) {
dist1[nx][ny] = dist1[cx][cy] + 1; // 거리(분) 기록
qu1.push({ nx,ny });
}
}
}
cout << "IMPOSSIBLE";
}
1차원에서의 BFS (즉, 4방향이 아니라)
- dx,dy 배열을 통해서
for(int i=0; i<4; i++)
에서nx, ny
를 구하는 것이, - 4방향이 아니라 필요에따라
for (auto nx : inArr[cx])
형태로 바뀐다던지 이런식으로도 응용한다는 것이다.
DFS : 큐->스택 변경시 동일하게 동작하며, 아래 그래프 쪽 파트에서 재귀 방식도 참고(백트래킹에서 많이 사용)
재귀(Recursion)
절차지향적 사고 -> 귀납적 사고
가 반드시 필요!!!
절차적으로 이해하지 마라!! 귀납적으로 그냥 이렇게 되겠구나라고 생각만 하라!! 도미노를 생각!!
-
수학적 귀납법
-
1번 도미노가 쓰러진다.
k번 도미노가 쓰러지면 k+1번 도미노도 쓰러진다.- 참(true) : 1번 도미노가 쓰러진다, k번 도미노가 쓰러지면(가정)
- 두 문장이 참이므로 k+1번 도미노.. 또한 귀납적으로 참
-
예시1) 아래 코드의 예시
- func1(1)이 1을 출력한다.
func1(k)가 k,k-1,k-2…1을 출력하면 func1(k+1)은 k+1,k,k-1,k-2…1을 출력한다.
- func1(1)이 1을 출력한다.
-
예시2) 하노이 탑 이동 순서(n개 원판을 기둥 1 -> 기둥 3 으로 이동)
- n-1개의 원판을 1->2 이동
n번 원판을 1->3 이동
n-1개의 원판을 2->3 이동
=> 규칙이 큰 원판 위에 작은 원판을 두는것이기 때문에 반드시 n-1개 원판이 기둥 2에 있고,
n번 원판이 기둥 1-> 기둥 3 으로 이동하는 경우의 존재는 생각해보면 자명하다. - 원판이 1개일 때 원판을 내가 원하는 곳으로 옮길 수 있다.
원판이 k개일 때 옮길 수 있으면 원판이 k+1개일 때에도 옮길 수 있다.
- n-1개의 원판을 1->2 이동
-
1번 도미노가 쓰러진다.
-
재귀 함수의 조건
- Base condition : 특정 입력에 대해서는 자기 자신을 호출하지 않고 종료되어야 함
- 모든 입력은 Base condition으로 수렴
-
재귀에 대한 정보
- 함수의 인자로 어떤 것을 받고 어디까지 계산한 후 자기 자신에게 넘겨줄지 명확하게 정해야 함
- 모든 재귀 함수는 for문으로 만들 수 있음
- 재귀는 for문 보다 메모리/시간에서 손해를 봄(함수 호출량 때문)
- 한 함수가 자기 자신을 여러 번 호출하게 되면 비효율적일 수 있음(계산 중복 때문)
- 예 : 피보나치 수열 => 해결법(DP로 중복 계산 재사용을 통해서 해결)
- 재귀함수가 자기 자신을 부를 때 스택 영역에 계속 누적이 됨
- 100000 정도 호출했을 때 정상동작 하지 않으면 구글 검색을 통해 스택 메모리 제한을 해제할 것
- 함수의 인자로 어떤 것을 받고 어디까지 계산한 후 자기 자신에게 넘겨줄지 명확하게 정해야 함
재귀로 로직 작성 순서
1. 함수의 정의
2. base condition
3. 재귀 식
void func1(int n) {
if(n==0) return; // base condition
cout << n << ' ';
func1(n-1);
}
백트래킹(Backtracking)
상태 공간트리
를 그려가면서 문제를 이해하고, 재귀 형태로 풀어나가면 된다.
깊이우선검색(=DFS)
을 기반으로 검색을 진행하며, 각 마디가 유망하지 않으면 그 마디의 부모 마디로 돌아가서 다시 검색을 계속하는 방식이다.
- 참고 :
백트래킹
은 깊이우선검색(DFS)을 기반으로 하지만,분기한정법
은 여러 검색 방법들을 지원하는 차이가 있다.
N과 M(1) - 대표적인 템플릿
// N과 M(1) : 1부터 N까지 자연수 중에서 `중복 없이(=visited 활용)` M개를 고른 수열
// 즉, (1 2) (1 3) 은 가능하지만 (1 1)은 불가
#include <bits/stdc++.h>
using namespace std;
int n,m;
int outArr[10];
bool visited[10];
void func(int depth) {
if(depth == m) { // base condition
for(int i = 0 ; i<depth; i++) cout << outArr[i] << ' ';
cout << '\n';
return;
}
for(int i = 1; i<=n; i++) {
if(!visited[i]) {
outArr[depth] = i;
visited[i] = true;
func(depth+1); // 재귀
visited[i] = false; // backtracking
}
}
}
N과 M 시리즈 - dfs 방식 백트래킹 사고력 확장
// 참고 : 1~N 순서대로 자연수가 들어왔기 때문에 수열을 구하면 사전순(오름차순)으로 출력
// 그러나, 랜덤 값이 들어온 경우엔 수열 구하기전에 sort작업이 필요
// `i > preValue`로 구하는 값 오름차순 (즉, 현재값 > 이전값)
for (int i = 1; i <= N; i++) {
if (!visited[i] && i > preValue) { // 만약 비내림차순은? `i>=preValue`
visited[i] = true;
outArr[depth] = i;
dfs(depth + 1, i);
visited[i] = false; // backtracking
}
}
// 1부터 N까지 자연수 중에서 `중복 허용(=visited 활용X)` M개를 고른 수열
for (int i = 1; i <= N; i++) {
if (!visited[i]) {
//visited[i] = true;
outArr[depth] = i;
dfs(depth + 1);
//visited[i] = false; // backtracking
}
}
// 애초에 입력이 1 2 3 4 가 아닌 1 1 2 3 이런식으로 중복으로 들어올 경우 처리방법 소개
// `tempNum != inArr[i]` 이 핵심
int tempNum = 0; // 이전값 기록해둬서 중복 수열 생성되지 않도록 하기 위함
for (int i = 1; i <= N; i++) {
if (!visited[i] && tempNum != inArr[i]) {
tempNum = inArr[i];
visited[i] = true;
outArr[depth] = inArr[i];
dfs(depth + 1, inArr[i]);
visited[i] = false;
}
}
// 마지막으로 시간초과를 해결하는 TIP => 아래 동작만 필요한 경우들에 사용!!
for (int i = index; i < chicken.size(); i++) { // 반드시 index부터 시작해야 시간초과 안전
p = chicken[i];
if (visited[p.first][p.second]) continue;
visited[p.first][p.second] = true;
outArr[depth] = i; // 선택된 치킨집 index 기록
dfs(depth + 1, i);
visited[p.first][p.second] = false; // backtracking
}
부분수열의 합 - 응용(사고력 확장)
// 이 문제도 위 코드와 같은 형태로 해결할 수 있지만, 좀 더 간단하게 해결할 수도 있다.
// 위 코드의 경우 하나의 형태로만 재귀를 한것이다.
// 하지만, 이 문제를 상태 트리 그려보면 값을 더한경우 안더한경우로 나눠 볼 수 있다.
// 즉, 2개 형태로 재귀를 할 수 있고 훨씬 코드도 간결해진다.
void func(int depth, int sum) {
if (depth == N) { // base condition
if (sum == S) result++;
return;
}
func(depth + 1, sum); // 안더함
func(depth + 1, sum + inArr[depth]); // 더함
}
STL next_permutation - 수열과 조합에 유용(N과 M같은 문제들)
- 배열 범위는
sort
처럼 end쪽 주소는 배열 사이즈 크기의 주소
// next_permutation : 순열을 나타낸다. do-while 형태로 사용
// 조합 구하는 예시로 5C2를 예로 들면,
// 만약 치킨집이 5개이고 M이 2라고 하면 brute는 {0, 0, 1, 1, 1} 로 0이 치킨집 2개 선택 의미
void permutation() {
// 조합 구하기 위한 setting
// M=2라면, {0,0,1,1,1...}
vector<int> vc(chicken.size(), 1);
int brute[55];
for (int i = 0; i < chicken.size(); i++) brute[i] = 1; // 전체 1로 세팅후
for (int i = 0; i < M; i++) brute[i] = 0; // M개만큼 0으로 세팅
int mn = 10000000;
do {
int dist = 0;
for (auto h : home) {
int tmp = 10000000;
for (int i = 0; i < chicken.size(); i++) {
if (brute[i] == 0) // 선택된 경우
tmp = min(tmp, abs(chicken[i].first - h.first) + abs(chicken[i].second - h.second));
}
dist += tmp;
}
mn = min(mn, dist);
} while (next_permutation(&brute[0], &brute[chicken.size()]));
result = mn;
}
Branch-and-Bound(분기한정법)
이건 이런게 있다란 개념만 이해해두자. 분기한정법-개념
-
분기한정법 : Backtracking + 다른 탐색들(DFS,BFS,우선순위큐 등)
-
Bound : 마디가 유망한지 여부를 결정하기 위해서 한계치(bound) 계산
-
So far the best : 현재까지 찾은 최적의 답
-
-
최적화 문제의 SST 비교
- 최적화 문제의 SST 비교 최소화 문제(예: TSP)
- 트리 깊어질수록 BOUND값이 절대 작아질 수 없음(Low Bound를 구함)
- 최적화 문제의 SST 비교 최대화 문제(예: 0-1냅색)
- 트리 깊어질수록 BOUND값이 절대 커질 수 없음(Upper Bound를 구함)
- 최적화 문제의 SST 비교 최소화 문제(예: TSP)
시뮬레이션(구현)
존나 풀어라… 단, 구현인 만큼 코드 작성 전 꼭 도식화를 진행
- 존나 풀어라 : 구현(실버급 인기 순)
-
자주 나오는 로직은 그냥 외우자
- 예로 2차원 배열 회전 로직은
B[x][y]=A[N-1-y][x]
- 예로 2차원 배열 회전 로직은
// 90도 회전 함수
void rotate() {
// 1. tmp에 복제
int tmp[12][12];
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
tmp[i][j] = inArr[i][j];
}
}
// 2. 배열요소 swap
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
inArr[i][j] = tmp[N-1-j][i]; // B[x][y]=A[N-1-y][x]
}
}
// 3. 배열크기 swap
swap(N,M);
}
정렬(Sort)
STL sort 이용 - O(NlogN)
- 응용을 예시 들면, 정렬 때 같은 수는 인접하는 성질을 이용해 중복 원소 제거도 가능
- 정렬은 아니지만 숫자를 바로 index와 매칭해서 받는 로직도 잘 기억
- 주의사항 : 혹시나
string
같은 클래스 객체 전달시 반드시reference
를 사용
#include<algorithm>
#include<vector>
vector<int> inArr[10000];
for i..N, order[x]=i; ...
등등 선언을 가정
// 비교하는 함수를 따로 커스텀!!!
bool comp(const int a, int b) { // 당연히 const 안써도 됨
return order[a] < order[b]; // inArr값을 order배열로 비교하게끔 커스텀
} // a가 b의 앞에 와야할 때 true, 그렇지 않을 때 false
sort(inArr[i].begin(), inArr[i].end(), comp); // end()는 마지막 index + 1
sort(&inArr[i][0], &inArr[i].back()+1, comp); // back()은 마지막 index
다이나믹 프로그래밍(DP)
DP란 여러 개의 하위 문제를 먼저 푼 후 그 결과를 쌓아올려 주어진 문제를 해결하는 알고리즘
참고로 DP는 정말 대표하는 문제들이 많고 유형들도 가지각색이라서 많은 경험이 필요
DP를 푸는 과정 - 테이블 잡고 식 찾는 연습이 매우매우 중요
1. 테이블 정의하기
2. 점화식 찾기
3. 초기값 정하기
EX) 1로 만들기 BOJ : 1463번
-
테이블 정의하기
- D[i]는 i를 1로 만들기 위해 필요한 연산 사용 횟수의 최솟값
-
점화식 찾기
- D[12]는 ??
3로 나누면 (D[12] = D[4] + 1)
2로 나누면 (D[12] = D[6] + 1)
1을 빼면 (D[12] = D[11] + 1)
=> D[12] = min(D[4]+1, D[6]+1, D[11]+1) - D[k] = ?
=> D[k] = min(D[k/3]+1, D[k/2]+1, D[k-1]+1)
- D[12]는 ??
-
초기값 정의하기
- D[1] = 0
int d[1000005];
int n;
int main() {
cin >> n;
d[1] = 0;
for(int i = 2; i<=n; i++) {
d[i] = d[i-1]+1;
if(i%2 == 0) d[i] = min(d[i], d[i/2]+1);
if(i%3 == 0) d[i] = min(d[i], d[i/3]+1);
}
cout << d[n];
}
그리디(Greedy)
그리디란 지금 가장 최적인 답을 근시안적으로 택하는 알고리즘 = 관찰을 통해 탐색 범위를 줄이는 알고리즘
구현자체는 굉장히 간단해서 따로 정해진 코드 탬플릿은 X
단, 코테에서 추천 전략은 문제가 100% 그리디 풀이란게 확신하면 짜서 제출 및 틀리면 빠르게 손절
100% 확신이 없으면, 일단 넘어가고 마지막에 남은 시간에 코딩 시작
수학(Math)
자세한 개념은 반드시 알고리즘 기초1 - 수학, 바킹독 - 수학 게시물을 함께 참고
1. 소수
개선시킨 소수판정과 소인수분해는 모두 O(√n)
1부터 n까지의 수 중에서 소수 목록을 구하는건 에라토스테네스의 체를 사용
-
소수 판정법 - O(√n)
- 합성수 N에서 1을 제외한 가장 작은 약수는 √N 이하이다.
EX) N=18 : 2<=√18, N=25 : 5<=√25, N=21 : 3<=√21
- 합성수 N에서 1을 제외한 가장 작은 약수는 √N 이하이다.
// √N 이하까지만 확인해도 소수 판별이 가능하므로 복잡도 N -> √N 개선
bool isPrime(int n) {
if(n == 1) return 0;
for(int i = 2; i*i<=n; i++) { // √N 이하까지 돈다.
if(n%i == 0) return 0;
}
return 1;
}
-
소수 목록 - 에라토스테네스의 체
- 소수는 자신보다 작은 수들의 배수값과 동일한게 있으면 안되는 특징을 이용
- 예로 2의 경우??
- 처음
prime[2]
는 소수로 그대로 있고,prime[4, 6, 8...]
배수들은 소수가 아닌것으로 걸러지는 형태이다.
- 처음
// 에라토스테네스의 체
// prime배열 의미 : false면 소수, true면 소수아님 => 초기값 false로 초기화(전역배열)
p = 2;
while (p * p <= N) {
if (prime[p] == false) {
for (i = p * p; i < N + 1; i += p) { // i+=p 는 p의 배수들 걸러내려고
prime[i] = true;
}
}
p++;
}
-
소인수분해
- 소인수분해 : 자연수를 소인수(약수들 중 소수)들의 곱으로 표현하는 것
EX) 60 = 2 x 2 x 3 x 5
- 방법1) N 이하의 모든 소수를 에라토스테네스의 체로 다 찾은 다음에 그 소수들로 N을 나누어보는 방법
- 방법2) 하지만, 이보다 더 괜찮은 방법이 존재(루트N 복잡도)
- i를 2,3,4,5… 올라가면서 N을 나누어 떨어질때마다 소인수 목록에 기록하는 방법
- 소인수분해 : 자연수를 소인수(약수들 중 소수)들의 곱으로 표현하는 것
for(int i = 2; i*i<=n; i++) { // √N 이하까지 돈다.
while(n%i == 0) {
cout << i << '\n'; // 구한 소인수
n/=i; // n이 1이 될때까지 갱신
}
if(n!=1) cout << n; // 마지막에 n이 1이 안될경우도 있는데, 그땐 n으로 나누면 1이 됨
}
- 참고) 팩토리얼 수에서 특정 수의 개수 구할때 유용한 특징 : 10!의 경우 5의 개수를 구하기 위해 5를 10으로 나누면, 1~10 곱한 값 사이의 5가 나온 개수가 출력된다.
2. 최대공약수, 최소공배수(GCD, LCM)
-
최대 공약수(GCD), 최소 공배수(LCM) - 유클리드 호제법
-
유클리드 호제법
: 2개의 자연수 a, b에 대해서 a를 b로 나눈 나머지를 r이라고 하면(단, a> b), a와 b의 최대 공약수는 b와 r의 최대 공약수와 같다.- (a, b) = (b, r)
- EX) 10 20 : (10, 20) = (20, 10) = (10, 0) = 10
-
// 최대공약수 -> (a, b) = (b, r)
int gcd(int a, int b) {
if(b==0) return a;
return gcd(b,a%b);
}
// 최소공배수 -> A X B = GCD(A,B) X LCM(A,B)
int lcm(int a, int b) {
return a / gcd(a,b) * b; // overflow 방지 위해 나누기 먼저(곱셈이라 가능)
}
3. 연립합동방정식
아래 그림에서 조건에 만족한 학생 수를 구하기 위해 0~29 학생 수 때의 상황을 테이블로 나타냈고,
정답은 27명임을 알 수 있으며 이런 문제를 연립합동방정식
이다.
다만, 이렇게 0~29 명 때를 전부 찾아보는게 아닌 5개만 확인하면 되는 효율적인 방법을 소개한다.
-
N%6==3
인 수들이 필요하고, 이는 처음 3부터 시작해서 +6인3,9,15,21,27
이다. - 이 5개의 수들 중에서
N%5==2
를 찾기만 하면 된다.
int chk() {
for(int i = 3; i<30; i+=6) { // N%6==3 인 경우들만 접근
if(i % 5 == 2) return i;
}
return -1;
}
4. 이항계수(조합)
방법1) 조합의 공식 : nCr = n!/[(n-r)!r!]
을 이용하는 방법
방법2) 조합 성질 : nCk = (n-1)C(k-1) + (n-1)Ck
를 이용해서 DP로 구하는 방법(추천!!)
- 점화식 :
d[i][j] = d[i-1][j-1] + d[i-1][j]
5. A진법 to B진법의 변환(관계)
필자는 이것을 A진법 -> 10진법 -> B진법
의 변화로 해결하고 있다.
백준11576 풀이 게시글 참고!!
-
A진법 -> 10진법
- 예로 2진법이라면??
- 먼저, 10진수 값이
10
이였다면 이를 2진법으로 표현 했을때는1010
이다.-
1010
의 일의 자리 수를 10진수로 표현하면0*2^0 = 0
이다. -
1010
의 십의 자리 수를 10진수로 표현하면1*2^1 = 2
이다. -
1010
의 백의 자리 수를 10진수로 표현하면0*2^2 = 0
이다. -
1010
의 천의 자리 수를 10진수로 표현하면1*2^3 = 8
이다.
-
- 마지막으로
1010
을 10진수로 표현한다면??0*2^0 + 1*2^1 + 0*2^2 + 1*2^3 = 0 + 2 + 0 + 8 = 10
- 먼저, 10진수 값이
- 예로 2진법이라면??
-
10진법 -> B진법
- 10진수를 기수 B로 계속 나누어서 나머지 값을 거꾸로 출력하는 방식이다.
- 그림으로 참고(추천 URL) : 10진수 A,B 진법 관계
이분탐색
정렬되어 있는 배열에서 특정 데이터를 찾기 위해 모든 데이터를 탐색 범위를 절반으로 줄여가며 찾는 탐색 방법
- STL
binary_search
함수를 활용 : 찾는 수가 등장했는지 안했는지를 알려줌(오름차순에 사용) - 분할정복처럼
st, en, mid
인덱스 활용하며mid=(st+en)/2
가 기본- 반드시 st,en을 1차이 두게 0,1로 가정하던지 해서 무한루프에 빠지는지 체크도 중요!!
- 무한루프에 빠지게 되는경우
mid=(left+right+1)/2
사용을 권장
- 무한루프에 빠지게 되는경우
- 반드시 st,en을 1차이 두게 0,1로 가정하던지 해서 무한루프에 빠지는지 체크도 중요!!
이분탐색 자료구조 binary_search
기본 사용법
#include<algorithm>
int main() {
for(int i = 0 ; i<n;i++) cin >> a[i]; // 배열에 수들을 입력받는다
cin>>t; // 찾을 target 수를 입력받는다
cout<<binary_search(a,a+n,t)<<'\n'; // t/f를 반환해줌
}
이분탐색으로 target의 왼쪽, 오른쪽 위치 구하는 함수 lower_bound, upper_bound
// a배열 = {1,2,2,2,3} 인 경우, t=2 가정
cout << upper_bound(a,a+n,t) - lower_bound(a,a+n,t) << '\n'; // 중복수의 개수 : 3
cout << upper_bound(a, a + n, t) - a << '\n'; // index 출력 : 4
cout << lower_bound(a, a + n, t) - a << '\n'; // index 출력 : 1
1. 좌표압축
나중에 문제를 풀다보면 입력값의 범위는 1에서 10^9 정도로 굉장히 큰데 그거를 배열 인덱스처럼 쓰고 싶을 수 있습니다.
그러면 뭔가 크기 순으로 번호를 0번부터 매기고 싶다는 생각이 들텐데 이런 일을 하는게 좌표압축입니다.
-
좌표압축 방법 : 정렬 -> 중복 제거 -> 이후 이분탐색 수행
- STL
unique
함수는 중복제거하고 원소들을 앞으로 모아준다, return은 시작주소를 반환
- STL
sort(uni.begin(), uni.end()); // 정렬
uni.erase(unique(uni.begin(), uni.end()), uni.end()); // 중복 제거
for(int i = 0 ; i<n;i++) // 이분탐색
cout << lower_bound(uni.begin(), uni.end(), x[i]) - uni.begin() << ' ';
좌표압축 다른 예
// 문자 -> index로 변환 함수(좌표압축 함수)
int getID(char c) {
if (c <= 'Z')
return c - 'A';
return c - 'a' + 26; // 26은 A~Z 이후에 자리를 두기 위함
} // 아스키코드는 A가 a보다 작음
투 포인터
투 포인터는 배열에서 원래 이중 for문으로 O(N^2)에 처리되는 작업을 2개 포인터의 움직임으로 O(N)에 해결하는 알고리즘
- 이분 탐색으로 투 포인터 문제를 풀던가, 투 포인터 문제를 이분 탐색으로 풀 수 있는 경우가 많다.
- 예시로 수 고르기 를 풀어본다.
- 아래 코드는 2개의 포인터를 적절히 조건에 맞게 움직여서 해를 구하는 모습이다.
for(int st = 0 ; st<n; st++){ // 총 N복잡도
while(en < n && a[en] - a[st] < m) en++; // 총 N복잡도
if(en == n) break;
mn = min(mn, a[en] - a[st]);
} // N+N=2N => O(N) 복잡도
cout << mn;
해시
해시 함수 : 임의 길이의 데이터를 고정될 길이의 데이터로 대응시키는 함수 (5135:Kim,…)
해시 테이블 : 아래 그림
- 같은 키가 삽입될 수 있어서 충돌은 어쩔 수 없다.
- 대표적인 충돌회피 방법은
Chaining, Open Addressing
이 존재한다.- Chaining : 연결리스트로 회피
- Open Addressing : Probing 방식으로 데이터 저장해서 회피
- Linear Probing : 충돌 발생 시 오른쪽으로 1칸씩 이동
- 단점 : Clustering(군집화) 발생
- Quadratic Probing : 충돌 발생 시 오른쪽으로 1,3,5,… 칸씩 이동
- 단점 : 약하긴 하지만 Clustering(군집화) 발생
- Double Hashing : 충돌 발생 시 이동 칸의 수를 새로운 해시함수로 계산하는 방식
- 단점 : 적중률이 낮아짐
- Linear Probing : 충돌 발생 시 오른쪽으로 1칸씩 이동
해시 자료구조로 이루어진 STL
-
unordered_set, unordered_multiset, unordered_map
이며, multiset의 경우 중복을 허용- Python 의 set, dict 자료구조랑 유사
- 사실 unordered_multimap 도 존재하는데 사용할 일이 거의 없음
- 이름이 비슷한 BST 자료구조인
set, multiset, map
과 구분할 것- 근데 해시보단
set, map
들 사용을 추천 - 왜??
insert, erase, find update
동작이 해시는 O(1), RBT는 O(logN) 이지만, - RBT는 원소가 크기 순으로 정렬되어 있다는 큰 장점이 있다. 즉, lower_bound같은걸 logN에 바로 구할 수 있다.
- 근데 해시보단
#include <unordered_set>
#include <unordered_map>
// unordered_set
void unordered_set_example() {
unordered_set<int> s;
s.insert(-10); s.insert(100); s.insert(15); // {-10, 100, 15}
s.insert(-10); // {-10, 100, 15}
cout << s.erase(100) << '\n'; // {-10, 15}, 출력:1
cout << s.erase(20) << '\n'; // {-10, 15}, 출력:0
if (s.find(15) != s.end()) cout << "15 in s\n";
else cout << "15 not in s\n";
cout << s.size() << '\n'; // 2
cout << s.count(50) << '\n'; // 0 => 수가 몇개있는지 카운트
for (auto e : s) cout << e << ' ';
}
// unordered_multiset
// 중복 허용이며 erase를 그냥쓰면 전체 삭제 되기 때문에 조심
void unordered_multiset_example() {
unordered_multiset<int> ms;
ms.insert(-10); ms.insert(100); ms.insert(15); // {-10, 100, 15}
ms.insert(-10); ms.insert(15); // {-10, -10, 100, 15, 15}
cout << ms.size() << '\n'; // 5
for (auto e : ms) cout << e << ' ';
cout << '\n';
cout << ms.erase(15) << '\n'; // {-10, -10, 100}, 출력:2 => 삭제 개수
ms.erase(ms.find(-10)); // {-10, 100} => 1개는 이렇게 삭제해줘야 함
ms.insert(100);
cout << ms.count(100) << '\n'; // 2 => 수가 몇개있는지 카운트
}
// unordered_map
// python의 dict처럼 쓰면 됨
void unordered_map_example() {
unordered_map<string, int> m;
m["hi"] = 123;
m["bkd"] = 1000;
m["gogo"] = 165; // {"hi", 123}, {"bkd", 1000}, {"gogo", 165}
cout << m.size() << '\n'; // 3
m["hi"] = -7; // {"hi", -7}, {"bkd", 1000}, {"gogo", 165}
if (m.find("hi") != m.end()) cout << "hi in m\n";
else cout << "hi not in m\n";
m.erase("bkd"); // {"hi", -7}, {"gogo", 165}
for (auto e : m)
cout << e.first << ' ' << e.second << '\n';
}
이진 검색, 레드-블랙 트리(BST, RBT)
c++에서 STL에는 이진 검색 트리 + 자가 균형을 이루는 RBT 자료 구조로 정의되어서 제공한다.
참고로 BST를 구현을 해야할 것 같을때는 배열로는 구현X, 차라리 class로 구현하자.
해시에서 비교를 했기 때문에 자세한 설명은 생략
- prev, next나 lower_bound 이런게 필요할 때 유용
- 개념 정리 게시글 참고 : 고급트리 개념
#include<set>
#include<map>
void set_example() {
set<int> s;
s.insert(-10); s.insert(100); s.insert(15); // {-10, 15, 100}
s.insert(-10); // {-10, 15, 100}
cout << s.erase(100) << '\n'; // {-10, 15}, 1
cout << s.erase(20) << '\n'; // {-10, 15}, 0
if (s.find(15) != s.end()) cout << "15 in s\n";
else cout << "15 not in s\n";
cout << s.size() << '\n'; // 2
cout << s.count(50) << '\n'; // 0
for (auto e : s) cout << e << ' ';
cout << '\n';
s.insert(-40); // {-40, -10, 15}
set<int>::iterator it1 = s.begin(); // {-40(<-it1), -10, 15}
it1++; // {-40, -10(<-it1), 15}
auto it2 = prev(it1); // {-40(<-it2), -10, 15}
it2 = next(it1); // {-40, -10, 15(<-it2)}
advance(it2, -2); // {-40(<-it2), -10, 15}
auto it3 = s.lower_bound(-20); // {-40, -10(<-it3), 15}
auto it4 = s.find(15); // {-40, -10, 15(<-it4)}
cout << *it1 << '\n'; // -10
cout << *it2 << '\n'; // -40
cout << *it3 << '\n'; // -10
cout << *it4 << '\n'; // 15
}
void multiset_example() {
multiset<int> ms;
// {-10, 15, 100}
ms.insert(-10); ms.insert(100); ms.insert(15); // {-10, -10, 15, 15, 100}
ms.insert(-10); ms.insert(15);
cout << ms.size() << '\n'; // 5
for (auto e : ms) cout << e << ' ';
cout << '\n';
cout << ms.erase(15) << '\n'; // {-10, -10, 100}, 2
ms.erase(ms.find(-10)); // {-10, 100}
ms.insert(100); // {-10, 100, 100}
cout << ms.count(100) << '\n'; // 2
auto it1 = ms.begin(); // {-10(<-it1), 100, 100}
auto it2 = ms.upper_bound(100); // {-10, 100, 100} (<-it2)
auto it3 = ms.find(100); // {-10, 100(<-it3), 100}
cout << *it1 << '\n'; // -10
cout << (it2 == ms.end()) << '\n'; // 1
cout << *it3 << '\n'; // 100
}
void map_example() {
map<string, int> m;
m["hi"] = 123;
m["bkd"] = 1000;
m["gogo"] = 165; // ("bkd", 1000), ("gogo", 165), ("hi", 123)
cout << m.size() << '\n'; // 3
m["hi"] = -7; // ("bkd", 1000), ("gogo", 165), ("hi", -7)
if (m.find("hi") != m.end()) cout << "hi in m\n";
else cout << "hi not in m\n";
m.erase("bkd"); // ("gogo", 165), ("hi", 123)
for (auto e : m)
cout << e.first << ' ' << e.second << '\n';
auto it1 = m.find("gogo");
cout << it1->first << ' ' << it1->second << '\n'; // gogo 165
}
우선순위 큐
우선순위 큐는 pop을 할 때 가장 먼저 들어온 원소가 나오는 대신 우선순위가 가장 높은 원소가 나오는 큐이다.
힙(최대 힙, 최소 힙) 자료구조를 사용해서 만든 우선순위 큐
- 원소 추가 - O(logN)
- 우선순위 높은 원소 확인 - O(1)
- 우선순위 높은 원소 제거 - O(logN)
- 배열 자료구조 + 우선순위 큐의 경우 => 순서대로 O(1), O(N), O(N)
-
set
도 최소값과 최대값을 빼고, 원소 추가하고 다 가능하고 복잡도도 심지어 동일하다.- 그래도 굳이
priority_queue
를 사용하는 이유는 같은 O(logN)이라고 해도set
보다 빠르고, 공간도 적게 차지한다. - 따라서
priority_queue
의 기능만 딱 필요할 경우엔set
이 아닌priority_queue
를 사용해주자!
- 그래도 굳이
힙을 배열로 나타낼 때
- x번지의 왼쪽, 오른쪽 자식 : 2x, 2x+1
- x번지의 부모 : x/2
최소 힙, 최대 힙 특징
- 최소 힙 : 부모가 자식보다 작음
- 최대 힙 : 부모가 자식보다 큼
STL priority_queue
제공!!
#include <queue>
int main() {
priority_queue<int> pq; // 최대 힙 (기본값)
// priority_queue<int, vector<int>, greater<int>>로 선언시 최소 힙
pq.push(10); pq.push(2); pq.push(5); pq.push(9); // {10, 2, 5, 9}
cout << pq.top() << '\n'; // 10
pq.pop(); // {2, 5, 9}
cout << pq.size() << '\n'; // 3
if (pq.empty()) cout << "PQ is empty\n";
else cout << "PQ is not empty\n";
pq.pop(); // {2, 5}
cout << pq.top() << '\n'; // 5
pq.push(5); pq.push(15); // {2, 5, 5, 15}
cout << pq.top() << '\n'; // 15
return 0;
}
그래프
그래프는 인접행렬과 인접리스트(list보다 vector가 더 편함)로 표현할 수 있다.
- 보통 인접리스트 사용이 많고, E가 V^2보다 훨씬 작을 때(sparse=희소) 사용한다.
- 모든 간선 길이가 동일하면 BFS로 거리를 구할 수 있고, 길이가 다르다면 플로이드나 다익스트라 알고리즘을 활용해야 한다.
그래프를 BFS, DFS
// 연결 그래프 BFS 순회
// 만약, 노드들 중에 연결 그래프 아닌것도 순회하고 싶으면 그냥 모든 노드를 돌리면 됨
// 즉, 아래 코드를 전체 for문으로 감싸고, 1 대신 for문의 i로 변경
vector<int> vc[10];
bool visited[10];
void bfs() {
queue<int> qu;
qu.push(1);
visited[1] = true;
while (!qu.empty()) {
int cur = qu.front();
qu.pop();
cout << cur << ' ';
for (auto next : vc[cur]) {
if (visited[next]) continue;
qu.push(next);
visited[next] = true;
}
}
}
// 연결 그래프 BFS 거리
vector<int> vc[10];
int dist[10];
void bfs() {
fill(dist, dist+10, -1);
queue<int> qu;
qu.push(1);
dist[1] = 0;
while (!qu.empty()) {
int cur = qu.front();
qu.pop();
for (auto next : vc[cur]) {
if (dist[next]!=-1) continue;
qu.push(next);
dist[next] = dist[cur]+1;
}
}
}
// DFS 재귀 (비재귀는 위 BFS에서 큐->스택)
vector<int> vc[10];
bool visited[10];
void dfs(int cur) {
visited[cur] = true;
cout << cur << ' ';
for(auto next : vc[cur]) {
if(visited[next]) continue;
dfs(next);
}
}
// 필자가 항상 작성하던 DFS 재귀 방식 (최대 depth(깊이)를 지정했었음)
// 장점 : 원하는 개수만큼 만 접근이 가능
void dfs(int depth, vector<int> val) {
if (depth == 4) { // base condition
result = 1;
return;
}
for (auto next : val) {
if (!visited[next]) {
visited[next] = true;
dfs(depth + 1, inArr[next]);
// visited[next] = false; // backtracking
}
}
}
트리
트리는 무방향이고 사이클이 없는 연결 그래프이다.
- 트리의 순회방식 - BFS&DFS 순회, 이진 트리의 순회
- 여러가지 트리 - Segment
- 참고로 BST, RBT는 위에서 이미 정리 그리고 우선순위 큐에 사용한 힙도 이진트리임
- 좀 더 다양한 트리(Splay, OST, Range, Interval)는 고급트리 개념 에서 참고
1. 트리의 BFS, DFS 순회
- 트리를 BFS 할 때 root부터 하므로 임의의 노드에서 그 노드의 부모는 이미 방문한거고, 자식들만 방문을 안한 상태인 것이다.
- 따라서 따로 visited배열이 필요없이 p배열(부모) 을 알고 있으면 된다.
// DFS는 큐->스택 바꾸면 동일하게 동작
vector<int> vc[10];
int p[10];
int depth[10];
void bfs(int root) {
queue<int> qu;
qu.push(root);
while(!qu.empty()) {
int cur = qu.front();
qu.pop();
cout << cur << ' ';
for (int next : vc[cur]) {
if(p[cur] == next) continue; // 부모는 방문한 상태라서 continue
qu.push(next);
p[next] = cur; // parent 기록
depth[next] = depth[cur] + 1; // dist배열 채우듯이 동일
}
}
}
2. 이진 트리의 순회
-
레벨 순회 : 높이 순서대로 방문한다.
- 기존이랑 동일. root를 시작점으로 bfs 돌리면 높이 순서대로 위에서부터 차례로 순회한다.
- 전위, 중위, 후위 순회
- 전위 : 위 -> 왼 -> 오
- 중위 : 왼 -> 위 -> 오
- 후위 : 왼 -> 오 -> 위
- 구현 쉬워서 코드 생략
3. Segment Tree(구간합)
여러개의 데이터가 연속으로 있을때 배열
에서는 그 구간의 합을 구할때 복잡도가 N이 걸리는데, 이걸 세그먼트 트리
를 이용해서 logN으로 개선시키려는 목적
- 리프 노드: 배열의 연속된 값 그 자체
- 내부 노드: 왼쪽 자식과 오른쪽 자식의 합
/*
아래 코드는 "구간 합 구하기" 백준 문제를 풀었던 코드를 예시로 가져옴
구간 합 구하는 함수 : segmentSum
트리의 노드 수정하는 함수 : segmentUpdate
*/
#include<iostream>
#include<vector>
#include<cmath>
using namespace std;
long long inArr[2000010];
long long outArr[5000010];
// 재귀함수(세그먼트 트리 만드는)
long long segmentBuild(int start, int end, int node) {
if (start == end) { // base condition
outArr[node] = inArr[start];
return outArr[node]; // 종료시점
}
int mid = (start + end) / 2; // mid 구해서 왼, 오 구간 나눔
outArr[node] = segmentBuild(start, mid, node * 2) + segmentBuild(mid + 1, end, node * 2 + 1);
return outArr[node]; // base condition (사실 바로 위 코드에 return 같이 적어도됨)
}
// 구하는 합 구간 left, right
// node가 담당하는 구간 start, end
long long segmentSum(int start, int end, int node, long long left, long long right) {
// 1. 겹치지 않는 경우 (left, right가 start, end의 왼쪽이거나 오른쪽 구간인 경우)
if ((left < start && right < start) || (left > end && right > end)) return 0;
// 2. 완전히 포함하는 경우 (left, right가 start, end를 완전히 포함하는 경우)
if (left <= start && right >= end) return outArr[node];
// 3. 나머지 경우 (자식 트리 탐색)
int mid = (start + end) / 2;
return segmentSum(start, mid, node * 2, left, right) + segmentSum(mid + 1, end, node * 2 + 1, left, right);
}
// node가 담당하는 구간 start, end
// 수정할 노드 index, 수정할 값 dif
void segmentUpdate(int start, int end, int node, long long index, long long dif) {
// 1. 겹치지 않는 경우 (왼쪽이거나 오른쪽 구간인경우)
if ((start < index && end < index) || (start > index && end > index)) return;
// 2. left, right가 start, end를 완전히 포함하는 경우
if (start <= index && index <= end) outArr[node] += dif;
// 무조건 종료시점(무한루프 안걸리게)
if (start == end) return;
// 3. 나머지 경우 (자식 트리 탐색)
int mid = (start + end) / 2;
segmentUpdate(start, mid, node * 2, index, dif);
segmentUpdate(mid + 1, end, node * 2 + 1, index, dif);
}
int N, M, K;
int main() {
ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> N >> M >> K;
for (int i = 0; i < N; i++) {
cin >> inArr[i];
}
segmentBuild(0, N-1, 1); // outArr 수정(index 1부터 저장)
long long a = 0;
long long b = 0;
long long c = 0;
for (int i = 0; i < M + K; i++) {
cin >> a >> b >> c;
if (a == 1) {
// b번째 수에 c를 더하는게 아닌,
// b번째 수를 c로 바꿔야 하는건 `c-inArr[b-1]` 로 바꿔야 한다는 의미이다.
segmentUpdate(0, N-1, 1, b-1, c-inArr[b-1]);
inArr[b - 1] = c; // b->c로 변환 (`c-inArr[b-1]` 에 사용되니까 바꿔줘야함)
}
else if (a == 2) {
cout << segmentSum(0, N-1, 1, b-1, c-1) << '\n';
}
}
return 0;
}
위상 정렬
위상 정렬(Topological Sort) : 방향 그래프에서 간선으로 주어진 정점 간 선후관계를 위배하지 않도록 나열하는 정렬
vector<int> adj[10];
int deg[10];
int n; // N개의 정점있고 번호는 1~indexed
queue<int> qu;
vector<int> vc; // result
for(int i=1; i<= n; i++)
if(deg[i] == 0) qu.push(i);
while(!qu.empty()){
int cur = qu.front(); qu.pop();
vc.push_back(cur);
for(int next : adj[cur]) {
deg[next]--;
if(deg[next] == 0) qu.push(next);
}
}
if(vc.size() != n)
cout << "cycle exists";
else {
for(auto x : vc) cout << x << ' ';
}
최소 신장 트리
- 바킹독 : https://blog.encrypted.gg/1024
- 예전에 정리한 개념글 : MST-Prim,Kruskal
크루스칼, 프림 알고리즘이 대표적
크루스칼 알고리즘은 Union Find라는 알고리즘을 선행학습 하고 아래 코드를 이해할 것.
프림 알고리즘은 우선순위 큐를 이용해서 구현을 하는데, 구현이 좀 복잡하다. 따라서 pass
최단 경로 트리
플로이드 알고리즘
은 모든 정점 쌍 사이의 최단 경로를 구하는 알고리즘이라 O(n3)의 큰 복잡도를 가진다.
다만, 구현이 3중 for문으로 간단히 할 수 있으며 n이 1000정도면 이 알고리즘을 써도 된다.
- 최단 거리 테이블(2차원) 이 채워진다 => 코드보면 d배열 관련인데 생각보다 쉬운 로직!
- 근데, 경로까지 구하고싶으면 테이블 하나 더 만들어서 기록해놔야 한다 => nxt배열에 기록
다익스트라 알고리즘
은 하나의 정점으로부터 다른 모든 정점까지의 최단 거리를 구하는 알고리즘이므로 복잡도는 플로이드보다 한단계 작은 O(n2) 이다.
- 참고 : 가중치가 음의 값이 있으면 이 알고리즘 사용 불가
- 동작원리는 간단한데 한 정점에서 갈 수 있는 최단 거리 정점을 택하면서 갱신해나가면 된다.
- 여기서 개선된 알고리즘이 있는데 우선순위 큐를 활용한 알고리즘이다. 이 로직을 기억
- 우선순위 큐에 거리를 다 넣어둔다음 매번 최소인걸 선택하는 로직
- 물론 경로까지 구하는 방법은 플로이드 알고리즘 때와 유사
?? KMP, 트라이 남음
컴퓨터그래픽스 관련
CCW, 교차여부, 다각형 면적, 다각형 포함, 볼록 껍질(외피=Convex Hull), 평면 소거법(직사각형 합집합=Plane Sweeping) 관련
- 개념 정리 게시글 참고 : 컴퓨터그래픽스 개념
1. 기본 기하 연산(between, intersect)
/*
ccw 함수(cw : counter clockwise = 시계방향)
leftTurn, rightTurn, collinear 함수 : 좌,우,일직선 검사
direction 함수 : 세점의 회전방향 검사에 따라서 1,-1,0 반환
between 함수 : 점 c가 선분 (a,b)에 위치하는가 검사
intersectProp 함수 : 교차여부를 구하는데, 선분의 끝점이 교차점 허용하지 않는 경우
intersect 함수 : 교차여부를 구하는데, 선분의 끝점이 교차점 허용하는 경우
이 중에서 between, intersect 가 중요
자료형 왠만하면 long long 쓰자
*/
#include<iostream>
using namespace std;
struct point {
long long x;
long long y;
};
typedef point Point;
// OP벡터, OQ벡터 (O는 원점)
// det(행렬식) => q가 p에 어느 방향인지 출력
long long ccw2(Point p, Point q) {
return p.x * q.y - p.y * q.x;
}
// RP벡터, RQ벡터 => 시점R을 원점으로 만들어서 계산
// 시점을 원점으로 이동 : p-r, q-r
int ccw(Point r, Point p, Point q) {
p.x -= r.x;
p.y -= r.y;
q.x -= r.x;
q.y -= r.y;
return ccw2(p, q);
}
// 세점 a,b,c의 회전방향이 좌,우,일직선인가 검사
bool leftTurn(Point a, Point b, Point c) {
return ccw(a, b, c) > 0; // 좌회전 검사
}
bool rightTurn(Point a, Point b, Point c) {
return ccw(a, b, c) < 0; // 우회전 검사
}
bool collinear(Point a, Point b, Point c) {
return ccw(a, b, c) == 0; // 일직선 검사
}
// 선분 교차 검사를 위해 회전 방향에 따른 값 출력
// 좌 : 1, 우 : -1, 일 : 0
int direction(Point a, Point b, Point c) {
if (leftTurn(a,b,c)) return 1;
if (rightTurn(a,b,c)) return -1;
if (collinear(a,b,c)) return 0;
}
// 점 c가 선분 (a,b)에 위치하는가
bool between(Point a, Point b, Point c) {
if (!collinear(a, b, c)) return false; // 일직선 상에 없으면 false
// 아래부턴 일직선 상에 있으니까 범위제한만 주면 된다.
if (a.x != b.x) { // 수직선 아니면
return (a.x <= c.x && c.x <= b.x) || (b.x <= c.x && c.x <= a.x);
}
else { // 수직선 이면
return (a.y <= c.y && c.y <= b.y) || (b.y <= c.y && c.y <= a.y);
}
}
// 선분 교차 검사 (교차점 허용) => 선분 ab, cd
int intersect(Point a, Point b, Point c, Point d) {
// (a,b,c)*(a,b,d) ==-1 && (c,d,a)*(c,d,b)==-1 성립하면 교차
// 추가조건 때문에 <=0로 판별
if (direction(a, b, c) * direction(a, b, d) <= 0 && direction(c, d, a) * direction(c, d, b) <= 0) {
// direction함수는 방향만을 나타내기 때문에 만약 0인 경우 일직선상인 건데,
// 두 직선이 서로 접하는지 범위제한을 줘서 판단할 필요가 있음(between 함수 활용)
if (direction(a, b, c) * direction(a, b, d) == 0 && direction(c, d, a) * direction(c, d, b) == 0) {
// 점 c또는 d가 a,b직선 상에 존재하나 판단
// 점 a또는 b가 c,d직선 상에 존재하나 판단
if (between(a, b, c) || between(a, b, d)|| between(c, d, a) || between(c, d, b)) {
return 1;
}
else
return 0;
}
return 1;
}
else
return 0;
}
// 선분 교차 검사 (교차점 제외) => 선분 ab, cd
bool intersectProp(Point a, Point b, Point c, Point d) {
return direction(a, b, c) * direction(a, b, d) < 0 && direction(c, d, a) * direction(c, d, b) < 0;
}
2. 다각형 면적과 포함 계산
/*
area 함수 : 다각형 면적 구하기
insidePolygon 함수 : 다각형 포함 구하기
*/
// 다각형 면적 구하기 (외전 전부 더하면 됨)
double area(vector<Point> p) {
int ret = 0;
int n = p.size(); // 다각형 p의 정점 개수
for (int i = 0; i < n; i++) {
int j = (i + 1) % n; // 마지막에 index n->0으로 바뀜
ret += (p[i].x * p[j].y - p[i].y * p[j].x);
}
return abs(ret) / 2.0;
}
// 다각형 포함 문제 (교차점 홀수면 내부 포함 인정)
bool insidePolygon(Point q, vector<Point> P) {
int n = P.size();
int crossings = 0;
for (int i = 0; i < n; i++) {
int iPlus1 = (i + 1) % n; // 마지막에 index n->0으로 바뀜
// 1. 점 q가 다각형 에지(표면)에 있나 먼저 확인
if (between(P[i], P[iPlus1], q)) return false;
// 2. 교차점 판별 => crossings에 +1
if((P[i].y<q.y&&P[iPlus1].y>=q.y&&leftTurn(P[i],P[iPlus1],q)) ||
(P[iPlus1].y<q.y&&P[i].y>=q.y&&leftTurn(P[iPlus1],P[i],q)))
crossings++;
}
if (crossings % 2 != 0) return true; // 홀수라면 true !!
return false;
}
3. 볼록 외피(Convex Hull)
O(NlogN ) 방법 소개 - 참고 사이트 : https://www.crocus.co.kr/1288
/*
1. 우선 기준점을 잡는다. (보통 기준점은 y좌표가 가장 작은 것을 기준으로 한다.)
=> y좌표, x좌표 기준 오름차순 정렬해서 기준점을 P[0]으로 바로 잡자
2. 그리고 이 기준점으로 하여 다른 점들을 반시계 방향으로 정렬한다.
=> 각도에 따라 정렬 핵심 (참고 : 원점 이동한 상대위치로 각도 구함)
3. 컨벡스 헐 알고리즘(그라함 스캔(Graham's Scan) 알고리즘)을 이용한다.
=> 사이트 그림보면 이해하기 쉬움
*/
#include<iostream>
#include<algorithm>
#include<cmath> // atan2 (아크탄젠트 - 두점사이 절대각도 구함)
#include<stack>
using namespace std;
struct point {
int x, y;
int p, q;
};
typedef point Point;
double getAngle(Point p) {
return atan2(p.q, p.p);
}
// 1. 각도 비교 사용시 각도 비교를 우선으로 정렬
// 2. 각도 비교 사용 안할시 y기준 정렬, x기준 정렬
bool comp(Point a, Point b) {
// p, q 사용시 아래 조건문 만족하게 됨
// p, q 사용안하면 0으로 초기화 되어있어서 아래 조건문 불만족
if (getAngle(a) != getAngle(b)) return getAngle(a) < getAngle(b);
if (a.y != b.y) return a.y < b.y;
return a.x < b.x;
}
Point P[100];
int main() {
int n; // 점 개수
cin >> n;
for (int i = 0; i < n; i++) {
int x, y;
cin >> x >> y;
P[i] = { x,y };
}
// y좌표, x좌표 기준 오름차순 정렬
sort(P, P + n, comp);
// 기준점(=P[0])기준 상대위치 기록 (따라서 i=1부터)
for (int i = 1; i < n; i++) {
P[i].p = P[i].x - P[0].x; // 기준점 빼서 원점으로 이동
P[i].q = P[i].y - P[0].y;
}
// 구했던 상대위치 값으로 반시계 방향 정렬(기준점 제외. 따라서 p+1부터)
// => 기준점에서 각도 비교로 반시계 정렬하면 됨
sort(P + 1, P + n, comp);
// 1st, 2st로 바로 초기값 사용 가능
stack<int> st;
st.push(0); st.push(1); // 0, 1 index 푸시
// 전처리 과정 끝
// 알고리즘 시작
int next = 2; // next는 index 2부터 판단 시작
while (next < n) { // 0부터 시작했으므로 next가 최종 n까지 가면 1바퀴 다 돈것이다
while (st.size() >= 2) {
int first, second;
second = st.top();
st.pop();
first = st.top();
// first, second, next가 좌회전 ( > 0 )이라면 second push 하고 next 이동
// 우회전( < 0 )이라면 위의 while문 계속 반복 (즉, 이전 first,second상태로 되돌아감)
if (ccw(P[first], P[second], P[next]) > 0) { // leftTurn써도 됨
st.push(second);
break;
}
}
// next push
st.push(next++);
}
cout << st.size(); // 볼록외피 좌표 index들 담겨있음
return 0;
}
4. 평면 소거법(Plane Sweeping) => 합집합
#include<iostream>
#include<vector>
using namespace std;
struct Rectangle {
int x1, y1, x2, y2;
};
vector<Rectangle> inArr;
int Q[10]; // 직사각형 x좌표 정렬해서 기록
int S[10]; // 직사각형 y좌표 정렬해서 기록
int counts[10]; // 겹쳐진 사각형 수
int m1 = 8; // events - Q size
int m2 = 8; // S size
// 합집합의 면적 계산 (합집합이므로 중복 면접 제외)
int UnionArea() {
int sweepLine = 0; int delta = 0; int area = 0; // 면적
for (int i = 0; i < m1; i++) {
sweepLine = Q[i];
Rectangle R = { 0,0,0,0 };
// 0. 사용할 사각형 선택(x값 겹칠때 마다 선택)
for (int j = 0; j < inArr.size(); j++) {
if (inArr[j].x1 == Q[i] || inArr[j].x2 == Q[i]) {
R = inArr[j];
break;
}
}
// 1. counts배열 구하기(y값 활용)
// sweepLine이 만나는 사각형의 왼쪽 가로선만 +1, 오른쪽 가로선은 -1
if (sweepLine == R.x1) delta = 1;
else delta = -1;
int y1 = R.y1; int y2 = R.y2;
for (int j = 0; j < m2; j++) {
if (y1 <= S[j] && S[j] < y2) {
counts[j] += delta; // S[j]~S[j+1] 구간에 겹쳐진 사각형의 수
}
}
// 2. 면적 계산 시작
// 현재 내부면적 세로줄(S[j + 1] - S[j])
int curlength = 0;
for (int j = 0; j < m2; j++) {
if (counts[j] > 0) { // counts배열 있는 값만(사각형 있는 경우에만)
curlength += S[j + 1] - S[j];
}
}
// 현재 내부면적 가로줄(Q[i+1]-Q[i])
if ((i + 1) < m1) {
area += curlength * (Q[i + 1] - Q[i]); // 현재 구한 면적 더함
}
}
return area;
}
int main() {
cin >> N;
for (int i = 0; i < N; i++) {
cin >> inArr[i].x1 >> inArr[i].y1 >> inArr[i].x2 >> inArr[i].y2;
Q[i * 2] = inArr[i].x1; Q[i * 2 + 1] = inArr[i].x2;
S[i * 2] = inArr[i].y1; S[i * 2 + 1] = inArr[i].y2;
}
m1 = N * 2; // 총 x축 개수
m2 = N * 2; // 총 y축 개수
// x축, y축 기록한 배열들 꼭 오름차순으로 정렬 후 사용
sort(&Q[0], &Q[m1]);
sort(&S[0], &S[m2]);
cout << unionArea();
return 0;
}
Network Flow(=Max-Flow, Min-Cut, 이분 매칭)
Ford-Fulkerson(DFS.복잡도:flow), Edmonds-Karp(BFS.복잡도:VE^2), Dinic’s Algorithm(BFS,DFS.복잡도:EV^2) 가 있으며 여기서 Dinic's
만 암기해도 충분
- Ford-Fulkerson과 Edmonds-Karp는 DFS, BFS만 다르며 나머지는 로직이 정확히 동일
- Dinic’s 는 BFS,DFS 뭘 쓰든 상관없는데 본인은 BFS로 레벨트리 구하고, DFS(재귀)로 연산을 진행
-
중요한 점
- “이분 매칭” 문제를 “Max-Flow” 문제로 치환해서 풀 수 있다.
- “Min-Cut” 문제는 그냥 “Max-Flow” 로 풀었을 때 답과 동일하다.
-
보통 문제를 푸는 방식은 문제에 적합하게 “그래프” 를 설계한 후 풀어나간다.
-
“그래프” 그리는 TIP
- 그래프에는 에지를 그릴텐데 반드시 유량이 아닌 용량을 적어줄 것
- 햇갈리면 안되는게 유량은 나중에 계산할때 값 생기기 시작
-
멀티 Source, Sink의 경우?? 외부에 S, T 노드를 임의로 추가해서 사용
- Max Flow는 S->T 로 흐르는 최대 유량을 구하는 형태이기 때문
- 임의로 S, T를 추가했으므로 그래프 그릴때 용량을 잘 설정해야함
- 복잡한 문제들은 에지를 안주고 직접 에지를 구해야 할때도 존재
- 예시 문제 : Jerry and Tom
- 참고 : 해당 예시에서는 에지를 직접 구해야하며, 해당 에지는 “선분교차” 여부에 따라 구해진다.
- 그래프에는 에지를 그릴텐데 반드시 유량이 아닌 용량을 적어줄 것
-
“그래프” 그리는 TIP
/*
BOJ 14286 문제. Min-Cut 문제 => "Max-Flow"
*/
#include<iostream>
#include<vector> // tree
#include<queue> // bfs
#include<algorithm> // fill
#define INF 987654321
using namespace std;
int c[505][505]; // 용량
int f[505][505]; // 유량
vector<int> vc[505]; // tree
int level[505]; // 레벨 그래프
int work[505];
int N, M, S, T; // 정점 수, 에지 수, 시작 노드, 끝 노드
bool bfs() {
// init
fill(level, level + 505, -1);
queue<int> qu; // bfs
level[S] = 0;
qu.push(S);
while (!qu.empty()) {
int cur = qu.front();
qu.pop();
for (int i = 0; i < vc[cur].size(); i++) {
int nxt = vc[cur][i];
if (c[cur][nxt] <= f[cur][nxt] || level[nxt] != -1) continue;
// 레벨이 아직 정해지지 않았고 잔여용량이 0이상은 계속 진행
level[nxt] = level[cur] + 1; // level 기록 (그냥 깊이 기록임)
qu.push(nxt);
}
}
return level[T] != -1; // -1 이면 false 반환(종료 의미)
}
int dfs(int cur, int flow) {
// base condition
if (cur == T) return flow; // cur -> depth로 이해
for (int i = work[cur]; i < vc[cur].size(); i++) {
int nxt = vc[cur][i];
if (level[nxt] - level[cur] != 1 || c[cur][nxt] <= f[cur][nxt]) continue;
// 레벨 차가 1이고, 잔여용량이 0이상은 계속 진행
// 최소 유량 탐색
int minF = dfs(nxt, min(c[cur][nxt] - f[cur][nxt], flow));
// 유량 update
if (minF > 0) {
f[cur][nxt] += minF;
f[nxt][cur] -= minF;
work[cur] = i; // index 기록해서 시간 단축
return minF;
}
}
work[cur] = vc[cur].size();
return 0;
}
int main() {
// speed up
ios::sync_with_stdio(0);
cin.tie(0);
// input
cin >> N >> M;
int n1, n2, w;
for (int i = 0; i < M; i++) {
cin >> n1 >> n2 >> w;
vc[n1].push_back(n2);
vc[n2].push_back(n1); // 양방향
c[n1][n2] += w; // 양방향 간선이므로, 양쪽으로 용량 할당
c[n2][n1] += w;
}
cin >> S >> T;
// run
int maximumFlow = 0; // 제거된 최소 간선 가중치 합
while (true) {
if (!bfs()) break; // 종료
fill(work, work + 505, 0);
int flow = dfs(S, INF);
// 최대 유량 update
maximumFlow += flow;
}
// output
cout << maximumFlow;
return 0;
}
참고
set, unordered_set, priority_queue
의 경우에 무슨 자료구조를 언제 사용할까를 정해두려고 한다.
- 우선
set
을 우선으로 사용하되prev, next, lower_bound
같은 동작들이 필요없고priority_queue
가 사용하는 동작만 필요하다면priority_queue
를 사용하자.
댓글남기기