반응형

주어진 문제는 위와 같다. 최대 100 개의 도시가 주어지고, 도시 간 이동이 가능한 버스 노선들이 주어졌을 때 모든 도시 간 이동 시의 최소 비용을 구하는 것이다. 최단 경로 탐색을 위한 알고리즘은 다익스트라, 벨만 포드, A* 탐색 등 여러 가지가 있지만 위와 같이 모든 노드 간 최단 경로를 구하기 위한 알고리즘으로 문제의 이름과 같은 플로이드 와샬 알고리즘이 있다.

 

플로이드 와샬 알고리즘에 대해 내가 기억하고 있는 것은, 노드 간 경로 가중치를 테이블에 저장하고 경로가 존재하지 않는 경우는 INF 값으로 저장한다. 그 뒤 스텝 별로 중간에 다른 노드를 경유하며 경로 가중치를 업데이트하는 방식으로 기억하고 있었다. 하지만 내가 기억하고 있는 알고리즘의 로직은 너무 모호해 문제 풀이에 적용할 수 없었고 따로 플로이드 와샬 알고리즘을 찾아본 뒤 문제를 해결했다.

 

플로이드 와샬 알고리즘

플로이드 와샬 알고리즘을 간단하게 설명하면, O(|V|^3) 의 시간 복잡도를 갖는 모든 쌍 최단 경로 탐색 알고리즘이다. 매 스텝 별로 X to Y (X, Y 는 노드) 경로의 가중치를 업데이트 해야 하므로 O(|V| ^ 2) 를 매 스텝 별로 실행하고, 각 스텝은 경유할 노드를 순차적으로 적용하므로 O(|V| ^ 2 * |V|) 의 시간 복잡도를 갖게 된다.

 

위의 그림을 통해, 알고리즘의 동작 방식을 설명할 수 있다. i, j 는 각각 노드 번호를 뜻하고 k 는 경유할 노드의 번호를 의미한다.

k = 0 일 때, 해당 테이블은 노드를 경유하지 않고 i - j 간 경로를 표현한 것이다.

k = 1 일 때, 변경된 table[2][3] 값은, 기존의 node(2) -> node(3) 의 경로 가중치는 3이고, 노드 1번을 경유한 node(2) -> node(1) -> node(3) 의 가중치 합이 4 + (-2) 이므로 새로운 값이 업데이트된 것을 볼 수 있다.

k = 2 일 때, node(4) -> node(1) 의 경로와 node(4) -> node(3) 의 경로가 2번 노드를 경유함으로 인해 가중치 값이 업데이트된 것을 확인할 수 있다.


이와 같이 경유할 노드를 변경해 가며 경로 가중치 값을 업데이트해 나가면 최종적으로 모든 노드 쌍 간의 최단 경로를 구할 수 있게 된다.

 

알고리즘을 적용한 코드는 3개의 for 문을 중첩함으로써 간단하게 구현이 가능하고, 이를 적용해 문제 해결에 사용한 코드는 다음과 같다.

 

#include <iostream>
using namespace std;

const int CITY = 100;
const int INF = 123456789;

int city, bus;
int dist[CITY + 1][CITY + 1];

void initialize() {
    for (int i = 1; i <= city; i++) {
        for (int j = 1; j <= city; j++) {
            dist[i][j] = INF;
        }
    }
}

void input() {
    cin >> city >> bus;
    initialize();

    for (int i = 0; i < bus; i++) {
        int a, b, c;
        cin >> a >> b >> c;
        dist[a][b] = dist[a][b] < c ? dist[a][b] : c;
    }
}

void output() {
    for (int i = 1; i <= city; i++) {
        for (int j = 1; j <= city; j++) {
            if (dist[i][j] == INF) {
                cout << "0 ";
            } else {
                cout << dist[i][j] << " ";
            }
        }
        cout << "\n";
    }
}

void solve() {
    for (int k = 1; k <= city; k++) {
        for (int i = 1; i <= city; i++) {
            for (int j = 1; j <= city; j++) {
                if (i == j)
                    continue;
                int k_dist = dist[i][k] + dist[k][j];
                dist[i][j] = dist[i][j] < k_dist ? dist[i][j] : k_dist;
            }
        }
    }
}

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

    input();

    solve();

    output();
}

 

# Reference.

 

플로이드-워셜 알고리즘 - 위키백과, 우리 모두의 백과사전

컴퓨터 과학에서, 플로이드-워셜 알고리즘(Floyd-Warshall Algorithm)은 변의 가중치가 음이거나 양인 (음수 사이클은 없는) 가중 그래프에서 최단 경로들을 찾는 알고리즘이다.[1][2] 알고리즘을 한 번

ko.wikipedia.org

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

