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://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://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

1. 문제

1) 문제 링크

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

 

1034번: 램프

첫째 줄에 N과 M이 주어진다. N은 행의 개수이고, M은 열의 개수이다. N과 M은 50보다 작거나 같은 자연수이다. 둘째 줄부터 N개의 줄에는 램프의 상태가 주어진다. 1이 켜져있는 상태이고, 0이 꺼져

www.acmicpc.net

 

2) 설명

램프의 열을 조작하는 스위치를 K번 조작해서 램프가 모두 켜져 있는 행의 개수를 최대로 만드는 문제

2. 풀이

1) 알고리즘

    : 브루트포스

 

2) 풀이 설명

   2-1) 재귀

    첫 시도는 재귀로 시도했으나 시간초과로 실패했다. 생각해보니 열이 최대 50개인데 50개의 열을 켜고 끄는 경우를 생각하면 2의 50제곱이 되어서 당연히 시간초과가 날 수밖에 없던 것 같다.

    그래서 해당 열까지 모든 램프가 켜진 행의 개수가 이전에 구했던 모든 램프가 켜진 행의 최대 개수보다 작거나 같다면 더 탐색을 진행하지 않는다. (왜냐하면 더 탐색을 해봤자 이전에 구한 최대값을 넘을 수 없기 때문에)

    이렇게 하니까 일단 답은 맞았다.

 

  2-2) 맵 사용

    다른 사람들의 풀이를 참고해보니 초기 램프 배열에서 패턴이 똑같은 행의 개수를 구하는 게 더 편해보였다.

    대신 이때 초기에 한 행에서 꺼져 있는 램프의 개수가 K개 이하여야 하고, 남은 스위치 조작 횟수(K - 꺼진 램프 개수)가 짝수 개여야 한다. 스위치를 조작해야될 횟수가 x번 남았을 때 x가 짝수라면 아무 열이나 껐다 켰다 두 번씩 반복하면 되는데, x가 홀수 번이라면 껐다 켰다를 반복하고나서 마지막에 결국 한 번 더 뒤집어야 하기 때문이다.(결국 램프가 다 켜진 행에서 한 번 더 스위치를 바꾸므로 한 개의 램프가 꺼지게 된다)

 

    그래서 맵(key : 램프 행, value : 개수)을 사용해서 초기 램프 행들 중 같은 패턴을 가지는 행 개수를 카운팅 해줬다.

 

    예를 들어

 

    1 - 1001

    2 - 0100

    3 - 1100

    4 - 1001

 

    이렇게 있다면 맵에는 {1001 : 2, 0100 : 1, 1100 : 1} 이렇게 들어가게 된다. 그리고 이 맵을 돌면서 행에서 꺼진 램프의 개수가 K개 이하이고, 꺼진 램프를 모두 켰을 때 (K - 꺼진 램프의 개수)가 짝수인 경우에 답을 업데이트해나갔다.

 

3. 코드

더보기
//재귀 풀이

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

using namespace std;

int N, M, K, answer = -1;
vector<string> lamp(50);

void reverseCol(int colIdx){
	//colIdx 열 램프 뒤집기
	for(int i=0;i<N;i++)
		lamp[i][colIdx] = (lamp[i][colIdx] == '0'? '1' : '0');
}

int countRow(int colIdx){
	//colIdx까지 몇 개의 램프가 켜져있는지 확인
	if(colIdx == 0)
		return 0;
	int i, j, answer = 0;
	for(i=0;i<N;i++){
		for(j=0;j<colIdx;j++){
			if(lamp[i][j] == '0')
				break;
		}
		if(j == colIdx)
			answer++;
	}
	return answer;
}

/*
	재귀 접근
	@colIdx 열 번호
	@changeCount 램프 열을 켜고 끌 수 있는 남은 횟수
*/
void solve(int colIdx, int changeCount){
	if(colIdx == M){
		//마지막 열일 경우
		if((K - changeCount)%2 == 0)
			//램프를 켜고 끌 수 있는 남은 횟수가 짝수일 경우에만 업데이트
			answer = max(answer, countRow(M));
		return;
	}
	
	//앞서 구한 최대로 켤 수 있는 램프 행의 수보다 켜져있는 램프의 수가 적으면 더 돌리지 않음
	if(countRow(colIdx + 1) > answer)
		solve(colIdx + 1, changeCount);

	reverseCol(colIdx);//뒤집기
	if(countRow(colIdx + 1) > answer && changeCount < K)
		//앞서 구한 최대로 켤 수 있는 램프 행의 수보다 켜져있는 램프 행의 수가 적으면 더 돌리지 않음
		solve(colIdx + 1, changeCount + 1);
	reverseCol(colIdx);//다시 뒤집기
}

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

	cin >> N >> M;
	for(int i=0;i<N;i++)
		cin >> lamp[i];
	cin >> K;
	
	solve(0, 0);

	cout << answer << endl;

	return 0;
}
//패턴과 맵을 이용한 풀이
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>

