1. 문제

1) 문제 링크

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

 

2281번: 데스노트

첫째 줄에 n(1 ≤ n ≤ 1,000), m(1 ≤ m ≤ 1,000)이 주어진다. m은 노트의 가로 칸의 개수(폭, 너비)이다. 다음 n개의 줄에는 각 사람의 이름의 길이가 노트에 적어야 할 순서대로 주어진다. 각 길이는 m

www.acmicpc.net

 

2) 설명

대강 요약하면 데스노트에 이름을 쓸 때 각 줄별로 뒤의 여백을 구하는 문제다.

각 줄에 이름을 이어서 쓰거나 새로운 줄에 쓸 수 있다. 다만 이름이 중간에 잘리면 그줄에 이어서 못 쓰고 반드시 다음 줄에 새로 써야 한다.

또한 이름 사이에는 한 칸씩 띄어야 한다.

이때 각 줄의 마지막 이름 뒤에 오는 빈 칸들의 제곱의 합을 최소화하는 문제다.

이때 맨 마지막 줄의 빈 칸은 계산할 때 제외한다.

 

2. 풀이

1) 알고리즘

    : DP

 

2) 풀이 설명

dp 배열에 대한 정의는 다음과 같다.

dp[ i ] : i번째 이름을 새로운 줄에서 시작했을 때, 그 뒤의 이름들로 데스노트를 채워서 얻을 수 있는 빈칸제곱합의 최소값

(말이 좀 어려울 수 있는 것 같다..)

 

아래 문제의 예시를 살펴보면

n = 11, m = 20

index value
0 7
1 4
2 2
3 3
4 2
5 5
6 1
7 12
8 7
9 5
10 6

 

dp[10]은 새로운 줄에 index = 10인 이름을 쓰고 (이러면 20 - 6 = 14만큼의 공백이 남는다) 뒤에 더 올 이름이 없으므로 14의 제곱인 196이 된다. 다만 이 경우는 마지막 줄이라 계산하지 않으므로 0이 된다..

 

dp[9]는 index = 9인 이름을 새 줄에서 시작했을 때의 최소값을 구하면 되는 거다. name[9]는 5글자이므로 뒤에 공백 15칸이 남는다. 그럼 이때 뒤에 남은 name[10]을

1. 이어서 붙여쓸 건지

2. 새 줄에 쓸 건지

에 따라 값이 달라지므로 이 두 가지 경우 중 최소값을 쓰면 된다.

1번처럼 name[10]을 이어서 붙여쓸 경우에는 남은 15칸의 공백 중 띄어쓰기 1칸, 10번째 이름 6칸이 추가로 쓰여지므로 15 - 1 - 6인 8만큼의 공백이 뒤에 남는다. 그러나 이 경우에는 해당 줄이 마지막 줄이 되므로 1의 결과값 후보는 64가 아닌 0이다.

2번처럼 name[10]을 새로운 줄에 쓸 경우에는 남은 15칸의 공백의 제곱인 225에 dp[10]을 더한 225가 결과값 후보가 된다.

결국 dp[9] = min(0, 225) = 0이 된다.

 

dp[8]도 구해보자.

dp[8]은 index = 8인 이름을 새 줄에 썼을 때의 최소값을 구하면 된다.

그럼 dp[8]은

1. name[9]를 새 줄에 띄어쓰는 경우 -> 결과값 후보 : (20 - 7)^2 + 0( = dp[9]) = 169

2. name[9]를 이어붙이고 name[10]을 새 줄에 쓰는 경우 -> 결과값 후보 : (20 - (7 + 5 + 1))^2 + 0( = dp[10]) = 49

3. name[9], name[10]을 모두 이어붙이는 경우 -> 결과값 후보 : 0

이므로 dp[8] = 0이 된다.

 

한 번은 0이 아닌 경우를 구해봐야 할 것 같아서 dp[7]이랑 dp[6]까지 값을 구해보면...(이미 이해 됐으면 굳이 안 봐도 될 것 같다...)

dp[7]은

1. name[8]을 새 줄에 띄어쓰는 경우 -> 결과값 후보 : (20 - 12)^2 + dp[8] = 64

2. name[8]은 이어붙이고 name[9] 부터 띄어쓰는 경우 -> 결과값 후보 : (20 - 12 - 1 - 7)^2 + dp[9] = 0

이므로 dp[7] = 0

이때 name[8], name[9]를 이어붙일 수는 없다..! 그러면 한 줄에 20을 넘어가기 때문에..

 

dp[6]은

1. name[7]을 새 줄에 띄어쓰는 경우 -> 결과값 후보 : (20 - 1)^2 + dp[9] = 361

2. name[7]을 이어붙이고 name[8]부터 새 줄에 쓰는 경우 -> (20 - 1 - 1 - 12)^2 + d[8] = 36

이므로 dp[6] = 36이 된다.

dp[7] 때와 마찬가지로 name[6], name[7], name[8]을 이어붙일 수는 없다. 1 + 1 + 12 + 1 + 7 = 22 > 20이기 때문이다.

 

이런 식으로 dp배열을 채워나가면 된다.

 

그렇게 구한 dp[0]은 name[0]을 새 줄에 썼을 때 최소의 결과값을 가지는 거다.

이게 우리가 구하는 값이 된다!(name[0]은 무조건 새 줄의 시작이므로..)

 

 

3. 코드

더보기
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>

using namespace std;

const int N_MAX = 1000;
int n, m;
vector<int> dp(N_MAX, INT_MAX);
vector<int> names(N_MAX, 0);

int solution(int idx){
	//계산한 적 있으면 바로 리턴
	if(dp[idx] < INT_MAX)
		return dp[idx];

	int remain = m - names[idx]; //뒤에 다음 이름을 이어붙일 수 있는지 계산하기 위한 변수

	for(int i=idx+1;i<=n && remain>=0;i++){
    	//i번째 이름을 계속 이어붙여가본다. 물론 줄을 넘지 않는 선에서..(remain >= 0)
		if(i == n){
        	//줄을 넘지 않고 모든 이름을 이어붙인 경우다.. 그러면 마지막 줄이라는 말이니까 무조건 dp[idx] = 0이 된다.
			dp[idx] = 0;
			break;
		}
		dp[idx] = min(dp[idx], remain * remain + solution(i)); //재귀 호출
		remain -= names[i] + 1; //names[i]를 이어붙였으므로 이름 사이의 빈 칸(1) + names[i] 길이만큼을 remain(남은 칸 수)에서 빼준다.
	}

	return dp[idx];
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	cin >> n >> m;
	for(int i=0;i<n;i++)
		cin >> names[i];

	dp[n - 1] = 0;

	cout << solution(0) << endl;

	return 0;
}

 

 