[BOJ # 1874] 스택 수열  (0) 2021.09.21
[BOJ # 1600] 말이 되고픈 원숭이  (0) 2021.09.19
<BOJ 2508> 나무 자르기  (0) 2019.02.14
반응형

주어진 문제는 위와 같다. 수열의 크기 n 이 주어지고, 수열을 이루는 1 ~ n 까지 정수가 하나씩 순서대로 주어진다. 이 때, 스택을 가지고 주어진 수열을 만들기 위해 필요한 연산을 한 줄에 하나씩 출력한다.

 

스택은 LIFO 구조로, push / pop 연산을 통해 자료를 저장하고 뺄 수 있다. 스택으로 수열을 만들기 위해서 필요한 조건은, 주어진 정수가 항상 스택에 저장된 top 값보다 크거나 같아야 한다는 것이다. 만약 스택의 top 에 저장된 값보다 작은 정수가 주어졌을 때는 그 값을 찾기 위해 스택에서 여러 번 pop 을 해야 하므로, 이 문제에서 주어진 조건을 만족할 수 없다.

 

때문에 문제의 풀이 방식은 간단하다. 주어진 정수가 스택에 저장된 top 의 값보다 크다면 값들을 push 해 주어진 정수가 스택에 저장되도록 한다. 그 뒤 스택의 top 에 있는 값을 꺼내면 된다. 만약 주어진 정수가 스택에 저장된 top 값과 같다면 그 값을 pop 연산을 통해 바로 꺼낼 수 있다. 이 때 문제는, 이미 스택에 push 된 값이 다시 push 될 수 있다는 점이다.

 

예를 들어, [1, 2, 3, 4] 의 순서로 push 가 이뤄지고 [4, 3] 순서로 pop 이 되었을 때 스택의 top 에 저장된 값은 '2' 이다. 이 때 '6' 이라는 정수가 주어지면 스택의 top 보다 큰 '3' 부터 '6' 까지 [3, 4, 5, 6] 순서로 push 를 하게 된다. 하지만 이 경우에 이미 [3, 4] 라는 값들은 push / pop 이 된 값이므로 이를 다시 수행하지 않도록 따로 기록을 해야 한다.

 

그래서 이 문제에서 추가적으로 활용하는 변수로 스택에 마지막으로 push 된 값을 기록한다. 만약 위의 경우에 마지막으로 push 한 값이 '4' 임을 안다면 '6' 이 주어졌을 때, [5, 6] 만 push 를 하면 됨을 알 수 있다. 따라서 중복되는 push / pop 연산이 이루어지지 않는다.

 

이러한 풀이로 작성한 소스 코드는 다음과 같다.

 

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

int N, num, cur;
stack<int> s;

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

    string ans = "";
    cin >> N;

    for (int i = 0; i < N; i++) {
        cin >> num;

        if (cur < num) {
            for (int k = cur + 1; k <= num; k++) {
                s.push(k);
                ans += "+\n";
            }
            cur = num;
        }
        else if (s.top() != num) {
            ans = "NO\n";
            break;
        }

        s.pop();
        ans += "-\n";
    }

    cout << ans;
}

문제 풀이 중간에 불가능한 값이 입력될 경우 ans 라는 문자열을 "NO" 로 바꾸고 반복을 종료한다. 그렇지 않을 경우 정상적으로 연산의 순서가 ans 에 저장될 것이다. 이를 콘솔에 출력함으로써 주어진 문제에 대한 답을 얻을 수 있다.

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

[BOJ # 11404] 플로이드  (0) 2021.09.23
[BOJ # 1600] 말이 되고픈 원숭이  (0) 2021.09.19
<BOJ 2508> 나무 자르기  (0) 2019.02.14
반응형

주어진 문제는 위와 같다. 두 개의 문자열 's1', 's2' 가 주어졌을 때, s1 을 scramble 해서 s2 으로 만들 수 있는 지를 확인하는 문제이다. 문자열을 scramble 한다는 것은 위에서 다음과 같이 표현한다.

  1. 문자열의 길이가 1이라면 멈춘다.
  2. 문자열의 길이가 1보다 큰다면, 다음을 따른다.
    • 문자열의 임의 인덱스를 정해서 해당 인덱스를 기준으로 두 개의 문자열로 나눈다. (두 개의 문자열은 빈 문자열이 될 수 없다.)
    • 나눠진 두 개의 문자열을 임의 순서로 배치한다.
    • 나눠진 문자열에 대해 1. 부터 다시 수행한다.

이 문제를 해결하기 위해, 재귀함수를 사용할 수 있었다. 두 문자열을 임의의 인덱스 (모든 인덱스에 대해) 기준으로 각각 두 개의 문자열로 쪼갠다. 그리고 쪼개진 문자열의 순서를 바꿔가며 비교한다.

 

예를 들어, s1 이 "ab", s2 가 "ba" 라고 주어졌다면 각각의 문자열을 두 개로 나눈다. (문자열의 순서를 바꾸며 비교하는 것을 표현하기 위해 극단적으로 길이가 2인 문자열로 예시를 만들었다.) s1 을 두 개의 문자열로 나누면 "a", "b" 가 되고, s2 를 두 개의 문자열로 나누면 "b", "a" 가 된다. 순서를 유지한 채로 비교를 하면 "a" == "b" / "b" == "a" 가 되고, 순서를 바꾸면 "b" == "b" / "a" == "a" 가 된다. 순서를 바꿔 비교했을 때 두 문자열이 같음을 확인할 수 있으므로 s1 과 s2 는 문제의 조건을 만족한다.

 

이와 같이, 각각의 문자열이 주어졌을 때 문자열을 나눠서 다시 그것을 판정하는 방식으로 재귀함수를 작성하면 문제를 해결할 수 있을 것으로 생각했다. 이를 적용한 소스 코드는 다음과 같다.

 

class Solution {
public:
    bool isScramble(string s1, string s2) {
        if(s1==s2)
            return true;
            
        int len = s1.length();
        int count[26] = {0};
        for(int i=0; i<len; i++)
        {
            count[s1[i]-'a']++;
            count[s2[i]-'a']--;
        }
        
        for(int i=0; i<26; i++)
        {
            if(count[i]!=0)
                return false;
        }
        
        for(int i=1; i<=len-1; i++)
        {
            if( isScramble(s1.substr(0,i), s2.substr(0,i)) && isScramble(s1.substr(i), s2.substr(i)))
                return true;
            if( isScramble(s1.substr(0,i), s2.substr(len-i)) && isScramble(s1.substr(i), s2.substr(0,len-i)))
                return true;
        }
        return false;
    }
};

이와 같이 작성한 소스코드는 문제를 정확히 풀이하는 데는 문제가 없었지만, 제출 시 TLE 가 발생했다. 재귀함수의 형태를 취했는데 알파벳의 개수를 비교한 것 외에 가지치기가 전혀 이루어지지 않은 점이 문제로 생각됐다. 추가적인 가지치기가 가능한 방법은, 이미 지나갔던 경로를 재탐색하지 않도록 하는 것이라 생각했다.

 

예를 들어, isScramble("abcde", "caebd") 은 false 를 반환한다. 만약 주어진 문제에서 부분문자열을 나누다가 이와 같은 형태를 마주쳤을 때 굳이 이 문자열을 나눠가며 판정하지 않고 바로 false 를 반환할 수 있다면 문제의 해결에 시간이 단축될 것이다. 하지만 이러한 값을 일일히 넣어줄 수 없으니 주어진 문자열을 가지고 로직을 수행하다 마주치는 경우에 대해 가지치기를 할 필요가 있어 보였다.

 

이를 위해 해시맵을 추가적으로 적용했으며, s1 의 문자열과 s2 의 문자열을 붙여 해시맵의 키로 활용했다. 이를 적용한 소스 코드는 다음과 같다.

 

class Solution {
public:
    map<string, bool> hash;

    bool isScramble(string s1, string s2) {
        if (s1 == s2) {
            return true;
        }
        if (hash.find(s1 + s2) != hash.end()) {
            return hash[s1 + s2];
        }

        int len = s1.length();
        int cnt[26] = {0,};
        for (int i = 0; i < len; i++) {
            cnt[s1[i] - 'a']++;
            cnt[s2[i] - 'a']--;
        }

        for (int i = 0; i < 26; i++) {
            if (cnt[i] != 0) {
                hash[s1 + s2] = false;
                return false;
            }
        }

        bool res = false;
        for (int i = 1; i < len; i++) {
            res = res || isScramble(s1.substr(0, i), s2.substr(0, i)) && isScramble(s1.substr(i), s2.substr(i));
            res = res || isScramble(s1.substr(0, i), s2.substr(len - i)) && isScramble(s1.substr(i), s2.substr(0, len - i));
        }
        hash[s1 + s2] = res;
        return res;
    }
};

위의 코드에서 볼 수 있듯이, 기존의 코드에서 변경된 부분은 해시맵이 적용된 부분이다. 가지치기를 위해 함수 호출 시 해시맵에서 이미 존재하는 경우인 지를 확인하고 이미 해시맵에 저장이 된 경우 저장된 값을 반환한다. 또한 현재 문자열 s1, s2 를 가지고 판정한 결과값을 해시맵에 저장함으로써 추후 가지치기에서 활용할 수 있도록 한다.

 

해시맵을 적용해 일종의 DP 방식을 활용한 결과 TLE 를 피하고 문제를 풀이할 수 있었다.

반응형

주어진 문제는 위와 같다. 2차원 행렬에서 (0, 0) -> (M - 1, N - 1) 까지의 최단 거리를 구하는 문제에 특별한 이동 방식이 추가된 형태이다. 특별한 이동 방식은 체스에서 나이트가 움직이는 방식과 같고 문제에서 정의한 K 만큼 이러한 방식으로 이동할 수 있다.

 

일반적인 BFS 문제는 큐를 사용해 각각의 이동으로 인한 현재 위치를 저장하고 그 위치가 목적지에 도달하면 종료하게 된다. 이 때, 동일한 위치에 대해서 재방문을 할 필요가 없기 때문에 각 위치 (x, y) 의 방문 여부를 저장한다. 이 문제는 이에 더해 그 위치까지 몇 번의 이동 능력을 사용했는 지가 중요하기 때문에 (x, y) 위치 + 이동 능력 사용 횟수 를 기준으로 방문 여부를 체크하면 해결할 수 있다.

 

이러한 풀이로 작성한 소스 코드는 다음과 같다.

 

#include <iostream>
#include <queue>

using namespace std;

const int MAX_SIZE = 200;

struct POINT {
    int r, c;
    int horse, cnt;

    POINT(int r, int c, int horse, int cnt) : r(r), c(c), horse(horse), cnt(cnt) {}
};

int dr[4] = {1, 0, -1, 0}, dc[4] = {0, 1, 0, -1};
int hr[8] = {-1, -2, -2, -1, 1, 2, 2, 1}, hc[8] = {-2, -1, 1, 2, 2, 1, -1, -2};

int N, M, K;
int map[MAX_SIZE][MAX_SIZE];
bool vis[MAX_SIZE][MAX_SIZE][31];

void input() {
    cin >> K;
    cin >> M >> N;

    for (int i = 0; i < N; i++) {
        for (int j = 0; j < M; j++) {
            cin >> map[i][j];
        }
    }
}

int solve() {
    queue<POINT> que;

    que.push(POINT(0, 0, 0, 0));
    while (que.size()) {
        POINT top = que.front();
        que.pop();

        if (top.r == N - 1 && top.c == M - 1) {
            return top.cnt;
        }

        if (vis[top.r][top.c][top.horse])
            continue;
        vis[top.r][top.c][top.horse] = true;

        if (top.horse < K) {
            for (int k = 0; k < 8; k++) {
                int nr = top.r + hr[k], nc = top.c + hc[k];
                if (nr < 0 || nr >= N || nc < 0 || nc >= M)
                    continue;
                if (map[nr][nc] || vis[nr][nc][top.horse + 1])
                    continue;
                que.push(POINT(nr, nc, top.horse + 1, top.cnt + 1));
            }
        }
        for (int k = 0; k < 4; k++) {
            int nr = top.r + dr[k], nc = top.c + dc[k];
            if (nr < 0 || nr >= N || nc < 0 || nc >= M)
                continue;
            if (map[nr][nc] || vis[nr][nc][top.horse])
                continue;
            que.push(POINT(nr, nc, top.horse, top.cnt + 1));
        }
    }

    return -1;
}

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

    input();
    cout << solve();

    return 0;
}

 

solve() 함수에서 기존 BFS 소스 코드와 차이점이 있다면, if (top.horse < K) 로 시작되는 분기문이다. POINT 구조체의 horse 는 현재까지 이동 능력의 사용 횟수이며 이 횟수가 K 보다 작다면 특별한 이동 방식으로 이동할 수 있도록 한다. 또한, bool 타입 3차원 행렬 vis 에서 마지막 인덱스로 horse 값을 사용해 해당 위치까지 가는 동안 사용한 이동 능력의 횟수를 확인해 재방문을 막는다.

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

[BOJ # 11404] 플로이드  (0) 2021.09.23
[BOJ # 1874] 스택 수열  (0) 2021.09.21
<BOJ 2508> 나무 자르기  (0) 2019.02.14
반응형

이 문제는 개인적으로 공부를 위해 풀이한 문제이다.

 

일반적으로 달팽이 수열은, 아래와 같이 숫자가 달팽이 모양으로 증가하는 형태의 수열을 의미한다.

// size = 3
1 2 3
8 9 4
7 6 5

중첩된 달팽이 수열은, 여러 단계(?) 로 중첩된 달팽이 수열을 표현하고자 지은 이름인데 의도를 표현하기 적합한 이름인 지는 모르겠다. 간단하게 설명을 하자면 각 단계 별로 수열의 Size 가 주어지고 수열 안에 중첩된 또다른 수열이 존재하는 방식이다. 이를 위처럼 표현하면 다음과 같을 것 같다.

// size = [3, 2]
 1  2  3 10 11 12 
 8  9  4 17 18 13 
 7  6  5 16 15 14 
28 29 30 19 20 21 
35 36 31 26 27 22 
34 33 32 25 24 23

제일 위에서 size 가 3일 때의 달팽이 수열을 표현했는데, 이러한 달팽이 수열을 내부에 갖는 size 가 2인 달팽이 수열을 표현한 것이다.

 

이를 그림으로 표현하면 다음과 같을 수 있겠다.

중첩 달팽이 수열의 size 는 배열로 주어지고, 그 배열이 [3, 2] 와 같을 때의 모습은 위와 같다. 우선, (2 x 2) 의 크기를 갖는 달팽이 수열이 존재하게 된다. 총 4개의 수열로 구성된 이 달팽이 수열은, 각각이 (3 x 3) 의 크기를 갖는 달팽이 수열이다. 만약 size 가 [3, 2, 2] 와 같이 주어진다면 이 수열이 의미하는 바는 위의 수열 4개로 구성된 또다른 달팽이 수열이 될 것이다.

 

우선 이 문제를 해결하기 위해 기본적으로, 달팽이 수열을 만들 수 있어야 하는데 이는 방향 정보를 갖는 변수 하나를 사용함으로써 수열을 표현할 수 있다. 방향의 순서는 오른쪽, 아래, 왼쪽, 위 방향으로 4 가지 방향이 존재하게 되고 이 순서대로 값을 채워나갈 수 있다면 달팽이 수열을 만들 수 있다.

 

그리고 위와 같이 중첩된 수열을 표현하기 위해 재귀함수를 사용할 수 있는데, 각 단계 별로 전체 수열의 크기가 어떻게 되는 지, 초기 수열에서 현재 표현해야 하는 수열의 위치가 어디인 지 정보를 줌으로써 각 함수 호출 단계에서 현재 표현해야 할 수열의 정보를 알 수 있다.

 

이와 같이 문제를 풀이했을 때 작성한 코드는 다음과 같다.

 

#define pow(X) ((X) * (X))

int dr[4] = {0, 1, 0, -1}, dc[4] = {1, 0, -1, 0};
int N;

void fill(vector<vector<int>>& matrix, int sy, int sx, int ey, int ex, int startNum) {
    int y = sy, x = sx, dir = 0, num = startNum;
    while (true) {
        if (y < sy || y > ey || x < sx || x > ex || matrix[y][x] != 0) {
            break;
        }
        matrix[y][x] = num++;
        int ny = y + dr[dir], nx = x + dc[dir];
        if (ny < sy || ny > ey || nx < sx || nx > ex || matrix[ny][nx] != 0) {
            dir = (dir + 1) % 4;
            ny = y + dr[dir], nx = x + dc[dir];
        }
        y = ny;
        x = nx;
    }
}

void solve(vector<vector<int>>& matrix, vector<int>& lengths, int idx, int sy, int sx, int n, int num) {
    if (idx == 0) {
        fill(matrix, sy, sx, sy + lengths[0] - 1, sx + lengths[0] - 1, num);
        return;
    }
    int length = lengths[idx];
    int offset = n / length;
    vector<vector<bool>> vis(length, vector<bool>(length, false));

    int i = 0, j = 0, dir = 0;
    int y = sy, x = sx;
    int nextNum = num;
    while (true) {
        if (vis[i][j])
            break;
        vis[i][j] = true;

        int ni, nj;
        ni = i + dr[dir];
        nj = j + dc[dir];
        if (ni < 0 || ni >= length || nj < 0 || nj >= length || vis[ni][nj]) {
            dir = (dir + 1) % 4;
            ni = i + dr[dir];
            nj = j + dc[dir];
        }

        solve(matrix, lengths, idx - 1, y, x, offset, nextNum);

        y += dr[dir] * offset;
        x += dc[dir] * offset;
        nextNum += pow(offset);

        i = ni;
        j = nj;
    }
}

vector<vector<int>> solution(vector<int> lengths) {
    N = 1;
    for (auto length : lengths) {
        N *= length;
    }

    vector<vector<int>> matrix(N, vector<int>(N, 0));
    solve(matrix, lengths, lengths.size() - 1, 0, 0, N, 1);
    return matrix;
}

 

반응형

주어진 문제는 위와 같다. 정수형 배열이 주어졌을 때, 배열의 부분 배열 중 Arithmetic Subsequence 를 만족하는 부분 배열의 개수를 찾는 것.

 

Arithmetic Subsequence 란, 배열 내 인접한 원소들이 모두 동일한 차이값을 갖는 것을 의미하는데 원소가 3개 이상일 때만 이 조건을 만족한다. 예를 들어 [1, 2, 3, 4, 5] 는 인접한 원소의 차가 1 로 모두 동일하므로 조건을 만족하지만 [1, 2, 2, 3, 4] 는 인접한 원소의 차가 동일하지 않아 조건을 만족하지 않는다.

 

문제의 해결을 위해 DP 알고리즘을 적용했는데 그 방법은 다음과 같다. 배열의 각 인덱스를 탐색하며 현 위치를 기준으로 이전 위치까지 일정한 차를 갖는 Arithmetic Subsequence 가 존재했는 지 확인한 뒤 존재한다면 그 값을 현재 위치에 저장한다. 그리고 만약 이전 위치에 저장된 값이 있는 경우 그 값을 저장한 뒤 추후 반환한다.

 

예를 들어 설명하자면, [1, 1, 2, 3, 4, 5] 라는 배열이 존재할 때 찾고자 하는 Arithmetic Subsequence 는 [1, 2, 3] (2개), [1, 2, 3, 4] (2개), [1, 2, 3, 4, 5] (2개), [2, 3, 4, 5], [3, 4, 5], [1, 3, 5] (2개) 로 모두 11 개가 존재한다.

 

index 0 1 2 3 4 5
value 1 1 2 3 4 5

위와 같이 각 인덱스의 값이 존재하게 되는데, 현재 인덱스를 i, 이전 인덱스를 j 라고 표현한다.

 

1) i = 1, j = 0
현재 인덱스보다 이전 인덱스가 항상 존재해야 로직이 동작하므로 i = 0 일 경우는 스킵한다. value[i] 와 value[j] 는 모두 1로 동일하므로 값의 차이가 0이다. 따라서 i 위치에서 0의 차이를 갖는 Subsequence 가 한 개 존재함을 저장한다. 두 개의 원소만 가지고 판단한 것이므로 i 위치에 저장된 값은 정답에 반영하지 않는다.

 

2) i = 2, j = 0

value[i] 는 2, value[j] 는 1이므로 원소의 차가 1이다. 따라서 i 위치에서 1의 차를 갖는 Subsequence 가 한 개 존재함을 저장한다.

 

3) i = 2, j = 1

2) 와 마찬가지로 i 위치에 Subsequence 가 한 개 존재함을 저장한다. 2), 3) 의 정보는 각각 개별 정보이므로 i 위치에 1의 차를 갖는 Subsequence 는 두 개 존재하는 것으로 저장된다.

 

