반응형

n 명의 아이들을 의미하는 정수형 배열이 주어졌을 때, 배열에 저장된 값은 해당 위치에 서 있는 아이의 우선순위를 의미한다. 아이들에게 Candy 를 나눠주고자 하는데 모든 아이들은 최소 1개 이상의 Candy 를 받아야 하고 이웃한 아이보다 우선순위가 높은 아이는 이웃 아이보다 Candy 를 더 받아야 한다. 이 때, 아이들에게 최소한의 갯수로 위 규칙을 만족시키며 Candy 를 분배하는 Candy 의 수를 구하는 문제

 

이 문제는, DP 방식으로 문제를 해결할 수 있었는데 우선 각 아이 별로 왼쪽에 있는 아이보다 많이 받는 경우와 오른쪽에 있는 아이보다 많이 받는 경우를 각각 계산했다. 그 후, 둘 중 더 많은 Candy 를 받는 경우를 계산하면 최소한의 Candy 로 규칙을 만족시키며 분배할 수 있다.

 

예를 들어, ratings 배열이 [1, 0, 2] 로 주어졌을 때 Candy 를 가져가는 개수는 [2, 1, 2] 로 총 5개가 필요하게 된다. 왼쪽에 있는 아이보다 많이 받는 경우를 계산하면 [1, 1, 2] 가 되는데 ratings[1] 은 0 이므로 ratings[0] 보다 작아 더 많이 받을 필요가 없고, ratings[2] 는 2 이고 ratings[1] 은 1 이므로 3번째 아이는 2번째 아이보다 Candy 를 많이 받아야 한다. 반대로 오른쪽에 있는 아이보다 많이 받는 경우는 [2, 1, 1] 이 되는데 ratings[0] 의 오른쪽에 ratings[1] 이 더 작으므로 첫 번째 아이가 Candy 를 많이 받게 된다. 두 경우에서 각 아이가 받는 Candy 의 개수는 최대가 되어야 왼쪽과 오른쪽의 경우를 모두 만족할 수 있으므로 결과적으로 [2, 1, 2] 로 Candy 를 분배하게 된다.

 

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

 

class Solution {
public:
    int candy(vector<int>& ratings) {
        int sum = 0, n = ratings.size();
        vector<int> left(n, 1);
        vector<int> right(n, 1);
        for (int i = 1; i < n; i++) {
            if (ratings[i] > ratings[i - 1]) {
                left[i] = left[i - 1] + 1;
            }
        }
        for (int i = n - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) {
                right[i] = right[i + 1] + 1;
            }
        }
        for (int i = 0; i < n; i++) {
            sum += max(left[i], right[i]);
        }
        return sum;
    }
};
반응형

정수형 배열 nums 가 주어졌을 때, 각 인덱스 별로 자기보다 오른쪽에 있는 수 중 자기보다 작은 수들의 개수를 구하는 문제

 

예를 들어, 배열 nums 가 [5, 2, 6, 1] 과 같이 주어졌을 때 정답은 [2, 1, 1, 0] 이다. 가장 오른쪽에 있는 '1' 은 자기보다 오른쪽에 있는 값이 없기 때문에 무조건 0이 되고, '6' 과 '2' 는 오른쪽에 '1' 이, '5'는 오른쪽에 '2', '1' 이 있어 각각 1 과 2 를 정답으로 갖는다.

 

이 문제를 풀기 위한 방법으로 두 가지 방법을 사용했다. 첫 번째 방법은 Merge Sort 를 활용한 방법이었고 두 번째는 Binary Index Tree 의 일종인 Fenwick Tree 를 활용한 방법이다.

 

우선, Merge Sort 를 활용한 방법은 아이디어가 전혀 떠오르지 않아 아래 Reference 를 참고했다. Merge Sort 는 기본적으로 Divide & Conquer 방식으로 정렬을 수행하는데, 이 때 매 단계 별로 시작 위치 ~ 끝 위치 중 가운데 위치를 선정하고, 시작 위치 ~ 가운데 위치 / 가운데 위치 + 1 ~ 끝 위치까지 정렬을 먼저 한 후 시작 위치 ~ 끝 위치의 정렬을 수행한다. 정렬을 할 때, 시작 위치 ~ 가운데 위치 / 가운데 위치 + 1 ~ 끝 위치는 각각 정렬이 된 상태이므로 (아래 단계에서 이미 정렬을 하고 난 상태) 두 배열을 (위치 별로 나눈 것을 두 배열이라 표현) 합치는 과정에서 두 배열 중 작은 값을 먼저 배치하는 방식으로 정렬을 한다. 이를 간단히 코드로 표현하면 다음과 같다.

 