4. 느낀점

오랜만에 풀어보는 재미있는 dp 문제였다!

'알고리즘 > 백준' 카테고리의 다른 글

[백준/7512] 연속하는 소수의 합  (0) 2021.09.20
[백준/1727] 커플 맞추기  (0) 2021.08.25
[백준/2110] 공유기 설치  (0) 2021.08.01
[BOJ/1034] 램  (0) 2021.07.27
[BOJ/7579] 앱  (0) 2020.12.29

1. 문제

1) 문제 링크

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

 

7512번: 연속하는 소수의 합

각 테스트 케이스마다 "Scenario i:"를 출력하고, 둘째 줄에 정답을 출력한다.

www.acmicpc.net

 

2) 설명

숫자 n_i들이 주어지면 각각 n_i 개만큼의 연속된 소수들의 합으로 만들 수 있는 최소의 소수를 구하는 문제다.

2. 풀이

1) 알고리즘

    : 에라토스테네스의 체, 슬라이딩 윈도우

 

2) 풀이 설명

 

일단 에라토스테네스의 체로 소수 배열(primes)을 구한다. 그리고 소수인지 아닌지 판별할 수 있는 isPrime배열까지채워넣는다.

사실 소수 구하는 코드는 기억을 더듬어 대강 한 거라 다른 풀이를 보는 게 더 나을 수도 있다.

 

이제부터 슬라이딩 윈도우로 문제를 풀면 된다.

먼저 각 케이스별로 구하는 답을 answer라고 한다.

 

이제 sumArray와 sumBeginIndex 배열을 이용해서 answer를 구해본다.

 

nArray[i] = 연속한 소수의 수

sumArray[i] = n_i개의 연속된 소수의 합으로 만든 소수

sumBeginIndex[i] = sumArray[i]의 소수를 만드는 연속된 소수들 중 첫번째 인덱스를 가리킨다.

 

예를 들어 nArray[i]가 3이면 sumArray와 sumBeginIndex에 가능한 값은 다음과 같다.

sumBeginIndex[3] 0 1 2
sumArray[3] 9 ( = 2 + 3 + 5 ) 근데 소수가 아니라 안 됨  15 ( = 3 + 5 + 7 ) 얘도 소수가 아니라 안 됨 23 ( = 5 + 7 + 11) 은 소수니까 ok

sumBeginIndex[3]에는 0, 1은 들어갈 수 없고 2부터 시작하게 된다.(소수 연속합이 소수여야 하므로)

 

그러면 그러면 이제 n_i별로 sumArray, sumBeginIndex를 구성할 수 있다.

 

첫 초기값을 구하고 슬라이딩 윈도우를 통해 계속 sumArray의 모든 원소가 answer와 같아질 떄까지 슬라이딩 + answer를 업데이트 해주면 된다.

 

