본문 바로가기

CodingTest/99클럽2024스터디

99클럽 코테 스터디 30일차 TIL, LeetCode / Minimum Operations to Make a Subsequence

 

class Solution {
public:
    int minOperations(vector<int>& target, vector<int>& arr) {
        vector<int> indices;
        unordered_map<int, int> numToIndex;

        // 자. arr 요소를 각각 target 에서 찾을 것이다.
        // 이를 쉽게 하기 위해서, target 각 요소가 몇번째 idx 에 있는지 map 에 저장한다.
        for (int i = 0; i < target.size(); ++i)
            numToIndex[target[i]] = i;

        // target 에서 찾은 arr 의 원소들만 indices 에 "몇번째 idx" 인지 그 정보를 넣어준다.
        for (const int a : arr)
            if (const auto it = numToIndex.find(a); it != numToIndex.end())
                indices.push_back(it->second);

        return target.size() - lengthOfLIS(indices);
    }

    private:
    // Same as 300. Longest Increasing Subsequence
    int lengthOfLIS(vector<int>& nums) {
        // tail[i] := the minimum tail of all the increasing subsequences having
        // length i + 1
        vector<int> tail;

        // tail 맨 마지막 원소에는, 가장 큰 값이 들어가 있게 된다.
        for (const int num : nums)
        {
            // cout << "---" << endl;
            // cout << "num : " << num << endl;
            // cout << "bef tail" << endl;
            // for (int t : tail) cout << t << ".";
            // cout << endl;

            if (tail.empty() || num > tail.back())  // tail 이 비거나, 현재 것이 가장 크면 push
                tail.push_back(num);
            else
            {
                // 즉, 자기보다 크거나 같은 요소에, 자기 자신을 넣어버리는 것.
                // 해당 값은, 맨 마지막 tail.back 이 될 수도 있다.
                // ex) tail 에 0,5 가 있었고, num 이 4 라면, 0,4 로 tail 이 변경된다.
                // 사실 이 lower_bound 함수를 이분 탐색으로 변경하면 될 것 같다.
                int foundIdx = firstGreaterEqual(tail, num);
                // cout << "foundIdx : " <<foundIdx << endl;
                tail[foundIdx] = num;
            }
            // cout << "aft tail" << endl;
            // for (int t : tail) cout << t << ".";
            // cout << endl;
        }

        return tail.size();
    }

    private:
    int firstGreaterEqual(const vector<int>& A, int target) {
        // LCS 에서 num 보다 크거나 같은 가장 작은 element return
        // 이를 통해서, LIS 가 increasing order 을 유지하면서
        // 새로운 element 를 LIS 에 넣어줄 수 있다.
        return ranges::lower_bound(A, target) - A.begin();
    }
};

https://leetcode.com/problems/minimum-operations-to-make-a-subsequence/

 

 

첫번째 풀이 : 이분탐색 (최소 조건 고려 X)

 

class Solution {
public:
    struct NumberStr
    {
        int num;
        int idx;
        bool operator < (const NumberStr& other)
        {
            if (num == other.num)
                return idx < other.idx;
            return num < other.num;
        }
    };
    int findIdxInArr(int target, int stIdx, vector<int>& arr)
    {
        int answer = -1;
        int st = 0, ed = arr.size() - 1;
        vector<NumberStr> numberStrs;
        numberStrs.reserve(arr.size());

        for (int idx = 0; idx < arr.size(); ++idx)
        {
            int val = arr[idx];
            numberStrs.push_back(NumberStr(val, idx));
        }

        sort(numberStrs.begin(), numberStrs.end());

        while(st <= ed)
        {
            int mid = (st + ed) / 2;

            if (numberStrs[mid].num == target)
            {
                if (numberStrs[mid].idx >= stIdx)
                {
                    answer = numberStrs[mid].idx;
                    ed = mid - 1;
                }
                else
                    st = mid + 1;
            }
            if (numberStrs[mid].num < target) st = mid + 1;
            if (numberStrs[mid].num > target) ed = mid - 1;
        }

        return answer;
    }
    int minOperations(vector<int>& target, vector<int>& arr) {
        // 그런데 이분 탐색을 하려면 기본적으로 정렬이 되어 있어야 한다.
        // 그러면 구조체를 만들어서, number 단위로 정렬. 단 만약 number 가 같다면, idx 단위로 오름차순 정렬

        int prevFoundIdx = -1;
        int answer = 0;
        vector<int> targetIdxs;
        targetIdxs.reserve(target.size());

        for (int i = 0; i < target.size(); ++i)
        {
            // 현재 target 을, 이전 탐색된 idx 이후부터 arr 에서 찾는다.
            // 만약 찾아지면 prevFoundIdx 를 update
            // 찾아지지 않으면 answer + 1 증가
            int cTarget = target[i];
            int idxInArr = findIdxInArr(cTarget, prevFoundIdx + 1, arr);
            if (idxInArr == -1)
                answer += 1;
            else
                prevFoundIdx = idxInArr;
        }
        
        return answer;

        /*
        (예외 케이스)
        target =
        [16,7,20,11,15,13,10,14,6,8]
        arr =
        [11,14,15,7,5,5,6,10,11,6]

        [(16),11,14,15,7(20),5,5,6,10,11(15)(13)(10)(14),6(8)]
        이렇게 7번 insert 되도록 나온다.

        그런데 실제 정답은 16, 20, 11, 15, 13, 8. 이렇게 insert 하여 6번만 해도 된다.
        target 에 있는 11이 arr 에서 찾아지더라도, 바로 11 로 넘어가는 것은 "최소" 가 아닐 수 있기 때문이다.
        */
    }
};

 

 

