이번에는 최단 경로 알고리즘 하면 떠오르는 알고리즘 중 하나인 다익스트라 알고리즘에 대해서 정리해보자.

다익스트라 알고리즘 (Dijkstra Algorithm)

다익스트라 알고리즘의 정의부터 보자.

하나의 정점에서 출발해서 다른 노드들로 가는 각각의 최단 경로를 모두 구해주는 알고리즘이다.

용어로는 '단일 출발 최단 경로' 라고도 한다.

또, '단일 도착 최단 경로' 도 뒤집으면 똑같은 말이기도 하다.

모든 정점에서 출발해서 어떤 하나의 정점까지의 최단 경로를 찾는 문제로 뒤집어서 생각하면 다익스트라와 똑같기 때문이다.

 

다만, 음의 간선에서는 사용할 수 없다. 이 때는 벨만-포드 알고리즘을 사용해야 한다. (아직 안 배웠다..)

 

사실 다익스트라 알고리즘 단계별 그림은 다른 블로그들의 설명도 너무 좋아서 핵심만 요약하는게 좋을 것 같다고 생각한다.

다익스트라 step

가장 먼저 출발 노드를 설정하기.

최단 거리 테이블을 무한대로 초기화 하기 (여기서 말하는 무한대는 아직 가보지 않은 곳을 무한대라고 표기한다.)

#1 방문하지 않는 노드 중에서 최단 거리가 가장 짧은 노드를 선택한다.

#2 해당 노드를 거쳐서 다른 노드로 가는 비용을 계산한 후 최단 거리 테이블을 갱신한다.

#1과 #2를 반복하면서 최단 거리를 갱신해 나간다.

 

중요한 건 한 단계마다 하나의 노드에 대한 최단 거리를 찾게 된다는 점이다.

다익스트라 구현

구현 방법으로는 2차원 배열을 이용한 방법과 인접 리스트와 우선순위 큐를 이용한 방법이 있다.

2차원 배열로 구현을 하면 시간 복잡도가 O(V^2)이고, 인접 리스트와 우선순위 큐로 구현을 하면 O(ElogV)로 더 빠르게 가능하다.

 

우선순위 큐의 등장은 힙의 자료구조를 사용하는 개념 때문에 도입됐다.

#1에서 방문하지 않는 노드 중에서 최단 거리가 가장 짧은 노드를 선택할 때 모든 노드를 탐색하면 O(V)이지만, 최소 힙을 통해 찾게 된다면 O(logV)만에 찾을 수 있기 때문이다.

 

보통 코딩 테스트에서는 개선된 다익스트라 알고리즘으로 구현하는 것이 높은 성공률을 보여주기 때문에, 우선순위 큐를 이용한 구현 방법을 살펴보자.

 

이것도 중요할 수 있다. 우선순위 큐에 들어가거나 나오는 요소는 어떤 의미를 가질까?

요소는 {비용, 노드} 로 되어있는데, 이를 뜻하는 바는 노드까지 가는 최단 거리가 비용이라는 의미다.

 

또!

우선순위 큐를 기반으로 구현하게 되면, '서로 다른 두 정점 사이에 여러 개의 간선이 존재할 수 도 있다' 라는 것도 해결할 수 있게 된다.

 

코드의 주석까지 자세하게 달아놨다. 미래의 내가 까먹더라도 어떤 의미인지 빠르게 찾기 위해서..

반복 숙달하자!

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

int n, m, start;
vector<pair<int, int> > adj[100001];
int dist[100001];

void dijkstra(int start) {
    priority_queue<pair<int, int> > pq; // default : max-heap
    pq.push({0, start}); // {비용, 노드}
    dist[start] = 0;
    while(!pq.empty()) {
        int cost = -pq.top().first; // 앞에 -를 붙이면 min-heap 으로 가능하다. (테크닉)
        int now = pq.top().second;
        pq.pop();
        
        // 중요: 앞서 말했듯이 우선순위 큐에서 요소를 뽑았을 때 그 의미는 다음과 같다.
        // now 노드까지 가는 최단 거리가 cost야.
        // 그런데 이미 더 적은 최단 거리로 갱신이 됬다면? 이 노드는 이미 처리가 됬다는 뜻이다.
        if(dist[now] < cost) continue;
        
        // 현재 노드와 연결된 다른 인접한 노드들을 확인한다.
        // 혹여나 현재 노드를 거쳐서 다른 노드의 거리를 갱신할 수 있으니까 확인해보는 것
        for(int i = 0; i < adj[now].size(); i++) {
            int next = adj[now][i].first;
            int nextCost = cost + adj[now][i].second;
            // 현재 노드를 거쳐서 다른 노드로 이동하는 거리가 더 짧은 경우 갱신해준다.
            // 현재 노드까지의 최단거리 + 다른 노드의 거리 < 테이블[다른 노드] 최단 거리
            if(nextCost < dist[next]) {
                dist[next] = nextCost;
                pq.push({-nextCost, next}); // 마찬가지로 넣을 때 비용은 앞에 -붙여서 넣으면 min-heap구동
            }
        }
    }
}

