본문 바로가기

프로그래밍/PS

[C++] 백준 1107번: 리모컨

반응형

문제 바로가기

 

1107번: 리모컨

첫째 줄에 수빈이가 이동하려고 하는 채널 N (0 ≤ N ≤ 500,000)이 주어진다.  둘째 줄에는 고장난 버튼의 개수 M (0 ≤ M ≤ 10)이 주어진다. 고장난 버튼이 있는 경우에는 셋째 줄에는 고장난 버튼

www.acmicpc.net


문제

 

수빈이는 악력이 지나치게 쎈가보다. 리모컨을 다루다가 일부 숫자 버튼이 고장났다.

  - 숫자 버튼이 고장났다는 내용을 문제 다 풀고 글 쓰는 지금 알았다. +, -도 고려하는 줄 알고 assert까지 써가봐 확인했는데... 역시 문제를 몇 번은 꼼꼼히 읽는 버릇을 들여야겠다.

0~9번까지의 숫자, +, - 버튼이 있다. 수빈이는 N번 채널로 이동하고 싶다.

어떤 버튼이 고장났는지 주어질 때, 채널 N으로 이동하려면 최소 몇 번 버튼을 눌러야 하는지 알아보자.

ps. 나라면 리모컨 새로 샀다.


조건

0 ≤ N ≤ 500,000

0 ≤ M ≤ 10


접근

아래 경우를 나누어 생각을 해보았다.

1. 모든 숫자 버튼이 망가진 경우

  - '+', '-' 버튼 밖에 못 쓴다. 따라서 \(|N-100|\)을 출력한다.

2. N == 0인 경우

  - 사용할 수 있는 버튼 중 가장 작은 수 + 1을 출력한다. (한 번 누르고 - 계속 누르기)

3. 0만 쓸 수 있는 경우

  - N < 50인 경우 N + 1을, 그 외에는 \(|N - 100|\)을 출력한다.

  - 0을 누르고 +를 누르냐, 아니면 그냥 +, -를 누르냐의 차이

4. 일반적인 경우

  - 만들 수 있는 수 중 제일 작은 N보다 큰 수 G제일 큰 N보다 작은 수 S를 구한다.

  - \(min(G - N + digit(G), N - S + digit(S), N - 100)\)를 출력한다.

  - G에서 -를 계속 누르기, S에서 +계속 누르기, 그냥 +, -를 누르기 중 제일 적은 횟수를 찾는거다.

  - digit 함수는 자릿수를 반환한다.

코드보기:

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

using namespace std;

#define FAST_IO cin.tie(NULL); ios::sync_with_stdio(false);
#define INF 987654321
#define EL "\n"

int digit(int n) {
	int d = 0;
	for (int i = n; i > 0; i /= 10)
		d++;
	return d;
}

int main() {
	FAST_IO;

	int N;
	int M;
	cin >> N >> M;

	char brokenButtons[10];
	for (int i = 0; i < M; i++)
		cin >> brokenButtons[i];
	
	// 1. All numbers is broken
	bool isAllBroken = true;
	for (int i = 0; i <= 9; i++) {
		bool isFound = false;
		for (int j = 0; j < M; j++)
			if ('0' + i == brokenButtons[j]) {
				isFound = true;
				break;
			}
		if (isFound == false) {
			isAllBroken = false;
			break;
		}
	}
	if (isAllBroken) {
		cout << abs(N - 100) << EL;
		return 0;
	}

	// 2. N == 0
	if (N == 0) {
		for (int i = 0; i <= 9; i++) {
			bool isFound = false;
			for (int j = 0; j < M; j++)
				if ('0' + i == brokenButtons[j]) {
					isFound = true;
					break;
				}
			if (isFound == false) {
				cout << i + 1 << EL;
				return 0;
			}
		}
	}

	// 3. Only 0 is available.
	bool isOnlyZero = true;
	for (int i = 1; i <= 9; i++) {
		bool isFound = false;
		for (int j = 0; j < M; j++)
			if ('0' + i == brokenButtons[j]) {
				isFound = true;
				break;
			}
		if (isFound == false) {
			isOnlyZero = false;
			break;
		}
	}
	if (isOnlyZero) {
		if (N < 50)
			cout << 1 + N << EL;
		else
			cout << abs(N - 100) << EL;
		return 0;
	}

	// 4. General
	// 4-1. A Smallest number of numbers greater than N.
	int greater= N;
	while (true) {
		for (int i = greater; i > 0; i /= 10) {
			for (int j = 0; j < M; j++) {
				if ('0' + i % 10 == brokenButtons[j]) {
					greater++;
					goto CONTINUE_G;
				}
			}
		}
		break;
	CONTINUE_G:;
	}
	// 4-2. A Greatest number of numbers smaller than N.
	int smaller = N;
	while (true) {
		for (int i = smaller; i > 0; i /= 10) {
			for (int j = 0; j < M; j++) {
				if ('0' + i % 10 == brokenButtons[j]) {
					smaller--;
					goto CONTINUE_S;
				}
			}
		}
		break;
	CONTINUE_S:
		// PS. When smaller is useless.
		if (smaller == 0) {
			smaller = -INF;
			break;
		}
	}
	// 4-3. Get minimum and print
	int result = min(min(greater - N + digit(greater), N - smaller + digit(smaller)), abs(N - 100));
	cout << result << EL;

	return 0;
}

