hyelie
hyelie
Hyeil Jeong
       
글쓰기    관리    수식입력
  • 전체보기 (495)
    • PS (283)
      • Algorithm (28)
      • PS Log (244)
      • Contest (6)
      • Tips (5)
    • Development (52)
      • Java (14)
      • Spring (23)
      • SQL (2)
      • Node.js (2)
      • Socket.io (3)
      • Study (4)
      • Utils (4)
    • DevOps (36)
      • Git (5)
      • Docker (4)
      • Kubernetes (2)
      • GCP (3)
      • Environment Set Up (8)
      • Tutorial (12)
      • Figma (2)
    • CS (74)
      • OOP (7)
      • OS (24)
      • DB (2)
      • Network (24)
      • Architecture (0)
      • Security (2)
      • Software Design (0)
      • Parallel Computing (15)
    • Project (15)
      • Project N2T (5)
      • Project ASG (0)
      • Project Meerkat (1)
      • Model Checking (7)
      • Ideas (2)
    • 내가 하고싶은 것! (34)
      • Plan (16)
      • Software Maestro (10)
      • 취준 (8)
hELLO · Designed By 정상우.
hyelie

hyelie

PS/PS Log

23.03.12. 풀었던 문제들 - 2023 KAKAO BLIND RECRUITMENT

Leetcode 23. Merge k Sorted Lists

문제

 k개의 정렬되어 있는 list를 정렬하는 문제이다. 제일 쉽게 접근한다면 모든 list element를 하나의 vector에 넣고 전체 sort하던가 또는 counting sort하는 방식을 생각할 수 있을 것이다.

 입력 조건은

  • -10000 <= 각 element 크기 <= 10000
  • 0 <= k <= 10000
  • 각 list 길이 : 500
  • 따라서 전체 element 개수는 5,000,000이다.

 모든 element를 하나로 합치고 정렬하는 방식은 풀리지 않을 것이다. 반면 문제 조건에서 정수 범위가 작으니 counting sort로 풀 수 있을 것이지만 element 크기가 미정이라면? 다른 알고리즘을 써야 한다.

 

 내가(그리고 보편적인 솔루션이 제시하는) 것은 O(nlogk) 솔루션이다.

 

접근

 merge-sort에서 merge한 두 배열을 합칠 때, 두 배열은 모두 오름차순 정렬되어 있다. 이 때 두 배열의 제일 앞에 있는 값들만 비교하면서 더 작은 값을 merge array에 push한다. 이와 유사하게 k개의 sorted list를 merge하는 것이기 때문에 비스하게 풀 수 있다.

 

 각 list는 모두 오름차순 정렬되어 있기 때문에 다음과 같은 접근을 사용한다.

  • 정렬한 후 제일 앞에 오는 element는 [모든 list의 제일 앞에 있는 것 중 min값]이다. 
  • 그 값을 빼면, 해당 list의 head를 pop한다.
  • 위 두 단계를 반복한다.

이다. 첫 단계는 총 k개의 리스트가 있으니 O(k), 두번째 단계는 O(1)이다. 세 번째 단계는 모든 element에 대해 이 연산을 수행해야 하므로 O(n)이다. 그러면 전체 O(nk)가 된다.

 

 여기서 priority queue를 이용해 첫 단계를 개선할 수 있다.

  • k개의 list의 제일 앞에 있는 값을 priority queue에 넣는다.
  • pq.pop()으로 [list의 제일 앞에 있는 것 중 min값]을 쉽게 뽑아낼 수 있다.
  • min 값을 뽑아내면 해당 list의 head를 pop하고, head->next에 있는 것을 pq에 push한다.
    • 이를 통해 pq에는 항상 [list의 제일 앞에 있는 element]가 존재하게 된다.
  • 단, NULL 처리를 잘 해야 한다.

이렇게 바꿈으로써 기존의 첫 단계가 항상 O(logk)로 바뀌게 된다. 

 개선한 코드에서 첫 단계 : O(klogk), 둘째 단계 : O(logk), 셋째 단계 : O(1 + logk), 넷째 단계 : O(n)이다. 따라서 O(klogk + nlogk) = O(nlogk)이다.

 

// Runtime 22 ms Beats 83.79%
// Memory 13.3 MB Beats 74.5%

struct cmp{
    bool operator()(ListNode* a, ListNode* b){  // pq.top()은 min value
        return a->val > b->val;
    }
};
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        priority_queue<ListNode*, vector<ListNode*>, cmp> pq;

        // k개의 list 제일 앞에 있는 것을 pq에 넣음. 이 때 pq에 NULL을 허용하지 않겠다.
        for(ListNode* v : lists){
            if(v != NULL) pq.push(v); // *** 유의! NULL인 경우 넣지 않음
        }

        // 새로 정렬할 Linked List
        ListNode* head = new ListNode();
        ListNode *tail = head;
        
        while(!pq.empty()){
            ListNode* top = pq.top(); pq.pop();
            // if(top != NULL){ // pq에는 NULL이 없음
            tail->next = top; // 새로 정렬할 Linked List의 끝에 push한다.
            tail = tail->next;
            if(top->next != NULL) pq.push(top->next); // min을 뺀 list의 다음 element를 pq에 넣는다
            // }       
        }

        return head->next;
    }
};

 

시간복잡도

 위에서 설명했듯 O(nlogk)

 

