문제 풀이

단순하게 완전탐색법으로 접근하면 1초 안에 해결 가능한 범위이지만,

문제에서 보면 메모리 제한이 12MB로 다른 문제들에 비해서 무척이나 작은 공간이다.

 

12MB => 12 x 1000 x 1000

만약에 2차원 배열을 사용한다고 하면?

int(4byte) x 1500 x 1500 => 9,000,000 충분하다고 생각하지만 여기에 헤더 파일 등등 포함되면 빡빡하게 된다.

따라서 모든 자료를 저장할 필요 없이 N번째 큰 수를 찾아야 한다.

 

즉, 일부를 알기 위해서 전부를 저장할 필요가 없다.

 

우선순위 큐를 이용하는 방법이다.

다만 이때 우선순위 큐도 전체 자료를 저장하게 된다면, 마찬가지로 메모리 초과가 발생하게 된다. 이유는 위와 같다.

따라서 우선순위 큐를 사용하지만 일정 범위 이상으로 넘쳐흐르게 된다면 그중에서 필요 없는 값을 빼고 새로 들어온 값을 넣으면서 일부만 저장하는 방법을 취한다.

 

N = 3 이라고 할 때

입력값 : 3 5 6 7 8 이라고 하면,

 

처음 3 5 6 은 우선순위 큐에 저장하게 된다.

 

다음으로 들어온 숫자 7은 우선순위 큐(최소 힙)의 루트는 3이므로 3을 버리고 7을 넣는다.

왜? 3이라는 숫자는 필요 없는 값이기 때문에, 그렇지만 7이라는 숫자는 의미 있는 숫자이기 때문이다.

 

숫자 8이 들어오면 우선순위 큐의 루트는 5이므로 5를 버리고 8을 넣는다.

 

최종적으로는 우선순위 큐에는 6 7 8 이 존재하게 된다.

여기서 우리가 구하고자 하는 N = 3번째로 큰 수는 우선순위 큐의 루트인 6이 정답이 된다.

 

소스 코드

 

#include <iostream>
#include <queue>
#include <algorithm>

using namespace std;

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

int main()
{
	ios_base::sync_with_stdio(0);
	cin.tie(0);

	cin >> n;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			cin >> x;
			pq.push(x);
			if (pq.size() > n)
				pq.pop();
		}
	}

	cout << pq.top() << endl;
	return 0;
}

 

'Algorithm' 카테고리의 다른 글

[백준 14235] 크리스마스 선물  (0) 2021.10.13
[백준 1417] 국회위원 선거  (0) 2021.10.13
[백준 16926] 배열 돌리기1  (0) 2021.10.12
[백준 17298] 오큰수  (0) 2021.10.11
[백준 14425] 문자열 집합  (0) 2021.10.11
문제 풀이

 

반시계 방향으로 배열을 돌리면 된다.

단순하게 생각해서 구현 문제이며, 어떻게 구현하냐에 따라 풀이가 제각각일 것 같다.

 

반시계 방향으로 배열을 돌린다고 생각할 때,

가장 바깥쪽부터 반시계 방향으로 회전하는 식으로 구현했다.

 

가장 바깥쪽 반시계 돌리기,

그다음 바깥쪽 반시계 돌리기,

언제까지? 좌표가 엇갈릴 때까지 돌려주면 된다.

 

바깥쪽의 구분은

왼쪽 대각선의 시작점 (sx, sy) 와 오른쪽 대각선 끝점 (ex, ey)를 기준으로 잡아서 시작과 끝을 구분했다.

엇갈린다는 것은 sx >= ex || sy >= ey 를 의미한다.

 

배열을 돌릴 때는

swap 개념을 이용해서 기존의 값을 임시 저장소 temp에 저장하고,

다 돌리고 나서 temp의 값을 넣어주면 된다.

이때 원소의 이동은 한 칸씩 옮겨주면 된다.

 

소스 코드

 

#include <iostream>

using namespace std;

const int MAX = 300 + 5;

int N, M, R;
int a[MAX][MAX];

void rotate()
{
    int sx = 1, sy = 1;
    int ex = N, ey = M;
    while(sx < ex && sy < ey)
    {
        int temp = a[sx][sy];
        
        //top
        for(int i = sy + 1; i <= ey; i++)
        {
            a[sx][i-1] = a[sx][i];
        }
        
        //right
        for(int i = sx + 1; i <= ex; i++)
        {
            a[i-1][ey] = a[i][ey];
        }
        
        //bottom
        for(int i = ey; i >= sy + 1; i--)
        {
            a[ex][i] = a[ex][i-1];
        }
        
        //left
        for(int i = ex; i >= sx + 1; i--)
        {
            a[i][sy] = a[i-1][sy];
        }
        
        a[sx+1][sy] = temp;
        
        sx++;
        sy++;
        ex--;
        ey--;
    }
}

void printMatrix()
{
    for(int i = 1; i <= N; i++)
    {
        for(int j = 1; j <= M; j++)
        {
            cout << a[i][j] << ' ';
        }
        cout << '\n';
    }
}

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    
    cin >> N >> M >> R;
    for(int i = 1; i <= N; i++)
    {
        for(int j = 1; j <= M; j++)
        {
            cin >> a[i][j];
        }
    }
    
    for(int i = 0; i < R; i++)
        rotate();
    
    printMatrix();
    
    return 0;
}