결과: 틀렸습니다.


분석

임의의 테스트케이스를 만들어서, 다른 블로그에 올라와 있던 정답코드와 출력을 대조했다.

  * N = rand() % 500001, 고장난 버튼은 boolean 배열로 만들어 각 값을 rand() % 2로 지정했다.

백만개 정도의 테스트 케이스를 돌려보니, 반례를 찾았다.

 

이를 확인한 후 CONTINUE_S 레이블 이하 코드를 지우고, digit 함수에서 0이 들어오면 1을 리턴하도록 수정했다. 왜냐하면, smaller가 0이 되는 경우는 불필요한 경우로 치부하여 -INF 값을 대입했는데, 이 경우가 필요한 때가 있었기 때문이다. 이를 수정한 후, 100만개의 테스트 케이스를 랜덤 생성하여 정답 코드와 출력을 3번 정도 더 했고, 이상이 없어서 제출했으나... 틀렸습니다.

 

아무리 랜덤 데이터라고 하지만, 모든 경우를 테스트할 수는 없었다. 입력 데이터로 주어질 수 있는 모든 경우의 수는...

$$500,000\times\sum_{i=1}^{10}\left(\begin{array}{c}10\\ i\end{array}\right)=511500000$$

정답 코드의 시간복잡도는 \(O(n)\)정도 되고 25,000개의 입력을 처리하는데 1분이 소요됐으니, 이를 모두 연산하려면 341시간 정도 소요될 수 있을 정도의 많은 입력이다. 출력 데이터도 너무 방대해서 텍스트 대조 프로그램도 제대로 돌아가지 못 할 것이다. 따라서 입력 데이터를 반례가 등장할 확률이 높은 구간으로 설정하기로 했다.

$$0\leq{N}\leq{200},\text{ }499800\leq{N}\leq{500000}$$

각 N마다 고장날 수 있는 숫자 버튼의 모든 조합 1023가지를 모두 입력 데이터로 만들었다. 그렇게 만들어진 입력 데이터의 수는 무려 411648개. 연산을 마치는 데에만 20분 정도가 소요됐다.

입력 데이터 생성 코드:

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

using namespace std;

#define EL "\n"

vector<vector<int>> combinations;
int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
bool select[10];

int N;

void combination(int idx, int cnt) {
	if (cnt == N) {
		vector<int> c;
		for (int i = 0; i < 10; i++)
			if (select[i])
				c.push_back(arr[i]);
		combi.push_back(c);
		return;
	}
	for (int i = idx; i < 10; i++) {
		if (select[i]) continue;
		select[i] = true;
		combination(i, cnt + 1);
		select[i] = false;
	}
}

int main() {

	freopen("C:\\Users\\**\\source\\repos\\**\\**\\input.txt", "w", stdout);

	cin.tie(NULL);
	ios::sync_with_stdio(false);

	vector<int> v;
	combinations.push_back(v);
	for (N = 1; N <= 10; N++) {
		combination(0, 0);
	}

	cout << 411648 << EL;
	for (N = 0; N <= 200; N++) {
		for (auto& c : combinations) {
			cout << N << " ";
			cout << c.size() << " ";
			for (auto& number : c)
				cout << number << " ";
			cout << EL;
		}
	}
	for (N = 499800; N <= 500000; N++) {
		for (auto& c : combinations) {
			cout << N << " ";
			cout << c.size() << " ";
			for (auto& number : c)
				cout << number << " ";
			cout << EL;
		}
	}
    
	return 0;
}

출력 데이터가 방대하여 일반적인 텍스트 비교 프로그램은 못 썼고, Beyond Compare라는 프로그램을 썼다.