공간복잡도

 priority queue는 총 O(k)의 공간을 사용한다.

 

 

 


 

 

 

2023 KAKAO BLIND RECRUITMENT

 프로그래머스 lv3를 거의 다 푼 기념으로 남은 2023 카카오 공채 코딩테스트를 봤다. 제한시간은 5시간, 커트라인은 약 3솔(1, 2, 3 푼 경우 떨, 1, 2, 5 푼 경우 합)이었다고 한다.

 

요약 및 결과

 4/7솔이다. 

 1번 : 35분에 풀었다. 뇌가 덜 풀려서 조금 헤멘 듯. 숫자 곱하는 것도 잘못 곱하기도 했고.

 2번 : 1시간 16분에 풀었다. 끝에서부터 가져오면 됨을 깨닫고 simulation 문제라 생각해서 풀었다. 실수 하나 했고... 다행히 반례를 빨리 생각해내 풀었다.

 3번 : 1시간 38분에 풀었다. 문제 자체가 중복순열이라는 그다지 어렵지 않는 DFS 구현을 다루기 때문에 빨리 푼 것 같다. 실수 하나 했고... 다행이 예제에서 잡혀서 빨리 디버깅했다.

 4번 : 3시간에 풀었다. 문제 푸는 데는 약 45분이 걸렸는데 디버깅에 45분이 걸렸다. 실수는 2개 했고... 하나는 생각해내서 풀었고 하나는 입력을 잘못 넣었다는 것을 45분만에 찾았다.

 6번 : 풀기 시작했을 때 2시간이 남았다. 좀 deep하게 내려가야 할 것 같아 보이는 DFS 문제라서 한 번 풀고 시간초과 나거나 안 풀리면 5번 풀어야지, 하고 손 대서 풀었더니 WA + TLE가 나서 스킵했다.

 5번 : 풀기 시작했을 때 1.5시간이 남았다. 딱 보니 구현만 빡세게 하면 되는 문제라 구현~구현~하고 50분정도 걸려 풀었는데 어떤 반례 때문에 패스를 못했다. 

 7번 : 읽지도 않음

 

 

개인정보 수집 유효기간

 시키는 대로 구현하면 된다. 사실상 string parsing 문제. [약관 종류, 유효기간]을 map에 넣고 필요하면 꺼내 오면 되는 식으로 하면 되고, privacies는 날짜와 약관 종류를 계산해 앞서 초기화 해 둔 map에서 값을 가져와 날짜 계산을 하면 된다. 다만 YYYY.MM.DD 형식으로 된 것의 비교가 귀찮기 때문에 모두 day로 바꾸어서 계산했다. 35분 걸렸다. 

 

실수한 점

  • month에 28을 곱해야 하는데 60을 곱했다. 정신줄 어따 놓고 왔는지... 
  • 시작 날짜도 포함되기 때문에 [만료일 = 날짜 + 만료기간 - 1]을 해야 한다.
map<string, int> termMap;

int date2int(string date){ // date : YYYY.MM.DD 형식
    int year = stoi(date.substr(0, 4)) - 2000;
    int month = stoi(date.substr(5, 2));
    int day = stoi(date.substr(8, 2));
    
    return day + month * 28 + year * 28 * 12;
}

vector<int> solution(string today, vector<string> terms, vector<string> privacies) {
    for(string str : terms){
        termMap[str.substr(0, 1)] = stoi(str.substr(2));
    }
    
    int currentDay = date2int(today);
    
    vector<int> answer;
    for(int i = 0; i<privacies.size(); i++){
        int privacyDay = date2int(privacies[i].substr(0, 10));
        string privacyType = privacies[i].substr(11);
        
        int expiredDay = privacyDay + 28 * termMap[privacyType] - 1; // 개월 수 * 28
        if(currentDay > expiredDay) answer.push_back(i+1);
    }

    return answer;
}

 

 

택배 배달과 수거하기

  40분 걸려 풀었다. greedy + simulation 문제. 문제가 길어서 읽는 데 시간을 조금 썼다.

  • 큰 index를 2번 왕복하는 것보다 작은 index를 2번 왕복하는 것이 더 낫기 때문에 큰 index부터 먼저 처리해야 한다.
    • 즉슨 제일 뒤부터 처리해야 한다.
    • 최대로 가져가서 제일 뒤에서부터 돌아오면서 배달할 수 있을 때까지 배달하고 돌아온다고 생각하자.
      • 그리고 이게 정해지면, 정해진 집부터 제일 뒤까지 가는 길에 배달을 할 수 있다.
      • 따라서 제일 뒤에서는 가지고 있는 cap이 0이다.
    • 따라서 전부 배달을 끝나면 제일 뒤에서부터 가져올 수 있을 만큼 가져오고 돌아올 수 있다.
    • 이 때 [배달해야 하는 마지막 집 거리, 가져와야 하는 마지막 집 거리] * 2만큼 이동한다.
  • 그러면 끝이다!

 