이때 sumArray[i] < answer라면, sumArray[i[ 를 슬라이딩시킨다.(그 합이 소수가 될 때까지)

만약 sumArray[i] > answer라면, 더이상 해당 answer 값은 모든 n_i 개의 연속합으로 만든 공통의 소수가 될 수 없다... i개의 연속합으로 answer를 만들 수 없다는 말이기 때문이다..

 

앞서 말했듯 이런 식으로 sumArray와 sumBeginIndex, answer 값을 sumArray의 모든 값과 answer 값이 같아질 때까지 반복하면 된다!

 

 

3. 코드

더보기
#include <iostream>
#include <cmath>
#include <algorithm>
#include <vector>
#include <numeric>

#define MAX_VAL 10000000

using namespace std;

vector<bool> isPrime(MAX_VAL, true); // 해당 수가 소수인지르 나타내는 배열
vector<int> primes; // 소수만 있는 배열

// 에라토스테네스의 체로 소수 구하는 함수
void setPrimes(){	
	isPrime[0] = isPrime[1] = false;
	isPrime[2] = true;
	for(int i=2;i<MAX_VAL;i++){
		if(isPrime[i] == false)
			continue;
		
		primes.push_back(i);
		
		for(int j=i*2;j<MAX_VAL;j+=i){
			isPrime[j] = false;
		}
	}
}

// 모든 sumArray가 같은 값을 갖고 있는지 확인
bool isCorrect(vector<int> sumArray, int val){
	for(auto x : sumArray)
		if(x != val)
			return false;
	return true;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	setPrimes();

	int t;
	cin >> t;
	for(int i=1;i<=t;i++){
		int m;
		cin >> m;
		vector<int> nArray(m, 0); // n_i 배열
		vector<int> sumArray(m, 0); // n_i만큼 연속된 소수의 합을 가진 배열(중요!! : 이 값은 소수여야 함)
		vector<int> sumBeginIndex(m, 0); // sumArray에 있는 연속된 소수의 합이 어디서부터 시작했는지를 가리키는 index의 배열

		//input
		for(int j=0;j<m;j++)
			cin >> nArray[j];

		//첫 sumArray와 sumBeginIndex 구하기
		for(int j=0;j<m;j++){
			sumArray[j] = accumulate(&primes[0], &primes[nArray[j]], 0);
			while(!isPrime[sumArray[j]]){ //구한 합이 소수인지 체크
				//소수가 아니라면 슬라이딩 윈도우로 증가
				sumArray[j] += primes[sumBeginIndex[j] + nArray[j]] - primes[sumBeginIndex[j]];//슬라이딩 윈도우로 다읍 값 더하고 맨 앞의 값 빼기 
				sumBeginIndex[j]++; //시작 인덱스 증가
			}
		}

		int answer = *max_element(sumArray.begin(), sumArray.end()); // answer 초기값 설정, 아마 min_element로 하든 뭐로 하든 sumArray 안의 값으로 하면 상관 없을 것 같다(어차피 아래 while문 통해서 업데이트 될 거라)
		
		while(!isCorrect(sumArray, answer)){ //sumArray 배열의 모든 값이 answer랑 똑같다면 정답을 구한 거임
			for(int j=0;j<m;j++){
				if(sumArray[j] < answer){
					//sumArray[j] 값이 answer보다 작으면 슬라이딩 윈도우로 beg ~ end -> beg+1 ~ end+1로 합 증가
					//이걸 새로 구한 소수의 연속합이 소수가 될 때까지 반복
					//초기값 구할 때와 달리 do while문인 이유는 얘는 이미 소수의 연속합이 answer보다 작기 때문에 최소한 한 번은 무조건 업데이트를 해줘야 하기 때문이다. 주석처리된 코드처럼 sumArray[j] += ~~~~~~; while(!isPrime[sumArray[j]]){~~~}로 해도 되지만 슬라이딩 윈도우하는 코드가 중복돼서 그냥 do while문으로 해줬다
					/*
					sumArray[j] += primes[sumBeginIndex[j] + nArray[j]] - primes[sumBeginIndex[j]];		
					sumBeginIndex[j]++;
					while(!isPrime[sumArray[j]]){
						sumArray[j] += primes[sumBeginIndex[j] + nArray[j]] - primes[sumBeginIndex[j]];		
						sumBeginIndex[j]++;
					}
					*/
					do {
						sumArray[j] += primes[sumBeginIndex[j] + nArray[j]] - primes[sumBeginIndex[j]];
						sumBeginIndex[j]++;
					} while(!isPrime[sumArray[j]]);
				} else {
					//sumArray 중에 answer보다 큰 값이 있으면 answer 값 업데이트
					answer = sumArray[j];
				}
			}
		}
		cout << "Scenario " << i << ":\n";
		cout << answer << "\n\n";
	}
	return 0;
}

 

 

4. 느낀점

개인적으로 소수 문제는 별로 선호하지 않는다.. (어렵기 때문 ㅎㅎ)

그래도 머리 싸매고 풀어내니까 기분이 좋더라..

근데 정작 내가 푼 알고리즘을 설명을 못 하겠다..

너무 복잡하게 푼 게 아닐까 싶어서 맞은 분들의 다른 풀이를 봤는데... 잘 모르겠더라..ㅎㅎㅎ

많이 부족함을 느낀다..

'알고리즘 > 백준' 카테고리의 다른 글

[백준/2281] 데스노트(1차원 dp)  (2) 2021.10.01
[백준/1727] 커플 맞추기  (0) 2021.08.25
[백준/2110] 공유기 설치  (0) 2021.08.01
[BOJ/1034] 램  (0) 2021.07.27
[BOJ/7579] 앱  (0) 2020.12.29

1. 문제

1) 문제 링크

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

 

코딩테스트 연습 - [1차] 뉴스 클러스터링

뉴스 클러스터링 여러 언론사에서 쏟아지는 뉴스, 특히 속보성 뉴스를 보면 비슷비슷한 제목의 기사가 많아 정작 필요한 기사를 찾기가 어렵다. Daum 뉴스의 개발 업무를 맡게 된 신입사원 튜브

programmers.co.kr

 

2) 설명

자카드 유사도를 활용해 두 문자열의 유사도를 비교하는 문제다

자카드 유사도에 대한 자세한 설명은 문제 링크에 있다

 

2. 풀이

1) 알고리즘

    : 집합?

 

2) 풀이 설명

C++로 sstream 사용해서 풀어도 되긴 하는데 파이썬으로 풀기에 너무 적합한 문제 같아서 그냥 파이썬으로 풀어봤다.

일단 소문자 대문자를 가리지 않기 때문에 문자열을 다 소문자로 변환했다.

그리고 각 문자열을 두 글자씩 떼서 리스트를 만든다. 이때 특수문자가 포함되어 있으면 건너뛴다.

이제 n(교집합) / n(합집합) 을 구하면 된다.(마지막에 한 번만 곱해주면 되기 때문에 65536을 곱하는 건 일단 생략하겠다...)

n(A U B) = n(A) + n(B) - n(A ^ B) // n(A ^ B)는 A B 교집합

공식을 이용한다.

교집합을 구하는 건 str1_arr(str1에서 두 글자씩 떼어낸 문자열 집합)에 있는 문자열 중 str2_arr(str2에서 두 글자씩 떼어낸 문자열 집합)에 있는 것들만 카운팅해준다. 카운팅하면 str2_arr에서 그 문자열은 빼줘야 한다. 왜냐하면 다음과 같은 경우를 방지하기 위해서다.

str1_arr = {1, 1, 1}

str2_arr = {1, 2, 3}

일 때 처음 1을 카운팅해주고 str2_arr에서 빼주지 않으면 두 번째, 세 번째 1에서도 str2_arr에 1 값이 있기 때문에 교집합의 개수가 3이 되기 때문이다.

 

여튼 그렇게 교집합의 개수를 구했으면 공식 따라서 자카드 유사도를 구해주면 된다.

 

 

3. 코드

더보기
def solution(str1, str2):
    mul_val = 65536
    
    # 소문자 변환
    str1 = str1.lower()
    str2 = str2.lower()
    
    # 두글자씩 떼어서 배열로 변환
    str1_arr = [ str1[i : i + 2] for i in range(len(str1) - 1) if str1[i : i + 2].isalpha() ]
    str2_arr = [ str2[i : i + 2] for i in range(len(str2) - 1) if str2[i : i + 2].isalpha() ]
    
    union_count = len(str1_arr) + len(str2_arr) # (A U B) = (A) + (B) - (A ^ B) 공식 사용 : 교집합 개수 구한 후 빼줘야 함
    # 교집합 개수 구하기
    intersection_count = 0
    for x in str1_arr:
        if x in str2_arr:
            intersection_count += 1
            str2_arr.remove(x)
    
    # 합집합 개수 구하기
    union_count -= intersection_count # 교집합 개수 빼주기
    
    if union_count == 0:
        return mul_val
    else:
        return int(intersection_count / union_count * mul_val)

 

 

4. 느낀점

리스트 컴프리헨션 최고!

문자열에 isalpha()라는 함수가 있다는 걸 처음 알았다..