void merge (int[] arr, int[] tmp, int st, int ed) {
	for (int i = st; i <= ed; i++)
    	tmp[i] = arr[i];
	int mid = (st + ed) / 2;
    int i = st, j = mid + 1, k = st;
    while (i <= mid && j <= ed) {
    	if (tmp[i] <= tmp[j])
        	arr[k++] = tmp[i++];
        else
        	arr[k++] = tmp[j++];
    }
    while (i <= mid)
    	arr[k++] = tmp[i++];
    while (j <= ed)
    	arr[k++] = tmp[j++];
}

 

이 문제를 해결하기 위해, Merge Sort 에 추가적으로 도입한 아이디어는 두 배열을 하나로 합치는 과정에서 뒤 배열의 값들이 앞 배열보다 먼저 배치될 경우 앞 배열의 값들은 원래 위치에서 자기보다 오른쪽에 먼저 배치된 값들이 존재한다는 것이다. 즉, 위의 merge 소스 코드에서 앞 배열과 뒤 배열의 값들을 서로 비교해 합치는 과정에서 뒤 배열의 값이 더 크다면 그러한 개수만큼 저장을 하고 앞 배열의 값을 배치할 때 해당 값이 원래 위치했던 인덱스의 결과를 증가시키면 되는 것이다.

 

이러한 해결 방법으로 작성한 코드는 아래와 같다.

 

struct node {
    int idx, val;
};

class Solution {
public:
    void merge(vector<int>& ans, vector<node>& numVector, vector<node>& copyVector, int st, int mid, int ed) {
        for (int i = st; i <= ed; i++) {
            copyVector[i] = numVector[i];
        }
        int i = st, j = mid + 1, k = st, cnt = 0;

        while (i <= mid && j <= ed) {
            if (copyVector[i].val <= copyVector[j].val) {
                ans[copyVector[i].idx] += cnt;
                numVector[k++] = copyVector[i++];
            }
            else {
                cnt++;
                numVector[k++] = copyVector[j++];
            }
        }

        while (i <= mid) {
            ans[copyVector[i].idx] += cnt;
            numVector[k++] = copyVector[i++];
        }
        while (j <= ed) {
            numVector[k++] = copyVector[j++];
        }
    }

    void mergeSort(vector<int>& ans, vector<node>& numVector, vector<node>& copyVector, int st, int ed) {
        if (st >= ed)
            return;
        int mid = (st + ed) / 2;

        mergeSort(ans, numVector, copyVector, st, mid);
        mergeSort(ans, numVector, copyVector,mid + 1, ed);

        merge(ans, numVector, copyVector, st, mid, ed);
    }

    vector<int> countSmaller(vector<int>& nums) {
        int n = nums.size();
        vector<int> ans(n, 0);
        vector<node> numVector(n);
        vector<node> copyVector(n);
        for (int i = 0; i < n; i++) {
            numVector[i].idx = i;
            numVector[i].val = nums[i];
        }
        mergeSort(ans, numVector, copyVector, 0, n - 1);
        return ans;
    }
};

 

위 방식으로 문제를 해결할 수는 있었지만, 시간복잡도는 O(NlogN) 으로 (N 은 주어진 nums 배열의 크기, 최대 10만) 수행시간이 생각보다 길어 다른 방식을 고민하게 되었다. 이 문제에서 nums 배열의 크기는 최대 10만이고, 배열에 저장되는 값들은 -1만 ~ 1만 의 값을 가진다. 즉 O(NlogN) 보다는, O(NlogM) 이 된다면 (이 때 M 은 배열에 저장되는 값의 범위, 이 문제에서는 최대 2만) 수행시간을 조금 더 단축할 수 있을 것으로 생각했다.

 

이 때, logN 을 logM 으로 바꿀 수 있는 방법으로 부분합을 생각했고, 부분합을 업데이트하는 횟수가 많아 세그먼트 트리보다는 펜윅 트리를 생각했다. 펜윅 트리에 대한 자세한 설명은 아래 Reference 를 참고하면 되고, 간단하게 얘기하면 값들의 업데이트가 많을 때 부분합을 구하고 업데이트하는 것의 시간복잡도가 모두 O(logN) 인 자료구조이다.

 