'Algorithm' 카테고리의 다른 글

[백준 1417] 국회위원 선거  (0) 2021.10.13
[백준 2075] N번째 큰 수  (0) 2021.10.13
[백준 17298] 오큰수  (0) 2021.10.11
[백준 14425] 문자열 집합  (0) 2021.10.11
[백준 1753] 최단경로  (0) 2021.10.11
위상 정렬이란?

 

사전 작업, 선행 작업 등 순서가 정해진 작업을 차례대로 수행해야 할 때, 그 순서를 나열한 것.

사이클이 존재하지 않는 방향 그래프의 모든 노드를 순서에 거스르지 않도록 나열한 것.

 

'스타크래프트' 게임을 생각하면 쉽게 이해할 수 있다.

'서플', '배럭'을 짓기 위해서는 선행 작업으로 '커맨트 센터'를 먼저 만들어야 한다.

이 순서를 어기게 되면, 영원히 '서플'과 '배럭'을 건설할 수 없게 된다.

 

이렇듯, 선행이 필수인 것부터 나열해서 정렬하는 것이 위상 정렬이다.

 

위상 정렬의 동작 과정

 

  1. 진입 차수가 0인 노드 먼저 큐에 넣는다.
  2. 큐가 빌 때까지 반복한다.
    1. 큐에서 원소를 꺼내고, 그 노드에서 출발하는 간선들을 모두 제거한다.
    2. 이때 새롭게 진입 차수가 0인 노드들은 큐에 넣어준다.

큐에서 빠져 나오는 순서가 바로 구하려고 하는 위상 정렬이다.

진입 차수(indegree)는 해당 노드로 들어오는 간선의 수를 의미한다.

 

위상 정렬의 시간 복잡도

 

V : 노드의 수

E : 간선의 수

시간 복잡도 : 인접 리스트로 구현하면 O(V + E)지만, 인접 행렬로 구현하면 O(V^2)

 

차례대로 모든 노드들을 확인하면서 해당 노드에서 출발하는 간선들을 차례대로 제거하므로

결과적으로는 모든 노드와 모든 간선을 확인하기 때문이다.

 

위상 정렬 구현

 

위상 정렬에 필요한 것은 Queue의 자료구조

입력 범위에 따라 인접 행렬, 인접 리스트로 그래프를 구성하고 해결한다.

#include <iostream>
#include <queue>
#include <algorithm>
#include <vector>

using namespace std;

const int MAX = 32000 + 1;

int V, E;
int indegree[MAX];
vector<int> adj[MAX];

void topologySort()
{
    vector<int> ret;
    queue<int> q;
    
    //처음 시작할 때는 진입차수가 0인 노드를 큐에 넣어준다.
    for(int i = 1; i <= V; i++)
    {
        if(indegree[i] == 0)
            q.push(i);
    }
    
    while(!q.empty())
    {
        int now = q.front();
        q.pop();
        
        //큐에서 빠져 나온 순서가 정렬 순서
        ret.emplace_back(now);
        
        //해당 원소와 연결된 노드들의 진입차수에서 1빼기
        for(int i = 0; i < adj[now].size(); i++)
        {
            int next = adj[now][i];
            indegree[next]--;
            //새롭게 진입차수가 0이 되는 노드는 큐에 넣어주자.
            if(indegree[next] == 0)
                q.push(next);
        }
    }
    
    for(const auto& i : ret)
    {
        cout << i << ' ';
    }
    cout << '\n';
}

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    
    cin >> V >> E;
    for(int i = 0; i < E; i++)
    {
        int a, b;
        cin >> a >> b;
        adj[a].push_back(b);
        indegree[b]++;
    }
    
    topologySort();
    
    return 0;
}
한 걸음 더 나아가서

 

위상 정렬 도중에 큐가 비게 된다는 것은 사이클이 존재한다는 것을 의미한다.

운영체제 시간에 배운 순환 대기 형태를 생각해보면 이해하기 쉽다.

 

이렇게 순환 대기 형태를 이루게 되면(사이클을 이루게 되면) 순환 대기 형태를 띠는 노드들은 진입 차수가 0이 될 수 없으므로,

위상 정렬 큐에 들어가지 못하고 중간에 끝나버리게 된다.

 

앞서 말했듯이 위상 정렬은 모든 노드들과 모든 간선을 확인해야 하는데,

모든 노드를 탐색하기도 전에 큐가 empty 형태가 되어 끝나버리게 되는 것이다.

 

즉, 이러한 사이클 구조를 발견하기 위해서는 모든 노드를 탐색하기 전에 정렬이 끝나버리는 것을 체크하면 해결할 수 있다.