Counter라는 걸 사용하면 교집합 합집합을 더 쉽게 구할 수 있는 것  같다. 틈 나면 공부해보자!(보통 이러면 안 한다)

'알고리즘 > 프로그래머스' 카테고리의 다른 글

[LV4] 4단 고음  (0) 2021.08.19
[LV3] 불량 사용자  (0) 2021.08.13
[LV4] 호텔 방 배정  (0) 2021.08.13
[LV2] 거리두기 확인하기  (0) 2021.07.29

1. 문제

1) 문제 링크

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

 

1727번: 커플 만들기

첫째 줄에 n, m(1≤n, m≤1,000)이 주어진다. 다음 줄에는 n명의 남자들의 성격이 주어진다. 그 다음 줄에는 m명의 여자들의 성격이 주어진다. 성격은 1,000,000이하의 자연수이다.

www.acmicpc.net

 

2) 설명

남자 n명, 여자 m명이 있고, 각각 점수가 매겨져 있을 때,

조건 1. 남자와 여자를 최대한 매칭시키고

조건 2. 그때 커플이 된 남녀 점수차의 합을 최소화

하는 문제다.

난이도 : Gold 3

 

2. 풀이

1) 알고리즘

    :  dp

 

2) 풀이 설명

처음에 top-down 방식으로 풀려다가 개고생했다..

뭔가 코드도 더럽고... 틀리기도 계속 틀리고...

결국 다른 사람들의 풀이를 참고했다.

 

위의 조건 1에서 남자와 여자를 최대한으로 매칭시킨다는 말은 남자 무리와 여자 무리 중 인원이 적은 쪽은 반드시 매칭시켜야 한다는 의미다.

예를 들어 남자 3명 여자 6명 있으면 커플은 최대 3쌍까지만 매칭될 수 있다. 일단 커플을 최대한 매칭시켜야 하기 때문에 무조건 세 쌍까지 매칭시켜야 한다.

 

그 이후로는 어떻게 매칭시키냐의 문제다.

일단 남자와 여자를 점수 순서대로 정렬해준다.

그 다음에는 커플들의 점수차의 합을 최소화해야하기 때문에 알맞은 점화식이 필요하다.

 

dp[x][y] 을 "남자 x명, 여자 y명이 있을 때 만들어진 커플들의 점수차 합의 최소값" 이라고 정의해보자.

말이 긴데 결국 남자 x, 여자 y명일 때 위의 조건1, 2를 만족시키는 값이라는 말이다.

 

이 dp[x][y]의 값은 3가지 경우로 나눠서 구할 수 있다.

1. x == y 인 경우

  x == y라면 남자와 여자의 수가 같다는 말이다. 그러면 조건 1에 따라(최대한 커플을 매칭시켜야하므로)

  dp[x][y] = dp[x - 1][y - 1] + abs(male[x] - female[y])

  가 된다.

2. x > y인 경우

  x > y는 남자가 여자보다 많다는 말이다. 그러므로 여자는 반드시 모두 커플이 돼야 하고 남자는 여자의 수만큼만 커플이 되면 된다.

  이때 x번째 남자는 y번째 여자와 커플이 될 수도 있고 안 될 수도 있다.(이게 x > y일 때 모든 경우의 수다)

  핵심은 x번째 남자가 y번째 여자와 커플이 안 된다고 y번 이전의 여자와 커플이 될 수는 없다는 것이다.

  왜냐하면 여자의 수가 적기 때문에 여자는 반드시 커플을 맺어야 하는데, 만약 x번째 남자가 y - 1번째 여자랑 짝을 맺으면 y번째 여자는 짝을 못 맺기 때문이다! 이는 조건 1에 맞지 않는다!

  그러므로

  dp[x][y] = min(dp[x - 1][y - 1] + abs(male[x] - female[y]) // x번 남자와 y번 여자가 커플이 되는 경우

                      , dp[x - 1][y])                                     // x번 남자와 y번 여자가 커플이 안 되는 경우

  가 된다.

3. x < y 인 경우

  이건 2번과 반대의 경우기 때문에 설명은 생략한다.

  dp[x][y] = min(dp[x - 1][y - 1] + abs(male[x] - female[y]) // x번 남자와 y번 여자가 커플이 되는 경우

                      , dp[x][y - 1])                                     // x번 남자와 y번 여자가 커플이 안 되는 경우

 

이 점화식은 남녀가 점수 순서대로 정렬되어 있다는 전제 하에 올바르게 작동한다.

만약

남자 : 10 5 1

여자 : 2  5

이런 경우에는(정렬이 안 되어있는) dp 배열이

0 0 0 

0 8 5

0 3 8

0 1 7

이런 식으로 나오게 된다. 

그럼 dp[3][2] = 7이 되니까 틀린 답이다. (제대로 된 답은 1이 나와야 한다)

이 알고리즘으로 이중 반복문을 돌고나면 결국 dp[n][m]값이 우리가 구하는 답이 된다.

3. 코드

더보기
#include <iostream>
#include <vector>
#include <math.h>
#include <algorithm>

using namespace std;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);

	int n, m;
	cin >> n >> m;
	vector<int> male(n + 1, 0);
	vector<int> female(m + 1, 0);
	vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));

	sort(male.begin(), male.end());
	sort(female.begin(), female.end());

	for(int i=1;i<=n;i++)
		cin >> male[i];
	for(int i=1;i<=m;i++)
		cin >> female[i];

	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(i == j){
				dp[i][j] = dp[i - 1][j - 1] + abs(male[i] - female[j]);
			} else if(i > j){
				dp[i][j] = min(dp[i - 1][j - 1] + abs(male[i] - female[j]), dp[i - 1][j]);
			} else {
				dp[i][j] = min(dp[i - 1][j - 1] + abs(male[i] - female[j]), dp[i][j - 1]);
			}
		}
	}

	cout << dp[n][m] << endl;

	return 0;
}

 

 

4. 느낀점

dp에서는 탑다운 방식이 직관적이라고 생각해서 항상 탑다운으로 먼저 시도를 하는 편이다..

근데 약간 탑다운 풀이에 한계를 느낀 것 같다..

아마도 계속 잡고 있으면 탑다운으로도 맞힐 수 있을 것 같기는 한데

