Closest Pick
- 문제 설명: N개의 티켓이 주어질 때 각 티켓은 1~K 사이의 정수를 가진다. 다른 티켓이 같은 정수를 가질 수도 있다. 1~K 사이의 정수 c가 랜덤으로 선택될 때 가지고 있는 티켓 중 하나가 N개의 티켓들보다 c에 가장 가까운 숫자일수록 이길 확률은 올라간다. 티켓 두 장을 구입해서 이길 확률을 최대화할 때 확률을 구하라.
- N개의 티켓에 적힌 숫자와 같은 티켓일 경우 어떤 점수도 얻지 못한다. 두 티켓이 모든 c에 대해 같은 차이를 가지기 때문이다.
- 입력
- 1 <= T <= 100
- 1 <= N <= 30
- Test Set 1: 1 <= K <= 30
- Test Set 2: 1 <= K <= 10^9
- 출력: 오차 10^-6 이하이면 ok
- 첫번째 접근법: Test Set 1은 K가 적기 때문에 브루트 포싱으로 해결할 수 있을 것이다.
- 먼저 N개의 티켓에 적힌 숫자들의 집합을 P라고 할 때, 1~K 사이의 모든 숫자에 대해 P에 있는 숫자들 중 가장 작은 차이(min_diff)를 구한다.
- P를 제외한 다른 숫자들 중에서 min_diff 보다 작은 차이를 가진 숫자를 찾는다. 참고로 숫자가 하나도 없다면 즉, P에 1~K 사이의 모든 숫자가 있다면 확률은 0이 될 것이다.
- 이전에 선택한 숫자 2개에 대해 확률을 계산해야 한다. 확률은 P에 있는 숫자들보다 가까운 c의 개수를 K로 나눈 것이 된다. 이 확률의 의미는 모든 가능한 c에서 몇 개의 c가 가장 가까운지에 대한 것이다.
- 여기서 P에 속한 숫자들을 제외한 것들 중 2개를 고르는 경우의 수는 (K-|P|)C(2) 이다. 여기서 C는 이항계수이다. Test Set 1은 K가 적기 때문에 브루트 포싱으로 해결할 수 있다.
- 시간 복잡도: O(K^3)
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <cmath>
using namespace std;
void solve() {
int N, K;
cin >> N >> K;
set<int> tickets;
for (int i = 0, tmp; i < N; i++) {
cin >> tmp;
tickets.insert(tmp);
}
int min_diff[K];
for (int c = 1; c <= K; c++) {
int& diff = min_diff[c-1];
diff = 1e9;
for (auto& t : tickets)
diff = min(diff, abs(t-c));
}
int max_wins = 0;
for (int a = 1; a <= K; a++) {
if (tickets.find(a) != tickets.end()) continue;
for (int b = a; b <= K; b++) {
if (tickets.find(b) != tickets.end()) continue;
int wins = 0;
for (int c = 1; c <= K; c++)
if (abs(a-c) < min_diff[c-1] or abs(b-c) < min_diff[c-1])
wins++;
max_wins = max(max_wins, wins);
}
}
double ans = (double)max_wins / K;
ans = round(ans * 1e6) / 1e6;
cout << ans << endl;
}
int main() {
int T;
cin >> T;
for (int tc = 1; tc <= T; tc++) {
cout << "Case #" << tc << ": ";
solve();
}
return 0;
}
- 두번째 접근법: Test Set 2 부터는 K가 너무 크기 때문에 모든 c에 대해 시도해볼 수 없다. 두 숫자를 선택할 때 후보의 개수를 줄일 필요가 있다. P에 속한 숫자의 좌우 양 옆의 숫자들이 후보에 적합한데 그 이유는 P에 속한 숫자로부터 멀리 떨어진 숫자를 선택하게 되면 P에 속한 숫자와 선택한 숫자 사이의 중앙값을 기준으로 가까운 숫자들이 나뉘게 되기 때문이다. 예를 들어 P=[1,2,3,7] 이고 K=7일 때 3 4 5 6 7 에서 5를 선택하는 것보다 4 또는 6을 선택하는 것이 가까운 숫자들을 더 많이 포함하게 된다. (5를 선택하면 숫자 5 1개만 가까운 숫자이지만, 4와 6을 선택하게 되면 4,5 또는 5,6을 가까운 숫자로 얻게 된다.)
- 또한, 후보에 있는 숫자들은 가까운 c의 개수를 계산할 때 어차피 P에 속한 숫자 사이에만 위치하기 때문에 1~K 사이의 모든 숫자에 대해서 가까운 c의 개수를 계산할 필요가 없다. 단순히 후보 숫자의 양 옆에 있는 Pi-1과 Pi 와의 차이를 계산하면 될 것이다. 그 외의 숫자는 어차피 Pi-1과 Pi와 더 가깝기 때문이다.
- 처음에 후보 숫자들을 고를 때 가까운 c의 개수를 미리 계산할 수 있고, 이후에 가까운 c의 개수를 내림차순으로 정렬했을 때 2개를 선택하면 된다.
- 참고로 P에 속한 숫자 집합의 양 끝에 대해서도 검사해주어야 한다. 범위 안에만 있다면 P1 - 1 과 Pn + 1 위치를 후보에 넣고 P1 - 1 그리고 K - Pn 을 미리 계산해준다.
- 예외 처리: Pi-1 과 Pi 사이에 숫자가 3개 이상 있을 때 홀수 개수이면 중간에 1개 숫자가 겹치므로 빼주어야 한다. 예를 들어 3 4 5 6 7 에서 3과 7이 P에 속할 때 4를 선택하면 4,5 해서 2점이고 6을 선택하면 5,6 해서 2점인데 여기서 큰 값 2개를 고르는데 후보가 4 와 6 밖에 없다면 5가 중복되니 가까운 c의 개수를 4와 6중에서 1개 빼주어야 한다. 후보가 무엇인지는 중요하지 않기 때문에 4,6을 선택하거나 4,X 또는 6,X 를 선택할 때 가까운 c의 개수는 정확하게 계산될 수 있다.
- 시간 복잡도는 O(N)
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <cmath>
#define pb push_back
#define sz(c) (int)(c).size()
using namespace std;
const static int _ = []() {
cin.tie(0); cout.tie(0); ios_base::sync_with_stdio(false);
return 0;
}();
void solve() {
int N, K;
cin >> N >> K;
set<int> pset;
for (int i = 0, tmp; i < N; i++) {
cin >> tmp;
pset.insert(tmp);
}
vector<int> tickets(pset.begin(), pset.end());
vector<int> scores;
if (tickets.front() > 1) scores.pb(tickets[0]-1);
if (tickets.back() < K) scores.pb(K - tickets.back());
for (int i = 1; i < sz(tickets); i++) {
int diff = tickets[i] - tickets[i-1];
if (diff > 1) { // 사이에 숫자가 1개 이상 있을 때
scores.pb(diff >> 1); // 가까운 c의 개수 계산 (두 숫자의 차이 / 2)
if (diff > 2) { // 숫자가 2개 이상 있을 때
if ((diff-1) & 1) scores.pb((diff >> 1) - 1); // 홀수개이면 중복 제외
else scores.pb(diff >> 1); // 짝수개이면 중복이 없으므로 그대로 계산
}
}
}
// 내림차순 정렬
sort(scores.begin(), scores.end(), [&](int a, int b) { return a > b; });
int wins = 0;
// 최대 2개를 선택
for (int i = 0; i < min(2, sz(scores)); i++) wins += scores[i];
double ans = (double)wins / K;
ans = round(ans * 1e6) / 1e6;
cout << ans << endl;
}
int main() {
int T;
cin >> T;
for (int tc = 1; tc <= T; tc++) {
cout << "Case #" << tc << ": ";
solve();
}
return 0;
}
Roaring Years
- 문제 설명: 입력으로 년도가 주어질 때 다음 roaring 년도를 구하라.
- roaring 의 유무는 숫자가 연속되는 숫자로 이루어진 것을 의미한다.
2021 -> 20 / 21
78910 -> 7 / 8 / 9 / 10
99100 -> 99 / 100
778, 122, 2020 같은 경우는 해당되지 않는다.
- roaring 의 유무는 숫자가 연속되는 숫자로 이루어진 것을 의미한다.
- 입력
- Test Set 1: 1 <= Y <= 10^6
- Test Set 2: 1 <= Y <= 10^18
- 첫번째 접근법: Y가 작기 때문에 1씩 증가시켜서 roaring 인지 파악하는 것으로 해결할 수 있다. 연속되려면 시작 번호는 최대 전체 자릿수의 절반정도 차지해야 하므로 L/2 부터 1까지 숫자를 나누는 단위를 줄이면서 roaring인지 시도해볼 수 있다. 큰 단위로 나누어질수록 가장 가까운 숫자이기 때문이다. 주의할 점은 자릿수 올림이 발생할 수 있기 때문에 L만큼 단위를 나눴을 때 연속되지 않는다면 L+1만큼 단위를 나눠서 검사할 필요가 있다. 또한 숫자의 맨 앞에 0이 오는 경우 예외 처리를 해주어야 한다.
- 시간 복잡도: O(Y)
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#define sz(c) (int)(c).size()
using namespace std;
bool is_roaring(const string y, int l) {
int prev = stoi(y.substr(0, l)), curr = -1;
for (int i = l; i < sz(y); i += l, prev = curr) {
if (y[i] == '0') return false;
if (i+l > sz(y)) return false;
curr = stoi(y.substr(i, l));
if (prev + 1 == curr) continue;
if (i+l+1 > sz(y)) return false;
curr = stoi(y.substr(i, l+1));
if (prev + 1 == curr) {
l++;
continue;
} else return false;
}
return true;
}
void solve() {
string year;
cin >> year;
int next = stoi(year);
while (next++) {
string y = to_string(next);
for (int l = sz(y)>>1; l > 0; l--) {
if (is_roaring(y, l)) {
cout << next << endl;
return;
}
}
}
}
int main() {
int T;
cin >> T;
for (int tc = 1; tc <= T; tc++) {
cout << "Case #" << tc << ": ";
solve();
}
return 0;
}
- 두번째 접근법: Y의 범위가 10^18 이기 때문에 위의 방법대로 하면 시간 초과가 발생한다. 풀이 아이디어는 모든 roaring year의 집합에서 시작 번호를 이분법(bisection method)으로 찾는 것이다. 시작번호만 알면 뒤에 연속되는 자릿수에 따라 roaring year를 알 수 있기 때문에 시작번호만 이분법으로 탐색한다. 개인적으로 다른 사람 풀이를 봤을 때는 매개변수 탐색이라고 보는 것이 맞는 듯하다. 알고리즘은 다음과 같다.
- 최초 연속되는 숫자의 개수를 2부터 시작하고 한 자릿수일 때 10^18까지 검사하기 위해 최대 18개까지 이분법을 수행한다.
- 시작 번호는 10^18의 절반인 10^9가 최댓값이 되고 최솟값은 1이 된다. 이분 탐색으로 시작번호를 찾을 때는 시작번호로 만들어진 숫자가 입력 Y보다 크면 시작번호를 줄이고, Y보다 작거나 같으면 시작번호를 늘린다. 모든 집합의 원소에서 Y 다음으로 올 수 있는 가장 작은 값을 찾는 것이기 때문에 입력 Y보다 크면 시작번호를 줄여야 된다.
- 매 연속되는 숫자 개수(2~18)마다 이분법을 수행해서 찾은 roaring year 중 최솟값을 반환하면 된다.
- 시간 복잡도: O(log2(1e9) * 18 * 18) = 29.897352853986263 * 18 * 18 < 9,720
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#define sz(c) (int)(c).size()
using namespace std;
typedef long long ll;
const ll INF = 2e18;
ll Y;
ll make_roaring_year(ll nbr, int num_consecutives) {
auto digits = [&](ll v) {
ll x = v, y = 1;
while (x) x /= 10, y *= 10;
return y;
};
ll ret = 0;
for (int i = 0; i < num_consecutives; i++, nbr++) {
ll d = digits(nbr);
// 이미 범위를 넘는 경우 roaring year은 없는 것이므로 INF를 반환한다.
if (ret > INF/d) return INF;
ret = ret * d + nbr;
ret = min(ret, INF);
}
return ret;
}
ll search(int num_consecutives) {
ll lo = 0, hi = 1e9;
while (lo + 1 < hi) {
ll mid = (lo + hi) >> 1;
if (make_roaring_year(mid, num_consecutives) > Y) hi = mid;
else lo = mid;
}
ll ret = make_roaring_year(hi, num_consecutives);
return ret;
}
void solve() {
cin >> Y;
ll ans = INF;
for (int n = 2; n <= 18; n++)
ans = min(ans, search(n));
cout << ans << endl;
}
int main() {
int T;
cin >> T;
for (int tc = 1; tc <= T; tc++) {
cout << "Case #" << tc << ": ";
solve();
}
return 0;
}
Double or NOTing
'Algorithm > Problem Solving' 카테고리의 다른 글
Google Code Jam 2021 Round 1B (0) | 2021.04.26 |
---|---|
Google Code Jam 2021 Round 1A (0) | 2021.04.26 |
[DFS, Tree] (프로그래머스 Lv 4) 매출 하락 최소화 (0) | 2021.04.24 |
[구현] (프로그래머스 Lv 4) 블록 게임 (0) | 2021.04.24 |
[PriorityQueue] (프로그래머스 Lv 3) 셔틀버스 (0) | 2021.04.24 |