4) i = 3, j = 2

j 가 0, 1 일 때는 생략을 했는데 i 가 3일 때 해당 정보는 정답에 반영되지 않기 때문이다. value[i] 와 value[j] 의 차는 1이고, j 위치에 저장된 subsequence 의 개수는 2개이다. 즉, i 에서 차가 1인 subsequence 의 개수는 2개가 발생한다. i 가 3이 되었을 때, subsequence 의 길이가 비로소 3 이상이 되므로 이를 정답에 반영한다.


이와 같이 현재 위치를 탐색하며 이전 위치들에서 발생한 subsequence 의 개수를 더해 나간다. 현재 위치에서 발생한 개수는 정답에 반영하지 않고 이전 위치에 값이 존재할 경우에만 정답에 반영하는데, 위에 간략히 언급한 것처럼 원소 두 개를 기준으로 탐색하고 있고, 현재 위치에서 발생한 subsequence 는 원소 두 개만을 기준으로 작성한 것이기 때문이다.

 

현재 위치에서 저장하고 다음 위치에서 현재 위치를 확인했을 때 subsequence 가 존재한다면 원소를 3개 이상 조회한 결과이므로 비로소 정답에 반영할 수 있게 된다.

 

위와 같은 풀이로 작성한 소스 코드는 다음과 같다. 

 