바텀업 풀이의 아이디어를 보면 이렇게 복잡하게 짜고 있는 탑다운 코드가 너무 야속해진다..

바텀업.. 연습하자...

'알고리즘 > 백준' 카테고리의 다른 글

[백준/2281] 데스노트(1차원 dp)  (2) 2021.10.01
[백준/7512] 연속하는 소수의 합  (0) 2021.09.20
[백준/2110] 공유기 설치  (0) 2021.08.01
[BOJ/1034] 램  (0) 2021.07.27
[BOJ/7579] 앱  (0) 2020.12.29

1. 문제

1) 문제 링크

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

 

코딩테스트 연습 - 4단 고음

4단 고음 I'm in my dream~↗ ~↗ ~↗ IU는 본인의 장기인 3단 고음으로 유명하다. 그러던 그녀가 어느 날 4단 고음을 성공했고 그녀의 고음은 학계에서 연구가 될 만큼 유명해졌다 [1]. [1] 견두헌, 배명

programmers.co.kr

 

2) 설명

문제를 읽어보니 *++을 이용해서 n을 만들어내는 경우의 수를 구하는 문제다.(*의 의미는 곱하기 3, +는 더하기 1이다)

다만 여기에 규칙이 몇 개가 있다.

 

규칙 1 : *++은 반드시 *가 나온 후에 + 두 개가 나와야 한다.

규칙 2 : *++ 계산을 하는 중간에 새로운 *++이 시작할 수도 있다.

 

예를 들어

1. *++*++은 올바르다. 이대로 계산을 하면

* + + * + +
3 4 5 15 16 17

이렇게 17을 만들 수 있다.

 

2. 반면 *+++*+ 은 올바르지 않다.

규칙 1에 위배된다.

 

3. *+*+++은 올바르다.

규칙 3에 따라 *+ ( *++) + 이렇게 중간에 새로운 *++이 시작한 것이다.

* + * + + +
3 4 12 13 14 15

이렇게 15를 만들 수 있다.

 

 

2. 풀이

1) 알고리즘

    : 재귀

 

2) 풀이 설명

처음에는 재귀 반복으로 해결하려 했는데 시간초과가 났다ㅠㅠ 그래서 다른 풀이들을 약간 참고했다.

 

이 풀이는 반드시 * 뒤에 ++가 나와야 한다는 규칙을 이용한다.

모든 *가 +에 우선해야 한다는(ex : ******++++++...) 의미가 아니라 *++ 쌍을 이루는 3단 고음이 * + + 순서대로 나와야 한다는 것이다.

dfs(재귀함수)에는 만들고 싶은 수 n과 남아있는 +의 수 plusCnt를 인자로 받는다.

예를 들어 20을 만들고 싶고 +가 4개가 나온 상황이라면 dfs(20, 4)가 된다.

재귀적으로 dfs함수를 호출해 n = 3, plusCnt = 2까지 줄일 수 있는 경우의 수를 세는 게 문제를 푸는 방법이다.

n이 3, plusCnt가 2라는 말은 *++를 의미한다.

n = 3, plusCnt = 2까지 못 만들면 해당 {n, plusCnt} 상태에서는 규칙에 맞게 *++을 만들 수 없다.

 

또한 (앞으로 나올 수 있는 *의 최대 개수 < 반드시 나와야 할 *의 최소 개수) 라면 더이상 재귀함수를 진행하는 것이 무의미하다. 왜냐면 어떻게 진행을 해도 최소한 나와야 하는 *의 개수를 만들 수 없게 되기 때문이다. -> log(n)/log(3) < (plusCnt + 1)/2 이면 진행을 멈춘다. ( log(n)/log(3)은 (로그 3의 n), 즉 3을 몇 번 제곱해야 n이 되냐는 말이다) (plusCnt + 1)인 이유는 plusCnt가 홀수 개일 때를 대비하기 위함이다. (ex : plusCnt = 5일 때 *는 최소 3개가 나와야 한다. 그러려면 (5 + 1)/2로 해줘야 함)

 

n이 3으로 나누어 떨어진다면 + plusCnt가 2 이상이라면 dfs(n/3, plusCnt - 2)를 호출한다.

n을 3으로 나눈다는 것은 *가 하나 등장했다는 의미이고 plusCnt가 2개 이상 있어야 규칙 1을 위반하지 않을 수 있다.

 

n이 3으로 나누어 떨어지든 안 떨어지든 dfs(n - 1, plusCnt + 1)을 호출한다.

n에서 1을 뺀다는 것은 +를 하나 늘렸다는 말이므로 plusCnt + 1을 해주는 것이다.

 

이 과정을 n은 3, plusCnt는 2가 될 때까지 반복해주면 된다.

 

참고로 첫 호출은 dfs(n - 2, 2)로 해주면 된다. 왜냐하면 마지막 두 개는 반드시 ++이기(plusCnt = 2) 때문이다.(그냥 dfs(n, 0)해도 상관 없다)

 

예를 들어서 n = 13일 때 어떻게 진행되는지 보자.

dfs(11, 2) 호출

-> 11%3 != 0이므로 dfs(10, 3)만 호출

    -> 10%3 != 0이므로 dfs(9, 4)만 호출

        -> 9%3 == 0이고 4(plusCnt) >= 2이므로 dfs(3, 2) 호출

            -> n == 3, plusCnt == 2 이니까 return 1

        -> dfs(8, 5) 호출

            -> log(8)/log(3)은 2보다 작고 (5(plusCnt) + 1)/2)인 3보다 작으므로 return 0

        : 1 + 0 해서 return 1

    : return 1

: reuturn 1

 

이런 식으로 진행된다.

 

3. 코드

더보기
#include <math.h>

// 전역 변수를 정의할 경우 함수 내에 초기화 코드를 꼭 작성해주세요.
using namespace std;