long long solution(int cap, int n, vector<int> deliveries, vector<int> pickups) {
    int lastPickupIdx = n-1, lastDeliverIdx = n-1;
    
    // *** 반례 : 끝부분이 0인 경우에는 안 가도 됨
    while(lastPickupIdx >= 0 && pickups[lastPickupIdx] == 0) lastPickupIdx--;
    while(lastDeliverIdx >= 0 && deliveries[lastDeliverIdx] == 0) lastDeliverIdx--;
    
    long long answer = 0, curDelivery, curPickup;
    
    while(1){
        if(lastPickupIdx < 0 && lastDeliverIdx < 0) break; // 둘 다 끝난 경우
        answer += (max(lastPickupIdx, lastDeliverIdx) + 1) * 2;
        
        // pickup 제일 뒤부터 계산
        curPickup = cap;
        while(curPickup >= 0 && lastPickupIdx >= 0){
            if(curPickup >= pickups[lastPickupIdx]){ // 당길 수 있으면 당김
                curPickup -= pickups[lastPickupIdx];
                pickups[lastPickupIdx] = 0;
                lastPickupIdx--;
            }
            else{ // 당기지는 못할 경우 그 양만큼 줄임
                pickups[lastPickupIdx] -= curPickup;
                curPickup = 0;
                break;
            }
        }
        
        // delivery 제일 뒤부터 계산
        curDelivery = cap;
        while(curDelivery >= 0 && lastDeliverIdx >= 0){
            if(curDelivery >= deliveries[lastDeliverIdx]){ 
                curDelivery -= deliveries[lastDeliverIdx];
                deliveries[lastDeliverIdx] = 0;
                lastDeliverIdx--;
            }
            else{ 
                deliveries[lastDeliverIdx] -= curDelivery;
                curDelivery = 0;
                break;
            }
        }
    }
    
    return answer;
}

실수한 점

  • 제일 처음에 [lastPickupIdx, lastDeliverIdx]를 n-1로 설정하는데, 만약 0인 경우는 거기 가지 않아도 된다. 끝에서부터 0이 아닐 때까지 앞으로 당겨와야 한다.

 

 

이모티콘 할인행사

 16분 걸려 풀었다. 단순 구현 문제. 2번 풀면서 머리가 좀 말랑말랑해지기도 했고 문제 난이도 자체도 쉬운 편이라 빨리 푼 것 같다.

 이모티콘 개수는 7개, 각각의 할인은 10%, 20%, 30%, 40%로 4개이므로 brute-force 해도 된다. 이 경우 중복순열 DFS로 각 쿠폰의 할인율을 구할 수 있다.

 이렇게 각 쿠폰의 할인율을 모두 구했다면

  • 모든 사용자에 대해
    • 모든 이모티콘 대해 할인율이 기준보다 높으면 산다.
    • 결제금이 기준 이상일 경우에는 멤버십에 가입하게 한다.
    • 결제금이 기준 이하일 경우에는 그만큼 산 것으로 한다.
  • 이후 1번 조건을 달성하는 경우 갱신하고, 1번 조건이 같은 경우 profit을 maximize한다.

 

vector<int> discounts(7); // discounts[i] : emoticon i의 할인율
vector<vector<int>> Users;
vector<int> Emoticons;
vector<int> ratio = {10, 20, 30, 40};
int userSize, emoticonSize;
int maxJoins = -1, maxProfit = -1;

int getDiscountPrice(int originPrice, int discountRatio){
    // price max : 1,000,000 / 따라서 100 더 곱해도 int 범위 안
    return originPrice * (100 - discountRatio) / 100;
}

void permutation(int depth, int max_depth){
    if(depth == max_depth){
        int numJoins = 0;
        int sumProfit = 0;
        for(vector<int> User : Users){ // for all user
            int payPrice = 0;
            for(int i = 0; i<emoticonSize; i++){ // for all emoticons
                if(User[0] <= discounts[i]){ // 할인율 더 큰 경우 삼
                    payPrice += getDiscountPrice(Emoticons[i], discounts[i]);
                }
            }
            if(payPrice >= User[1]) numJoins++; // 결제금이 기준 이상인 경우 join
            else sumProfit += payPrice;
        }
        
        
        if(numJoins >= maxJoins){ // 실수 : 1번 조건과 2번 조건을 혼용했음
            maxJoins = numJoins;
            maxProfit = max(maxProfit, sumProfit);
        }
        return;
    }
    
    for(int i = 0; i<ratio.size(); i++){
        discounts[depth] = ratio[i];
        permutation(depth+1, max_depth);
    }
}

// users[i] : [비율, 가격] / 비율보다 큰 할인율 사고 / 가격보다 크면 플러스 가입 / size 100
// emoticions[i] : 정가, 100 배수 / 10, 20, 30, 40 할인 가능
vector<int> solution(vector<vector<int>> users, vector<int> emoticons) {
    userSize = users.size();
    emoticonSize = emoticons.size();
    Users = users;
    Emoticons = emoticons;
    
    permutation(0, emoticonSize);

    return {maxJoins, maxProfit};
}

실수한 점

  • 멤버십 인원이 더 큰 경우는 profit을 그 값으로 갱신해야 한다.
  • 멤버십 인원이 같은 경우에만 profit = max(profit, 계산한 profit)해야 했다.
  • 1번 조건과 2번 조건을 혼용해 예제에서 틀렸고, 다행히 예제에서 잡혀 오랜 시간 디버깅하지 않았다.

 

 