void topologySort()
{
	vector<int> ret;
	priority_queue<int, vector<int>, greater<int> > q;

	for (int i = 1; i <= v; i++)
	{
		if (indegree[i] == 0) q.push(i);
	}

	for (int i = 1; i <= v; i++)
	{
		// 위상 정렬 도중에 큐가 비게 된다면 사이클이 존재한다는 의미
		if (q.empty())
		{
			cout << -1 << '\n';
			return;
		}

		int now = q.top(); q.pop();
		ret.push_back(now);

		for (int i = 0; i < graph[now].size(); i++)
		{
			int next = graph[now][i];
			indegree[next]--;
			if (indegree[next] == 0) q.push(next);
		}
	}

	for (int n : ret) cout << n << '\n';
}

 

위상 정렬의 답은 1개가 아니다?

 

때로는 위상 정렬의 답은 여러개가 될 수 있다.

진입 차수가 0이 되는 노드가 2개 이상이 큐에 들어가는 경우가 발생하면 여러 가지 답이 나올 수 있다.

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

트라이 (Trie) 자료구조  (0) 2021.10.18
행렬의 다양한 연산들  (0) 2021.10.11
플로이드 워셜 (Floyd Warshall)  (0) 2021.09.03
투 포인터  (0) 2021.08.25
다익스트라 알고리즘  (0) 2021.08.24
문제 풀이

 

오큰수를 다시 정리하면 다음과 같다.

나보다 크며, 오른쪽 중에서는 가장 작은 수여야 한다.

[3 5 2 7]에서 내가 5라고 가정하면, 오큰수는 7이다.

 

단순하게 완전 탐색으로 돌리게 되면 O(N^2)이 걸리게 된다.

N이 100만이기 때문에 시간 초과가 발생하므로 조금 더 빠르게 오큰수를 찾는 방법이 필요하다.

 

이런 문제는 스택 자료구조를 사용하면 O(N)으로 해결할 수 있게 된다.

스택은 아래와 같은 방법으로 동작한다.

1) 스택이 비어있으면 -1 (오큰수가 없으므로) 처리하고, 스택에 push 한다. 푸시하는 이유는 다음의 오큰수에 해당될 수 있으므로

2) 스택이 비어있지 않으면,

입력된 arr[i] < stack의 top이면? stack의 top이 해당 오큰수를 뜻하고,

>= 이면 <가 나올때까지 스택에서 계속해서 오큰수를 pop 하면서 찾는다.

만약에 찾게 된다면 해당 수가 오큰수이며, 입력된 arr[i]를 스택에 push 한다.

못 찾게 된다면 해당 수에 오큰수가 존재하지 않으므로 -1이다.

 

입력된 배열의 역순으로 하나씩 확인하면서 스택을 응용하면 된다.

 

문제에서 주어진 예시로 다시 확인해보자.

입력된 수 스택 자료구조 오큰수 배열
7 empty  
  7 [0, 0, 0, -1]
2 7  
  7 2 [0, 0, 7, -1]
5 7 2  
  7 5 [0, 7, 7, -1]
3 7 5  
  7 5 3 [5, 7, 7, -1]

입력된 수 : 7

현재 스택이 비어있기 때문에 오큰수는 -1이며, 스택에 7을 삽입하면 된다.

 

입력된 수 : 2

현재 스택의 top(7)이 2보다 크므로 2의 오큰수는 7이며, 스택에 2를 삽입한다.

 

입력된 수 : 5

현재 스택의 top(2)보다 5가 더 크므로 스택에서 하나를 뺀다.

현재 스택의 top(7)이 5보다 크므로 5의 오큰수는 7이며, 스택에 5를 삽입한다.

 

입력된 수 : 3

현재 스택의 top(5)이 3보다 크므로 3의 오큰수는 5이며, 스택에 3을 삽입한다.

 

소스 코드

 

#include <iostream>
#include <stack>

using namespace std;

int N;
int arr[1000001];
int ans[1000001];

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    
    cin >> N;
    for(int i = 0; i < N; i++)
        cin >> arr[i];
    
    stack<int> st;
    for(int i = N - 1; i >= 0; i--)
    {
        while(!st.empty() && st.top() <= arr[i])
            st.pop();
        
        if(!st.empty())
        {
            ans[i] = st.top();
        }
        else
            ans[i] = -1;
        
        st.push(arr[i]);
    }
    
    for(int i = 0; i < N; i++)
        cout << ans[i] << ' ';
    
    return 0;
}

'Algorithm' 카테고리의 다른 글

[백준 2075] N번째 큰 수  (0) 2021.10.13
[백준 16926] 배열 돌리기1  (0) 2021.10.12
[백준 14425] 문자열 집합  (0) 2021.10.11
[백준 1753] 최단경로  (0) 2021.10.11
[HackerRank] 2D Array - DS  (0) 2021.09.09
문제 풀이

 

주어진 문자열 집합 S에서 입력되는 M개의 문자열이 같은 게 몇 개인지를 찾는 문제다.

단순히 생각해보면 전체 집합 S에서 전체 M개의 문자열을 2중 탐색으로 반복해서 문자열 비교까지 하게 된다면

시간 복잡도가 O(10,000 x 10,000 x 500) = 2초를 훌쩍 넘는다.

 

여기서 줄일 수 있는 방법은 문자열 탐색 속도를 줄이는 것이다.

