https://programmers.co.kr/learn/courses/30/lessons/42893

 

코딩테스트 연습 - 매칭 점수

매칭 점수 프렌즈 대학교 조교였던 제이지는 허드렛일만 시키는 네오 학과장님의 마수에서 벗어나, 카카오에 입사하게 되었다. 평소에 관심있어하던 검색에 마침 결원이 발생하여, 검색개발팀

programmers.co.kr

 

문제 풀이

이 문제는 카카오답게 문자열을 다루는 스킬을 확인하는 문제인 것 같다. 이 문제에서 찾아야할 문자열은 url, 검색하고자 하는 단어, 링크가 걸린 url을 찾아야한다. 각각 다음과 같은 방식으로 찾는다.

 

1. url 검색 : url이 담긴 문자열은 다음과 같이 시작한다.

<meta property="og:url" content="https://

이 문자열로 시작하는 부분을 찾아 content="________"에 밑줄에 해당하는 부분을 저장한다.

 

2. 검색하고자 하는 단어 : 대문자로 한 번, 소문자로 한 번 찾는다.

대문자로 문자를 바꿔주는 toupper()와 소문자로 문자를 바꿔주는 tolower를 이용해 word의 첫 문자를 대문자, 소문자로 찾아 일치하는 단어가 있는지 비교한다. 비교는 모든 문자열을 대문자로 바꿔 비교한다.

 

3. 링크가 걸린 url : 링크 주소 문자열은 다음과 같이 시작한다.

<a href=\"https://

이 문자열로 시작하는 부분을 전부 찾아 자신을 가리키는 페이지의 인덱스를 저장하는 배열에 넣어주고 현재 페이지에서 가리키는 링크의 수를 증가시킨다.

 

위의 과정대로 짠 코드이다.

 

소스 코드

#include <string>
#include <iostream>
#include <vector>
#include <map>
#include <set>
using namespace std;

string url[21]; //index - url 매핑
double basic[21]; //기본점수
int linknum[21]; //각 노드의 외부링크
map<string, set<int>> links; //자기를 가리키는 링크 저장

bool isSame(string &a, string &b) { //문자열 비교 함수
    for(int i = 0; i < a.length(); i++) {
        if(!isalpha(b[i])) { // 알파벳이 아니면 false 리턴
            return false;
            break;
        }
        if(toupper(a[i]) != toupper(b[i])) { //같지 않으면 false 리턴
            return false;
            break;
        }
    }
    return true;
} 

int solution(string word, vector<string> pages) {
    int answer = 0;
    double mnum = 0;
    for(int i = 0; i < pages.size(); i++) {
        string &now = pages[i];
        //url찾기
        int start = now.find("<meta property=\"og:url\" content=\"https://"); //url이 담긴 문자열 검색
        start += 33; //url 시작 주소로 이동
        int end = now.find("\"", start); //"로 끝나는 위치 저장
        string nowurl = now.substr(start, end - start); //url 저장
        url[i] = nowurl;
        
        
        start = now.find("<html");
        string body = now.substr(start);
        int idx = 0;
        while(idx < body.size()) { //기본 점수 계산 소문자로 찾기
            int pos = body.find(tolower(word[0]), idx);
            if(pos == string::npos) break; //단어 없으면 종료
            if(pos + word.size() > body.size()) break; //범위 벗어나면 종료
            string tmp = body.substr(pos, word.size()); //길이를 벗어나면 break
            if(word.size() == tmp.size() && isSame(word, tmp)) {//길이가 같은 문자열 저장
                if(!isalpha(body[pos-1]) && !isalpha(body[pos + word.size()])) //앞 뒤로 알파벳이 없을 때 증가
                    basic[i]+=1;
            }
            idx = pos+1;
        }
        idx = 0;
        while(idx < body.size()) { //기본 점수 계산 대문자로 찾기
            int pos = body.find(toupper(word[0]), idx); //첫 글자가 같은 위치 검색
            if(pos == string::npos) break; //단어 없으면 종료
            if(pos + word.size() > body.size()) break; //길이를 벗어나면 break
            string tmp = body.substr(pos, word.size()); //길이가 같은 문자열 저장
            if(word.size() == tmp.size() && isSame(word, tmp)) { //사이즈도 같고 같은 문자일 때
                if(!isalpha(body[pos-1]) && !isalpha(body[pos + word.size()])) //앞 뒤로 알파벳이 없을 때 증가
                    basic[i]+=1;
            }
            idx = pos+1; //다음 위치부터 다시 검색
        }
        idx = 0;
        while(idx < body.size()) { //링크 주소 찾기
            int start = body.find("<a href=\"https://", idx);
            if(start == string::npos) break;
            start += 9;
            int end = body.find("\"", start);
            string link = body.substr(start, end - start); //링크 문자열
            links[link].insert(i); //자신을 가리키는 페이지 인덱스에 insert
            linknum[i]++; //현재 페이지의 링크 수 증가
            idx = end;
        }
    }

    for(int i = 0; i < pages.size(); i++) { //최고점 찾기
        double score = basic[i]; //기본 점수
        for(auto it = links[url[i]].begin(); it != links[url[i]].end(); it++) {
            score += ((basic[*it]) / linknum[*it]); //링크 점수 더하기
        }
        if(mnum < score) { //최대값 갱신
            mnum = score;
            answer = i;
        }
    }
    


    return answer;
}

index - url 매핑한 url 배열, 기본점수를 저장할 basic 배열, 각 노드의 외부 링크 수를 저장할 linknum배열, 자신을 가리키는 링크의 인덱스를 저장할 links map을 선언하였다.

 

주의할 점과 실수한 부분

이 문제는 문자열을 파싱하는 부분이 조금 까다로웠고 점수가 double형으로 계산되어야 했다. 처음 int형으로 basic을 저장하고 캐스팅하여 점수를 계산하였을 때 몇 개의 테스트케이스를 통과하지 못했었다. 그 다음 최대값을 저장한 mnum 변수도 int형으로 선언되어있었는데 이것 때문에 8번, 17번만 통과를 하지 못하고 있었다. 이 실수를 모르고 계속 다른 부분에서 오류를 찾아 시간낭비를 많이 했다. mnum도 double로 바꾸는 순간 모든 테스트 케이스도 통과할 수 있었다.

 

채점 결과