펜윅 트리는 기본적으로 인덱스를 사용해 인덱스 ? ~ ? 까지의 합을 저장하는 방식으로 사용하는데, 이 문제에서는 인덱스 대신 nums 배열의 값들을 사용했고 값이 -1만 ~ 1만의 범위를 가지기 때문에, Offset 으로 10001 을 더해서 사용했다. 트리에 저장되는 값들은 해당 값이 등장한 횟수이다.

 

펜윅 트리를 사용해 작성한 코드는 아래와 같다. 수행시간도 생각한 대로 Merge Sort 를 사용한 방식보다 빨랐고, 펜윅 트리의 Update 나 Sum 을 구하는 코드가 모두 간단하기 때문에 코드도 간단하게 작성할 수 있었다.

 

class Solution {
public:
    const int OFFSET = 10001;
    const int SIZE = 20010;
    vector<int> countSmaller(vector<int>& nums) {
        int n = nums.size();
        vector<int> tree(SIZE, 0);
        vector<int> ans(n, 0);

        for (int i = n - 1; i >= 0; i--) {
            int cnt = 0;
            for (int j = nums[i] + OFFSET - 1; j > 0; j -= (j & -j))
                ans[i] += tree[j];
            for (int j = nums[i] + OFFSET; j < SIZE; j += (j & -j))
                tree[j]++;
        }

        return ans;
    }
};

 

위 코드를 로직에만 집중하도록 간단하게 모듈화를 조금 더 하자면 다음과 같이 작성할 수 있을 것 같다.

 

class Solution {
public:
    const int OFFSET = 10001;
    const int SIZE = 20010;
    void update(vector<int>& tree, int idx) {
        while (idx < SIZE) {
            tree[idx]++;
            idx += (idx & -idx);
        }
    }
    int sum(vector<int>& tree, int idx) {
        int sum = 0;
        while (idx > 0) {
            sum += tree[idx];
            idx -= (idx & -idx);
        }
        return sum;
    }
    vector<int> countSmaller(vector<int>& nums) {
        int n = nums.size();
        vector<int> tree(SIZE, 0);
        vector<int> ans(n, 0);

        for (int i = n - 1; i >= 0; i--) {
            int cnt = 0;
            ans[i] = sum(tree, nums[i] + OFFSET - 1);
            update(tree, nums[i] + OFFSET);
        }

        return ans;
    }
};

 

- Reference

[GeeksforGeeks - using MergeSort] https://www.geeksforgeeks.org/count-of-smaller-elements-on-right-side-of-each-element-in-an-array-using-merge-sort/

 

Count of smaller elements on right side of each element in an Array using Merge sort - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

[Baekjoon - Fenwick Tree] https://www.acmicpc.net/blog/view/21

 

펜윅 트리 (바이너리 인덱스 트리)

블로그: 세그먼트 트리 (Segment Tree) 에서 풀어본 문제를 Fenwick Tree를 이용해서 풀어보겠습니다. Fenwick Tree는 Binary Indexed Tree라고도 하며, 줄여서 BIT라고 합니다. Fenwick Tree를 구현하려면, 어떤 수 X

www.acmicpc.net

 

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