균형 이진 탐색 트리를 사용하는 set이나 map을 이용하게 되면 O(logN) 만에 탐색 속도를 줄일 수 있다.

 

소스 코드

 

#include <iostream>
#include <map>
#include <string>
#include <vector>

using namespace std;

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    
    int N, M;
    cin >> N >> M;
    map<string, bool> m;
    for(int i = 0; i < N; i++)
    {
        string str;
        cin >> str;
        m[str] = true;
    }
    
    int cnt = 0;
    for(int i = 0; i < M; i++)
    {
        string str;
        cin >> str;
        if(m[str] == true)
        {
            cnt++;
        }
    }
    
    cout << cnt << endl;
    return 0;
}
트라이 이용

 

신기하게도 이 문제는 트라이 자료구조를 이용해서 풀 수 있다.

다만 시간 복잡도가 map은 O(문자열의 개수 10000 x 탐색 시간(log10000))이지만

트라이는 O(문자열의 개수 10000 x 문자열의 길이 500)이므로 여기서는 트라이가 더 느리다.

 

트라이가 항상 빠르다고 생각하면 큰 오산이 되는 문제다.

 

트라이에 문자열 집합 S를 넣어서 완성시키고, 쿼리마다 find 함수를 통해서 등록되어 있으면 카운팅 해주면 된다.

소스 코드(트라이)

 

#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct Trie
{
    Trie* ch[26];
    bool end;
    
    Trie() : end(false)
    {
        for(int i = 0; i < 26; i++)
            ch[i] = nullptr;
    }
    
    ~Trie()
    {
        for(int i = 0; i < 26; i++)
            if(ch[i]) delete ch[i];
    }
    
    void insert(const char* str)
    {
        if(!*str)
        {
            end = true;
            return;
        }
        
        int cur = *str - 'a';
        if(!ch[cur]) ch[cur] = new Trie();
        ch[cur]->insert(str + 1);
    }
    
    bool find(const char* str)
    {
        if(!*str)
        {
            if(end) return true;
            return false;
        }
        int cur = *str - 'a';
        if(!ch[cur]) return false;
        return ch[cur]->find(str + 1);
    }
};

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    
    int n, m;
    cin >> n >> m;
    Trie* root = new Trie();
    string str;
    for(int i = 0; i < n; i++)
    {
        cin >> str;
        
        root->insert(str.c_str());
    }
    
    int cnt = 0;
    for(int i = 0; i < m; i++)
    {
        cin >> str;
        
        if(root->find(str.c_str()))
        {
            cnt++;
        }
    }
    
    cout << cnt << endl;
    delete root;
    return 0;
}

'Algorithm' 카테고리의 다른 글

[백준 16926] 배열 돌리기1  (0) 2021.10.12
[백준 17298] 오큰수  (0) 2021.10.11
[백준 1753] 최단경로  (0) 2021.10.11
[HackerRank] 2D Array - DS  (0) 2021.09.09
[프로그래머스] 방문 길이 (C++)  (0) 2021.06.04
문제 풀이

 

방향 그래프이면서 하나의 시작점에서 다른 여러 개의 모든 지점의 최단 경로를 구하는 방법은 다익스트라 알고리즘을 구현하면 해결할 수 있다.

 

소스 코드 (우선순위 큐를 이용한 구현)

 

#include <iostream>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

const int INF = 987654321;

int V, E, startNode;
vector<pair<int, int> > edges[20001];
int dist[20001];

void dijkstra()
{
    dist[startNode] = 0;
    priority_queue<pair<int, int> > pq;
    pq.push({0, startNode});
    while(!pq.empty())
    {
        int distance = -pq.top().first;
        int curNode = pq.top().second;
        pq.pop();
        
        if(dist[curNode] < distance)
            continue;
        
        for(int i = 0; i < edges[curNode].size(); i++)
        {
            int nextNode = edges[curNode][i].first;
            int nextDistance = edges[curNode][i].second + distance;
            if(nextDistance < dist[nextNode])
            {
                dist[nextNode] = nextDistance;
                pq.push({-nextDistance, nextNode});
            }
        }
    }
}

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    
    cin >> V >> E >> startNode;
    fill(dist, dist + 20001, INF);
    for(int i = 0; i < E; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        
        edges[u].emplace_back(v,w);
    }
    
    dijkstra();
    
    for(int i = 1; i <= V; i++)
    {
        if(dist[i] == INF)
            cout << "INF\n";
        else
            cout << dist[i] << '\n';
    }
    
    return 0;
}

'Algorithm' 카테고리의 다른 글

[백준 17298] 오큰수  (0) 2021.10.11
[백준 14425] 문자열 집합  (0) 2021.10.11
[HackerRank] 2D Array - DS  (0) 2021.09.09
[프로그래머스] 방문 길이 (C++)  (0) 2021.06.04
[프로그래머스] 영어 끝말잇기 (C++)  (0) 2021.06.04

간단하게 행렬을 표현하는 방법에서 코딩 테스트에서 자주 나오는 행렬에 대한 몇 가지 구현 방법을 공부해보자.

 

행렬의 표현

 

행렬의 표현은 자료구조에서 보통 2차원 배열로 표현하게 된다.