표현 가능한 이진 트리

 푸는 데 1시간 30분 걸렸다. 디버깅이 45분...

 일단 문제 조건에서 각 숫자가 $10^{15}$이기 때문에 모든 수를 만들어 두는 건 안 된다. 따라서 각 수가 makeable인지 판단해야 한다. 문제를 읽다보면 2진수로 어떻게 하면 되나? 라는 생각이 든다.

  • 일단 full binary tree의 vertex 개수를 저장한다. numbers 원소인 $10^{15}$ 직후까지. arr라고 하자.
    • $2^{1} - 1$, $2^{2} - 1$, $2^{3} - 1$, ...
    • 1, 3, 7, 15, ...로 이어진다.
  • 주어진 수 n을 2진수로 표현한다. O(logn)이 걸린다. n이 worst $10^{15}$인데 $2^{10}$이 약 $10^{3}$이므로 약 $2^{50}$임을 알 수 있다. 따라서 string으로 표기한다.
  • arr에서 [n을 2진수로 표현한 string].length의 lower bound를 찾는다 : O(log($2^{50}$)
  • [n을 2진수로 표현한 string]가 부족한 만큼 앞에 0을 채워 넣는다.
    • 예를 들어 1이라면 1로 끝난다.
    • 2라면 10이지만 full binary tree로 표련하기 위해서는 010로 바꾸어야 한다.
    • 3이라면 11이지만 full binary tree로 표현하기 위해 011로 바꾸어야 한다.
    • 4라면 100이므로 0을 추가하지 않아도 된다.
    • 8이라면 1000이므로 full binary tree로 표현하기 위해서는 0001000으로 표기해야 한다.
    • full binary tree로 표현하기 위해 부족한 vertex 개수를 채워 넣는 것이다.
  • 이제 이 string을 양쪽으로 search해서 subtree를 만들지 못하는 경우를 찾는다.
    • mid가 0인 경우 -> 범위 내 모든 게 0이어야 만들 수 있다. 그렇지 않다면 not makeable
    • mid가 1인 경우 -> 왼쪽, 오른쪽이 makeable해야 makeable이다. 왼쪽/오른쪽으로 recurse

 

 포인트는 full binary tree vertex 개수를 lower bound 해야 하고, 이후 양쪽으로 recurse하며 탐색해야 한다는 것이다.

 

 이걸 소스코드로 표현하면 아래와 같다.

typedef long long ll;

vector<ll> treeSizes; // full binary tree의 vertex 개수

// full binary tree vertex 개수 초기화
ll MAX_VALUE = 1000000000000000;
void init(){
    ll power = 2L, num = 1L;
    while(1){
        if(num >= MAX_VALUE) break;
        num *= power;
        treeSizes.push_back(num - 1);
    }
}

string toBinaryString(ll n){
    string result = "";
    while(n){
        result = to_string(n % 2) + result;
        n /= 2;
    }
    return result;
}

// 전체 size가 n이어야 할 때 앞자리에 0을 채워줌
string fillZeros(string str, ll n){
    while(str.length() != n){
        str = "0" + str;
    }
    return str;
}

// node 개수 n을 넣었을 때 full binary tree의 node 개수
ll getFullTreeSize(ll n){
    return *(lower_bound(treeSizes.begin(), treeSizes.end(), n));
}

// s.length()는 2^k-1으로 정해져 있음
bool isMakeable(string s, ll start, ll end){
    if(start == end) return true; // base case : size가 1이 경우 항상 makeable
    
    // *** 실수 : 0001000과 같은 경우를 생각하지 못함
    // 가운데가 0인데 나머지에 1이 있는 경우 : 못만듬. 전부 다 0이면 makeable
    // 가운데가 1인 경우 : 양 subtree를 만들 수 있어야 함
    ll mid = start + (end - start) / 2;
    if(s[mid] == '0'){
        ll left = start, right = end;
        while(left < mid){
            if(s[left] == '1' || s[right] == '1') return false;
            left++; right--;
        }
        return true;
    }
    
    bool left = isMakeable(s, start, mid-1);
    bool right = isMakeable(s, mid+1, end);
    
    if(left && right) return true; // 좌 우 모두 탐색 가능해야 full binary tree를 만들 수 있다.
    else return false;
}

vector<int> solution(vector<long long> numbers) {
    init();
    
    vector<int> answer;
    for(ll n : numbers){
        string binaryString = toBinaryString(n); // n의 2진수 string
        // n으로 full binary tree를 만든다고 생각하고 부족한 0을 채움
        binaryString = fillZeros(binaryString, getFullTreeSize(binaryString.length()));
        bool result = isMakeable(binaryString, 0, binaryString.length()-1); // *** 실수 : length()를 넣었음... -1 해야 함
        answer.push_back(result);
    }
    
    return answer;
}

실수한 점

  • isMakeable 함수는 [시작 index, 끝 index]를 넣기로 정의했는데 solution() 함수에서 넣을 때 length()를 넣었다. 이것 때문에 45분을 잡아먹었다.
  • 또, mid가 0이라도 그쪽에 있는 게 다 0이라면 makeable이다. 이걸 생각하지 못했다.

 

 

표 병합

 처음에 보자마자 reference같은 걸 써서 같은 걸 참조하면 안되나? 라고 생각했는데 unmerge때문에 힘들거라 생각했다. 해설을 보니 이 접근이 맞았고 union-find를 사용해서 merge하면 merge 하는 쪽의 root를 가리키게 만들고 값 수정 시 해당 값만 수정해 주면 되었다. unmerge 시 전체를 검색해서 root가 같은 걸 해제하면 되었다.

 

 command 길이는 1000이고, r, c 각각 50이므로 table 크기는 2500. 따라서 모든 command에서 r, c를 모두 순회해도 2,500,000으로 시간 내에 충분히 풀 수 있을 거라 생각하고 merge된 것들의 set을 두었다. set을 두면 전체 순회하지 않아도 find할 수 있기 때문에 이런 선택을 했다.

 나머지는 문제에서 주는 대로 구현이다. 유의할 점은 string을 split하는 거? 이건 어려운 것도 아니니 나중에 코테 친다고 하면 외워두긴 해야 한다.

  • linked는 merge된 cell들의 set이 있는 vector이다. 어떤 cell이 merge인지 아닌지 찾아보려면 linked를 순회하며 element의 존재 여부를 찾아야 한다. 이게 findLinkedIndex() 함수.
  • update 1번 : linked의 모든 set을 순회하며 어떤 set에 입력값 [r, c]가 있다면 그 set의 모든 값에 값을 변경해 준다.
  • update 2번 : 전체 board를 순회하며 값을 찾아 변경해 준다.
  • merge : [r1, c1], [r2, c2] 2개의 점이 주어진다. 
    • 1이 병합 된 상태 + 2가 병합 된 상태 : 2를 전부 빼서 1에 넣고 값 update
    • 1이 병합 된 상태 + 2가 병합되지 않은 상태 : 2를 1에 넣고 값 update
    • 1이 병합되지 않은 상태 + 2가 병합 된 상태 : 1을 2의 set에 넣고 값 update
    • 1이 병합되지 않은 상태 + 2가 병합되지 않은 상태 : 새로운 set을 만든다.
    • 이렇게 처리하고, 원래 있던 값에 따라 문제에서 주어진 대로 처리한다.
  • unmerge : findLinkedIndex() 함수를 이용해 linked를 찾고 그 안에 있는 모든 element의 값을 초기화한다.
  • print : [r, c]에 있는 것을 출력해 주기만 하면 된다.

 

 주어진 대로 구현하기만 하면 되었다.

typedef pair<int, int> pii;

// input을 공백 기준으로 split
vector<string> splitString(string input) {
    vector<string> result;
    istringstream iss(input);
    string buffer;
 
    while (getline(iss, buffer, ' ')) result.push_back(buffer);

    return result;
}

vector<set<pii>> linked; // linked : 병합된 cell들
string EMPTY = "EMPTY";
int tableSize = 51, NOT_FOUND = -1;
vector<vector<string>> table(tableSize, vector<string>(tableSize, EMPTY));
vector<string> printResult;

// [r, c]가 속해있는 linked set의 index. 없을 시 NOT_FOUND return
int findLinkedIndex(int r, int c){
    for(int i = 0; i<linked.size(); i++){
        if(linked[i].find({r, c}) != linked[i].end()) return i;
    }
    return NOT_FOUND;
}

void UpdateOP1(int r, int c, string value){
    int linkedIdx = findLinkedIndex(r, c);
    
    if(linkedIdx == NOT_FOUND) table[r][c] = value;
    else{
        for(pii cell : linked[linkedIdx]){
            table[cell.first][cell.second] = value;
        }
    }
}

void UpdateOP2(string value1, string value2){
    for(int i = 0; i < tableSize; i++){
        for(int j = 0; j < tableSize; j++){
            if(table[i][j] == value1) table[i][j] = value2;
        }
    }
}

/*
두 셀 모두 값을 가진 경우 -> r1, c1의 값으로 : merge 후 UpdateOp1 실행하면 될듯?
하나만 값을 가진 경우 : 그 값을 가짐
case가 많네...

1 병합 O + 2 병합 O : 2를 전부 빼서 1에 넣고, 값을 가지고 있는 경우 처리
1 병합 O + 2 병합 X : 2를 1에 넣고 값을 가지고 있는 경우 처리 UpdateOp1
1 병합 X + 2 병합 O : 1을 2의 set에 넣고 값을 가지고 있는 경우 처리
1 병합 X + 2 병합 X : 새로운 set 만들기 + 값 가지고 있는 경우 처리
*/
void MergeOP(int r1, int c1, int r2, int c2){
    if(r1 == r2 && c1 == c2) return; // *** 실수 : r1==c1으로 적음..하...

    int leftLinkedIdx = findLinkedIndex(r1, c1);    
    int rightLinkedIdx = findLinkedIndex(r2, c2);    
    string leftValue = table[r1][c1], rightValue = table[r2][c2];
    
    if(leftLinkedIdx != NOT_FOUND && rightLinkedIdx != NOT_FOUND){
        if(leftLinkedIdx == rightLinkedIdx) return; // *** 이미 merge되어 있는 것을 merge하지 않게 해야 함
        for(pii cell : linked[rightLinkedIdx]){
            linked[leftLinkedIdx].insert(cell);    
        }
        linked.erase(linked.begin() + rightLinkedIdx);
    }
    else if(leftLinkedIdx != NOT_FOUND && rightLinkedIdx == NOT_FOUND){
        linked[leftLinkedIdx].insert({r2, c2});
    }
    else if(leftLinkedIdx == NOT_FOUND && rightLinkedIdx != NOT_FOUND){
        linked[rightLinkedIdx].insert({r1, c1});
    }
    else{ // leftLinkedIdx == NOT_FOUND && rightLinkedIdx == NOT_FOUND
        set<pii> cells; cells.insert({r1, c1}); cells.insert({r2, c2});
        linked.push_back(cells);
    }
    
    if(leftValue == EMPTY && rightValue != EMPTY){
        UpdateOP1(r1, c1, rightValue);
    }
    else{
        UpdateOP1(r1, c1, leftValue);
    }
}

void UnmergeOP(int r, int c){
    string prevValue = table[r][c];
    int linkedIdx = findLinkedIndex(r, c);   
    if(linkedIdx != NOT_FOUND){
        for(pii cell : linked[linkedIdx]){
            table[cell.first][cell.second] = EMPTY;
        }
        linked.erase(linked.begin() + linkedIdx);
    }
    
    table[r][c] = prevValue;
}

void PrintOP(int r, int c){
    printResult.push_back(table[r][c]);
}

vector<string> solution(vector<string> commands) {
    for(string command : commands){
        vector<string> params = splitString(command);
        if(params[0] == "UPDATE"){
            if(params.size() == 4){ // 1번
                int r = stoi(params[1]), c = stoi(params[2]);
                UpdateOP1(r, c, params[3]);
            }
            else{ // 2번
                UpdateOP2(params[1], params[2]);
            }
        }
        else if(params[0] == "MERGE"){ // 3번
            int r1 = stoi(params[1]), c1 = stoi(params[2]);
            int r2 = stoi(params[3]), c2 = stoi(params[4]);
            MergeOP(r1, c1, r2, c2);
        }
        else if(params[0] == "UNMERGE"){ // 4번
            int r = stoi(params[1]), c = stoi(params[2]);
            UnmergeOP(r, c);
        }
        else if(params[0] == "PRINT"){ // 5번
            int r = stoi(params[1]), c = stoi(params[2]);
            PrintOP(r, c);
        }
    }

    return printResult;
}

실수한 점

  • 이미 merge된 [r1, c1]과 [r2, c2]를 merge한다면 아무것도 하지 않게 해야 했었다. 이 조건을 넣지 않는다면 2를 전부 빼서 1에 넣고, 해당 linked를 삭제해 버리기 때문에 오류가 난다.
  • 정신줄을 잡자. r1 == r2 && c1 == c2라고 적었어야 했는데 r1 == c1 && r2 == c2라고 적었다.

 

 

미로 탈출 명령어

 출발점에서 도착점까지 도달하는 keyword를 사전순으로 만들면 되는 문제. 처음에 문제를 잘못 보아서 "각 vertex를 2번만 방문할 수 있다"고 생각해서 DFS로 구현했다. 그러면 V = 5000, 각 위치마다 edge가 8개니 40000으로 시간 안에 풀 수 있을 것이라 생각했기 때문이다. 그러나 문제 조건은 "각 vertex를 2번 이상 방문할 수 있다"였다. 이렇게 되면 각 위치마다 4개의 경우의 수가 생겨 $4^{2500}$이 되어 무조건 TLE가 난다.

 

 추후 upsolving하며 문제를 잘못 읽은 걸 깨달아 기존 DFS를 수정했다.

 DFS 구현 시 사전순으로 direction을 설정해 둔다면, 만약 어떤 경로를 거쳐 도착점에 도달한다면 그 순간 최소 사전순 배열일 것이다. 그렇지 않다면 사전순으로 더 느린 탐색 결과가 도착점에 도달하기 때문. 따라서 첫 탐색이 성공한다면 그 값을 바로 리턴한다.

 또 하나의 pruning으로 다음 탐색할 위치에서 도착점까지 [k - 현재 이동거리] 안에 도달하지 못한다면 무슨 수를 쓰더라도 도착점까지 도달할 수 없다. 이 두 조건이 핵심이었다.

// 순서대로 n, m, r, c, k
int maxr, maxc, exitr, exitc, targetdist;
string IMPOSSIBLE = "impossible";
string result = IMPOSSIBLE;

int dr[4] = {1, 0, 0, -1};
int dc[4] = {0, -1, 1, 0};
vector<string> direction = {"d", "l", "r", "u"};

int getDist(int r1, int c1, int r2, int c2){
    return abs(r1 - r2) + abs(c1 - c2);
}

void DFS(int curDepth, int curr, int curc, string path){
    if(curDepth == targetdist){ // k만큼 탐색한 경우
        if(curr == exitr && curc == exitc){
            if(result == IMPOSSIBLE) result = path;
            else return;
        }
        return;
    }
    if(curDepth > targetdist) return;
    
    for(int i = 0; i<4; i++){
        int nextr = curr + dr[i], nextc = curc + dc[i];
        if(0 <= nextr && nextr < maxr && 0 <= nextc && nextc < maxc){
            // pruning *** 여기랑
            if(getDist(nextr, nextc, exitr, exitc) > (targetdist - curDepth)) continue;
            
            // deeper
            DFS(curDepth + 1, nextr, nextc, path + direction[i]);
            if(result != IMPOSSIBLE) return; // *** 여기
        }
    }
}

string solution(int n, int m, int x, int y, int r, int c, int k) {
    
    
    // initialize
    x--; y--; r--; c--; // zero-indexing
    maxr = n; maxc = m; exitr = r; exitc = c; targetdist = k;
    
    if((getDist(x, y, exitr, exitc) % 2) != (k %= 2)) return IMPOSSIBLE;
    
    DFS(0, x, y, "");
    return result;
}

실수한 점

  • 문제를 잘못 읽었다. 제발!

 

 

 추후 upsolving 시 바꾼 버전. 무조건 k만큼 탐색 후 도착 위치에 도달해 있어야 하며 사전순으로 제일 앞의 것을 탐색해야 한다. 결국 greedy 문제이다.

  • 현 위치에서 d로 갈 수 있으며 + d로 이동했을 때 거리가 k-1보다 작거나 같다면 탐색 가능하므로 그곳으로 이동
  • 현 위치에서 d로 갈 수 없을 때(즉 d로 이동했을 때 위치상 도착점에 도달하지 못하는 경우) l로 이동.
  • 현 위치에서 d, l로 갈 수 없을 때 r로 이동
  • 현 위치에서 d, l, r로 갈 수 없을 때 u로 이동

이 while문을 거지면 무조건 답이 나온다.

string IMPOSSIBLE = "impossible";
string result = "";

int getDist(int r1, int c1, int r2, int c2){
    return abs(r1 - r2) + abs(c1 - c2);
}
// d l r u

string solution(int n, int m, int x, int y, int r, int c, int k) {
    while(1){
        if(x == r && y == c && k == 0) break;
        if(getDist(x + 1, y, r, c) <= k-1 && 0 < x + 1 && x + 1 <= n){
            result += "d"; x++;
        }
        else if(getDist(x, y-1, r, c) <= k-1 && 0 < y - 1 && y - 1 <= m){
            result += "l"; y--;
        }
        else if(getDist(x, y+1, r, c) <= k-1 && 0 < y + 1 && y + 1 <= m){
            result += "r"; y++;
        }
        else if(getDist(x - 1, y, r, c) <= k-1 && 0 < x - 1 && x - 1 <= n){
            result += "u"; x--;
        }
        else return IMPOSSIBLE;
        k--;
    }
    return result == "" ? IMPOSSIBLE : result;
}

 

 

1, 2, 3 떨어트리기

 문제는 간단하지만 구현이 크게 2단계로 나뉘어서 빡센 문제.

 node size가 100이기 때문에 한 탐색에 worst case O(100)의 시간이 걸린다. target 값과 target의 길이도 100이기 때문에 각각의 탐색을 모든 target이 100이 될 때까지 한다고 해도 100 * 100 * 100 = 1,000,000으로 시간 내에 풀 수 있다. 따라서 brute-force 문제.

 

 구현은 크게 2단계로 나뉜다.

  • 무조건 1씩 떨어뜨려서 target을 만들 수 있는지 없는지 판정하는 부분
  • target을 만들 수 있다면 어떤 순서로 채워야 하는지 판정하는 부분

 

1. 첫째 단계

 먼저 [무조건 1씩 떨어트려서 target을 만들 수 있는지 없는지 탐색하는 부분]은 간단하다. 일단 brute-force 탐색을 위해 graph와 edge를 설정하고 각 탐색 시 다음 vertex를 가리키게 수정한다. vector<vector<int>> edge, vector<int> vertex와 DFS() 함수를 이용해 쉽게 구현할 수 있다. DFS 탐색 시 leaf인 경우 그 곳에 쌓인다.

 이렇게 쌓인 result vector의 값들을 target과 비교한다. 

  • result[i] > target[i]라면 target[i]를 만들 수 없다. 따라서 return IMPOSSIBLE
  • result[i] <= target[i] <= 3*result[i]라면 result[i]를 적절히 조절해 target을 표현할 수 있다. 주어지는 수가 1, 2, 3이기 때문. 모든 i에 대해 이게 성립한다면 return MAKEABLE
  • 아니라면, 즉 3 * result[i] < target[i]라면 아직 result가 덜 쌓인 것이므로 MAKING을 리턴하고 DFS를 더 순회한다.

 

2. 둘째 단계

 이렇게 쌓인 result vector를 vector<vector<int>> result로 바꾸어서 순회 결과로 쌓이는 결과를 저장한다. MAKEABLE이라면 사전순으로 오는 결과를 저장해야 하기 때문에 greedy하게 subproblem을 풀면 된다.

  • 앞에서부터 작은 수를 넣을 수 있다면 작은 수를 넣으면 된다.
  • 남은 숫자 개수가 1이라면 그 값을 넣어 주어야 한다.
  • 남은 숫자 개수가 2 이상인 경우
    • 현 위치에서 1을 택했을 때 남은 숫자들로 목표값을 채울 수 있는 경우 1을 택한다.
    • 현 위치에서 2을 택했을 때 남은 숫자들로 목표값을 채울 수 있는 경우 2을 택한다.
    • 현 위치에서 3을 택했을 때 남은 숫자들로 목표값을 채울 수 있는 경우 3을 택한다.

 

이렇게 만든 결과가 답이 된다.

int vsize;
vector<vector<int>> edge; // edge[i] : vertex i부터 출발하는 edge
vector<int> vertex; // vertex[i] : vertex i가 보고 있는 child index
vector<vector<int>> result; // 순회 결과로 쌓이는 결과
vector<int> Target; // target의 전역변수
int cnt = 1;

int MAKEABLE = 1, IMPOSSIBLE = 2, MAKING = 0;
int makeable(){
    for(int i = 0; i < result.size(); i++){
        if(result[i].size() > Target[i]) return IMPOSSIBLE;
        if(result[i].size() <= Target[i] && Target[i] <= 3 * result[i].size()) continue;
        else return MAKING;
    }
    return MAKEABLE;
}

int DFS(int curV){
    if(edge[curV].size() == 0){ // leaf
        result[curV-1].push_back(cnt); cnt++;
        return makeable();
    }
    
    // deeper
    int nextV = edge[curV][vertex[curV]];
    vertex[curV] = (vertex[curV] + 1) % edge[curV].size();
    return DFS(nextV);
}

vector<int> solution(vector<vector<int>> edges, vector<int> target) {
    vsize = edges.size() + 1;
    vertex.resize(vsize + 1);
    fill(vertex.begin(), vertex.end(), 0);
    edge.resize(vsize+1);
    for(vector<int> vi : edges){
        edge[vi[0]].push_back(vi[1]);
    }
    for(vector<int>& vi : edge) sort(vi.begin(), vi.end(), less<int>());
    result.resize(target.size());
    Target = target;
    
    while(1){
        int DFSresult = DFS(1);
        if(DFSresult == IMPOSSIBLE) return {-1};
        else if(DFSresult == MAKING) continue;
        else{ // MAKEABLE
            vector<int> answer(cnt-1, 0);
            
            for(int i = 0; i<result.size(); i++){
                int object = Target[i];
                for(int ridx = 0; ridx < result[i].size(); ridx++){
                    int numleft = result[i].size() - ridx;
                    int answeridx = result[i][ridx];
                    // 앞에서부터 작은 수를 너흥ㄹ 수 있다면 그 수를 넣으면 됨
                    // 넣을 수 있는 개수
                    if(numleft == 1){ // 1개짜리면 원래 값 넣어 주어야 함
                        answer[answeridx - 1] = object;
                    }
                    else if(object - 1 <= 3*(numleft - 1)){ // 1을 넣었을 때 나머지 값으로 충당 가능
                        answer[answeridx - 1] = 1;
                        object -= 1;
                    }
                    else if(object - 2 <= 3 * (numleft - 1)){ // 2를 ~
                        answer[answeridx - 1] = 2;
                        object -= 2;
                    }
                    else{ // 3을 ~
                        answer[answeridx - 1] = 3;
                        object -= 3;
                    }
                }
            }
            return answer;
        }
    }
    
    return {-1};
}

 

 

총평

문제를 푼 직후

 4솔로, 합격 기준에는 넘어가지만 아슬아슬했다. 특히 4번 문제 디버깅 못했으면 그냥 떨어졌을 거 아냐. 아찔하다... 문제 푸는 속도가 아직 너무 느리고, 반례 생각을 너무 안하는 것 같다. (반례를 생각하는 게 너무 어렵다) 2번, 3번, 4번, 5번 전부 다 반례 하나때문에 시간을 훨씬 많이 썼다. indexing 실수는 없지만 문제가 주는 조건을 너무 긍정적으로 생각해서 이렇게 되었다.

 또 논리는 맞게 생각했지만 타이핑치면서 잘못 적은 부분도 많다. 코드를 적고 나면 검토를 하자...

 

upsolving하며

 6번은 문제를 잘못 읽어서(...) 못 풀었다. 접근은 맞았고, 최대 2번 visit 가능하다고 읽어서 이렇게 되었다.. 문제를 꼼꼼하게 읽자... 아.. 진짜 ㅋㅋ 사전순 정렬 순서대로 DFS하고, 만약 [어떤 결과가 만들어졌다면 그것보다 빠른 경우가 존재하지 않으므로 return]해버리면 된다. + [현 위치에서 정답까지 가지 못한다면 자르기] 2개의 기법만 사용하면 바로 풀린다. 그리고 visit 방식에 제한이 없었다면 처음부터 greedy로 풀었을 것 같다.

 5번 문제는 코드 잘못된 거 찾았어도 붙잡고 있었을 것 같다. 이미 merge된거에 merge를 안한다는 조건은 없었지만 당연히 없을거라 유추하고 코드를 작성해서 예상치 못한 결과가 나왔다.

 자잘자잘한 실수만 조금 줄이고, 문제를 꼼꼼하게 읽으면서 문제가 주는 제한조건을 확실하게 읽자. - 그 후에 이런 입력은? 저런 입력은? 고민해 봐야 한다. 사실상 문제 똑바로 읽었으면 6번도 솔이고 조금 더 꼼꼼하게 생각했다면 5번도 솔이다.

 지금 코테 실력은 어느 정도 궤도에 올라가 있는 것 같다. 그렇게 못하지는 않는 것 같은데, 막상 까 보니 그렇게 잘하는 것 같지도 않다. 딱 커트라인에 아슬아슬하게 걸치는 것 같다. 조금 더 해서 안정적으로 통과할 수 있게 해 보자.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

저작자표시 (새창열림)

'PS > PS Log' 카테고리의 다른 글

23.03.14. 풀었던 문제들  (0) 2023.03.14
23.03.13. 풀었던 문제들  (0) 2023.03.13
23.03.11. 풀었던 문제들  (0) 2023.03.11
23.03.10. 풀었던 문제들  (0) 2023.03.10
23.03.09. 풀었던 문제들  (0) 2023.03.09
    hyelie
    hyelie

    티스토리툴바