[LeetCode # 60] Permutation Sequence  (0) 2021.09.01
[LeetCode # 135] Candy  (0) 2021.06.27
[LeetCode # 576] Out of Boundary Paths  (0) 2021.06.25
[LeetCode # 52] N-Queens II  (0) 2021.06.18
[LeetCode # 51] N-Queens  (0) 2021.06.18
반응형

m x n 크기의 Grid 와 하나의 Ball 이 Grid 의 [startRow, startCol] 위치에 주어졌을 때, 이 Ball 이 Grid 밖으로 나가게 되는 경우의 수를 구하는 문제. 문제에서 Ball 은 한 번에 가로 / 세로 방향으로만 움직일 수 있고, 최대 움직임 횟수는 제한된다.

 

처음 문제를 확인하고 Brute Force 방식으로 해결하면 될 것 같다는 생각을 하고 풀이를 시작했는데, 공이 한 번 위치한 장소에 다시 돌아올 수 있기 때문에 완전 탐색 방식은 시간이 너무 오래 걸렸다. 그래서 변경한 풀이 방식은, m x n 행렬에 움직임의 횟수 별로 공이 각 칸에 위치할 수 있는 경우의 수를 저장하고 이를 통해 Grid 밖으로 나갈 수 있는 경우의 수를 구하는 방식이다.

 

위의 그림을 통해 설명하면, 공의 초기 위치가 (0, 0) 이므로 초기 행렬은 아래와 같이 표현이 가능하다. 이 때, 공이 Grid 밖으로 나갈 수 있는 경우의 수는 (0, 0) 에서 위 방향 / 왼쪽 방향 총 두 경우이다.

1 0
0 0

공이 한 번 움직이고 난 다음의 행렬은 아래와 같다. 이 때, 공이 Grid 밖으로 나가는 경우는 (0, 1) 위치에서 위 / 오른쪽 방향, (1, 0) 위치에서 왼쪽 / 아래 방향, 총 네 가지이다.

0 1
1 0

위 그림에서, 공이 움직일 수 있는 최대 횟수는 2번으로 제한되어 있으므로 두 번 움직이고 난 후의 경우는 구할 필요가 없다. 한 번 움직여서 Grid 를 벗어나는 경우가 2가지, 두 번 움직여서 Grid 를 벗어나는 경우가 4가지 이므로 총 6개가 답이 된다.

 

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

 

const int MOD = (int)1e9 + 7;
int dr[4] = {-1, 0, 1, 0}, dc[4] = {0, -1, 0, 1};
int dp[51][51], tmp[51][51];

class Solution {
public:
    int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
        for (int i = 0; i < m; i++)
            for (int j = 0; j < n; j++)
                dp[i][j] = tmp[i][j] = 0;
        dp[startRow][startColumn] = 1;

        int ans = 0;
        for (int move = 1; move <= maxMove; move++) {
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    if (i == 0) ans = (ans + dp[i][j]) % MOD;
                    if (j == 0) ans = (ans + dp[i][j]) % MOD;
                    if (i == m - 1) ans = (ans + dp[i][j]) % MOD;
                    if (j == n - 1) ans = (ans + dp[i][j]) % MOD;
                    
                    tmp[i][j] = 0;
                    if (i > 0)
                        tmp[i][j] = (tmp[i][j] + dp[i - 1][j]) % MOD;
                    if (j > 0)
                        tmp[i][j] = (tmp[i][j] + dp[i][j - 1]) % MOD;
                    if (i < m - 1)
                        tmp[i][j] = (tmp[i][j] + dp[i + 1][j]) % MOD;
                    if (j < n - 1)
                        tmp[i][j] = (tmp[i][j] + dp[i][j + 1]) % MOD;
                }
            }
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    dp[i][j] = tmp[i][j];
                }
            }
        }
        return ans;
    }
};

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