int main() {
    cin >> n >> m >> start;
    
    // 모든 간선 정보를 입력받기
    for(int i = 0; i < m; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        // a 노드에서 b 노드로 가는 비용이 c 라는 의미
        adj[a].push_back({b,c});
    }
    
    // 문제에 따라 다르겠지만 보통은 나올 수 없는 값으로 무한대를 초기화 시켜준다.
    fill(dist, dist + 100001, 987654321);
    
    // 다익스트라 알고리즘 수행
    dijkstra(start);
    
    // 거리 테이블 출력
    for(int i = 1; i <= n; i++) {
        if(dist[i] == 987654321)
            cout << "INF\n";
        else
            cout << dist[i] << '\n';
    }
    
    return 0;
}

연습 문제들

확실히 어려운 개념이다 보니 랭크도 다르다..

최단경로

파티

녹색 옷 입은 애가 젤다지?

거의 최단경로

도로포장

해킹

도로검문

특정한 최단경로

KCM Travel

지름길

최소비용 구하기

택배 배송

간선 이어가기 2

백도어

'Algorithm > Algorithm Concept' 카테고리의 다른 글

플로이드 워셜 (Floyd Warshall)  (0) 2021.09.03
투 포인터  (0) 2021.08.25
최단 경로 알고리즘(Shortest Path)  (0) 2021.08.23
수학 관련  (0) 2021.08.18
백트래킹 (Backtracking)  (0) 2021.07.28

오늘은 최단 경로 알고리즘에 대해서 간단하게 개요를 정리하자.

최단 경로란?

 

그래프 상에서 가장 짧은 경로를 찾는 경로, 가중치 그래프에서는 간선의 가중치의 합이 최소가 되는 경로를 찾는 문제다.

그런데, 최단 경로라고 해도 문제마다 다르다. 어떤 경우들이 있을까?

 

단일 출발 최단 경로 : 어떤 하나의 정점에서 출발하여 나머지 모든 정점까지의 최단 경로

단일 도착 최단 경로 : 모든 정점에서 출발하여 어떤 하나의 정점까지의 최단 경로 (뒤집어서 생각하면 단일 출발 최단 경로다)

단일 쌍 최단 경로 : 모든 정점에서 모든 정점까지의 최단 경로

최단 경로 알고리즘

 

최단 경로 문제가 나오게 되면 이를 해결할 알고리즘들이 있다.

 

BFS(완전 탐색)

가중치가 없거나 가중치가 모두 동일한 경우(미로 탐색 문제)에서는 최단 경로를 구할 수 있게 된다.

(1, 1) -> (N, M)

# 특이하게도, 0의 가중치와 1의 가중치가 동시에 존재하는 0-1 BFS도 있다.

이럴 때는 0의 가중치를 큐의 앞부분에 우선적으로 배치하여야 한다.

 

다익스트라 알고리즘

음이 아닌 가중치의 그래프에서만 사용할 수 있는 알고리즘.

어떤 하나의 정점에서 나머지 모든 정점까지의 최단 경로를 구할 때 사용한다.

 

벨만 포드 알고리즘

음인 가중치도 사용할 수 있는 알고리즘

나머지는 다익스트라 알고리즘과 비슷하다.

 

플로이드 워셜 알고리즘

모든 정점 사이의 최단 경로를 모두 구하는 알고리즘

3중 포문으로 구현이 쉽다.

 

여러 알고리즘을 알았으니, 제일 좋은 공부법은 알고리즘 틀을 배우고 빠르게 여러 문제들을 풀면서 복습하는 느낌으로 공부하자.

'Algorithm > Algorithm Concept' 카테고리의 다른 글