using namespace std;

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

	int N, M, K, answer = 0;
	cin >> N >> M;
	string lamp;
	//램프 행, 개수
	map<string, int> lampMap;

	for(int i=0;i<N;i++){
		cin >> lamp;
		//램프 맵에 카운트 증가
		lampMap[lamp] += 1;
	}
	cin >> K;

	for(auto x : lampMap){
		string row = x.first;
		int val	= x.second;
		//행에 있는 0의 개수
		int zeroCnt = count(row.begin(), row.end(), '0');
		
		//행에 있는 0을 모두 켰을 때 램프 열을 뒤집을 남은 횟수가 짝수여야 함.
		if(zeroCnt <= K && (K - zeroCnt)%2 == 0)
			answer = max(answer, val);
	}

	cout << answer << endl;

	return 0;
}

 

4. 느낀점

- 재귀에서 시간초과난다고 포기하지 않은 나 자신 칭찬한다

- 하지만 아집을 버리고 포기했더라면 더 좋은 방법을 찾을 수도 있었을 텐데 미한한 나 자신 혼낸다

- 사실 스트링 패턴으로 접근하는 풀이는 나 혼자서는 오래 고민한다 해도 생각해내기 힘들었을 것 같다. 좀 더 열린, 다른 시각으로 접근하는 연습을 해보도록 하자

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

[백준/1727] 커플 맞추기  (0) 2021.08.25
[백준/2110] 공유기 설치  (0) 2021.08.01
[BOJ/7579] 앱  (0) 2020.12.29
[BOJ/14891] 톱니바퀴  (0) 2020.12.18
[백준/9656] 돌 게임 2  (0) 2020.11.02

1. 문제

1) 문제 링크

    : www.acmicpc.net/problem/7579

 

7579번: 앱

입력은 3줄로 이루어져 있다. 첫 줄에는 정수 N과 M이 공백문자로 구분되어 주어지며, 둘째 줄과 셋째 줄에는 각각 N개의 정수가 공백문자로 구분되어 주어진다. 둘째 줄의 N개의 정수는 현재 활

www.acmicpc.net

 

2) 설명

 각각의 프로세스가 메모리를 m_i만큼 사용하고 있고, 각 프로세스를 비활성화시킬 때 드는 비용이 c_i라면, 최소 M만큼의 메모리 공간을 확보하는 데 필요한 최소 비용을 구하는 문제

2. 풀이

1) 알고리즘

    : DP(배낭)

 

2) 풀이 설명

 A) 첫 시도 : 2차원 배열 사용하기

    - 처음에는 메모리 생각을 안 하고 100(N 최댓값) x 10000000(M 최댓값) 2차원 배열을 사용했다.

     메모리 초과가 나서 안 되니까 map을 사용해보자는 생각으로 map을 사용했지만 역시나 메모리 초과로 실패했다.     (어차피 사용량 많아지면 비슷해지는 것 같다)

 

 B) 두번째 시도 : 1차원 배열 사용하기

     - 한참 고민하다가 결국 다른 사람들 풀이를 참고해서 풀게 되었다.

      dp 배열 : 1차원 배열로 dp[cost] : cost를 사용해서 최대로 확보할 수 있는 memory

     이제 이중 반복문을 통해 dp 배열을 업데이트해나갔다.

     점화식 : dp[j] = max(dp[j], dp[j - C[i]] + A[i])

      i번째 프로세스를 비활성화할 경우 dp 배열을 업데이트 해나가는 점화식이다.

      만약 활성화된 채로 두는 게 더 큰 memory를 확보할 수 있다면 dp[j] 가 선택 될 것이고, 비활성화하는 게 더 큰 메모리를 확보할 수 있다면 dp[j-C[i]] + A[i] 가 선택될 것이다.

 

