알고리즘/프로그래머스 2단계

프로그래머스 - [1차] 캐시 - C++

게임만드는학생 2024. 7. 31. 19:20

https://school.programmers.co.kr/learn/courses/30/lessons/17680

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

 

#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;

int solution(int cacheSize, vector<string> cities) {
    int answer = 0;

    map<string, int> m;
    vector<pair<int, string>> cacheList;

    if (cacheSize == 0)
        return cities.size() * 5;

    for (int i = 0; i < cities.size(); i++)
        transform(cities[i].begin(), cities[i].end(), cities[i].begin(), ::tolower);
    int i;
    for (i = 0; m.size()<cacheSize; i++)
    {
        if (m.find(cities[i]) != m.end())
        {
            answer += 1;
            m[cities[i]] = i;
            for (int j = 0; j <cacheList.size(); j++)
                if (cacheList[j].second == cities[i])
                    cacheList[j].first = i;
        }
        else
        {
            answer += 5;
            m.insert({ cities[i],i });
            cacheList.push_back({ i,cities[i] });
        }

    }

    for (; i < cities.size(); i++)
    {
        // 캐시 히트
        if (m.find(cities[i]) != m.end())
        {
            answer += 1;
            m[cities[i]] = i;
            for (int j = 0; j < cacheSize; j++)
                if (cacheList[j].second == cities[i])
                    cacheList[j].first = i;
        }
        // 캐시 미스
        else
        {
            answer += 5;
            int minId = 0;
            // 가장오래된거 빼고 새로 넣어야함
            for (int j = 1; j < cacheSize; j++)
            {
                if (cacheList[minId].first > cacheList[j].first)
                    minId = j;
            }
            m.erase(cacheList[minId].second);
            cacheList[minId] = make_pair(i, cities[i]);

            m.insert({ cities[i],i });
        }
    }

    return answer;
}

 

먼저 이 문제를 풀려면 LRU(Least Recently Used) 를 알아야 한다. 

간단하게 말하자면 캐시교체 알고리즘 중 하나로, 가장 오랫동안 참조되지 않은 페이지를 교체하는 방식이다. 

 

이 방식으로 동작하는 캐시가 있을 때, 총 실행시간을 구하는 문제이다. 

캐시의 크기와 도시들의 이름이 벡터로 주어진다. 

 

먼저 도시를 map으로 저장하였다. 

그리고 가장 오래된 페이지를 찾기위해서 순회를 해야되기 때문에 vector를 하나 더 만들었다. 

원소들은 pair<int, string> 으로 언제 참조되었는지와 도시 이름으로 구성되었다. 

 

크게 두개로 for문이 나뉘었는데 첫번째는 캐시가 다 차기전의 동작이고, 두번째는 캐시가 다 찼을 때의 동작이다. 

캐시 히트일때는 map과 vector에서 해당 페이지의 참조시간을 현재로 바꾼다.

캐시 미스일때는 캐시가 다 차지 않았기 때문에 바로 추가한다. 

 

두번째 for문에서는 똑같이 동작하는데 캐시 미스일때 해당 페이지를 삭제후, 새로운 페이지를 캐시에 저장한다.

 

chatgpt한테 물어보니 자료구조를 바꾸면 더 효율적으로 구성이 가능하다.

 

먼저 map은 이진탐색트리로 구성돼있으며 O(logN)의 시간복잡도이다. 

하지만 unordered_map 은 해시맵으로 구성돼있으며 평균적으로 O(1) 이다. 

 

또 vector를 통해 전체를 순회하기 보다 list로 구성해 최근참조한 것을 맨앞으로 옮기는 과정으로 해서 순회를 없앴다. 

또한 지금처럼 삭제, 삽입이 빈번한 경우 list가 더 효과적이다. 이중리스트로 구성돼있어 해당 포인터만 알고있다면 o(1)의 성능이지만 vector의 경우 해당 요소이후 모든 요소를 이동시켜야하기 때문이다. 

 

그리고 unordered_map에서 여기 필요한 포인터를 값으로 가지면된다. 

 

#include <string>
#include <vector>
#include <list>
#include <unordered_map>
#include <algorithm>

using namespace std;

int solution(int cacheSize, vector<string> cities) {
    if (cacheSize == 0) {
        return cities.size() * 5;
    }

    list<string> cache;
    unordered_map<string, list<string>::iterator> cache_map;
    int time = 0;

    for (auto& city : cities) {
        transform(city.begin(), city.end(), city.begin(), ::tolower);
        if (cache_map.find(city) != cache_map.end()) {
            // Cache hit
            time += 1;
            cache.erase(cache_map[city]);
            cache.push_front(city);
            cache_map[city] = cache.begin();
        } else {
            // Cache miss
            time += 5;
            if (cache.size() == cacheSize) {
                // Remove the least recently used item
                cache_map.erase(cache.back());
                cache.pop_back();
            }
            // Add the new city to the cache
            cache.push_front(city);
            cache_map[city] = cache.begin();
        }
    }

    return time;
}

자료구조를 무엇을 쓸지 또 언제쓸지 각 자료구조에 대해 학습하고 잘 파악해놔야겠다.