투 포인터  (0) 2021.08.25
다익스트라 알고리즘  (0) 2021.08.24
수학 관련  (0) 2021.08.18
백트래킹 (Backtracking)  (0) 2021.07.28
파라메트릭서치 (Parametric Search)  (0) 2021.05.09

최근 코테에서 자주 나오는 부분이 모듈러 연산, 최대 공약수, 소수다.

자주 풀어보지 않기 때문에 막상 나오면 헤매는 경우가 잦기 때문에 오늘 정리하려고 한다.

아래의 정리는 백준의 강의를 듣고 감명받은 점들과 나의 생각을 정리해서 적어놓았다.

모듈러 연산(나머지 연산, modular)

말 자체가 처음에는 와닿지 않는 용어지만 % 기호를 보면 한 번에 이해하기 쉽다.

10 % 7 => 3
6 % 7 => 6
0 % 7 => 0
7 % 7 => 0

어떤 수를 7로 모듈러 연산한다고 하면 나올 수 있는 경우의 수는 0 ~ 6 다.

아무리 큰 수가 와도 저 안에 머무른다는 소리다.

 

응용

어떤 물고기가 (1,1) 에서 5 x 5 격자 안에서 움직인다고 할 때 5의 속력으로 동쪽으로 이동한다고 하면?

(1,1) -> (1,2) -> (1,3) -> (1,4) -> (1,5) -> (1,1)

한 칸 씩 이동한다고 구현하면 위와 같이 5번을 다 계산하지만 모듈러 연산을 쓰면 다음과 같이 편하게 압축(최소화)할 수 있다.

y축(열)만 보면 (1 + 5) % 5 = 1로 쉽게 압축할 수 있다.

 

음수는 예외적으로 뒤에다가 나머지 연산하는 수를 붙이지만 덧셈과 곱셈은 다음과 같이 풀어서 사용이 가능하다.

단, 나누기는 제외

(a + b) % c = (a % c + b % c) % c
(a * b) % c = (a % c * b % c) % c
(a - b) % c = (a % c - b % c + c) % c

이렇게 풀어서 사용하는 경우는 오버플로우를 방지하기 위해서다.

 

최대공약수 GCD( Greatest Common Divisor)

정의는 두 수 a와 b의 공통된 약수 중에서 가장 큰 정수다.

최대공약수를 빠르게 구하는 알고리즘은 유클리드 호제법이다.

반복문보다는 재귀로 구현하는 게 훨씬 기억하기 쉽고 실수를 최대한으로 줄일 수 있다.

시간 복잡도는 매 턴마다 b로 나누기 때문에 O(lgB) = O(lgN)

//재귀 구현
int gcd(int a, int b) {
	if(b == 0) return a;
    return gcd(b, a%b);
}

//반복문 구현
int gcd(int a, int b) {
	while(b != 0) {
    	int r = a % b;
        a = b;
        b = r;
    }
}

응용

최대공약수를 구할 수 있으면 최소공배수도 쉽게 구할 수 있다.

중간중간에 나눠주는 이유는 오버플로우 방지

//g: 최대공약수
int lcm(int a, int b, int g) {
	return g * (a / g) * (b / g);
}

소수

정의는 약수가 1과 자기 자신밖에 없는 수를 소수라고 한다.

 

어떤 수가 소수인지 아닌지 판별하는 방법

판별하는 방법은 그 수를 n이라고 하면 2부터 sqrt(n)까지 나누어 떨어지면 그 수는 소수가 아니라는 방법을 이용해서 구현한다.

보통은 루트는 정수로 떨어지지 않기 때문에 정수로 떨어트리기 위해서 양변을 제곱하는 형태로 취하는 것이 좋다고 한다.

2 <= i <= sqrt(n)
4 <= i * i <= n

시간 복잡도 O(sqrt(N))

bool isPrime(int n) {
	if(n < 2) return false;
    for(int i = 2; i * i <= n; i++) {
    	if(n % i == 0) return false;
    }
    return true;
}

대량의 소수를 빠르게 구하는 방법 => 에라토스테네스의 체

이 방법은 다음과 같은 방법으로 소수를 판별한다.

2는 소수이고, 2의 배수는 모두 지운다.

3은 소수이고, 3의 배수는 모두 지운다.

5는 소수이고, 5의 배수는 모두 지운다.

....