3. 코드

더보기
/*
	앱
	DP
    1차원 배열을 사용한 풀이
*/
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>

#define N_MAX 100
#define C_MAX 100
#define M_MAX 10000000

using namespace std;

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

	int N, M, maxCost, answer;
	vector<int> A(N_MAX, 0), C(N_MAX, 0);
	vector<int> dp(N_MAX*C_MAX + 1, 0);

	cin >> N >> M;
	
	for(int i=0;i<N;i++) cin >> A[i];
	for(int i=0;i<N;i++) cin >> C[i];
	
	maxCost = accumulate(C.begin(), C.end(), 0);
	
	for(int i=0;i<N;i++)
		for(int j=maxCost;j>=C[i];j--)
			dp[j] = max(dp[j], dp[j - C[i]] + A[i]);

	for(answer=0;answer<maxCost && dp[answer]<M;answer++);
	cout << answer << "\n";
}

 

/*
	앱
	DP
    vector와 map을 중첩하여 사용한 풀이
*/
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>

#define N_MAX 100
#define C_MAX 100
#define M_MAX 10000000

using namespace std;

int N, M;
vector<int> A(N_MAX, 0), C(N_MAX, 0);
vector<map<int, int>> dp(N_MAX);

int solve(int idx, int needMemory){
	if(idx >= N)
		return M_MAX;	
	if(needMemory <= 0)
		return 0;

	if(dp[idx][needMemory] <= 0)
		dp[idx][needMemory] = min(solve(idx + 1, needMemory), C[idx] + solve(idx + 1, needMemory - A[idx]));

	return dp[idx][needMemory];
}

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

	cin >> N >> M;
	
	for(int i=0;i<N;i++) cin >> A[i];
	for(int i=0;i<N;i++) cin >> C[i];

	cout << solve(0, M) << endl;
}

 

4. 느낀점

1) 좀 더 다방면의 방법을 생각해야겠다. 너무 N, M 값에만 집중해서 생각하다보니 다른 방법을 잘 못 찾았다.

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

[백준/2110] 공유기 설치  (0) 2021.08.01
[BOJ/1034] 램  (0) 2021.07.27
[BOJ/14891] 톱니바퀴  (0) 2020.12.18
[백준/9656] 돌 게임 2  (0) 2020.11.02
[백준/2631] 줄세우기  (0) 2020.11.02

1. 문제

1) 문제 링크

    : www.acmicpc.net/problem/14891

 

14891번: 톱니바퀴

첫째 줄에 1번 톱니바퀴의 상태, 둘째 줄에 2번 톱니바퀴의 상태, 셋째 줄에 3번 톱니바퀴의 상태, 넷째 줄에 4번 톱니바퀴의 상태가 주어진다. 상태는 8개의 정수로 이루어져 있고, 12시방향부터

www.acmicpc.net

 

2) 설명

삼성 기출인 것 같다. 시뮬레이션 문제..

톱니바퀴 4개를 두고 i번째 톱니를 시계방향 혹은 반시계방향으로 돌리는 문제다.

이때 1, 2, 3, 4 톱니들이 서로 맞물려 있는 바퀴의 값이 다를 때만 옆의 톱니바퀴를 반대방향으로 돌린다.

예를 들어 2번 톱니를 돌리는데 돌리기 전 1번, 2번 톱니가 맞물린 쪽이 S, N으로 다르다면 1번도 돌리고 S, S로 같다면 돌리지 않는다.

 

다 돌리고 나서 각 톱니의 12시 방향에 있는 값을 구하면 된다.

2. 풀이

1) 알고리즘

    : 시뮬레이션

 

2) 풀이 설명

톱니를 돌리는 걸 처음에 직접 구현하려다가 그냥 rotate 함수를 썼다.

사실 시뮬레이션 문제를 잘 안 풀어봐서 처음 써보다보니 음의 방향으로 돌리는 걸 v.begin()기준으로 해서 헷갈렸다.(v.end() 기준으로 해서 조금 더 간단하게 할 수 있었다)

어떤 톱니까지 돌릴지는 rotate 전에 diff라는 vector를 사용해서 미리 구했다.

그리고 diff에 체크된 배열들을 gear_idx 톱니(입력으로 돌리는 톱니)를 기준으로 왼쪽, 오른쪽으로 돌려나갔다.

 

3. 코드

더보기

 

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