만약에 큐브라면 3차원 배열로 구현하면 된다.

//2 x 2 행렬
int a[2][2] = 
{
    {1, 1},
    {1, 1}
};

//3 x 3 행렬
int b[3][3] =
{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

// 2 x 5 행렬
int c[2][5] = 
{
    {1, 2},
    {3, 4},
    {5, 6},
    {7, 8},
    {9, 10}
};
행렬의 곱셈

 

"A의 행렬 x B의 행렬" 를 물어보는 문제가 간간히 등장한다.

이럴 때 행렬의 곱셈을 구현하는 방법을 따로 연습하지 않았더라면 꽤나 시간을 잡아먹을 수 있다!

 

핵심은 다음과 같다.

C의 행렬[i][j] = A의 행렬의 i 행 x B의 행렬의 j열

 

구현은 다음과 같이 할 수 있다.

#include <iostream>

using namespace std;

int main()
{
    int a[2][3] = {{1,2,3},{4,5,6}};
    int b[3][2] = {{7,8},{9,10},{11,12}};
    int c[2][2] = {{0,0},{0,0}};
    
    // 행렬의 곱셈 구현 : 시간복잡도 O(N^3)
    for(int i = 0; i < 2; i++) // a의 행
    {
        for(int j = 0; j < 2; j++) // b의 열
        {
            for(int k = 0; k < 3; k++) // a의 열 == b의 행
            {
                c[i][j] += a[i][k] * b[k][j];
            }
        }
    }
    
    for(int i = 0; i < 2; i++)
    {
        for(int j = 0; j < 2; j++)
        {
            cout << c[i][j] << ' ';
        }
        cout << '\n';
    }
    
    return 0;
}
행렬의 회전

 

행렬의 회전하는 것도 자주 등장하는 문제 중 하나다.

시계방향인지, 반시계방향인지 생각하고 구현 방법을 배워놓으면 나중에 실수를 줄일 수 있다.

#include <iostream>
#include <vector>

using namespace std;

typedef vector<vector<int> > matrix;

// N x N 시계방향으로 90도 회전
matrix rotateRight(const matrix& a)
{
    int n = static_cast<int>(a.size());
    matrix ret(n, vector<int>(n, 0));
    
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            ret[j][n - 1 - i] = a[i][j];
        }
    }
    return ret;
}

// N x M 시계방향으로 90도 회전
matrix rotateRightOther(const matrix& a)
{
    int r = static_cast<int>(a.size());
    int c = static_cast<int>(a[0].size());
    //주의! M x N 으로 만들자.
    matrix ret(c, vector<int>(r,0));
    
    for(int i = 0; i < r; i++)
    {
        for(int j = 0; j < c; j++)
        {
            ret[j][r - 1 - i] = a[i][j];
        }
    }
    return ret;
}

//N x N 반시계방향으로 90도 회전
matrix rotateLeft(const matrix& a)
{
    int n = static_cast<int>(a.size());
    matrix ret(n, vector<int>(n,0));
    
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            ret[n - 1 - j][i] = a[i][j];
        }
    }
    return ret;
}

// N x M 반시계방향으로 90도 회전
matrix rotateLeftOther(const matrix& a)
{
    int r = static_cast<int>(a.size());
    int c = static_cast<int>(a[0].size());
    matrix ret(c, vector<int>(r,0));
    
    for(int i = 0; i < r; i++)
    {
        for(int j = 0; j < c; j++)
        {
            ret[c - 1 - j][i] = a[i][j];
        }
    }
    return ret;
}

void printMatrix(const matrix& a)
{
    int r = static_cast<int>(a.size());
    int c = static_cast<int>(a[0].size());
    for(int i = 0; i < r; i++)
    {
        for(int j = 0; j < c; j++)
        {
            cout << a[i][j] << ' ';
        }
        cout << '\n';
    }
    cout << '\n';
}

int main()
{
    matrix a(2, vector<int>(2, 0));
    for(int i = 0; i < 2; i++)
        for(int j = 0; j < 2; j++)
            a[i][j] = i * 2 + j;
    
    cout << "a의 회전하기 전의 모양" << endl;
    printMatrix(a);
    
    cout << "a를 시계방향으로 90도 회전" << endl;
    printMatrix(a = rotateRight(a));
    
    cout << "a를 시계방향으로 180도 회전" << endl;
    printMatrix(a = rotateRight(a));
    
    cout << "a를 시계방향으로 270도 회전" << endl;
    printMatrix(a = rotateRight(a));
    
    cout << "a를 시계방향으로 360도 회전" << endl;
    printMatrix(a = rotateRight(a));
    
    cout << "a를 반시계방향으로 90도 회전" << endl;
    printMatrix(a = rotateLeft(a));
    
    cout << "a를 반시계방향으로 180도 회전" << endl;
    printMatrix(a = rotateLeft(a));
    
    cout << "a를 반시계방향으로 270도 회전" << endl;
    printMatrix(a = rotateLeft(a));
    
    cout << "a를 반시계방향으로 360도 회전" << endl;
    printMatrix(a = rotateLeft(a));
    
    matrix b(2, vector<int>(3, 0));
    int num = 0;
    for(int i = 0; i < 2; i++)
        for(int j = 0; j < 3; j++)
            b[i][j] = num++;
    
    cout << "b의 회전하기 전의 모양" << endl;
    printMatrix(b);
    
    cout << "b를 시계방향으로 90도 회전" << endl;
    printMatrix(b = rotateRightOther(b));
    
    cout << "b를 시계방향으로 180도 회전" << endl;
    printMatrix(b = rotateRightOther(b));
    
    cout << "b를 시계방향으로 270도 회전" << endl;
    printMatrix(b = rotateRightOther(b));
    
    cout << "b를 시계방향으로 360도 회전" << endl;
    printMatrix(b = rotateRightOther(b));
    
    cout << "b를 반시계방향으로 90도 회전" << endl;
    printMatrix(b = rotateLeftOther(b));
    
    cout << "b를 반시계방향으로 180도 회전" << endl;
    printMatrix(b = rotateLeftOther(b));
    
    cout << "b를 반시계방향으로 270도 회전" << endl;
    printMatrix(b = rotateLeftOther(b));
    
    cout << "b를 반시계방향으로 360도 회전" << endl;
    printMatrix(b = rotateLeftOther(b));
    
    return 0;
}

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