class Solution {
public:
    int numberOfArithmeticSlices(vector<int> &nums) {
        if (nums.size() < 3) {
            return 0;
        }
        int ans = 0;
        vector<map<int, int>> dp(nums.size()); // [index, [diff, count]]

        for (int i = 1; i < nums.size(); i++) {
            for (int j = 0; j < i; j++) {
                long delta = (long) nums[i] - nums[j];
                if (delta >= INT_MAX || delta <= INT_MIN) {
                    continue;
                }
                int diff = nums[i] - nums[j];
                dp[i][diff] = dp[i][diff] + 1;
                if (dp[j].find(diff) != dp[j].end()) {
                    dp[i][diff] += dp[j][diff];
                    ans += dp[j][diff];
                }
            }
        }
        return ans;
    }
};

위의 설명과 코드를 연결해 설명하자면, dp 라고 선언한 map 의 배열이 위에서 subsequence 의 개수를 저장하기 위한 수단이다. 배열을 인덱스 개수만큼 생성을 해서 각 인덱스마다 따로 subsequence 를 저장하도록 했고, subsequence 는 단순하게 [공통의 차가 얼마인지, 해당 위치에 저장된 subsequence 의 개수가 몇 개인지] 를 표현하도록 했다.

반응형

주어진 문제는 n 의 길이를 갖는 문자열과 정수 배열이 주어졌을 때, 특정 조건에 맞게 문자열을 shift 한 결과를 반환하는 것이다.

 

