반응형

주어진 문제는 위와 같다. 두 개의 문자열 '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 를 피하고 문제를 풀이할 수 있었다.

반응형

주어진 문제는 위와 같다. 정수형 배열이 주어졌을 때, 배열의 부분 배열 중 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 을 추가적으로 받도록 해 마지막 문장인 경우 문제의 추가 조건을 만족시킬 수 있도록 구현했다.

반응형

주어진 문제는 위와 같다. n 을 크기로 갖는 문자열 'keysPressed' 와 배열 'releaseTimes' 이 주어지는데 'keysPressed' 는 어떤 순간 어떤 키가 눌렸는 지, 'releaseTimes' 는 각 키가 떼진 순간의 시간을 의미한다.

 

i 번째로 눌린 키가 눌려진 기간은 releaseTimes[i] - releaseTimes[i - 1] 로 표현된다. (0 번째 키는 releaseTimes[0]) 주어진 정보를 바탕으로 가장 오랫동안 눌려진 키를 찾는 문제이다. 서로 다른 순간에 같은 키가 눌릴 수 있고, 같은 키가 눌린 시간은 각각의 시간으로만 평가하고 누적되지 않는다. 가장 오래 눌려진 키가 여러 개일 경우에는, 사전적으로 더 큰 값을 갖는 키를 정답으로 갖는다.

 

이 문제는, 단순히 0 번째부터 n - 1 번째까지 (0 - indexed) 각각의 키가 눌린 시간을 계산해 그 중 가장 큰 값을 찾도록 하는 방식으로 문제를 해결했고, 해결을 위해 작성한 소스 코드는 다음과 같다.

 

class Solution {
public:
    char slowestKey(vector<int> &releaseTimes, string keysPressed) {
        int len = keysPressed.length();
        char ans = keysPressed[0];
        int longestDuration = releaseTimes[0];

        for (int i = 1; i < len; i++) {
            int duration = releaseTimes[i] - releaseTimes[i - 1];

            if (duration > longestDuration || (duration == longestDuration && keysPressed[i] > ans)) {
                ans = keysPressed[i];
                longestDuration = duration;
            }
        }

        return ans;
    }
};
반응형

주어진 문제는 위와 같다. 트리 노드의 개수인 n 이 주어지면, n 개의 노드를 갖는 BST 들을 찾는 것이다. 이 때, n 개의 노드들은 각각 1 부터 n 까지의 값을 갖는다.

 

BST (Binary Search Tree) 는 기본적으로 각 서브트리에서 루트 노드보다 작은 값들만 루트 노드의 Left 서브 트리에 저장될 수 있고, 루트 노드보다 큰 값들만 루트 노드의 Right 서브 트리에 저장될 수 있다는 특징을 갖는다.

 

이러한 특징을 기반으로 재귀를 활용해 문제를 풀이했다. 풀이 방식은 각 재귀 단계마다 BST 를 구성하려는 값의 범위 start ~ end 가 주어지고 그 범위 내의 값들을 루트 노드로 하는 BST 를 찾는 것이다.

 

간단한 슈도 코드로 이를 표현하자면, 다음과 같다.

recursiveBST(start, end):
	for i in (start, end):
    	leftLists <- recursive_bst(start, i - 1)
        rightLists <- recursive_bst(i + 1, end)
        
        for left in leftLists:
        	for right in rightLists:
            	root <- new node(i)
                
                root.left <- left
                root.right <- right

(start, end) 범위 내에서 각각의 값을 갖는 노드를 루트 노드로 구성하고, 그 값보다 작은 값들을 갖는 서브 트리들과 그 값보다 큰 값들을 갖는 서브 트리들을 각각 루트 노드의 왼쪽 서브 트리, 오른쪽 서브 트리로 구성한다.

 

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

 

class Solution {
public:
    vector<TreeNode*> generateTrees(int start, int end) {
        vector<TreeNode*> binarySearchTrees;
        if (start > end) {
            binarySearchTrees.push_back(NULL);
            return binarySearchTrees;
        }
        if (start == end) {
            binarySearchTrees.push_back(new TreeNode(start));
            return binarySearchTrees;
        }
        for (int i = start; i <= end; i++) {
            vector<TreeNode*> leftList = generateTrees(start, i - 1);
            vector<TreeNode*> rightList = generateTrees(i + 1, end);

            for (int l = 0; l < leftList.size(); l++) {
                for (int r = 0; r < rightList.size(); r++) {
                    TreeNode* root = new TreeNode(i);
                    root->left = leftList[l];
                    root->right = rightList[r];

                    binarySearchTrees.push_back(root);
                }
            }
        }

        return binarySearchTrees;
    }

    vector<TreeNode*> generateTrees(int n) {
        return generateTrees(1, n);
    }
};

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