트라이 (Trie) 자료구조  (0) 2021.10.18
위상 정렬 (TopologySort)  (0) 2021.10.12
플로이드 워셜 (Floyd Warshall)  (0) 2021.09.03
투 포인터  (0) 2021.08.25
다익스트라 알고리즘  (0) 2021.08.24

문제 풀이:)

2차원 배열에서 나올 수 있는 모든 모래시계를 구한 다음에 그 모래시계의 합의 최댓값을 구하면 되는 문제다.

모래시계를 어떻게 구할 수 있을까?

 

시작 좌표가 0부터 시작한다고 하면 다음과 같이 모래시계를 구할 수 있다.

(0,0) (0,1) (0,2)
      (1,1)
(2,0) (2,1) (2,2)

2차원 배열에서 나올 수 있는 모든 모래시계는 어떻게 찾을 수 있을까?

 

하나의 좌표를 기준으로 확인해보면 된다.

여기서 하나의 좌표를 기준은 모래시계의 최상단 왼쪽 배열인 (0,0)을 기준으로 확인하면 된다.

만약에 6 x 6 배열에서 찾고자 한다면 범위는

열로 보면 0 ~ 3까지만,

행로 보면 0 ~ 3까지만,

탐색하면 된다.

 

만약에 4를 보면?

(0,4) (0,5) (0,6)
      (1,5)
(2,4) (2,5) (2,6)

이미 인덱스를 넘어서게 된다.

 

이러한 범위만 주의해서 코드를 짜면 문제를 해결할 수 있다.

int hourglassSum(vector<vector<int>> arr) {
    int ans = -987654321;
    int row = arr.size();
    int col = arr[0].size();
    for(int i = 0; i < row - 2; i++) {
        for(int j = 0; j < col - 2; j++) {
            int sum = 0;
            sum += arr[i][j] + arr[i][j+1] + arr[i][j+2];
            sum += arr[i+1][j+1];
            sum += arr[i+2][j] + arr[i+2][j+1] + arr[i+2][j+2];
            ans = max(ans, sum);
        }
    }
    return ans;
}

 

'Algorithm' 카테고리의 다른 글

[백준 14425] 문자열 집합  (0) 2021.10.11
[백준 1753] 최단경로  (0) 2021.10.11
[프로그래머스] 방문 길이 (C++)  (0) 2021.06.04
[프로그래머스] 영어 끝말잇기 (C++)  (0) 2021.06.04
[프로그래머스] 위장 (C++)  (0) 2021.05.31

이번에는 최단 경로 알고리즘 중 하나인 플로이드 워셜에 대해서 공부해보자.

모든 지점에서 다른 모든 지점까지의 최단경로를 모두 구해주는 알고리즘에 대해서 배워보자.

플로이드 워셜

모든 지점에서 다른 모든 지점까지의 최단 경로를 모두 구해주는 알고리즘이다.

 

쉽게 설명하면, A 노드에서 B 노드로 가는 최단 경로가

A -> B (한 번에 가는 것)

A -> K -> B (어떤 노드를 거쳐서 가는 것)

가 있을 텐데 이중 우리는 더 짧은 경로를 원하므로 min(A -> B, A -> K -> B)를 갱신하면 된다.

 

단계마다 거쳐 가는 노드를 기준으로 더 짧은 경로를 선택하며 반복하는 것이다.

이때 단계마다 비용이 O(N^2)이다.

거쳐 가는 노드를 기준으로 시작 노드에서 도착 노드까지 모든 경로를 검사하는데 드는 비용이 N^2 이므로

거쳐 가는 노드가 N개이면? 시간 복잡도는 O(N x N^2) = O(N^3)

 

플로이드 워셜 알고리즘은 다이나믹 프로그래밍의 기법이기도 하다.

점화식을 살펴보면 D[a][b] = min(D[a][b], D[a][k] + D[k][b])

구현 방법