두 번째 풀이 : LCS 활용

 

인자로 주어진 target, arr 의 LCS 를 구하면, 그외 에 겹치지 않는 부분만 추가하면 되는 개념이다.

 

그런데 dp 를 만드는 과정에서 메모리 초과 발생 

 

그 전에 시간 초과도 발생할 수 있다.

 

왜냐하면 dp[][] 2차원 배열 탐색 시간이 target,size() * arr.size() 인데

둘다 최대 10^ 5 이다. 즉, 최대 10^10 의 시간이 걸릴 수 있다.

 

class Solution {
public:
    int getLCS(const vector<int>& target, const vector<int>& arr)
    {
        vector<vector<int>> dp;
        dp.resize(target.size() + 1, vector<int>(arr.size() + 1, 0));

        for (int tIdx = 1; tIdx <= target.size(); ++tIdx)
        {
            for (int aIdx = 1; aIdx <= arr.size(); ++aIdx)
            {
                if (target[tIdx - 1] == arr[aIdx - 1])
                    dp[tIdx][aIdx] = dp[tIdx - 1][aIdx - 1] + 1;
                else 
                    dp[tIdx][aIdx] = max(dp[tIdx][aIdx - 1], dp[tIdx - 1][aIdx]);
            }
        }

        return dp[target.size()][arr.size()];
    }

    int minOperations(vector<int>& target, vector<int>& arr) {
        // 결국은 2개 vec 사이에 LCS (Longest Common Subsequence) 를 찾고
        // 그외, 추가할 부분만 insert 해주면 되는 문제이다.
        // 즉, target.size() - LCS
        // LCS -> dp[i][j] : 1번째 vec 의 i 번째 / 2번째 vec 의 j 번째까지의 LCS
        int lcs = getLCS(target, arr);
        cout << "lcs : " << lcs << endl;
        return target.size() - lcs;
    }
};

 

 

세 번째 풀이 : LIS 형태로 변환

 

arr 의 각 요소를 target 에서 찾은 idx 들을 배열로 만든 다음

LIS 를 구하면 두 문자열의 LCS 가 되는 개념이다.

 

그런데 일반적인 LIS dp 로직을 적용하면 이 역시 시간 복잡도 초과가 발생한다.

왜냐하면 LCS 안에서 이중 for 문을 순회하기 때문이다.

 

class Solution {
public:
    struct NumberStr
    {
        int num;
        int idx;
        bool operator < (const NumberStr& other)
        {
            return num < other.num;
        }
    };
    vector<int> getLCS(const vector<int>& nums)
    {
        vector<int> LCS;

        // N^2 LCS 는 시간 초과가 난다.
        LCS.resize(nums.size(), 1);
        for (int s = 1; s < nums.size(); ++s)
        {
            for (int f = 0; f < s; ++f)
            {
                if (nums[s] > nums[f])
                    LCS[s] = max(LCS[s], LCS[f] + 1);
            }
        }

        return LCS;
    }
    int findIdx(const vector<NumberStr>& targets, int targetNum)
    {
        int answer = -1;
        int st = 0, ed = targets.size() - 1;
        while(st <= ed)
        {
            int mid = (st + ed) / 2;
            if (targets[mid].num == targetNum)
            {
                answer = targets[mid].idx;
                break;
            }
            if (targets[mid].num > targetNum)
                ed = mid - 1;
            else 
                st = mid + 1;
        }

        return answer;
    }
    int minOperations(vector<int>& target, vector<int>& arr) {
        // arr 은 중복 가능. target 은 중복 불가능.
        // 일단, 각 arr 을 기준으로, target 에서의 idx 를 찾아서 모아준다.
        // 찾지 못하면, 모아주지 않는다.
        // 이때 이분 탐색으로 target 에서 찾는다.
        // 그리고 그것에 대한 LCS 를 가져오고
        // 그 LCS 에서 가장 큰 값을 구한 다음
        // target.size() 에서 빼준다.
        vector<int> foundIdxs;
        int tempMaxN = -1;
        vector<NumberStr> numberStrs;
        foundIdxs.reserve(arr.size());
        numberStrs.reserve(target.size());

        for (int idx = 0; idx < target.size(); ++idx)
        {
            int val = target[idx];
            numberStrs.push_back(NumberStr(val, idx));
        }

        sort(numberStrs.begin(), numberStrs.end());

        cout << "--fIdx--" << endl;

        for (int n : arr)
        {
            int fIdx = findIdx(numberStrs, n);
            if (fIdx == -1) continue;
            cout << fIdx << ".";
            foundIdxs.push_back(fIdx);
        }
        cout << endl;

        const vector<int>& returnLCS = getLCS(foundIdxs);
        for (int lcs : returnLCS) tempMaxN = max(tempMaxN, lcs);

        return tempMaxN == -1 ? target.size() : target.size() - tempMaxN;
    }
};

 

 

4번째 풀이 : LCS 로직 최적화

 

현재 num 을 LCS 에 계속 채워주는데, 그 방식이

LCS 에서 자기보다 크거나 같은 값을 자기 자신으로 update 해가면서

LCS 를 만들어준다. 이때 이분탐색을 활용한다.