[LeetCode # 68] Text Justification  (0) 2021.09.07
[LeetCode # 1629] Slowest Keys  (0) 2021.09.07
[LeetCode # 153] Find Minimum in Rotated Sorted Array  (0) 2021.09.01
[LeetCode # 60] Permutation Sequence  (0) 2021.09.01
[LeetCode # 135] Candy  (0) 2021.06.27
반응형

주어진 문제는, 어떤 배열이 주어졌을 때 그 배열에서 가장 작은 값을 찾는 문제이다. 이 때, 배열은 정렬된 배열을 rotation 시킨 배열로 주어진다.

 

예를 들어, [0, 1, 2, 3, 4, 5, 6, 7] 과 같이 정렬된 배열이 주어질 수도 있고 [4, 5, 6, 7, 0, 1, 2, 3] 과 같이 앞의 정렬된 배열에서 [0, 1, 2, 3] 의 위치가 뒤로 이동한 배열이 주어질 수도 있다.

 

이 문제에서 주어진 조건은, 문제 해결 시 O(log n) 의 시간복잡도를 갖는 알고리즘을 사용해야 한다는 것이다.

 

문제 해결 전략은 다음과 같다. 기본적으로 이분 탐색을 사용해 배열을 반씩 분할해 각 분할마다 최소값을 찾고, 그 중 가장 최소값을 찾는다.

로테이션이 된 배열이기 때문에, 전체 배열은 정렬이 되지 않은 상태일 수 있지만 부분 배열은 정렬이 된 상태일 수 있다. 위의 [4, 5, 6, 7, 0, 1, 2, 3] 의 배열을 반으로 분할해 보면, [4, 5, 6, 7], [0, 1, 2, 3] 과 같이 표현된다. 이 때 각 부분배열은 모두 정렬된 상태이므로 최소값을 쉽게 찾을 수 있다.

 

만약, 분할된 부분배열이 정렬되지 않은 배열일지라도, 예를 들어 [4, 5, 0] 이라고 하면 이러한 배열은 다시 또 분할을 해서 정렬된 상태가 될 때까지 분할을 하면 된다. 계속 반으로 분할을 하다보면, 언젠가는 정렬된 부분배열이 될 것이고 (혹은 한 개의 원소만 남은 배열이 되거나) 그 때의 최소값은 바로 탐색할 수 있기 때문에 이 방법은 O(log n) 의 시간복잡도를 갖는 알고리즘이라 할 수 있다.

 

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

 

class Solution {
public:
    int binarySearch(vector<int>& nums, int s, int e) {
        if (s >= e) {
            return nums[s];
        }
        if (nums[s] < nums[e]) {
            return nums[s];
        }

        int m = (s + e) / 2;
        int l = binarySearch(nums, s, m);
        int r = binarySearch(nums, m + 1, e);
        return l < r ? l : r;
    }
    int findMin(vector<int> &nums) {
        int min = binarySearch(nums, 0, nums.size() - 1);
        return min;
    }
};
반응형

주어진 문제는, 주어진 n, k 에 해당하는 순열을 찾는 것이다. n (1 <= n <= 9) 은 순열에 포함되는 수의 개수이고, k (1 <= k <= n!) 는 순열의 순서이다.

 

예를 들어, (n, k) 가 (3, 3) 으로 주어졌다면 기대되는 출력값은 "213" 이 된다. 위의 그림에서 보여지듯, "123", "132", "213", ... 의 순서로 순열을 생성할 수 있고, 그 중 k 번째 수열은 "213" 이다. (1-indexed)

 

이 문제를 해결하기 위한 전략은 다음과 같다. n 이 3일 때 생성되는 순열은 "123", "132" / "213", "231" / "312", "321" 이다. '/' 로 나눠놓은 것을 보면 각각 수열이 갖는 첫번째 수가 같은 것끼리 묶여있음을 알 수 있고, 이를 바탕으로 문제를 해결할 수 있음을 유추할 수 있다.

 

(n, k) 가 (4, 7) 로 주어진 경우를 생각해 보면, n 이 4이므로 사용 가능한 숫자들의 목록은 [1, 2, 3, 4] 가 된다. 세 개의 숫자로 구성할 수 있는 수열은 6개이므로 "1", "2", "3", "4" 로 시작하는 각각의 수열의 개수는 6개임을 알 수 있다. 이 때, 각 숫자의 순서대로 수열이 순서를 가지므로 k 가 7이라는 것은, "2" 로 시작하는 수열을 찾고자 함을 알 수 있고, 그 중 1번째 수열임을 알 수 있다.

 

이러한 방식으로, 첫 번째 숫자를 찾고 그 뒤에는 남은 [1, 3, 4] 를 통해 앞으로 어떤 숫자들이 수열에 순서대로 위치할 지를 찾아낼 수 있다. 문제를 풀이한 소스 코드는 다음과 같이 작성했다.

 

class Solution {
public:
    int factorial(int n) {
        int result = 1;
        while (n > 1) {
            result *= n--;
        }

        return result;
    }

    string getPermutation(int n, int k) {
        vector<int> nums;
        for (int i = 1; i <= n; i++) {
            nums.push_back(i);
        }

        string ans = "";

        while (n > 1) {
            int blockSize = factorial(n - 1);
            int block = k / blockSize;
            k %= blockSize;

            ans += to_string(nums[block]);
            nums.erase(nums.begin() + block);
            n--;
        }

        return ans + to_string(nums[0]);
    }
};

+ Recent posts