위의 <예제 1> 을 가지고 설명을 하자면 문자열 "abc" 와 정수 배열 {3, 5, 9} 가 주어졌을 때, 앞에서부터 부분 문자열 "a" 에 shift() 연산을 세 번 수행해 "dbc" 라는 문자열을 만들고, 그 다음은 "db" 에 대해 shift() 를 수행해 "igc" 를 만든 뒤 마지막으로 "igc" 에 대해 shift() 를 수행해 "rpl" 이라는 문자열을 만든다.

 

즉, 각 자리 별로 i 번째 자리의 문자는 총 shifts[i] + shifts[i + 1] + ... + shifts[n - 1] 만큼 shift() 연산을 수행하게 된다. 이 문제를 해결하기 위해 적용한 해결 방법은 맨 마지막 자리부터 shift() 연산을 수행하는 것이다.

 

'n - 1' 번째 문자는 shifts[n - 1] 만큼만 shift() 연산을 수행하므로 이를 수행하고 역순으로 shifts 값을 누적시키며 shift() 연산을 수행하도록 하면 문제를 해결할 수 있을 거라 생각했다. 또한, 주어진 조건에 shifts[i] 는 최대 10억의 값을 가질 수 있는데 shift() 연산의 결과는 항상 영어 소문자이므로 'a' ~ 'z' 까지만의 shift 만 가능하고 shift() 연산의 결과는 26번을 기준으로 동일하기 때문에 shifts[i] 의 값을 26 으로 나눈 나머지값을 가지고 shift() 를 수행하도록 했다.

 