지워진 수는 소수가 아니고 남아있는 수가 소수라는 뜻이다.

시간 복잡도 O(NlglgN)

int prime[100];  // 소수 저장
int pn = 0;      // 소수의 개수
bool check[101]; // 지웠다면 true, 아니라면 false
int n = 100;

for(int i = 2; i <= n; i++) {
	//2는 냅두고,
	if(check[i] == false) {
    	//소수를 저장하고 싶으면
    	prime[pn++] = i;
    	//2의 배수는 모두 지운다.
    	for(int j = i + i; j <= n; j += i) {
        	check[j] = true;
        }
    }
}

자주 자주 나오는 문제의 유형은 아니지만 꼭 기억해두자.

 

연습문제

소수 찾기

소수 구하기

나머지

최대공약수와 최소공배수

최소공배수

GCD 합

골드바흐의 추측

'Algorithm > Algorithm Concept' 카테고리의 다른 글

다익스트라 알고리즘  (0) 2021.08.24
최단 경로 알고리즘(Shortest Path)  (0) 2021.08.23
백트래킹 (Backtracking)  (0) 2021.07.28
파라메트릭서치 (Parametric Search)  (0) 2021.05.09
[cmath] math 관련 함수들  (0) 2021.05.07
백트래킹(Backtracking)

 

알고리즘을 어느 정도 공부하게 되면 백트래킹이라는 용어를 많이 들어봤을 것이다.

가끔 백트래킹이랑 완전 탐색을 같은 용어로 사용하게 되는데 오늘 확실히 정리해서 이해하도록 하자.

 

많은 블로그들을 검색하면서 공부했지만 이게 가장 나에게는 이해가 되는 정의였다.

 

해를 찾는 도중 다음의 경우가 해가 아니라면 되돌아가서 다시 해를 구하는 기법

 

무슨 말이냐면 분명 이 길로 가면 막히는 곳이 나오는데?라고 생각되면 다른 곳으로 방향을 트는 것이다.

이미 그 길은 내가 끝까지 가보지 않더라도 막힌 길을 알기 때문이다.

 

DFS 기법으로 하게 되면 막힌 곳을 알더라도 끝까지 가서 확인한다.  -무식한 방법-

백트래킹 기법으로 하게 되면 "아 이 길은 막힌 곳이니까 다른 곳으로 가자." -현명한 방법-

 

백트래킹에 사용되는 용어들

 

유망하다(promising) : 해가 될 가능성이 존재하는 루트

가지치기(pruning) : 배제하다. 유망하지 않다.

 

막힌 곳인 걸 알면 그 길은 배제하고 나머지 가능한 길만 생각한다는 것.

현재 위치에서 다음 위치에 대해서 유망한 지 검사하기.

 

테크닉, 상상해보자.

현재 돌에서 a, b, c, d 돌로 넘어가야 한다.

내가 현재 돌에서 a로 일단 이동해본다.

a로 왔는데 규칙에 어긋나지 않다면?(유망하다면) 다음 단계로 진행하고,

아니라면 현재 돌에서 a를 배제한 나머지 돌에서 다시 이동을 시도해본다.

 

또 다른 방법은

일단 두고 시작한다.

바둑 판을 상상해보면 일단 바둑알을 바둑 판에 두고 경기를 끝내기(하나의 경우의 수)

다시 돌아와서 바둑 판에 뒀던 바둑알을 무르고 다시 다른 곳에 두는 것이다.

돌아올 때는 반드시 내가 방문했던 곳을 지워야 한다. (복원하기)

어찌 보면 유망하기보다는 완전 탐색에 가깝다고 생각된다.. 써보고 보니까

 

+(추가)

아래와 같이 이런 류의 기법들은 흔히 완전 탐색이지만,

중간에 가지치기를 하는 것이 있다면 백트래킹이라고 생각하면 된다.

백트래킹은 그냥 완탐 처럼 보이지만 중간에 자를 수 있거나 배제할 수 있다면 이것 또한 백트래킹이기 때문이다.

// 흔히 보는 완탐
vis[i] = true;
dfs(cnt + 1);
vis[i] = false;

// 하지만? 이렇다면 백트래킹
if(promising() == false) continue;
정리

 

한마디로 모든 경우의 수를 보는 완전 탐색과 비슷하지만, 조건을 만족할 때(유망할 때)만 확인한다는 점에서 다르다.

 