Mutex란?

  • Mutual exclusion의 줄임말이다. lock을 건다는 말과 같다.
  • 스레드간에 동기화와 여러 스레드에서 같은 메모리에 쓸 때 공유되는 데이터를 보호한다.
  • mutex 변수는 공유 데이터를 접근하는 것을 보호하기 위해 lock처럼 사용된다.
  • mutex변수를 공유하는 스레드 중 하나의 스레드만이 mutex 변수에 lock을 걸 수 있다.
  • 공유된 변수를 번갈아가며 사용할 때 사용된다 (race condition 방지)

Mutex의 사용 예시

위 그림과 같이 은행 데이터베이스에 shyeon은 1000원, gildong은 1500원이 있다고 하자. 이때 A에서 shyeon에게 500원을 송금하고자하고 동시에 B에서 700원을 송금하고자 한다. 이때 무슨 일이 일어날까? 각각 A, B에서는 이런 일이 발생할 것이다.

  • 은행 DB로부터 shyeon의 계좌 정보를 변수에 저장한다.
  • 그 변수에 500원을 더한다.
  • 은행 DB에 계산 결과값을 저장한다.

A, B에서 위의 일이 동시에 발생한다면 A, B 모두 은행 계좌로부터 shyeon의 돈은 1000원이 있다고 읽어오고 A는 1000원에 500원을 더한 1500원을 은행 DB에 보낼 것이고 B는 700원을 더한 1700원을 DB에 보낼 것이다. 이때 더 늦게 도착하는 값으로 DB에 있는 shyeon의 잔고가 갱신될 것이다. 이런 상황을 race condition이라고 말한다. 어떤게 먼저 도착하냐에 따라 값이 달라지는 상황이다. 이런 일을 방지하려면 어떻게 해야할까? A나 B가 순서를 정하여 DB에 접근하면 된다. 이런 순서를 정하기 위해 mutex가 사용된다.

 

mutex 관련 함수

1. pthread_mutex_init

int pthread_mutex_init(ptrhead_mutex_t* mutex, const pthread_mutexatt_t *attr);

    mutex변수를 초기화하는데 사용하는 변수이다. mutex변수를 선언하여 첫 번째 인자에 주소값을 넘겨 초기화 할 수 있다. attr인자는 NULL로 하면 가본 속성으로 생성할 수 있다. 리눅스에서는 기본값인 NULL값을 사용한다. mutex변수를 초기화하는 또다른 방법으로는 다음과 같이 선언하면 된다.

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    위의 코드처럼 mutex변수에 직접 대입하는 방법이다. 이 방법은 attr인자를 사용할 수 없다는 단점이 있지만 리눅스에서는 고려하지 않는다.

 

2. pthread_mutex_destroy

int pthread_mutex_destroy(pthread_mutex_t *mutex);

    사용이 끝난 mutex 변수를 해제하는 함수이다. malloc으로 동적으로 선언한 mutex 변수는 free()를 호출하기 전에 꼭 pthread_mutex_destroy()를 먼저 호출해야한다.

 

3. pthread_mutex_lock

int pthread_mutex_lock(pthread_mutex_t *mutex);

    mutex 변수에 lock을 거는 함수이다. 두 스레드의 함수에서 같은 mutex변수에 대해 각각 pthread_mutex_lock을 호출할 경우 두 스레드 중 하나의 스레드만 mutex 변수에 대해 lock을 얻고 다른 스레드에서는 pthread_mutex_lock에서 기다리고 있다. 이때 mutex를 얻은 스레드에서 pthread_mutex_unlock()을 호출할 경우 다른 스레드에서 mutex변수에 lock을 걸 수 있다.

 

예제

아래 코드는 두 스레드에서 공유하는 하나의 변수에 번갈아가며 출력하는 예제이다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int value;

void *func1(void *arg) { //1번 스레드가 실행할 함수
	for(int  i = 0; i < 5; i++) {
		pthread_mutex_lock(&mutex); //mutex 변수 lock(다른 변수에서 접근 불가능)
		printf("thread1 : %d\n", value); //변수 출력
		value++; //변수 증가
		pthread_mutex_unlock(&mutex); //mutex 변수 unlock(다른 변수에서도 접근 가능)
		sleep(1);
	}

	pthread_exit(NULL); //스레드 함수 종료
}

void *func2(void *arg) { //2번 스레드가 실행할 함수
	for(int i = 0; i < 5; i++) { 
		pthread_mutex_lock(&mutex); //mutex 변수 lock(다른 변수에서 접근 불가능)
		printf("thread2 : %d\n", value); //변수 출력
		value++; //변수 증가
		pthread_mutex_unlock(&mutex); //mutex 변수 unlock(다른 변수에서 접근 가능)
		sleep(1);
	}

	pthread_exit(NULL); //스레드 함수 종료
}

int main() {
	pthread_t tid1, tid2; //스레드 변수 선언

	value = 0; //공유 변수 초기화

	if(pthread_create(&tid1, NULL, func1, NULL) != 0) { //1번 스레드 생성
		fprintf(stderr, "pthread create error\n");
		exit(1);
	}

	if(pthread_create(&tid2, NULL, func2, NULL) != 0) { //2번 스레드 생성
		fprintf(stderr, "pthread create error\n");
		exit(1);
	}

	if(pthread_join(tid1, NULL) != 0) { //1번 스레드 종료 후 리소스 회수
		fprintf(stderr, "pthread join error\n");
		exit(1);
	}

	if(pthread_join(tid2, NULL) != 0) { //2번 스레드 종료 후 리소스 회수
		fprintf(stderr, "pthread join error\n");
		exit(1);
	}

	pthread_mutex_destroy(&mutex); //mutex 변수 해제

	exit(0);
}

 

실행 결과

 

mutex lock 실행 결과
mutex lock 주석처리 후 실행 결과

    첫 번째 사진은 mutex lock을 사용한 결과이고 두 번째 사진은 pthread_mutex_lock(), pthread_mutex_unlock()부분을 주석처리하고 실행한 결과이다. 실행 결과 1번 사진과 같이 1번 스레드와 2번 스레드가 번갈아가며 변수를 증가하며 출력하는 것을 볼 수있다. 2번 사진은 공유된 변수에 동시에 접근하여 같이 출력되는 것을 알 수 있다. 이렇게 여러 스레드가 공유 변수에 접근하고자할 때 race condition을 방지하기 위해 mutex를 사용할 수 있다.

 

    pthread 라이브러리를 사용할 때는 컴파일할 때 -lpthread를 포함해줘야한다. 그렇지 않으면 에러가 발생하고 컴파일이 되지 않는다.

 