추가적으로 25번의 shift 를 각 자리별로 일일히 수행하게 될 경우, 이 또한 불필요한 연산을 반복한다 생각했다. 현재 문자가 몇 번째 소문자인지만 확인한 뒤 몇 번의 shift 를 수행하는 지만 확인하면 되기 때문이다. 예를 들어, 'a' 라는 문자는 0 번째 소문자이고 5번의 shift() 연산을 수행한다면 'f' 라는 문자로 치환할 수 있다. 이러한 방식으로 문제를 해결할 수 있었고, 문제의 해결에 사용한 코드는 다음과 같다.

 

class Solution {
public:
    string shiftingLetters(string s, vector<int>& shifts) {
        int sum = 0;
        for (int i = s.size() - 1; i >= 0; i--) {
            sum = (sum + shifts[i]) % 26;
            s[i] = (s[i] - 'a' + sum) % 26 + 'a';
        }
        return s;
    }
};
반응형

주어진 문제는 위와 같이 히스토그램에서 가장 큰 넓이를 갖는 직사각형을 찾는 것이다.

 

가장 간단한 해결 방법은, 각 위치 별로 좌우로 확장시키며 가질 수 있는 최대 넓이를 확인하는 것인데 이러한 방법은 O(n^2) 의 시간복잡도를 갖는 해결 방법이기 때문에 사용할 수 없었다.

 