#define N_GEAR 4
#define N_WHEEL 8

using namespace std;


int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	
	int K, gear_idx, direction, answer = 0;
	vector<vector<bool>> gear(N_GEAR+1, vector<bool>(N_WHEEL, false));
	string input;
	for(int i=1;i<=N_GEAR;i++){
		cin >> input;
		for(int j=0;j<N_WHEEL;j++){
			gear[i][j] = input[j]=='0'? 0 : 1;
		}
	}

	cin >> K;
	vector<bool> diff(5, false);

	while(K--){
		cin >> gear_idx >> direction;
		
		fill(diff.begin(), diff.end(), false);
	
		int left_direction = -direction;
		int right_direction = -direction;

		for(int i=gear_idx+1;i<=N_GEAR;i++)
			diff[i] = gear[i][6] != gear[i-1][2];

		for(int i=gear_idx-1;i>0;i--)
			diff[i] = gear[i][2] != gear[i+1][6];
	
		rotate(gear[gear_idx].begin(), (direction==1? gear[gear_idx].end() - 1 : gear[gear_idx].begin() + 1), gear[gear_idx].end());

		for(int i=gear_idx+1;i<=N_GEAR && diff[i];i++, left_direction *= -1)
			rotate(gear[i].begin(), (left_direction==1? gear[i].end() - 1 : gear[i].begin() + 1), gear[i].end());

		for(int i=gear_idx-1;i>0 && diff[i];i--, right_direction *= -1)
			rotate(gear[i].begin(), (right_direction==1? gear[i].end() - 1 : gear[i].begin() + 1), gear[i].end());
	}
	vector<int> squared = {0, 1, 2, 4, 8};
	for(int i=1;i<=N_GEAR;i++){
		if(gear[i][0])
			answer += squared[i];
	}
	cout << answer << "\n";


	return 0;
}

 

4. 느낀점

 - 시뮬레이션은 단순한 개념을 깔끔하고 효율적으로 구현해내야 하는 게 중요한 것 같다

 - 단순해보인다고 냅다 구현하려다 보면 코드 더러워지면서 혼난다.. 내 코드다 많이 더러워서 나중에 다시 손봐야 할 것 같다..

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

[BOJ/1034] 램  (0) 2021.07.27
[BOJ/7579] 앱  (0) 2020.12.29
[백준/9656] 돌 게임 2  (0) 2020.11.02
[백준/2631] 줄세우기  (0) 2020.11.02
[백준/9252] LCS 2  (0) 2020.10.27

1. 문제

1) 문제 링크

    : www.acmicpc.net/problem/9656

 

9656번: 돌 게임 2

상근이가 게임을 이기면 SK를, 창영이가 게임을 이기면 CY을 출력한다.

www.acmicpc.net

 

2) 설명

돌 게임(9655)이랑 다른 게 없는 문제같다.. 마지막 돌을 가져간 사람이 이기네 지네 차이일 뿐...

2. 풀이

1) 알고리즘

    : DP, 수학

 

2) 풀이 설명

돌 게임 때는 알고리즘이 DP로 분류돼 있어서 DP로 풀었는데 생각해보니까 DP를 쓸 필요가 없는 것 같다..

문제에서 분류해준 알고리즘에 갇혀서 효율적인 풀이를 생각하지 못 했다..

그냥 N이 홀수인지 짝수인지만 판별하면 됐다.. 왜냐하면 N-2번째 돌을 가져가는 사람이 N번째 돌을 가져갈 수밖에 없기 때문이다.(상대방이 N-1번째 돌을 하나 가져가면 N번째 돌을 가져가야 하기 때문) 그럼 결국 N이 홀수일 때는 무조건 상근이가, 짝수일 때는 창영이가 마지막 돌을 가져갈 수밖에 없어진다.

 

3. 코드

더보기
/*
	돌게임2
	DP
*/
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

using namespace std;

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

	int N;
	cin >> N;

	cout << (N%2? "CY\n" : "SK\n");
	
	return 0;
}

 

 

4. 느낀점

1. 고정관념에 갇히지 말자!

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

[BOJ/7579] 앱  (0) 2020.12.29
[BOJ/14891] 톱니바퀴  (0) 2020.12.18
[백준/2631] 줄세우기  (0) 2020.11.02
[백준/9252] LCS 2  (0) 2020.10.27
[백준/11501] 주식  (0) 2020.10.18

+ Recent posts