개발 환경

wsl2 Ubuntu-20.04

gcc (Ubuntu 9.3.0-17ubuntu1~20.04)

https://www.acmicpc.net/problem/2110

 

2110번: 공유기 설치

첫째 줄에 집의 개수 N (2 ≤ N ≤ 200,000)과 공유기의 개수 C (2 ≤ C ≤ N)이 하나 이상의 빈 칸을 사이에 두고 주어진다. 둘째 줄부터 N개의 줄에는 집의 좌표를 나타내는 xi (0 ≤ xi ≤ 1,000,000,000)가

www.acmicpc.net

문제

도현이의 집 N개가 수직선 위에 있다. 각각의 집의 좌표는 x1, ..., xN이고, 집 여러개가 같은 좌표를 가지는 일은 없다.

도현이는 언제 어디서나 와이파이를 즐기기 위해서 집에 공유기 C개를 설치하려고 한다. 최대한 많은 곳에서 와이파이를 사용하려고 하기 때문에, 한 집에는 공유기를 하나만 설치할 수 있고, 가장 인접한 두 공유기 사이의 거리를 가능한 크게 하여 설치하려고 한다.

C개의 공유기를 N개의 집에 적당히 설치해서, 가장 인접한 두 공유기 사이의 거리를 최대로 하는 프로그램을 작성하시오.

 

예제

풀이

    이 문제는 이분 탐색으로 접근해야 하는 문제이다. 이분 탐색으로 푸는 문제는 문제 내용에서 힌트를 얻을 수 있다. 이분 탐색 문제는 최대나 최소를 구하는 문제가 많이 나온다. 이 문제도 "C개의 공유기를 N개의 집에 적당히 설치해서, 가장 인접한 두 공유기 사이의 거리를 최대로 하는 프로그램을 작성하시오."라고 나와있다. 이 부분에서 이분탐색을 이용해야한다는 것을 알아차릴 수 있다.

    이 문제에서 mid로 설정해야할 값은 공유기 사이의 거리이다. 즉 mid보다 큰 차이가 나면 공유기를 하나 설치해야한다는 것이다. 따라서 설치해야하는 공유기의 수를 매번 계산하여 C개의 공유기가 설치될 때의 최대값을 구할 수 있다.

 

소스 코드

#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

vector<int> arr;

int main() {
    int N, C; cin >> N >> C;
    for(int i = 0; i < N; i++) {
        int a; cin >> a;
        arr.push_back(a);
    }
    sort(arr.begin(), arr.end());
    int left = 1, right = arr[arr.size() - 1] + 1;
    for(int i = 0; i < 30; i++) {
        int mid = (left + right) / 2;
        int pos1 = arr.front();
        int pos2;
        int wifi = 1;
        for(int j = 1; j < arr.size(); j++) {
            pos2 = arr[j];
            int dist = pos2 - pos1;
            if(dist >= mid) {
                wifi++;
                pos1 = pos2;
            }
        }
        if(wifi >= C) 
            left = mid;
        else
            right = mid;
    }
    cout << left;
}

문제에서 죄표의 최대 값은 10^9이다. 이 값은 2^30보다 작은 값이므로 이분 탐색을 하였을 때 30번만 반복한다면 충분히 커버할 수 있다. 초기 left 값은 1, right값은 가장 큰 인덱스에 1을 더한 값이다. 1을 더한 이유는 정답은 left인데 만약 마지막 인덱스가 정답일 경우 left가 도달하지 못할 수 있기 때문이다. 이 문제는 조건을 만족하는 최대값을 구하는 문제이므로 공유기의 수가 C와 같을 경우에도 left에 mid값을 넣어준다. 그렇다면 C와 같을 때에도 left값은 이를 만족하는 조건에서의 최대 값을 가질 수 있다.

 

채점 결과

https://www.acmicpc.net/problem/13460

 

13460번: 구슬 탈출 2

첫 번째 줄에는 보드의 세로, 가로 크기를 의미하는 두 정수 N, M (3 ≤ N, M ≤ 10)이 주어진다. 다음 N개의 줄에 보드의 모양을 나타내는 길이 M의 문자열이 주어진다. 이 문자열은 '.', '#', 'O', 'R', 'B'

www.acmicpc.net

문제

스타트링크에서 판매하는 어린이용 장난감 중에서 가장 인기가 많은 제품은 구슬 탈출이다. 구슬 탈출은 직사각형 보드에 빨간 구슬과 파란 구슬을 하나씩 넣은 다음, 빨간 구슬을 구멍을 통해 빼내는 게임이다.

보드의 세로 크기는 N, 가로 크기는 M이고, 편의상 1×1크기의 칸으로 나누어져 있다. 가장 바깥 행과 열은 모두 막혀져 있고, 보드에는 구멍이 하나 있다. 빨간 구슬과 파란 구슬의 크기는 보드에서 1×1크기의 칸을 가득 채우는 사이즈이고, 각각 하나씩 들어가 있다. 게임의 목표는 빨간 구슬을 구멍을 통해서 빼내는 것이다. 이때, 파란 구슬이 구멍에 들어가면 안 된다.

이때, 구슬을 손으로 건드릴 수는 없고, 중력을 이용해서 이리 저리 굴려야 한다. 왼쪽으로 기울이기, 오른쪽으로 기울이기, 위쪽으로 기울이기, 아래쪽으로 기울이기와 같은 네 가지 동작이 가능하다.

각각의 동작에서 공은 동시에 움직인다. 빨간 구슬이 구멍에 빠지면 성공이지만, 파란 구슬이 구멍에 빠지면 실패이다. 빨간 구슬과 파란 구슬이 동시에 구멍에 빠져도 실패이다. 빨간 구슬과 파란 구슬은 동시에 같은 칸에 있을 수 없다. 또, 빨간 구슬과 파란 구슬의 크기는 한 칸을 모두 차지한다. 기울이는 동작을 그만하는 것은 더 이상 구슬이 움직이지 않을 때 까지이다.

보드의 상태가 주어졌을 때, 최소 몇 번 만에 빨간 구슬을 구멍을 통해 빼낼 수 있는지 구하는 프로그램을 작성하시오.

 

입력 예시

풀이