대표적인 문제는  N-Queen

 

'Algorithm > Algorithm Concept' 카테고리의 다른 글

최단 경로 알고리즘(Shortest Path)  (0) 2021.08.23
수학 관련  (0) 2021.08.18
파라메트릭서치 (Parametric Search)  (0) 2021.05.09
[cmath] math 관련 함수들  (0) 2021.05.07
[GCD, LCM] 유클리드 호제법  (0) 2021.05.07

풀이

재밌는 문제다.

좌표상에서 U, D, R, L 조작을 해서 내가 건너간 다리가 처음 다리라면 경우의 수를 추가해주고, 이미 건넜던 다리라면 그냥 건너가기만 하면 된다.

 

핵심은 내가 건너간 다리가 처음 방문한 다리인지 아닌지를 체크하는 것이다.

어떻게 체크할까?

시작 좌표는(0, 0) 에서 U이라는 명령을 받았다면 내가 건너게 될 다리는 다음과 같다.

(0,0) -> (-1,0)

(-1,0) <- (0,0)

이렇게 양방향으로 하는 이유는 둘 다 동일한 다리이기 때문이다.

 

이렇게 건넌 다리를 체크하기 위해서 Node라는 구조체를 만들고, 방문 배열에 해당 Node를 추가하는 방식으로 방문 배열을 만들었다. 다리를 통과할 때 해당 다리를 전에 방문했는지 안 했는지 체크는 배열을 돌면서 다리와 일치하면 이미 건넌 다리고 아니라면 처음 건너는 다리로 구현해주면 된다.

 

다 풀고 다른 사람이 푼 것을 봤는데 아래의 방법도 좋은 방법이다.

1. 방문 배열을 4차원 배열로 설정하는 방법

bool vis[x1][y1][x2][y2];

2. set 자료구조를 이용하는 방법

set<pair<pair<int, int>, pair<int, int> > > s;
//양방향으로 넣어주고
s.insert({{x1,y1},{x2,y2}});
s.insert({{x2,y2},{x1,y1}});
//마지막에 답을 도출할 때 2로 나눠주면 끝
ans = s.size() / 2;
#include <string>
#include <queue>
#include <map>
#include <vector>
using namespace std;

struct Node {
public:
    int x1, y1;
    int x2, y2;

    Node(int a, int b, int c, int d) : x1(a), y1(b), x2(c), y2(d) {}
    bool operator==(const Node& other) {
        if(x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2) return true;
        if(x1 == other.x2 && y1 == other.y2 && x2 == other.x1 && y2 == other.y1) return true;
        return false;
    }
};

int solution(string dirs) {
    map<char, pair<int, int>> m;
    m['U'] = { -1,0 };
    m['D'] = { 1,0 };
    m['R'] = { 0,1 };
    m['L'] = { 0,-1 };
    int x = 0;
    int y = 0;
    int ans = 0;
    queue<char> q;
    vector<Node> vis;
    for (char c : dirs) q.push(c);
    while (!q.empty()) {
        int nx = x + m[q.front()].first;
        int ny = y + m[q.front()].second;
        q.pop();

        if (nx < -5 || ny < -5 || nx > 5 || ny > 5) continue;
        Node node(x, y, nx, ny);
        bool ok = true;
        for (Node n : vis) {
            if(n == node) {
                ok = false;
                break;
            }
        }
        if (ok) {
            vis.push_back(node);
            ans++;
        }
        x = nx;
        y = ny;
    }
    return ans;
}

'Algorithm' 카테고리의 다른 글

[백준 1753] 최단경로  (0) 2021.10.11
[HackerRank] 2D Array - DS  (0) 2021.09.09
[프로그래머스] 영어 끝말잇기 (C++)  (0) 2021.06.04
[프로그래머스] 위장 (C++)  (0) 2021.05.31
[프로그래머스] 괄호 회전하기 (C++)  (0) 2021.05.31

풀이

N명의 순서대로 영어 끝말잇기를 진행하는데, 모두 올바른 답을 했다면 [0, 0]을 리턴하고, 아니라면

틀린 사람의 번호와 몇 번째로 틀렸는지를 리턴하면 된다.

 

문제를 쪼개서 해결해보자.

한 번 말했던 단어는 다시 등장하면 안 된다. 어떻게 체크할까?