플로이드 워셜은 사전 작업이 필요하다.

  • 2차원 배열을 무한 값으로 초기화
  • 자기 자신에서 자기 자신으로 가는 비용은 0으로 갱신
  • 이후 간선의 정보를 입력받고 플로이드 워셜을 수행
//플로이드 워셜
#include <iostream>
#include <algorithm>
using namespace std;

const int INF = 1e9;

int n, m;
int graph[501][501];
int main() {
    ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n >> m;
    //최단 거리 테이블을 모두 무한으로 초기화
    fill(graph[0], graph[0] + 501 * 501, INF);
    
    //자기 자신으로 가는 비용은 0으로
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            if(i == j) graph[i][j] = 0;
        }
    }
    
    //각 간선에 대한 정보를 입력 받아 그 값으로 초기화
    for(int i = 0; i < m; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        graph[a][b] = c;
    }
    
    //점화식에 따라 플로이드 워셜 알고리즘 수행
    //D[a][b] = min(D[a][b], D[a][k] + D[k][b])
    for(int k = 1; k <= n; k++) {
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= n; j++) {
                graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j]);
            }
        }
    }
    
    //그래프 출력
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= n; j++) {
            if(graph[i][j] == INF)
                cout << "INF\n";
            else
                cout << graph[i][j] << ' ';
        }
        cout << '\n';
    }
    return 0;
}

연습문제

플로이드

역사

회장뽑기

운동

플로이드2

경로찾기

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

위상 정렬 (TopologySort)  (0) 2021.10.12
행렬의 다양한 연산들  (0) 2021.10.11
투 포인터  (0) 2021.08.25
다익스트라 알고리즘  (0) 2021.08.24
최단 경로 알고리즘(Shortest Path)  (0) 2021.08.23

오늘은 투 포인터 알고리즘에 대해서 공부해보자.

이 투 포인터를 모르면 코테에서 낭패를 볼 수 있다?라고 말할 정도로 생각보다 중요한 알고리즘 테크닉이다.

남들도 다 아는 알고리즘을 나만 몰라서 틀린다.. 이거 매우 억울한 일이다. 얼른 공부해서 습득하자!

투 포인터 (Two pointer)

이름과 똑같이 두 개의 포인터를 가지고 문제를 해결하는 알고리즘이다.

문제를 풀다보면 배열의 길이가 2중 포문으로 해결을 할 수 없을 정도로 큰 배열이 주어지게 됐을 때, 구간의 합이 M인 곳이 몇 개 인지? 물어보는 문제에서는 완전 탐색으로는 시간 초과로 해결할 수 없는 문제가 있다.

대표적으로 BOJ2003 수들의 합2 문제와 BOJ2470 두 용액 문제다.

 

이런 문제들의 특징은 입력 크기가 너무 커서 완전 탐색으로는 해결할 수 없는 문제들이다.

이럴 때 사용하는 것이 바로 투 포인터 알고리즘이다.

 

투 포인터 알고리즘이 강력한 이유

두 개의 포인터만 하나의 배열에서 움직이므로 O(N) + O(N) = O(2N) => 시간 복잡도 O(N)

 

투 포인터 알고리즘의 유형은 2가지가 있는데 다음과 같다.

  1. 포인터 2개가 같은 방향으로 진행하기
  2. 포인터 하나는 왼쪽에서 오른쪽으로, 다른 하나는 오른쪽에서 왼쪽으로 진행하기

성능은 알겠는데 어떻게 구현하고 사용할까?

 

BOJ2003 수들의 합 2 

위의 문제를 예시로 살펴보자.

이 문제는 투 포인터 알고리즘 유형의 1번에 속하는 문제다.

포인터 2개가 같은 방향으로 진행하기

두 포인터가 배열의 첫 번째부터 시작해서 한 step씩 확인하면서 구간의 합이 M이 되는지 체크해서 문제를 해결해나간다.

들어가기 앞서서 사전 작업이 있다.

  • left 포인터와 right 포인터는 모두 0으로 시작한다.
  • 현재 구간의 합은 left와 right모두 0을 가리키고 있기 때문에 배열의 첫 번째 값이 된다.
  • 반드시 left는 right보다 같거나 작아야 한다. 이 순서가 꼬이게 되면 알 수 없는 상황이 발생할 수 있다.

위의 사전 작업을 숙지하고 아래를 보자.

  1. 현재 구간의 합 sum < M
    1. 아직 구간의 합이 M보다 작기 때문에 늘리려면 어떻게 할까? right 포인터를 한 칸 오른쪽으로 늘리면 된다.
  2. 현재 구간의 합 sum > M
    1. 구간의 합이 M보다 커졌다. 이럴 때는 줄여야 한다. left 포인터를 한 칸 오른쪽으로 늘리면 된다.
  3. 현재 구간의 합 sum == M
    1. 우리가 원하는 구간의 합 M을 찾았기 때문에 정답을 +1 하자.
    2. 그런 다음 두 포인터를 모두 한 칸씩 늘리면 된다. 왜?
      1. 현재 구간의 합이 답이 되기 때문에 left를 한 칸 늘리거나, right를 한 칸 늘려도 절대 답이 될 수 없기 때문이다.