이 문제는 방문 빨간 공과 파란 공의 위치가 바뀌었을 때 정확히 어디에 위치하는지를 잘 계산하여야한다. 문제를 본 순간 DFS를 이용하여 풀어야겠다고 생각하였다. 하지만 BFS를 이용하면 더 간단한 풀이가 가능했다. 직접 구현한 코드는 DFS를 이용하였다. DFS를 이용하여 빨간 공, 파란 공이 상하좌우로 기울어졌을 때 멈추는 지점을 계산하였고 공이 동시에 빠지는 지를 판단하였다. 파란 공만 빠지는 경우를 생각해야하므로 파란 공을 먼저 생각하였다. 코드의 실행은 다음과 같다.

 

  1. 파란 공이 멈출 때까지 한 방향으로 이동한다.
  2. 만약 파란 공이 구멍에 빠진 경우 다른 방향으로 계속한다.
  3. 만약 빨간 공이 부딪힌 경우 체크한다. (만약 빨간공이 구멍에 빠질 경우 같이 빠질 수 있고 빨간 공이 멈춘 경우 빨간 공 옆에 멈출 것이기 때문에 지금 위치를 판단할 수 없다)
  4. 파란공이 멈춘 위치를 저장하고 빨간공과 부딪히지 않은 경우에만 위치를 배열에서 수정(부딪힌 경우 빨간공에 의해 위치가 결정되므로)
  5. 빨간 공이 멈출 때까지 같은 방향으로 이동한다.
  6. 만약 구멍에 빠졌을 경우
    • 아까 체크한 파란공이 빨간공과 부딪힌 경우는 파란 공을 원래 위치로 되돌리고 계속 무시
    • 체크되지 않을 경우 depth가 최소값인 경우 정답을 갱신하고 다른 방향으로 계속
  7. 빨간 공이 멈춘 위치를 저장하고 빨간공 위치를 배열에서 수정
  8. 만약 파란 공이 빨간 공과 부딪힌 경우 파란공 위치를 다 빨간공 옆으로 수정하고 배열에서도 위치 수정
  9. DFS로 수정된 빨간 공과 파란 공을 탐색 계속
  10. 만약 현재 빨간 공과 파란 공의 위치가 이전에 방문했을 때의 회전 횟수보다 많은 경우는 리턴하여 불필요한 재탐색을 하지 않는다.
  11. 그렇지 않은 경우 현재 빨간 공, 파란 공의 위치일 때의 회전 횟수를 갱신하고 탐색을 계속한다.
  12. 탐색이 끝난 경우 빨간 공, 파란 공 위치를 다시 되돌리고 다른 방향으로 계속한다.

위 실행 순서를 구현한 코드이다.

코드

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

const int MN = 11;

string arr[MN];

int dx[4] = {1, 0, 0, -1};
int dy[4] = {0, 1, -1, 0};

int visit[MN][MN][MN][MN];

int ans = 11;

void dfs(pair<int,int> red, pair<int,int> blue, int depth) {
    if(depth > 10) return; //10번 이상 움직인 경우 리턴
    int redx = red.first, redy = red.second;
    int bluex = blue.first, bluey = blue.second;
    if(visit[redx][redy][bluex][bluey] <= depth) return; //이미 방문한 경우 리턴
    
    visit[redx][redy][bluex][bluey] = depth;

    for(int d = 0; d < 4; d++) { //네 가지 방향으로
        pair<int,int> nblue; //파란공 이동
        int nx = bluex + dx[d]; //다음 파란공 위치
        int ny = bluey + dy[d];
        bool chk = false; //빨간공과 부딪히는지 여부 판단
        while(arr[nx][ny] == '.') { //멈출 때까지
            nx += dx[d];
            ny += dy[d];
        }
        if(arr[nx][ny] == 'O') continue; //파란 공이 구멍에 빠진 경우 계속
        if(arr[nx][ny] == 'R') { //만약 빨간 공을 부딪힌 경우
            chk = true; //부딪혔다고 표시
        }
        nx -= dx[d]; //멈춘 곳으로 위치
        ny -= dy[d];
        nblue = {nx, ny}; //파란공 위치 저장
        
        if(!chk) { //부딪히지 않았을 경우에만 수정
            arr[bluex][bluey] = '.'; //파란공 위치 수정
            arr[nx][ny] = 'B';
        }

        pair<int,int> nred; //빨간공 이동
        nx = redx + dx[d]; //다음 빨간공 위치
        ny = redy + dy[d];
        
        while(arr[nx][ny] == '.') { //멈출 때까지
            nx += dx[d];
            ny += dy[d];
        }
        if(arr[nx][ny] == 'O') { //구멍에 빠진 경우
            if(!chk) {//파란 공이 같이 빠지지 않을 경우에만 정답 갱신
                ans = min(ans, depth+1);
                arr[nblue.first][nblue.second] = '.'; //파란 공 위치 원래대로 돌리고 계속
                arr[bluex][bluey] = 'B';
            }
            continue;
        }
        nx -= dx[d]; //빨간공 멈춘 곳으로 저장
        ny -= dy[d];
        nred = {nx, ny}; //빨간공 위치 저장
        arr[redx][redy] = '.'; //빨간공 위치 수정
        arr[nx][ny] = 'R';

        if(chk) { //만약 파란공이 빨간공과 부딪힌 경우
            nblue.first = nx - dx[d]; //파란공 위치를 빨간공 옆으로 수정
            nblue.second = ny - dy[d];
            arr[nblue.first][nblue.second] = 'B'; //파란공 위치 표시
            arr[bluex][bluey] = '.';
        }
        dfs(nred, nblue, depth+1); //dfs로 탐색 계속
        arr[nred.first][nred.second] = '.'; //빨강, 파란공 위치 처음으로 되돌리기
        arr[nblue.first][nblue.second] = '.';
        arr[redx][redy] = 'R';
        arr[bluex][bluey] = 'B';
    }
}

int main() {
    int N, M; cin >> N >> M;
    fill(&visit[0][0][0][0], &visit[MN-1][MN-1][MN-1][MN], 11); //방문 배열 초기화
    pair<int,int> red;
    pair<int,int> blue;
    for(int i = 0; i < N; i++) {
        cin >> arr[i];
        for(int j = 0; j < arr[i].size(); j++) {
            if(arr[i][j] == 'R') { //빨간공 위치 저장
                red = {i, j};
            }
            if(arr[i][j] == 'B') { //파란공 위치 저장
                blue = {i, j};
            }
        }
    }
    dfs(red, blue, 0);

    if(ans > 10) cout << -1; //10번 이상 걸린 경우 -1출력
    else cout << ans; //그렇지 않은 경우 걸린 횟수 출력
}