여러 가지 방법이 있지만 et 자료구조를 이용하면 쉽게 중복을 체크할 수 있다.

s.find(words[i]) != s.end()
-> 조건을 만족한다면 아직 해당 단어는 처음 등장한 단어다.
-> 조건을 만족하지 못한다면 이미 전에 등장했던 단어를 의미한다.

 

사람의 번호 사이클은 어떻게 구할까?

그 사람이 말했던 단어는 몇 번째로 틀렸을까?

n = 3이라고 하면, mod 연산을 이용하면 쉽게 구할 수 있다.

person = index % n;

cycle = index  / n;

index 0 1 2 3 4 5 6 7 8
person 0 1 2 0 1 2 0 1 2
cycle 0 0 0 1 1 1 2 2 2

주의할 점은 사람의 번호는 1번부터 시작하므로 마지막에 각각 +1씩 해야 된다.

 

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

vector<int> solution(int n, vector<string> words) {
    vector<int> answer(2, 0);
    int person_num = 0;
    set<string> s;
    string previous;
    for(int i = 0; i < words.size(); i++) {
        person_num = i % n;
        if(previous == "") previous = words[i];
        else if(previous.back() != words[i].front() || s.find(words[i]) != s.end()) {
            answer[0] = person_num + 1;
            answer[1] = i / n + 1;
            break;
        }
        s.insert(words[i]);
        previous = words[i];
    }
    return answer;
}

풀이

핵심은 선택하지 않는 경우를 고려해야 한다.

입출력 예시를 보면

headgear에는 yellowhat, green_turban 2개

eyewear에는 bluesunglasses 1개

위장을 하려면 headgear만 쓰거나 eyewear만 쓰거나 혹은 headgear, eyewear 같이 쓰는 경우다.

그런데 headgear만 쓴다는 것은 eyewear는 쓰지 않는다는 것을 의미하니 다음과 같이 생각할 수 있다.

[headgear, x]

[eyewear, x]

[headgear, eyewear]

위를 고려하면,

heargear는 기존 2개에 +1(선택하지 않는 경우) => 3개

eyewear는 기존 1개에 +1(선택하지 않는 경우) => 2개

두 옷을 골라서 조합하면

3 x 2 = 6가지가 나오게 되는데, 위장은 최소 한 개의 옷은 입어야 하므로 마지막의 경우에 [x, x] 는 빼줘야 한다.

따라서 6 - 1 = 5가지가 나오게 된다.

 

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

int solution(vector<vector<string>> clothes) {
    map<string, int> m;
    for(auto v : clothes) {
        m[v[1]]++;
    }
    
    int answer = 1;
    for(auto iter = m.begin(); iter != m.end(); iter++) {
        answer *= (iter->second + 1);
    }
    return answer - 1;
}

 

풀이

주어진 문자열을 왼쪽으로 X칸만큼 회전시켰을 때, 주어진 문자열이 올바른 괄호 문자열이면 개수를 카운트하는 문제다. 쪼개서 문제를 해결해보자.

 

올바른 괄호 문자열인지 판단하는 방법은 stack 자료구조를 이용하면 쉽게 판별할 수 있다.

여는 괄호 '(', '{', '[' 가 들어오면 stack에 push

닫는 괄호 ')', '}', ']' 가 나오면 아래의 조건을 체크해야 한다.

1. 스택이 비어있는데 닫는 괄호가 나오면 이 문자열은 올바른 괄호 문자열이 아니다.

2. 스택이 비어있지 않다면 가장 윗단의 여는 괄호와 닫는 괄호를 비교해서 맞다면 pop 아니라면 올바른 괄호 문자열이 아니다.

 

왼쪽으로 x칸만큼 회전시키는 방법은 deque 자료구조를 이용하면 쉽게 구현할 수 있다.

deque는 양쪽에 삽입 및 삭제가 가능하기 때문이다.

따라서 해당 x칸만큼 반복문을 돌려서 앞에서 빼고, 뒤에 삽입하면 된다.

 

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