int dfs(int n, int plusCnt){
    if(n < 1 || log(n)/log(3) < (plusCnt + 1)/2){
        // log(n)/log(3)	: 앞으로 나올 수 있는 최대한의 * 횟수(n이 되기 위해 3을 거듭제곱해야 하는 횟수)
        // (plusCnt + 1)/2	: *가 나와야 하는 최소한의 횟수
  		// 앞으로 *가 최대로 나오는 경우(log(n)/log(3)) 가 반드시 나와야 하는 *의 횟수((plusCnt + 1)/2) 보다 작다면 더 재귀를 진행해봤자 무의미함으로 return
        return 0;
    }
    if(n == 3 && plusCnt == 2){
        // * : 하나, + : 둘 이므로 1 return
        return 1;
    }
    
    int ret = 0;
    if(n%3 == 0 && plusCnt >= 2){
    	//3으로 나눠지고 남은 +가 두 개 이상이어야 *가 나올 수 있음
        ret += dfs(n/3, plusCnt - 2);
	}
    ret += dfs(n - 1, plusCnt + 1); //+ 하나를 늘리는 경우
    
    return ret;
}

int solution(int n) {
    return dfs(n - 2, 2); // 시작 시 맨 마지막 두 개는 무조건 ++이어야 하므로 plusCnt를 2로 시작
}

 

 

4. 느낀점

오랜만에 log를 봤다...

종종 사용하면 좋을 것 같다.

'알고리즘 > 프로그래머스' 카테고리의 다른 글

[LV2] 뉴스 클러스터링  (0) 2021.08.26
[LV3] 불량 사용자  (0) 2021.08.13
[LV4] 호텔 방 배정  (0) 2021.08.13
[LV2] 거리두기 확인하기  (0) 2021.07.29

1. 문제

1) 문제 링크

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

 

코딩테스트 연습 - 불량 사용자

개발팀 내에서 이벤트 개발을 담당하고 있는 "무지"는 최근 진행된 카카오이모티콘 이벤트에 비정상적인 방법으로 당첨을 시도한 응모자들을 발견하였습니다. 이런 응모자들을 따로 모아 불량

programmers.co.kr

 

2) 설명

    user id 배열과 불량 사용자 id 배열이 주어진다.

불량 사용자 id는 중간중간 *(wild mask)가 들어갈 수 있다.(길이는 반드시 맞아야 한다)

이때 불량 사용자가 될 수 있는 user id들의 집합의 수를 구하면 된다.

 

2. 풀이

1) 알고리즘

    : set, bit연산

 

2) 풀이 설명

일단 user_id 배열이나 banned_id 배열이나 8개 이하라서 그냥 조합을 다 살펴보면서 세면 될 것 같았다.

이 조합의 수 세기를 재귀적으로 풀기 위해 전역변수로 my_user_id, my_banned_id, my_answer, id_set 들을 선언해주었다.

(매번 함수 인자로 넘기기 귀찮아서..)

그리고 user_id가 banned_id 에 해당하는지를 확인하는 is_matched함수도 하나 만들었다.(wild mask 때문에 새로 만들어줘야 했다.)

 

calculate_comb_num 함수는 banned_id의 index를 인자로 받아서 거기에 맞는 user_id들을 선택한 다음에  다음에 체크할 banned_id의 순번(banned_idx)와 해당 user_id를 선택했다는 사실(id_bit_check에 선택한 user_id의 index를 &연산)을 인자로 해서 자기 자신을 호출한다.

이 과정은 banned id를 다 셀 때까지 계속되고, 그때 구한 조합의 집합(id_bit_check)가 id_set에 없을 때 myAnswer 값을 1 증가시켜준다.(id_set에 있다는 말은 이전에 이미 카운팅했다는 말임으로)

 

3. 코드

더보기
// 풀이

#include <string>
#include <vector>
#include <set>

using namespace std;

vector<string> my_user_id, my_banned_id;
int my_answer = 0;
set<int> id_set;

bool is_matched(string id, string form){
    //불량사용자 form에 id가 맞는지 확인하는 함수
    if(id.length() != form.length())
        return false;
    for(int i=0;i<id.length();i++){
        if(id[i] == '*' || form[i] == '*')
            continue;
        if(id[i] != form[i])
            return false;
    }
    return true;
}

void calculate_comb_num(int banned_id_idx, int id_bit_check){
    /*
        @banned_id_idx        = 다음에 택할 불량사용자 index
        @id_bit_check = bit로 표현한 불량사용자로 선택된 user 집합
    */
    if(banned_id_idx == my_banned_id.size()){ 
        //불량사용자를 다 배정했으면 진입
        if(id_set.find(id_bit_check) != id_set.end()){
            //이미 셌던 집합이면 return
            return;
        } 
        my_answer++; //조합 개수 증가
        id_set.insert(id_bit_check); //id_set에 추가
        return;
    }
    for(int i=0;i<my_user_id.size();i++){
        if(!(id_bit_check & (1 << i)) && is_matched(my_user_id[i], my_banned_id[banned_id_idx])){
            //불량 사용자로 선택했던 user가 아니면 + 불량사용자 조건에 해당하면...
            calculate_comb_num(banned_id_idx + 1, id_bit_check | (1 << i));
        }
    }
}

int solution(vector<string> user_id, vector<string> banned_id) {
    my_user_id = user_id;
    my_banned_id = banned_id;
    
    calculate_comb_num(0, 0);
    
    return my_answer;
}

 

 

4. 느낀점

문제에서 스네이크 표기법 써서 그냥 통일해서 쓰는데 언더바 치기 귀찮다...

카멜표기법이 게으른 나에게 더 맞는 것 같다..

'알고리즘 > 프로그래머스' 카테고리의 다른 글

[LV2] 뉴스 클러스터링  (0) 2021.08.26
[LV4] 4단 고음  (0) 2021.08.19
[LV4] 호텔 방 배정  (0) 2021.08.13
[LV2] 거리두기 확인하기  (0) 2021.07.29

1. 문제

1) 문제 링크

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

 

코딩테스트 연습 - 호텔 방 배정

 

programmers.co.kr

 

2) 설명

   사용자들이 원하는 방의 배열이 주어지고 순서대로 사용자들이 원하는 방에 배정한다.

이미 방이 사용 중이라면 원하는 방보다 높은 숫자의 빈 방 중 가장 낮은 빈 방을 배정받는다.

배정받은 방의 번호들의 배열을 리턴한다.

2. 풀이

1) 알고리즘

    : Solution 1 : set 사용

      Solution 2 : map, find 사용

 

2) 풀이 설명

  풀이 1. set 사용

일단 데이터 타입들이 long long이어서 효율성 문제가 있겠거니 생각했다.