복잡해보이고 눈에 확 들어오는 코드는 아니지만 스스로 판단하기에 메모이제이션을 이용해 불필요한 탐색을 줄였다고 생각한다.

생각하지 못한 반례들이 꽤 있어서 질문검색에 있는 반례들을 많이 참고하였다. 빨간공의 좌표, 파란공의 좌표를 출력해보며 잘못 된 부분을 고쳐보며 스스로 풀 수 있었다. 이 문제의 티어는 골드2로 꽤 높은 편이다. 이러한 문제를 많이 풀어봐야할 것 같다.

 

채점 결과

 

스레드란?

  • 프로세스 내의 제어 흐름
  • 일반적으로 우리가 작성하는 코드는 단일 스레드 단일 프로세스
  • 다중 스레드 프로세스는 하나의 프로세스에 여러 컨트롤이 존재함

쉽게 말해 스레드란 우리가 프로그램을 실행할 때 코드가 실행되는 흐름이라고 할 수 있다.

 

특징

  • 동일 프로세스에서 동작하는 여러 개의 스레드는 코드 영역, 데이터 영역, 리소스(파일 디스크립터, 시그널 등)를 공유한다.
  • 스레드는 각각 Program Counter(PC)를 가지고 있다 - 이는 생각해보면 당연하다. 각 스레드는 실행하는 코드의 위치가 다르다. 즉 실행하는 명령어가 다르다. 따라서 각각 PC를 가지고 있어야한다.
  • 스레드는 스택 영역, 레지스터 집합, 스레드 ID를  각자 가지고 있다.

 

예시

그러하면 실제로 다중 스레드는 어디에 이용될까? 우리가 사용하는 웹 브라우저에서도 다중 스레드는 사용된다. 예를 들어 이미지, 텍스트 등 화면에 홈페이지를 표시해주는 스레드가 1개 존재한다면 백엔드로부터 데이터를 읽어오는 역할을 수행하는 스레드가 따로 동작하고 있을 수 있다. 이렇게 동시에 여러 기능을 수행하려면 다중 스레드를 이용하여야 한다.

 

함수

리눅스에서 제공하느 pthread 시스템 호출 함수를 이용하여 다중 스레드를 사용할 수 있다. 그럼 스레드를 제어할 수 있는 함수는 어떤 것들이 있는지 간단히 살펴보겠다.

  • pthread_create()     
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);

 - 새로운 스레드를 생성하는 함수이다. 함수의 원형은 위의 코드와 같다.

 - 첫 번째 인자로 새로 생성할 스레드의 ID가 저장될 주소 값을 넘겨준다.

 - 두 번째 인자는 기본 특성을 가지는 스레드의 경우 NULL을 넣어주고 pthread_attr_init()으로 pthread_attr_t 구조체를 초기화하여 사용자 모드 스레드나 커널 모드 스레드를 사용할 수 있다.

 - 세 번째 인자는 새로 생성되는 스레드가 실행할 함수의 포인터를 준다.

 - 네 번째 인자는 세 번째 인자의 함수에게 넘기고 싶은 정보를 구조체에 저졍하여 그 포인터를 넘겨주어 스레드가 실행하는 함수에서 그 정보를 사용할 수 있게 한다.

  • pthread_exit()
void pthread_exit(void *rval_ptr);

  - 스레드를 종료시키는 함수. 인자로 준 rval_ptr 값은 pthread_join의 두번 째 인자에서 받을 수 있다. 만약 pthread_detach()를 하지 않은 경우에는 스레드가 종료되었어도 스레드의 자원은 회수되지 않는다.

  • pthread_join()
int pthread_join(pthread_t thread, void **rval_ptr);

 - main스레드가 생성한 스레드가 종료될 때까지 기다리는 함수. 종료된 스레드의 자원을 회수하는 역할을 한다.

 - pthread_join()을 호출한 스레드는 그 스레드가 pthread_exit()을 호출할 때까지 대기한다.

 - main스레드의 종료로 인해 다른 스레드들이 강제로 종료되는 것을 방지한다.

 - 첫 번째 인자는 종료를 기다리는 스레드의 id이다.

 - 두 번째 인자는 pthread_exit()에서 넘겨주는 인자와 같은 값이다. 만약 스레드가 정상적으로 종료되었다면 두 번째 인자에 리턴 코드가 저장되고 스레드가 취소되면 rval_ptr이 가리키는 곳이 PTHREAD_CANCELED가 설정된다.

 - 만약 pthread_create의 두 번째 인자인 pthread_attr_t의 인자로 NULL을 준 경우는 스레드를 join해야한다.

  • pthread_detach() 
int pthread_detach(pthread_t tid);

 - pthread_join과 다르게 메인에서 스레드를 기다리지 않는 함수.

 - 스레드가 종료될 경우 자동으로 자원이 회수된다. (pthread_join을 할 필요 없다)

 - phtread_detach한 경우 pthread_join으로 스레드를 기다릴 수 없다. 즉 joinable하지 않다.

예제

실제로 다중 스레드를 이용하여 모두 공유하는 전역변수를 2개의 스레드가 1씩 증가시키며 출력하는 프로그램이다.

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

int var = 0;

void *func1(void *arg) { //1번 스레드 실행 함수
	while(var < 10) {
		printf("thread1 : %d\n", ++var);
		sleep(1);
	}
	pthread_exit(NULL); //1번 스레드 종료
}

void *func2(void *arg) { //2번 스레드 실행 함수
	while(var < 10) {
		printf("thread2 : %d\n", ++var);
		sleep(1);
	}
	pthread_exit(NULL); //2번 스레드 종료
}


int main() {
	pthread_t tid1, tid2;

	if(pthread_create(&tid1, NULL, func1, NULL) != 0) { //1번 thread 생성
		fprintf(stderr, "thread create error\n");
		exit(1);
	}

	if(pthread_create(&tid2, NULL, func2, NULL) != 0) { //2번 thread 생성
		fprintf(stderr, "thread create error\n");
		exit(1);
	}

	pthread_join(tid1, NULL); //1번 스레드 자원 회수
	pthread_join(tid2, NULL); //2번 스레드 자원 회수

	return 0;
}

 