int solution(string s) {
    int answer = 0;
    int loop = s.size();
    bool isValid;
    deque<char> dq(s.begin(), s.end());
    while(loop--) {
        dq.push_back(dq.front());
        dq.pop_front();
        stack<char> st;
        isValid = true;
        for(int i = 0; i < dq.size(); i++) {
            if(dq[i] == '[' || dq[i] == '(' || dq[i] == '{') st.push(dq[i]);
            else {
                if(st.empty()) {
                    isValid = false;
                    break;
                }
                else if(dq[i] == ']' && st.top() == '[') st.pop();
                else if(dq[i] == ')' && st.top() == '(') st.pop();
                else if(dq[i] == '}' && st.top() == '{') st.pop();
            }
        }
        
        if(!st.empty()) isValid = false;
        if(isValid) answer++;
    }
    
    
    return answer;
}

 

 

 

풀이

문제가 길지만 결국 요약하면 A라는 사람과 B라는 사람이 만나게 되는 대진표가 몇 번째 라운드인지만 출력하면 된다.

N명의 사람만큼 배열을 채워 넣고, 대진을 비교하면 된다.

언제까지 비교하면 될까?

8 -> 4 -> 2 -> 1

8명이라면 총 3번의 토너먼트만 돌리면 된다.

매칭 비교는

A라는 사람 혹은 B라는 사람이 다른 사람과 매칭 하게 되면 반드시 A 혹은 B가 이기고,

그 외에는 왼쪽 사람이 이긴다고 가정하면 된다. (왼쪽이나 오른쪽이나 누가 올라와도 결국 A와 B가 이기게 되므로)

 

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

int solution(int n, int a, int b)
{
    vector<int> v(n, 0);
    for(int i = 0; i < n; i++) v[i] = i + 1;
    int round = 0;
    while(n != 1) {
        vector<int> win;
        round++;
        for(int i = 1; i <= n; i+=2) {
            if((v[i-1] == a && v[i] == b) || (v[i-1] == b && v[i] == a)) return round;
            else if(v[i-1] == a || v[i-1] == b) win.push_back(v[i-1]);
            else if(v[i] == a || v[i] == b) win.push_back(v[i]);
            else win.push_back(v[i-1]);
        }
        v = win;
        n /= 2;
    }
    return -1;
}

 

 

풀이

문제는 간단한데 생각보다 어려운 문제였다.

어려웠던 부분은 "어떻게 하면 쪼개서 나열할 수 있을까?" 였다.

이 과정에서 결국 답을 봤지만, 생각보다 간단했다.

쪼개고, 순열 돌리기

주어진 numbers의 문자열을 하나씩 문자 형태로 쪼개서 배열에 넣어준 다음에,

그 배열을 정렬하고 순열을 돌리면 된다.

 

예시를 보자.

numbers = "17"; 이라는 문자열이 들어오게 되면
paper[0] = '1';
paper[1] = '7';

이렇게 쪼개서 넣어준 다음에, 해당 배열을 정렬한다.

정렬하는 이유는 순열을 돌리기 위한 전제 조건이기 때문에 꼭 해주자. 정렬을 하지 않으면 처음부터 끝까지 모든 순열의 정보를 얻을 수 없기 때문이다.

 

순열을 돌리면 아래와 같이 2가지의 경로가 나오게 된다.

차례대로 하나씩 정수형으로 변환하여 넣어주면 아래와 같게 만들어진다.

첫 번째 순열 결과 : '1', '7' -> "1", "17" -> 1, 17
두 번째 순열 결과 : '7', '1' -> "7", "71" -> 7, 71

set<int> : 1, 7, 17, 71

여기서 set을 사용한 이유는 중복될 수 있기 때문에 사전에 중복된 숫자는 지워주기 위해서 사용했다.

종이로 만들 수 있는 모든 숫자를 만든 뒤에 해당 숫자가 소수인지 아닌지 파악하는 간단한 소수 함수를 만들어서 판별하면 답을 구할 수 있게 된다.

bool isPrime(int n) {
    if (n == 0 || n == 1) return false;
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) return false;
    }
    return true;
}

전체 소스 코드

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

bool isPrime(int n) {
    if (n == 0 || n == 1) return false;
    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) return false;
    }
    return true;
}

int solution(string numbers) {
    int answer = 0;
    vector<char> paper;
    set<int> s;
    for (char c : numbers) paper.push_back(c);

    sort(paper.begin(), paper.end());
    do {
        string temp;
        for (int i = 0; i < paper.size(); i++) {
            temp += paper[i];
            s.insert(stoi(temp));
        }
    } while (next_permutation(paper.begin(), paper.end()));

    for (int n : s) {
        if (isPrime(n)) answer++;
    }

    return answer;
}

+ Recent posts