효율성은 나중에 생각하고 일단 풀어보자 생각해서 set을 사용해서 풀어봤다.

 

1. get_allocated_room 함수를 통해 방을 배정받는다.

2. 원하는 방이 이미 배정된 방이라면 다음 방(target_room + 1)을 재귀적으로 확인한다.

3. 배정받는 방은 모두 room_set에 넣어준다.

 

알고리즘은 맞지만 예상대로 효율성을 통과하지 못했다.

왜냐하면 탐색할 때마다 room_set을 탐색해야 하는데 이 탐색이 원하는 방 + 1씩 증가하면서 계속 탐색하기 때문이다.

만약 1~1000,000,000 번 방이 다 꽉 차있고 다음 사람이 1번방을 원한다면 1~1000,000,000번 방을 set에서 찾아봐야 하기 때문이다...

그래서 다른 방법을 생각해봤는데 딱히 안 떠올라서 다른 풀이들을 약간 참고했다.

 

   풀이 2. map, find 사용

set을 사용할 때와 엄청나게 다른 풀이는 아니다. 다만 union-find 알고리즘 일부가 추가되었을 뿐이다.

room_map에는 key는 방 번호이고 value는 (비어있다면 0, 이미 배정된 방이라면 다음에 배정될 수 있는 방 번호) 이 된다.

그래서 탐색할 때마다 이 맵을 업데이트 해준다.

예를 들어 room_map 상황이 다음과 같다고 가정해본다.

{

    1 : 4,

    2 : 4,

    3 : 4,

}

이 상황에서 다음 사람이 2번 방을 원한다면 get_allocated_room 함수에서는

room_map[2]은 0이 아니므로 배정된 방이구나. -> 4번 방으로 가라네? 하고 get_allocated_room(4)를 호출한다.

그리고  room_map[4]는 0이니까 배정된 거네. 하고 4번 방을 리턴한다.

그러면 4번 방은 배정이 된 거니까 그 다음 방인 5번 방이 room_map[4]에 배정된다.

그래서 room_map은 다음과 같이 바뀐다.

{

    1 : 4

    2 : 4

    3 : 4

    4 : 5

}

그리고 다음에 2번방을 원하는 사람이 왔을 때 2 -> 4 -> 5의 탐색 과정을 다시 거치지 않도록 room_map[2]도 5로 업데이트 해준다. (어차피 get_allocated_room 함수는 재귀적으로 돌아가고 배정하는 건 그냥 한 줄 추가하는 거라 시간복잡도에 큰 영향은 없다. 즉 탐색 과정을 줄여서 얻는 속도 절감분이 room_map 업데이트를 위해 한 줄 추가해서 얻는 증가분보다 훨씬 크다)

그러면 room_map은 다시 다음과 같이 바뀐다.

{

    1 : 4,

    2 : 5,

    3 : 4,

    4 : 5

}

물론 1번이나 3번 방을 원하는 사람이 오면 1 -> 4 -> 5 나 3 -> 4 -> 5의 과정을 더 거쳐야하지만 2번 방에서만큼은 확실히 다음 탐색 때 속도를 줄여줄 수 있다.

또한 room_map이 다음 상황일 때는 

{

    1 : 2,

    2 : 3,

    3 : 4,

    4 : 5,

    ... (5 ~ 999999)

    1000000 : 0

}

3번 방을 원하는 사람이 왔을 때 3 -> 4 -> ... -> 1000000까지 탐색해야하지만 중간에 4번 방을 찾는 사람이 있어서 room_map[4] 값을 업데이트 해줬었다면, 즉 room_map이 다음과 같은 상황이라면

{

    1 : 2,

    2 : 3,

    3 : 4,

    4 : 1000000,

    ... (5 ~ 999999)

    1000000 : 0

}

3 -> 4 -> 1000000 으로 훨씬 단축시킬 수 있다.

 

이렇게 함수를 재귀적으로 돌려서 효율성 테스트까지 통과시킬 수 있었다.

3. 코드

더보기
//SOLUTION 1 : SET 사용
#include <vector>
#include <set>

using namespace std;

set<int> room_set;

long long get_allocated_room(long long target_room){
    if(room_set.find(target_room) == room_set.end()){
        return target_room;
    } else {
        return get_allocated_room(target_room + 1);
    }
}

vector<long long> solution(long long k, vector<long long> room_number) {
    vector<long long> answer;
    for(int i=0;i<room_number.size();i++){
        long long allocated_room = get_allocated_room(room_number[i]);
        answer.push_back(allocated_room);
        room_set.insert(allocated_room);
    }
    return answer;
}
//SOLUTION 2 : MAP, FIND 사용

#include <vector>
#include <map>

using namespace std;

map<long long, long long> room_map;

long long get_allocated_room(long long target_room){
    if(room_map[target_room] == 0) {
        return target_room;
    } else {
        room_map[target_room] = get_allocated_room(room_map[target_room]);
        return room_map[target_room];
    }
}

vector<long long> solution(long long k, vector<long long> room_number) {
    vector<long long> answer;
    for(int i=0;i<room_number.size();i++){
		long long allocated_room = get_allocated_room(room_number[i]);
 		answer.push_back(allocated_room);
		room_map[allocated_room] = allocated_room + 1;
    }
    return answer;
}

 

4. 느낀점

union-find를 잘 안 다뤄봐서 생각해내지 못했던 것 같다.

관련 문제들을 좀 더 풀어보자

'알고리즘 > 프로그래머스' 카테고리의 다른 글

[LV2] 뉴스 클러스터링  (0) 2021.08.26
[LV4] 4단 고음  (0) 2021.08.19
[LV3] 불량 사용자  (0) 2021.08.13
[LV2] 거리두기 확인하기  (0) 2021.07.29

1. 문제

1) 문제 링크

    : 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

 

2) 설명

수평선 위에 N개의 집이 있고 N개의 집에 C개의 공유기를 설치하는 문제.

문제에서 말하는 "가장 인접한 두 공유기 사이의 거리를 최대로 하는" 이 조금 이해하기 힘들었지만 결국

공유기가 설치된 집들 간의 거리의 최소값을 최대로 만들면 된다.(말이 조금 어렵다)

예를 들어