결과

위 코드 실행 결과

위 그림처럼 두 개의 스레드가 각각 공유하는 전역 변수를 1씩 증가시키며 출력하는 것을 볼 수 있다. 하지만 이상한 점이 있다. 스레드가 실행되는 순서가 일정하지 않다는 것이다. 이렇게 스레드가 실행되는 순서는 생성되는 순서와 전혀 관계가 없다. 스레드의 실행 순서를 제어하려면 동기화가 필요하다. 동기화는 이후에 다뤄보겠다.

 

개발환경

OS : wsl2 Ubuntu-20.04

gcc : Ubuntu 9.3.0-17ubuntu1~20.04

https://programmers.co.kr/learn/courses/30/lessons/72412

 

코딩테스트 연습 - 순위 검색

["java backend junior pizza 150","python frontend senior chicken 210","python frontend senior chicken 150","cpp backend senior pizza 260","java backend junior chicken 80","python backend senior chicken 50"] ["java and backend and junior and pizza 100","pyt

programmers.co.kr

이 문제는 스스로 시간초과 문제를 해결하지 못하여 풀이를 보고 해결할 수 있었다.

문제

카카오는 하반기 경력 개발자 공개채용을 진행 중에 있으며 현재 지원서 접수와 코딩테스트가 종료되었습니다. 이번 채용에서 지원자는 지원서 작성 시 아래와 같이 4가지 항목을 반드시 선택하도록 하였습니다.

  • 코딩테스트 참여 개발언어 항목에 cpp, java, python 중 하나를 선택해야 합니다.
  • 지원 직군 항목에 backend와 frontend 중 하나를 선택해야 합니다.
  • 지원 경력구분 항목에 junior와 senior 중 하나를 선택해야 합니다.
  • 선호하는 소울푸드로 chicken과 pizza 중 하나를 선택해야 합니다.

인재영입팀에 근무하고 있는 니니즈는 코딩테스트 결과를 분석하여 채용에 참여한 개발팀들에 제공하기 위해 지원자들의 지원 조건을 선택하면 해당 조건에 맞는 지원자가 몇 명인 지 쉽게 알 수 있는 도구를 만들고 있습니다.
예를 들어, 개발팀에서 궁금해하는 문의사항은 다음과 같은 형태가 될 수 있습니다.
코딩테스트에 java로 참여했으며, backend 직군을 선택했고, junior 경력이면서, 소울푸드로 pizza를 선택한 사람 중 코딩테스트 점수를 50점 이상 받은 지원자는 몇 명인가?

물론 이 외에도 각 개발팀의 상황에 따라 아래와 같이 다양한 형태의 문의가 있을 수 있습니다.

  • 코딩테스트에 python으로 참여했으며, frontend 직군을 선택했고, senior 경력이면서, 소울푸드로 chicken을 선택한 사람 중 코딩테스트 점수를 100점 이상 받은 사람은 모두 몇 명인가?
  • 코딩테스트에 cpp로 참여했으며, senior 경력이면서, 소울푸드로 pizza를 선택한 사람 중 코딩테스트 점수를 100점 이상 받은 사람은 모두 몇 명인가?
  • backend 직군을 선택했고, senior 경력이면서 코딩테스트 점수를 200점 이상 받은 사람은 모두 몇 명인가?
  • 소울푸드로 chicken을 선택한 사람 중 코딩테스트 점수를 250점 이상 받은 사람은 모두 몇 명인가?
  • 코딩테스트 점수를 150점 이상 받은 사람은 모두 몇 명인가?
  • query의 각 문자열은 "[조건] X" 형식입니다.
    • [조건]은 "개발언어 and 직군 and 경력 and 소울푸드" 형식의 문자열입니다.
    • 언어는 cpp, java, python, - 중 하나입니다.
    • 직군은 backend, frontend, - 중 하나입니다.
    • 경력은 junior, senior, - 중 하나입니다.
    • 소울푸드는 chicken, pizza, - 중 하나입니다.
    • '-' 표시는 해당 조건을 고려하지 않겠다는 의미입니다.
    • X는 코딩테스트 점수를 의미하며 조건을 만족하는 사람 중 X점 이상 받은 사람은 모두 몇 명인 지를 의미합니다.
    • 각 단어는 공백문자(스페이스 바) 하나로 구분되어 있습니다.
    • 예를 들면, "cpp and - and senior and pizza 500"은 "cpp로 코딩테스트를 봤으며, 경력은 senior 이면서 소울푸드로 pizza를 선택한 지원자 중 코딩테스트 점수를 500점 이상 받은 사람은 모두 몇 명인가?"를 의미합니다.

조건이 주어지는 query를 만족하는 인원이 몇명인지를 vector에 담아 리턴하는 문제이다.

 

예시

위 그림과 같이 info로 각 사람들의 지원 조건이 주어지면 query를 만족하는 인원이 각각 몇명인지를 구하는 문제이다.

 

이 문제는 각각 인원을 모두 구하고 겹치는 인원들을 찾아내어 인원을 세면 시간초과가 걸린다. 따라서 각 조건을 만족하는 사람을 전부 가지고 유지하고 있어야한다. 따라서 info에 주어지는 언어, 직군, 경력, 음식을 하나로 생각해서 그 조건을 만족하는 사람들의 점수를 가지고 있어야한다. 글쓴이는 언어, 직군, 경력,음식을 각가 1차원으로 생각하여 4차원 배열을 만들었다. 코드는 다음과 같다.

 

소스코드

#include <string>
#include <vector>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

vector<int> database[3][2][2][2]; //데이터베이스 구축
//cpp = 0, java = 1, python = 2;
//backend = 0, frontend = 1;
//junior = 0, senior = 1;
//chicken = 0, pizza = 1;


vector<int> solution(vector<string> info, vector<string> query) {
    vector<int> answer;
    for(int i =0; i < info.size(); i++) {
        int lan, end, career, food;
        int now = 0;
        string s = info[i]; //문자열을 나눔
        if(s[0] == 'c') {
            lan = 0;
            now = 4;
        }
        else if(s[0] == 'j') {
            lan = 1;
            now = 5;
        }
        else{
            lan = 2;
            now = 7;
        }
        
        if(s[now] == 'b') {
            end = 0;
            now += 8;
        }
        else {
            end = 1;
            now += 9;
        }
        
        if(s[now] == 'j') {
            career = 0;
        }
        else {
            career = 1;
        }
        now += 7;
        
        if(s[now] == 'c') {
            food = 0;
            now += 8;
        }
        else {
            food = 1;
            now += 6;
        }
        
        int num = stoi(s.substr(now));
        database[lan][end][career][food].push_back(num); //해당하는 데이터베이스에 점수 추가
    }
    
  for(int i=0; i<3; i++)
    for(int j=0; j<2; j++)
      for(int k=0; k<2; k++)
        for(int l=0; l<2; l++)
          sort(database[i][j][k][l].begin(),database[i][j][k][l].end());
    
    for(string q : query) {
        int now = 0;
        int s1, s2, s3, s4, e1, e2, e3, e4;
        int lan = -1, end = -1, career = -1, food = -1;
        if(q[now] == 'c') { //언어 분류
            lan = 0;
            now += 8;
        }
        else if(q[now] == 'j') {
            lan = 1;
            now += 9;
        }
        else if(q[now] == 'p') {
            lan = 2;
            now += 11;
        }
        else {
            now += 6;
        }
        
        if(q[now] == 'b') {
            end = 0;
            now += 12;
        }
        else if(q[now] == 'f') {
            end = 1;
            now += 13;
        }
        else {
            now += 6;
        }
        
        if(q[now] == 'j') {
            career = 0;
            now += 11;
        }
        else if(q[now] == 's') {
            career = 1;
            now += 11;
        }
        else {
            now += 6;
        }
        
        if(q[now] == 'p') {
            food = 1;
            now += 6;
        }
        else if(q[now] == 'c') {
            food = 0;
            now += 8;
        }
        else {
            now += 2;
        }
        
        int num = stoi(q.substr(now));
        
        int people = 0;
        for(int i = 0; i <= 2; i++) {
            if(lan != i && lan != -1) continue;
            for(int j = 0; j <= 1; j++) {
                if(end != j && end != -1) continue;
                for(int k = 0; k <= 1; k++) {
                    if(career != -1 && career != k) continue;
                    for(int l = 0; l <= 1; l++) {
                        if(food != -1 && food != l) continue;
                        if(database[i][j][k][l].size()) {
                            auto lo = lower_bound(database[i][j][k][l].begin(), database[i][j][k][l].end(), num);
                            people += database[i][j][k][l].end() - lo;
                        }
                    }
                }
            }
        }
        answer.push_back(people);
    }
    
    return answer;
}

문자열을 분류하는 코드 때문에 상당히 복잡해보이지만 그 부분을 제외하면 간단한 코드이다. 각 조건에 따라서 언어, 직군, 경력, 음식에 해당하는 인덱스가 결정된다. 그 조건을 만족하는 벡터에 점수를 push한다. 그렇다면 같은 조건을 가진 사람들의 점수가 유지될 것이다. 그리고 조건이 되는 점수를 이분탐색하기 위하여 모두 정렬한다. 4중 for문이지만 3 * 2 * 2 * 2 = 24로 24번밖에 반복하지 않는다. 그 다음 query에서 주어지는 조건에 따라 탐색하고자 하는 인덱스를 결정한다. 모든 초기값을 -1로 주어 만약 -1인 경우 해당 조건의 모든 점수를 탐색할 수 있게 하였다. 위 코드를 보았을 때 

 if(lan != i && lan != -1) continue;

이 부분은 언어가 i번째 인덱스도 아니고 -1도 아닐 경우 건너 뛴다는 의미이다. 즉 -1인 경우 모든 인덱스를 탐색하고 그렇지 않은 경우 원하는 조건인 lan에 해당하는 인덱스만 탐색한다는 뜻이다. 그렇게 4가지 조건을 모두 탐색하여 lower_bound라는 이분탐색 알고리즘을 이용하여 조건을 만족하는 점수의 이터레이터를 얻는다. 그리고 end와의 차이를 이용해 조건으로 주어진 값보다 크거나 같은 값의 수를 구할 수 있다. 그렇게 구한 인원을 각 query마다 answer에 push_back하여 원하는 값을 구할 수 있다.

 

채점 결과

 

https://programmers.co.kr/learn/courses/30/lessons/77886

 

코딩테스트 연습 - 110 옮기기

0과 1로 이루어진 어떤 문자열 x에 대해서, 당신은 다음과 같은 행동을 통해 x를 최대한 사전 순으로 앞에 오도록 만들고자 합니다. x에 있는 "110"을 뽑아서, 임의의 위치에 다시 삽입합니다. 예를

programmers.co.kr

문제

0과 1로 이루어진 어떤 문자열 x에 대해서, 당신은 다음과 같은 행동을 통해 x를 최대한 사전 순으로 앞에 오도록 만들고자 합니다.

  • x에 있는 "110"을 뽑아서, 임의의 위치에 다시 삽입합니다.

예를 들어, x = "11100" 일 때, 여기서 중앙에 있는 "110"을 뽑으면 x = "10" 이 됩니다. 뽑았던 "110"을 x의 맨 앞에 다시 삽입하면 x = "11010" 이 됩니다.

변형시킬 문자열 x가 여러 개 들어있는 문자열 배열 s가 주어졌을 때, 각 문자열에 대해서 위의 행동으로 변형해서 만들 수 있는 문자열 중 사전 순으로 가장 앞에 오는 문자열을 배열에 담아 return 하도록 solution 함수를 완성해주세요.

 

풀이

    이 문제는 혼자 풀었을 때 시간초과를 해결하지 못하여 풀이를 보고 풀었다. 우선 110이 사라지고 난 다음 생기는 110도 처리를 해주어야된다는 것을 뒤늦게 알았다. 풀이를 보고 stack을 이용하면 쉽게 풀린다는 것을 알았다. 또 110을 삽입하는 위치가 오른쪽에서 시작하여 연속되는 1의 앞부분에 붙여 넣으면 된다는 것을 문제만 보고 떠올리기 쉽지 않았다. 이 부분만 알고 풀면 굉장히 쉽게 풀 수 있는 문제이다. 코드의 순서는 다음과 같다.

 

  1. 지워진 문자열의 문자를 하나씩 stack에 넣으며 110을 찾으면 지우고 카운트(110의 개수)를 센다.
  2. 110이 모두 지워진 문자열에서 뒤에서부터 1이 아닌 숫자가 나올 때까지 탐색한다. 즉 예를들어 001001111이 있을 경우 110들은 00100____1111 위치에 들어가야지 최소 숫자가 나오게 된다.
  3. 위치를 찾은 경우 삽입하려는 위치 앞부분의 문자열을 정답 문자열에 붙여넣는다.
  4. 110의 개수만큼 정답 문자열에 붙여넣는다.
  5. 삽임하려는 위치 뒷부분 문자열을 정답 문자열에 붙여넣는다.

코드

#include <string>
#include <vector>
#include <iostream>

using namespace std;

vector<string> solution(vector<string> s) {
    vector<string> answer;
    vector<char> st; //110을 지우고 남은 문자열
    int cnt = 0; //지워진 110의 개수
    for(string str : s) {
        cnt = 0;
        st.clear();
        for(int i = 0; i < str.size(); i++) { //110을 모두 지우기
            if(str[i] == '1') st.push_back('1'); //만약 0이 아니면 스택에 넣음
            else {
                if(st.size() >= 2 && st[st.size()-1] == '1' && st[st.size()-2] == '1') { //110을 찾으면 110개수를 늘리고 스택에서 지우기
                    cnt++;
                    st.pop_back();
                    st.pop_back();
                }
                else
                    st.push_back('0');
            }
        }
        int i;
        for(i = st.size()-1; i >= 0; i--) { //뒤에서 시작하여 연속된 1의 앞부분에 110붙이기
            if(st[i] == '1') continue;
            else break;
        }
        
        string ret = ""; 
        
        for(int k = 0; k <= i; k++) //연속된 1 앞부분 붙여넣기
            ret += st[k];
        
        for(int c = 0; c < cnt; c++) //110 붙여넣기
            ret += "110";
        
        for(int k = i+1; k < st.size(); k++) //연속된 1 뒷부분 붙여넣기
            ret += st[k];
        
        answer.push_back(ret);
    }
    
    return answer;
}

결과

 

 

풀이 방법만 알면 절대 어렵지 않은 문제였지만 이런 풀이과정을 생각해낸다는 것이 굉장히 어렵다. stack을 잘 다룬다고 생각하였지만 이렇게 실전에 적용하는 생각은 잘 하지 못하였다. 스택, 큐, 힙 등을 실제에 사용하는 문제를 많이 풀어봐야할 것 같다.

문제

https://www.acmicpc.net/problem/1937

 

1937번: 욕심쟁이 판다

n × n의 크기의 대나무 숲이 있다. 욕심쟁이 판다는 어떤 지역에서 대나무를 먹기 시작한다. 그리고 그 곳의 대나무를 다 먹어 치우면 상, 하, 좌, 우 중 한 곳으로 이동을 한다. 그리고 또 그곳에

www.acmicpc.net

 

이 문제는 DFS나 BFS로 간단히 풀 수 있는 문제처럼 보인다. 하지만 기본적인 DFS나 BFS로 풀 경우 시간초과가 발생한다. 따라서 DFS와 DP를 결합한 메모이제이션 방식을 이용해야한다. 그렇다면 메모이제이션을 어떻게 사용해야할까?

 

예시

입력 배열

만약 (0,0)인 5에서 시작한다면 5, 6, 7, 8, 9, 15까지 탐색 가능하며 2차원인 DP배열은 다음과 같을 것이다.

DP 배열

5에서 시작하면 총 7번까지 탐색 가능하다. 만약 그렇다면 (1,0)인 3에서 탐색을 시작한다면 어떻게 될까? 3에서는 5방향으로 탐색한다면 5에서 시작한 것과 똑같이 탐색할 것이다. 5부터는 결과도 똑같을 것이다. 그렇다면 3은 5에 이미 탐색한 결과가 있을 경우 DP배열의 (0,0)에 1을 더해주기만 하면 탐색 가능한 최대값을 얻을 수 있을 것이다. 만약 그렇지 않고 5부터 다시 탐색한다면 같은 반복을 여러번할 것이다. 만약 3부터 탐색하고 (1,1)인 2부터 탐색한다면 같은 작업을 3번 반복하는 것이다. 엄청난 시간낭비가 발생한다. 그렇기 때문에 이미 계산한 결과가 있는 경우 탐색을 멈추고 그 결과값을 이용해야한다. 코드로 나타내면 다음과 같다.

#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;

const int MN = 501;

int dp[MN][MN]; //DP 배열
int arr[MN][MN]; //입력 배열
bool visit[MN][MN]; //방문 배열
int N;
int di[4] = {1, 0, 0, -1}; //상하좌우 표시
int dj[4] = {0, 1, -1, 0};

int dfs(int i, int j) {
    if(visit[i][j]) return dp[i][j]; //만약 이미 계산한 결과값이 있는 경우 사용
    visit[i][j] = true; //방문 표시
    dp[i][j] = 1; //첫 방문인 경우 1로 초기화
    for(int d = 0; d < 4; d++) { //상하좌우를 탐색
        int ni = i + di[d];
        int nj = j + dj[d];
        if(ni >= 0 && ni < N && nj >= 0 && nj < N) { //배열 범위를 벗어나지 않는 조건
            if(arr[ni][nj] > arr[i][j]) //더 많은 대나무가 있는 곳으로
                dp[i][j] = max(dp[i][j],dfs(ni, nj) + 1); //탐색하고 최대값 갱신
        }
    }
    return dp[i][j]; //현 위치에서의 최대값 리턴
}

int main() {
    cin >> N;
    int ans = 1;
    for(int i = 0; i < N; i++) { //입력받기
        for(int j = 0; j < N; j++) {
            cin >> arr[i][j];
        }
    }
    for(int i = 0; i < N; i++) {
        for(int j = 0; j < N; j++) {
            ans = max(ans,dfs(i, j)); //각 위치에서 시작하여 최대값 갱신
        }
    }

    cout << ans; //최대값 출력
    return 0;
}

DFS와 DP를 이용하여 불필요한 재방문을 스킵하면 시간을 줄일 수 있다. 메모이제이션은 DP가 어떤 값을 기억하는지를 정확히 알고 이해해야지 사용할 수 있다. 카카오 코딩테스트에서도 가끔 메모이제이션을 사용하는 문제가 나오기 때문에 이런 문제를 많이 풀어보는 것이 중요하다.

+ Recent posts