대신 문제의 해결을 위해 스택을 사용하는 방식을 적용할 수 있었다. 왼쪽부터 오른쪽으로 탐색하며 히스토그램 블록의 높이가 점차 증가하도록 스택을 관리한다. 히스토그램 블록의 높이가 지속적으로 증가한다면 계속해서 스택에 해당 위치를 저장하고, 높이가 감소할 경우 현재 스택에 저장된 최대 높이를 갖는 블록에 대해 넓이를 계산한다.

 

위의 <예제 1> 을 통해 설명을 보강하자면, 처음 스택은 빈 상태로 시작을 하고 첫 번째 인덱스를 저장하게 된다.

스택 : {0}, 최대 넓이 : 0

인덱스가 1일 때, 히스토그램의 높이는 스택에 저장된 인덱스 블록보다 낮으므로 현재까지의 넓이를 계산해 저장한다. 그 뒤 스택이 비어있으므로 인덱스를 다시 스택에 저장한다.

스택 : {}, 최대 넓이 : 2

그 뒤로는, 블록의 높이가 점차 높아지므로 계속해 스택에 인덱스를 저장한다.

스택 : {1, 2, 3}, 최대 넓이 : 2

인덱스가 4가 됐을 때, 다시 블록의 높이가 낮아지므로 높이를 계산한다. 이 때, 현재 탐색 중인 인덱스는 4이고 스택의 탑에 저장된 인덱스는 3, 그 다음 탑의 인덱스는 2이므로 인덱스 3의 높이를 가지고 계산할 수 있는 넓이는 하나의 블록 넓이 뿐이다.

스택 : {1, 2}, 최대 넓이 : 6

다시 인덱스 4에서, 블록의 높이가 아직 낮으므로 높이를 다시 계산한다. 현재 탐색중인 인덱스는 4, 스택의 탑에 저장된 인덱스는 2, 그 다음 인덱스는 1이므로 인덱스 2의 높이를 가지고 계산할 수 있는 넓이는 인덱스 2와 3의 블록 넓이이다. 따라서 넓이는 5 * 2 = 10 이 된다.

스택 : {1}, 최대 넓이 : 10

이러한 방식으로, 스택에 계속해 인덱스를 저장하고 높이를 계산하는 것을 반복하며 최대 넓이를 계산할 수 있다. 이러한 풀이 방식은, 인덱스를 0 에서 n - 1 까지 순차 탐색하며 저장 / 계산 최대 2번 연산을 수행하므로 O(2n) 의 시간 복잡도 (O(2n) 은 O(n) 으로 표현 가능하다) 를 갖는다고 볼 수 있다.

 

이러한 풀이 방식으로 작성한 소스 코드는 다음과 같다.

 

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        stack<int> s;
        int maxArea = 0;
        for (int i = 0; i <= heights.size(); i++) {
            int h = i == heights.size() ? 0 : heights[i];
            if (s.empty() || h >= heights[s.top()]) {
                s.push(i);
            }
            else {
                int top = s.top();
                s.pop();
                int area = heights[top] * (s.empty() ? i : i - 1 - s.top());
                maxArea = maxArea > area ? maxArea : area;
                i--;
            }
        }
        
        return maxArea;
    }
};
반응형

주어진 문제는 위와 같다. 단방향 연결 리스트가 주어졌을 때, 이를 반전시키는 것.

 

문제의 해결 방법은 간단하지만 두 가지 방법으로 풀이가 가능하다. 반복문을 사용하는 방법과 재귀함수를 사용하는 방법.

 