index 0 1 2 3 4 5
위치 1 3 8 10 14 20
공유기 설치 O X X O X O

위와 같은 상황이라면 답은 0번째 집, 3번째 집, 5번째 집에 설치를 해서 공유기가 설치된 이웃집 간 최소 거리의 최대값인 9 ( maximize( min( dist(house[0], house[3]), dist(house[3], house[5]) ) ) ) 이 된다.

 

2. 풀이

1) 알고리즘

    : 이분탐색

 

2) 풀이 설명

 공유기가 설치된 이웃집 거리는 1에서 (수평선에서 가장 오른쪽 집과 가장 왼쪽 집 거리) 범위 안에 있을 수 있다.

그래서 그 거리를 구하기 위해 + 이분 탐색을 위해 houses(집 위치 벡터)를 정렬해준다.

이후 이분탐색을 통해서 최소 거리의 최대값을 찾아나간다.

이때 getMinDist 함수를 통해서 인자로 입력받은 거리 이상으로 공유기 설치가 가능한지 판단하고 불가능하다면 -1을, 가능하다면 그때의 거리 최소값(인자로 입력받은 거리 이상의)을 리턴해주도록한다.

 

getMinDist 함수에서 첫 번째 집(가장 왼쪽)은 반드시 공유기를 설치한다고 가정한다. 그 이유는 다음과 같다.

처음 공유기가 설치되는 집이 x번째 ( x > 1 )라고 해보자. 그러면 x번쨰 집(hosues[x])과 그 다음 공유기가 설치된 집(houses[x + n]) 간의 거리는

1. 공유기가 설치된 집 중 가장 가까운 거리

2. 공유기가 설치된 집 중 가장 가깝지는 않은 거리

중 하나다.

당연한 말 같아보이지만 첫 번째 집에 공유기를 설치해야되는 이유를 설명하는 데 이 두 가지 경우를 나눠서 생각하는 게 필요하다고 생각한다.

이제 x번째 집에서 공유기를 제거하고 첫 번째 집(houses[0])에 공유기를 설치한다고 생각해보자.

그러면 그 다음 공유기가 설치된 집(houses[x + n])과 첫 번째 공유기가 설치된 집(이제는 houses[0]) 사이의 거리는 공유기를 x번째에 설치했을 때보다 멀어질 것이다.

그러면 위의 1번의 경우에는 공유기가 설치된 집 간 거리 중 최소값이 증가하게 된 것이니까 답을 잘못 구한 게 된다.(더 큰 최소값을 찾을 수 있다는 말)

그리고 2번의 경우에는 이 첫 번째 공유기가 설치된 집과 두 번째 공유기가 설치된 집 사이의 거리가 늘어나도 어차피 최소 거리보다는 크기 때문에 전혀 상관이 없다.

결론적으로 문제에서 요구하는 "가장 인접한 두 공유기 사이의 거리를 최대로 하는 집들"을 선택했을 때 그 시작이 첫 번째(houses[0])집이 아니라면 1번의 경우에는 더 큰 최소값이 있으므로 답을 잘못 구한 게 되고 2번의 경우에는 첫 번째 집을 고르든 말든 전혀 상관이 없다는 말이다. 즉 첫 번째 집에 무조건 공유기를 설치했다고 가정하는 것은 답을 구하는 데 충분조건으로 작용한다는 말이다.

이 논리는 반대로 마지막 집에도 똑같이 적용된다.

 

또한 사실 getMinDist 함수를 인자로 받은 거리로 공유기 설치가 가능한지만 판단해줘도 되는데 모든 집 사이의 거리가 넓은 경우에 시간을 더 단축할 수 있지 않을까 해서 인자로 받은 거리 이상의 거리(즉 공유기가 설치된 집 간의 거리가 인자로 받은 거리 이상으로 위치하게 하는 실제 거리)를 리턴하도록 해주었다. (근데 속도를 보니 크게 시간 단축의 효과는 없는 것 같았다ㅠㅠ)

 

3. 코드

더보기

 

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef long long LL;

const LL MAX_VAL = 999999999;
LL N, C;
vector<LL> houses;

LL getMinDist(LL minDist){
	LL answer = MAX_VAL;
	int prevHouse = 0; // 이전 집과의 거리를 구하기 위해 이전 집의 index를 저장해놓을 변수
	LL numOfAP = 1; // 설치된 공유기의 개수. 첫 집에 설치된 상태에서 시작하기 때문에 초기값 1

	for(int i=1;i<houses.size() && numOfAP<=C;i++){
		if(houses[i] - houses[prevHouse] >= minDist){
        	//집 간 거리가 인자로 받은 최소거리보다 크다면 업데이트
			answer = min(answer, houses[i] - houses[prevHouse]); // 거리 업데이트
			prevHouse = i; // 이전 집 index 업데이트
			numOfAP++; // 설치된 공유기 개수 증가
		}
	}

	if(numOfAP < C){
		//공유기를 적게 놓았으면 -1 return
		return -1;
	} else {
		return answer;
	}
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);

	LL input, answer = 0;
	cin >> N >> C;
	
	houses.reserve(N);
	for(int i=0;i<N;i++){
		cin >> input;
		houses.push_back(input);
	}
	
	//sort
	sort(houses.begin(), houses.end());
	
	//binary search
	LL s = 0;
	LL e = houses.back() - houses.front();
	while(s<=e){
		LL mid = (s + e)/2;
		LL minDist = getMinDist(mid);//mid 이상의 거리의 최소값을 구함

		if(minDist >= mid){
			s = minDist + 1;
			answer = max(answer, minDist);// 최소 거리의 최대값을 구해야 하므로 minDist >= mid 일 때만 업데이트
		} else {
			e = mid - 1;
		}
	}
	
	cout << answer << endl;

	return 0;
}

 

 

4. 느낀점

이분탐색은 자주 구현해도 어려운 것 같다...

'알고리즘 > 백준' 카테고리의 다른 글

[백준/7512] 연속하는 소수의 합  (0) 2021.09.20
[백준/1727] 커플 맞추기  (0) 2021.08.25
[BOJ/1034] 램  (0) 2021.07.27
[BOJ/7579] 앱  (0) 2020.12.29
[BOJ/14891] 톱니바퀴  (0) 2020.12.18

+ Recent posts