[LeetCode # 135] Candy  (0) 2021.06.27
[LeetCode # 315] Count of Smaller Numbers After Self  (0) 2021.06.26
[LeetCode # 52] N-Queens II  (0) 2021.06.18
[LeetCode # 51] N-Queens  (0) 2021.06.18
[LeetCode # 44] Wildcard Matching  (0) 2021.06.17
반응형

N-Queens (https://ggop-n.tistory.com/34) 문제와 유사하지만, 앞선 문제에서 N 개의 Queen 을 배치한 체스판을 결과로 return 하는 게 아닌, N 개의 Queen 을 배치한 체스판이 몇 개의 모양으로 존재할 수 있는 지를 구하는 문제

 

N-Queens 문제 풀이와 풀이 방식은 동일하게 진행했고, 차이가 있었던 점은 결과값으로 몇 개의 경우가 존재하는 지만 저장하도록 했다.

 

작성한 코드는 다음과 같다.

 

class Solution {
public:
    int ans;
    bool check(vector<vector<bool>>& board, int r, int c, int n) {
        for (int k = 1; r - k >= 0; k++) {
            if (board[r - k][c])
                return false;
            if (c - k >= 0 && board[r - k][c - k])
                return false;
            if (c + k < n && board[r - k][c + k])
                return false;
        }
        return true;
    }
    void solveNQueen(vector<vector<bool>>& board, int row, int n) {
        if (row == n) {
            ans++;
            return;
        }
        for (int j = 0; j < n; j++) {
            if (check(board, row, j, n)) {
                board[row][j] = true;
                solveNQueen(board, row + 1, n);
                board[row][j] = false;
            }
        }
    }
    int totalNQueens(int n) {
        vector<vector<bool>> board(n, vector<bool>(n, false));
        solveNQueen(board, 0, n);
        return ans;
    }
};
반응형

체스판 위에 N 개의 Queen 을 서로 공격할 수 없도록 놓는 문제

 

유명한 BackTracking 문제라 DFS 를 활용해 문제를 풀이했다. 문제의 답으로 체스판을 문자열로 표현해야 해서 빈 체스판을 먼저 저장해 놓고, Queen 을 한 개씩 배치하며 해당 위치에 배치 가능한 지 확인했다. 총 N 개 Queen 이 배치된 후 체스판을 결과물 저장하는 Vector 에 추가했다.

 

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

 

class Solution {
public:
    vector<vector<string>> ans;
    int dr[4] = {-1, -1, 1, 1}, dc[4] = {-1, 1, -1, 1};
    
    bool check(vector<string>& curVec, int i, int j, int n) {
        // 가로 세로
        for (int k = 0; k<n; k++) {
            if (curVec[i][k] == 'Q')
                return false;
            if (curVec[k][j] == 'Q')
                return false;
        }
        // 대각선
        for (int k = 1; ; k++) {
            bool flag = false;
            for (int l = 0; l < 4; l++) {
                int ni = i + dr[l] * k, nj = j + dc[l] * k;
                if (ni >= 0 && ni < n && nj >= 0 && nj < n) {
                    flag = true;
                    if (curVec[ni][nj] == 'Q')
                        return false;
                }
            }
            if (!flag)
                break;
        }
        return true;
    }
    void solveRecursive(vector<string>& curVec, int r, int cnt, int n) {
        if (cnt == n) {
            ans.push_back(curVec);
            return;
        }

        for (int i = r; i<n; i++) {
            for (int j = 0; j<n; j++) {
                if (curVec[i][j] == '.' && check(curVec, i, j, n)) {
                    curVec[i][j] = 'Q';
                    solveRecursive(curVec, i + 1, cnt + 1, n);
                    curVec[i][j] = '.';
                }
            }
        }
    }
    vector<vector<string>> solveNQueens(int n) {
        string s;
        for (int i = 0; i<n; i++) {
            s += ".";
        }
        vector<string> vec;
        for (int i = 0; i<n; i++) {
            vec.push_back(s);
        }
        solveRecursive(vec, 0, 0, n);
        return ans;
    }
};

 

위와 같이 코드를 작성했더니 시간이 너무 오래 걸렸다. 불필요한 조건 검사가 많고 생각을 너무 단순하게 해서 그런 것으로 생각이 되었다. 우선, 위에서 가로 검사의 경우는 한 줄에 하나의 Queen 만 배치가 가능하므로 굳이 검사를 할 필요가 없고, 세로 검사의 경우도 현재 위치보다 아래 줄은 검사를 할 필요가 없다. 또한 대각선 검사의 경우도 마찬가지로 현재 위치보다 윗줄만 검사를 하면 되어 이렇게 수정해 코드를 다시 제출했다.

 

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> ans;
        vector<string> nQueen(n, string(n, '.'));
        solveNQueens(ans, nQueen, 0, n);
        return ans;
    }
    void solveNQueens(vector<vector<string>>& ans, vector<string>& nQueen, int r, int n) {
        if (r == n) {
            ans.push_back(nQueen);
            return;
        }

        for (int j = 0; j<n; j++) {
            if (check(nQueen, r, j)) {
                nQueen[r][j] = 'Q';
                solveNQueens(ans, nQueen, r + 1, n);
                nQueen[r][j] = '.';
            }
        }
    }
    bool check(vector<string>& nQueen, int i, int j) {
        for (int k = 1; i - k >= 0; k++) {
            if (nQueen[i - k][j] == 'Q')
                return false;
            if (j - k >= 0 && nQueen[i - k][j - k] == 'Q')
                return false;
            if (j + k < nQueen.size() && nQueen[i - k][j + k] == 'Q')
                return false;
        }
        return true;
    }
};

 

이렇게 풀이를 변경한 후 전반적인 소스 코드도 더 간결해졌고, 시간도 훨씬 적게 걸렸다.

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

[LeetCode # 576] Out of Boundary Paths  (0) 2021.06.25
[LeetCode # 52] N-Queens II  (0) 2021.06.18
[LeetCode # 44] Wildcard Matching  (0) 2021.06.17
[LeetCode # 42] Trapping Rain Water  (0) 2021.06.15
[LeetCode # 41] First Missing Positive  (0) 2021.06.14
반응형

주어진 문자열 패턴 p 가 문자열 s 를 커버할 수 있는 지 체크하는 문제

문자열 s 와 패턴 p 의 길이는 각각 0 ~ 2000 으로 제한되고 s 는 영어 소문자로만 이루어진 문자열, p 는 영어 소문자와 '?' '*' 로만 이루어진 문자열 패턴으로 주어진다. '?' 은 어떤 문자든 하나만 매치가 가능하고, '*' 은 어떤 문자열의 연속이든 매치될 수 있다. (빈 문자열도 가능)

 

문제의 풀이 방식은 다음과 같다. s 와 p 의 인덱스를 각각 저장하고 현재 인덱스의 문자열과 패턴을 비교해 어떤 인덱스를 증가시킬 것인지 선택한다.

 

우선, s 의 현재 문자를 sc, p 의 현재 문자를 pc 라 하면 sc 와 pc 가 같거나 pc 가 '?' 일 때는 두 인덱스를 모두 증가시킨다.

그렇지 않고, pc 가 '*' 일 경우, '*' 문자가 등장한 패턴의 위치와 문자열의 위치를 저장한다. 이후 문자열과 패턴이 일치하지 않을 때 다시 돌아가서 확인하기 위함이다.

마지막으로, 위의 조건에 부합하지 않은 경우는 기존에 저장된 '*' 문자가 있는 지를 확인한 후, 해당 위치로 돌아가서 문자열 인덱스를 하나 증가시킨다. '*' 문자는 어떤 문자열과도 매칭이 가능하기 때문에 다시 해당 위치부터 매칭 조건을 확인하도록 한다.

문자열 탐색이 완료된 후 패턴의 인덱스가 패턴의 길이와 일치하지 않는 경우는, 남은 패턴의 문자가 모두 '*' 문자인 지 확인한 후 그렇지 않으면 매칭이 되지 않는다 판단한다. 또한 어떤 조건에도 부합하지 않는 경우에도, 매칭이 불가능한 문자열과 패턴으로 판단한다.

 

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

 

class Solution {
public:
    bool isMatch(string s, string p) {
        int si = 0, pi = 0;
        int star = -1, stari = 0;
        while (si < s.length()) {
            if (p[pi] == '?' || s[si] == p[pi]) {
                si++;
                pi++;
                continue;
            }

            if (p[pi] == '*') {
                star = pi++;
                stari = si;
                continue;
            }

            if (star != -1) {
                pi = star + 1;
                si = ++stari;
                continue;
            }

            return false;
        }

        while (pi < p.length()) {
            if (p[pi] != '*')
                return false;
            pi++;
        }

        return true;
    }
};

 

이 문제는 아래의 블로그 포스팅을 참고해 문제 풀이를 작성했다.

 

Reference : http://yucoding.blogspot.com/2013/02/leetcode-question-123-wildcard-matching.html

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

[LeetCode # 52] N-Queens II  (0) 2021.06.18
[LeetCode # 51] N-Queens  (0) 2021.06.18
[LeetCode # 42] Trapping Rain Water  (0) 2021.06.15
[LeetCode # 41] First Missing Positive  (0) 2021.06.14
[LeetCode # 37] Sudoku Solver  (0) 2021.06.08
반응형

정수형 배열이 주어지고, 이 배열이 저장하고 있는 값이 해당 위치의 벽의 높이라고 했을 때, 빗물을 받을 수 있는 총량을 구하는 문제

 

이 문제는 Two Pointer 방식으로 문제를 해결할 수 있었는데, Left 와 Right 에 각각 인덱스 포인터를 두고 이를 이동하면서 문제를 풀이하는 방식이다. 인덱스 포인터를 이동하면서 Left 에서의 최대값과 Right 에서의 최대값을 각각 저장해 두고 현재 인덱스 위치가 이보다 값이 작을 경우 빗물을 받을 수 있게 된다.

 

예를 들어, 위의 그림에서 left 는 0부터 right 는 11부터 시작한다. left, right 인덱스 위치를 보고 높이가 낮은 쪽을 우선한다.

현재 left 인덱스가 1인 경우, left max (0) 보다 현재 높이가 높으므로 left max 를 갱신한다. 그 다음 left 가 2가 되었을 때, 현재 높이보다 left max 가 크므로 left max 와 현재 높이의 차이만큼 물을 받을 수 있게 된다.

 

이러한 방식으로 left 와 right 포인터를 각각 이동하며 빗물을 받는 양을 구할 수 있다. 이러한 풀이로 작성한 코드는 다음과 같다.

 

class Solution {
public:
    int trap(vector<int>& height) {
        int ans = 0, left_max = 0, right_max = 0;
        int left, right;
        for (left = 0, right = height.size() - 1; left < right; ) {
            if (height[left] < height[right]) {
                if (height[left] >= left_max) {
                    left_max = height[left];
                }
                else {
                    ans += left_max - height[left];
                }
                left++;
            }
            else {
                if (height[right] >= right_max) {
                    right_max = height[right];
                }
                else {
                    ans += right_max - height[right];
                }
                right--;
            }
        }
        return ans;
    }
};
반응형

정렬되지 않은 정수형 배열 nums 가 주어졌을 때, 배열에서 등장하지 않은 양의 정수 중 가장 작은 수를 찾는 문제

문제 조건으로 시간 복잡도는 O(n), 공간 복잡도는 O(1) 로 주어졌다.

 

문제에서 주어진 시간 복잡도 조건이 O(n) 이기 때문에 정렬을 하거나 하는 등의 방법은 사용이 불가능했고, 단순히 순차 탐색을 통해서만 문제를 해결해야 했다. 이 문제의 경우 풀이 방법을 고민하다가 다른 사람들의 풀이를 보고 아이디어를 얻어 문제를 풀었는데 그 아이디어는 다음과 같다.

 

문제에서 찾고자 하는 수는 nums 의 길이가 n 이라고 할 때, 1 ~ n+1 사이에 존재한다.

 

이 아이디어를 통해 생각한 풀이는, 숫자들을 순서대로 나열하면 그 중 빈 자리가 존재할 것이고, 그 위치에 있어야 하는 수가 정답이 되는 것이다. 그래서 순차적으로 배열을 탐색하고 탐색 과정에서 저장된 수들이 각자 맞는 위치에 들어가 있는 지를 확인하고 그렇지 않다면 자리를 서로 바꿔준다. 

예를 들어 [1, 2, 3, 4, 5] 는 순서대로 숫자들이 들어가 있기 때문에 자리를 바꿔줄 필요가 없지만 [1, 2, 5, 4, 3] 은 [5, 4, 3] 의 자리가 순서에 어긋나므로 [3, 4, 5] 로 변경해 [1, 2, 3, 4, 5] 의 모양을 갖도록 한다. 이렇게 1 부터 n 까지의 수가 차례대로 저장된 경우는 그 다음으로 큰 수인 n + 1 이 문제의 답이 된다. 그렇지 않고 [1, 2, 4, 5, 6] 과 같은 모양을 하게 된다면 이를 원래 자리로 숫자들을 옮기면 [1, 2, 6, 4, 5] 가 될 것이다. 왜냐하면, 6 은 이 배열의 길이 5 보다 큰 수이기 때문에 적절한 위치가 없다. 그러므로 배열 탐색 중 중간에 3이 들어가야 할 위치에 6이 들어가 있고 문제의 답은 3이 된다.

 

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

 

class Solution {
public:
    int firstMissingPositive(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0; i < n; i++) {
            // 일단 숫자가 1 ~ n 사이인 지 확인하고
            // 자기 자리 찾아주기
            while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                swap(nums[nums[i] - 1], nums[i]);
            }
        }
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }
        return n + 1;
    }
};
반응형

빈 칸이 존재하는 Sudoku 게임판을 보고, Sudoku 규칙에 맞게 빈 칸들을 모두 채우는 문제.

 

이 문제는 Hard 난이도지만, 간단하게 DFS 방식으로 문제 해결이 가능했다. 우선, 빈 칸들의 위치를 찾아 Vector 에 저장하고 각 위치들에 숫자를 1~9 까지 하나씩 넣어 가며 모든 빈 칸들을 채운다. 끝까지 채워진 게 확인되면 실행을 종료하도록 한다.

 

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

 

struct _POINT {
    int y, x;
};

class Solution {
public:
    vector<_POINT> vp;
    bool check(vector<vector<char>>& board, _POINT loc, int num) {
        int y = loc.y, x = loc.x;
        // 가로
        for (int j = 0; j<9; j++){
            if (board[y][j] - '0' == num){
                return false;
            }
        }
        // 세로
        for (int i = 0; i<9; i++){
            if (board[i][x] - '0' == num){
                return false;
            }
        }
        // 블락
        for (int i = 0; i<3; i++){
            for (int j = 0; j<3; j++){
                if (board[(y / 3) * 3 + i][(x / 3) * 3 + j] - '0' == num){
                    return false;
                }
            }
        }
        return true;
    }

    int dfs(vector<vector<char>>& board, int idx) {
        if (idx == vp.size())
            return 1;

        for (int num = 1; num <= 9; num++) {
            if (check(board, vp[idx], num)){
                board[vp[idx].y][vp[idx].x] = num + '0';
                int res = dfs(board, idx + 1);
                if (res == 1)
                    return 1;
            }
        }
        board[vp[idx].y][vp[idx].x] = '.';
        return 0;
    }

    void solveSudoku(vector<vector<char>>& board) {
        for (int i = 0; i<9; i++){
            for (int j = 0; j<9; j++){
                if (board[i][j] == '.'){
                    vp.push_back({i, j});
                }
            }
        }
        dfs(board, 0);
    }
};
반응형

문자열 s 와, 각각이 동일한 길이를 갖는 단어들 words 가 주어졌을 때 words 를 모두 연결해서 나올 수 있는 substring 을 찾는 문제

예를 들어, s 가 "barfoothefoobarman" 이고 words 가 ["foo","bar"] 이면 words 를 모두 연결했을 때 나올 수 있는 문자열은 "foobar" 와 "barfoo" 두 가지이다. 문자열 s에서 위 문자열은 다음과 같이 나타난다. "barfoothefoobarman". 이 때 반환값은 문자열 s에서 찾고자 하는 문자열 words 의 결합이 나타나는 곳의 시작 위치이다.

 

이 문제에서 중요한 것은, words 의 결합된 문자열의 시작 위치가 어떤 순서로 반환되는 지는 상관이 없다는 점이다. 문제를 해결하기 위해 생각한 방법은 주어진 words 에서 각각의 word 의 등장 횟수를 저장하고, s 를 순차적으로 탐색하면서 해당 위치에서 부분 문자열이 words 에 주어진 word 중 하나인지, 맞다면 count 값을 증가시켜 각 word 별로 등장 횟수가 words 에서 얻은 정보와 일치하는 지를 확인한다.

 

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

 

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        vector<int> indexes;
        unordered_map<string, int> counts;
        for (auto word : words) {
            counts[word]++;
        }
        int len = s.length(), n = words.size(), wordLen = words[0].length();
        for (int i = 0; i < len - n * wordLen + 1; i++){
            unordered_map<string, int> seen;
            int j;
            for (j = 0; j < n; j++){
                string word = s.substr(i + j * wordLen, wordLen);
                if (counts.find(word) != counts.end()) {
                    seen[word]++;
                    if (seen[word] > counts[word])
                        break;
                }
                else
                    break;
            }
            if (j == n)
                indexes.push_back(i);
        }
        return indexes;
    }
};

 

위의 코드는 매 탐색 별로 이전 탐색의 정보를 이용하지 않지만, 보완할 수 있다면 위의 코드를 Sliding Window 기법을 활용해 이전 탐색에 이어서 탐색을 이어갈 수 있도록 하면 성능 면에서 더 효율적으로 동작할 수 있을 것 같다.

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

[LeetCode # 41] First Missing Positive  (0) 2021.06.14
[LeetCode # 37] Sudoku Solver  (0) 2021.06.08
[LeetCode # 29] Divide Two Integers  (0) 2021.05.31
[LeetCode # 25] Reverse Nodes in k-Group  (0) 2021.05.31
[LeetCode # 65] Valid Number  (0) 2021.05.21

+ Recent posts