먼저, 반복문을 사용하는 방법은 노드를 앞에서부터 탐색하면서 해당 노드를 새로운 리스트의 헤더 노드로 만드는 방법이다. 새로운 리스트의 헤더 노드를 현재 탐색중인 노드와 연결한 뒤 헤더 노드를 현재 탐색중인 노드로 변경한다. 이러한 방법으로 리스트의 순서를 뒤집을 수 있다.

 

아래는 반복문을 사용한 소스 코드이다.

 

class Solution {
public:
    ListNode *reverseList(ListNode *head) {
        ListNode* reversedHead = nullptr;
        ListNode* tmp = head;
        while (tmp != nullptr) {
            ListNode* next = tmp->next;
            tmp->next = reversedHead;
            reversedHead = tmp;
            tmp = next;
        }
        return reversedHead;
    }
};

다음은 재귀함수를 사용한 방법인데, 노드를 끝까지 먼저 탐색한 뒤 마지막 노드를 반전된 리스트의 헤더 노드로 만든다. 그 뒤 기존의 연결 방향을 반대로 바꾸는 방법으로 문제를 해결한다.

 

아래는 재귀함수를 사용한 소스 코드이다.

 

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (!head || !(head->next)) {
            return head;
        }
        ListNode* res = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return res;
    }
};
반응형

주어진 문제는 위 그림과 같다. 문자열을 원소로 갖는 배열 words 와 문장의 최대 폭을 의미하는 maxWidth 값이 주어졌을 때, 주어진 words 를 문장의 형태로 포맷팅하는 것. 

 

우선, 각 문장에 words 의 각 단어들을 그리디하게 패킹한 뒤, 남는 빈 공간들은 공백 문자로 채우는 것이 목표이다. 문제의 해결을 위해 각 단어들마다 한 문장에 포함될 수 있는 지 여부를 확인했다. 단계 별로 문장에 단어가 포함될 수 있는 경우 문장에 단어를 포함시킨 뒤 다음 단어를 탐색하고, 포함될 수 없는 경우 (현재 단어를 문장에 추가할 경우, 단어 사이의 공백이 한 문자 이상 들어갈 수 없을 때 포함될 수 없다고 판단한다.) 현재 문장을 주어진 포맷에 맞게 수정한 뒤 저장하고 새로운 문장에 단어를 추가하는 방식으로 문제를 해결한다.

 

문제의 조건으로, 마지막 문장의 경우는 단어의 개수에 상관 없이 좌측 정렬 (단어 사이의 공백은 한 개로 지정한 뒤, 문장의 뒤에 남는 공간을 모두 공백으로 채움) 하고, 문장에 하나의 단어만 존재하는 경우에도 좌측 정렬을 하도록 되어 있다.

 

위의 문제 해결 방법을 적용한 소스 코드는 다음과 같다.

 

class Solution {
public:
    vector<string> ans;

    string makeSentence(vector<string>& words, int maxWidth, bool option) {
        if (words.size() == 1 || option) {
            string sentence = "";
            for (int i = 0; i < words.size(); i++) {
                sentence += words[i];
                if (i != words.size() - 1) {
                    sentence += " ";
                }
            }
            while (sentence.length() < maxWidth) {
                sentence += " ";
            }
            return sentence;
        }
        
        int len = 0;
        for (auto word : words) {
            len += word.length();
        }

        int blankSpace = maxWidth - len;
        int eachBlank = blankSpace / (words.size() - 1);
        int leftBlank = blankSpace % (words.size() - 1);

        string sentence = "";
        for (int i = 0; i < words.size(); i++) {
            sentence += words[i];
            if (i == words.size() - 1)
                break;
            for (int k = 0; k < eachBlank; k++) {
                sentence += " ";
            }
            if (leftBlank) {
                sentence += " ";
                leftBlank--;
            }
        }

        return sentence;
    }

    void justify(vector<string>& curWords, int totalLength, vector<string>& words, int idx, int maxWidth) {
        if (idx == words.size()) {
            if (!curWords.empty()) {
                ans.push_back(makeSentence(curWords, maxWidth, true));
            }
            return;
        }

        if (totalLength + curWords.size() + words[idx].size() > maxWidth) {
            ans.push_back(makeSentence(curWords, maxWidth, false));
            curWords.clear();
            totalLength = 0;
        }

        curWords.push_back(words[idx]);

        justify(curWords, totalLength + words[idx].length(), words, idx + 1, maxWidth);
    }

    vector<string> fullJustify(vector<string>& words, int maxWidth) {
        vector<string> curWords;
        justify(curWords, 0, words, 0, maxWidth);

        return ans;
    }
};

소스 코드는 다른 문제들에 비해 길게 작성되었지만, 전체적인 구조는 위에서 설명한 바와 같다.

 

* makeSentence 로직 작성 시, Option 을 추가적으로 받도록 해 마지막 문장인 경우 문제의 추가 조건을 만족시킬 수 있도록 구현했다.

+ Recent posts