데이터를 불러오고 수 초의 로딩 후 찾아난 반례가 아래다. 아래가 정답 코드로 돌린 결과다.


결과

상당히 감격스러웠다. 보아하니 문제는 smaller의 0에 대한 처리였다. 위에서 digit 함수에서 0이 들어오면 1로 반환하라고 수정했었다. 하지만 위의 반례에서 처럼, smaller == 0이고, 0이 사용할 수 있는 경우 누락이 발생하게 된다. 따라서, CONTINUE_S 플래그 아래에 smaller == 0이면서 0이 사용할 수 없을 때에만 smaller에 -INF를 대입하도록 했다. 그리고 제출을 했더니... 맞았습니다!


코드

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

using namespace std;

#define FAST_IO cin.tie(NULL); ios::sync_with_stdio(false);
#define INF 987654321
#define EL "\n"

int digit(int n) {
	if (n == 0)
		return 1;
	int d = 0;
	for (int i = n; i > 0; i /= 10)
		d++;
	return d;
}

int main() {
	FAST_IO;

	int N;
	int M;
	cin >> N >> M;

	char brokenButtons[10];
	for (int i = 0; i < M; i++)
		cin >> brokenButtons[i];

	// 1. All numbers is broken
	bool isAllBroken = true;
	for (int i = 0; i <= 9; i++) {
		bool isFound = false;
		for (int j = 0; j < M; j++)
			if ('0' + i == brokenButtons[j]) {
				isFound = true;
				break;
			}
		if (isFound == false) {
			isAllBroken = false;
			break;
		}
	}
	if (isAllBroken) {
		cout << abs(N - 100) << EL;
		return 0;
	}

	// 2. N == 0
	if (N == 0) {
		for (int i = 0; i <= 9; i++) {
			bool isFound = false;
			for (int j = 0; j < M; j++)
				if ('0' + i == brokenButtons[j]) {
					isFound = true;
					break;
				}
			if (isFound == false) {
				cout << i + 1 << EL;
				return 0;
			}
		}
	}

	// 3. Only 0 is available.
	bool isOnlyZero = true;
	for (int i = 1; i <= 9; i++) {
		bool isFound = false;
		for (int j = 0; j < M; j++)
			if ('0' + i == brokenButtons[j]) {
				isFound = true;
				break;
			}
		if (isFound == false) {
			isOnlyZero = false;
			break;
		}
	}
	if (isOnlyZero) {
		if (N < 50)
			cout << 1 + N << EL;
		else
			cout << abs(N - 100) << EL;
		return 0;
	}

	// 4. General
	// 4-1. A Smallest number of numbers greater than N.
	int greater = N;
	while (true) {
		for (int i = greater; i > 0; i /= 10) {
			for (int j = 0; j < M; j++) {
				if ('0' + i % 10 == brokenButtons[j]) {
					greater++;
					goto CONTINUE_G;
				}
			}
		}
		break;
	CONTINUE_G:;
	}
	// 4-2. A Greatest number of numbers smaller than N.
	int smaller = N;
	while (true) {
		for (int i = smaller; i > 0; i /= 10) {
			for (int j = 0; j < M; j++) {
				if ('0' + i % 10 == brokenButtons[j]) {
					smaller--;
					goto CONTINUE_S;
				}
			}
		}
		break;
	CONTINUE_S:
		if (smaller == 0) {
			int isZeroUnavailable = false;
			for (int i = 0; i < M; i++)
				if (brokenButtons[i] == '0')
					isZeroUnavailable = true;
			if (isZeroUnavailable) {
				smaller = -INF;
				break;
			}
		}
	}
	// 4-3. Get minimum and print
	int result = min(min(greater - N + digit(greater), N - smaller + digit(smaller)), abs(N - 100));
	cout << result << EL;

	return 0;
}

ps. 다른 정답자 코드를 보면 0부터 적당한 수까지 매 번 버튼을 누르는 횟수를 세며 최솟값을 찾는 노가다를 했다. 사실 이 문제의 의도가 그런거다. 이 풀이에서 볼 수 있다시피, 고려해야 할 요소가 많으니까.

 

반응형

'프로그래밍 > PS' 카테고리의 다른 글

[C++] 백준 2580번: 스도쿠  (0) 2020.11.22
[C++] 백준 16236번: 아기 상어  (0) 2020.11.01
[C++] 백준 10026번: 적록색약  (0) 2020.10.29
[C++] 백준 15686번: 치킨 배달  (0) 2020.10.29
[C++] 백준 7569번: 토마토  (0) 2020.10.28