위의 이론을 바탕으로 구현을 하게 되면 아래의 구현처럼 된다.

//2003 수들의 합2
#include <iostream>
using namespace std;

int main() {
    ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    int n, m;
    int arr[10001];
    
    cin >> n >> m;
    for(int i = 0; i < n; i++) cin >> arr[i];
    
    int left = 0;
    int right = 0;
    int sum = arr[0]; //처음에 left, right가 모두 0을 가르키고 있기 때문에 합은 첫번째 배열의 값이다.
    int ans = 0;
    while(right < n) {
        //합이 m보다 작으면 right 포인터를 오른쪽으로 이동
        if(sum < m) {
            right++;
            if(right < n)
                sum += arr[right];
        }
        //합이 m보다 크다면 left 포인터를 오른쪽으로 이동
        else if(sum > m) {
            sum -= arr[left];
            left++;
        }
        //합이 같다면 두 포인터 모두 오른쪽으로 이동하고, 정답을 +1
        else {
            sum -= arr[left];
            left++;
            right++;
            if(right < n)
                sum += arr[right];
            
            ans++;
        }
    }
    cout << ans << endl;
    return 0;
}

처음 봐도 이해가 잘 되지 않는다면, 공책을 펼쳐놓고 직접 디버깅해보자!

어떤 알고리즘이든 한 번에 습득하기 어렵다. 반복 숙달하자.

 

BOJ2470 두 용액

이번에는 다른 스타일의 문제를 살펴보자.

이 문제의 유형은 위에서 말했듯이 2번 스타일이다.

포인터 하나는 왼쪽에서 오른쪽, 다른 하나는 오른쪽에서 왼쪽으로 진행

 

그렇다면 사전 작업은 어떻게 할까?

  • left는 0, right는 n - 1
  • ans는 초기 두 용액의 합으로 나올 수 있는 절댓값인 20억으로 세팅하기
  • 합이 0에 가까운 두 용액을 저장할 변수

사전 작업을 숙지하고 어떻게 풀지 생각해보자.

두 용액의 합을 0에 가깝게 만들기 위해서는 최대한 왼쪽에 있는 애들은 오른쪽 애들보다 작아야 할 것이다.

그렇기 때문에 처음에 정렬을 수행한다. 이렇게 되면 왼쪽에는 작은 애들이 몰리고, 오른쪽은 큰 애들이 몰린다.

이렇게 세팅을 한 후 두 용액을 합쳤을 때 기존에 저장된 ans보다 더 작고 0에 가깝다면 갱신을 시키면서 문제를 해결해나간다.

정리하면 다음과 같다.

  • 우선 입력된 배열을 정렬한다.
  • 두 용액의 합의 절댓값이 기존에 저장된 ans보다 더 작다면? 갱신하자.
    • ans = abs(arr[left] + arr[right]
    • ret[0] = arr[left] 두 용액 중 왼쪽 용액 저장
    • ret[1] = arr[right] 두 용액 중 오른쪽 용액 저장
  • arr[left] + arr[right] < 0
    • 두 용액의 합이 0보다 작다는 말은 무슨 뜻일까?
    • 0에 가깝게 만들기 위해서는 left를 오른쪽으로 옮겨주면 된다.
  • arr[left] + arr[right] > 0
    • 반대로 0보다 크다면?
    • 0에 가깝게 만들기 위해서는 right를 왼쪽으로 옮겨주면 된다.

위의 이론을 바탕으로 구현을 하게 되면 아래의 구현처럼 된다.

//2470 두 용액
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;

int n;
int arr[100001];

int main() {
    ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    
    cin >> n;
    for(int i = 0; i < n; i++) cin >> arr[i];
    sort(arr, arr + n); //정렬 필수, 서로 합이 0에 가깝게 만들기 위해서
    
    int left = 0;
    int right = n - 1;
    int ans = 2e9; // 두 용액의 합을 최대 20억으로 잡아두고 시작
    int ret[2];
    while(left < right) {
        //두 용액의 합이 0에 가까운지 확인하기
        int value = arr[left] + arr[right];
        
        if(ans > abs(value)) {
            ret[0] = arr[left];
            ret[1] = arr[right];
            ans = abs(value);
            
            // 합이 0이면 탈출하자.
            if(value == 0) break;
        }
        
        if(value > 0) right--;
        else left++;
    }
    cout << ret[0] << ' ' << ret[1] << endl;
    return 0;
}

정리해보면 투 포인터 알고리즘은 다음과 같다.

유형에 익숙해지는 것도 좋지만,

투 포인터를 어떤 조건일 때는 증가시키고 감소시키는지를 잘 결정하면 어떤 유형이 나오더라도 해결할 수 있을 것이다.

연습문제

부분합

수 고르기

회전 초밥

소수의 연속합

세 용액

[카카오 인턴] 보석 쇼핑

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

행렬의 다양한 연산들  (0) 2021.10.11
플로이드 워셜 (Floyd Warshall)  (0) 2021.09.03
다익스트라 알고리즘  (0) 2021.08.24
최단 경로 알고리즘(Shortest Path)  (0) 2021.08.23
수학 관련  (0) 2021.08.18

+ Recent posts