풀이#
정말 빡빡하게도, 정석적인 풀이법의 소요시간과 시간초과 기준점이 딱 붙어있게 설계된 문제인 것 같다. 그래서 람다함수에서 병목현상이 일어나 시간초과가 뜬 것 같다. 하지만 람다함수 + 나의 아주 제너럴한 이분탐색 코드를 std::upper_bound
코드로 교체하자 바로 Pass가 떴다. upper_bound
함수는 간단히 주어진 범위 안에서 value <= *it
인지 아닌지만 검사하기 때문에 속도가 빠르다.
어쨌든 기본적인 풀이법은 완전 동일하다.
- 말의 위치에 대해서만 정렬을 수행한다. (소 ↔ 말 벡터는 사실 뭘 먼저 해도 상관은 없음)
- 어떤 소 cow에 대해서 말과의 거리를 최소로 하는 말을 찾는다.
- 그 말과의 거리가
min_dist
보다 작으면count
와min_dist
의 값을 초기화한다.- 같으면
count
값을 1 증가시킨다.
- 같으면
2번 과정을 빠르게 수행하는 것이 이 문제의 관건인데, 이를 위해서 이분탐색을 활용한다. 이때 거리를 계산한다고 절댓값을 취해선 절대로 안된다. 참/거짓 구성이 흐트러질 수 있기 때문이다. 그러면 어떻게 하느냐? 이분탐색을 두 번 하는 것에서 아이디어는 출발한다.
- 소 ≤ 말을 만족하는 첫번째 말을 찾는다.
- 말 ≤ 소를 만족하는 첫번째 말을 찾는다.
이렇게 두고보니 사실 1번말을 찾으면 자연스럽게 2번말이 튀어나오게 된다. 1번말에 바로 이전 말이 2번말이기 때문!
sol1#
나의 제너럴한 이분탐색 알고리즘이다. T로는 인덱스(숫자)가 와도 되고, STL 처럼 이터레이터가 와도 전혀 무관하다. 핵심은 predicate 안에 있기 때문이다. 함수 이름에서부터 predicate가 true를 만족하는 바로 첫번째 녀석을 이 함수가 찾아주기 때문이다.
/**
F-...-F-F-F-T-T-T-...-T
^
*/
template <typename T, class Predicate>
inline T first_true(T begin, T end, Predicate const &pred) {
T l = begin;
T r = end;
while (l < r) {
T mid = l + (r - l) / 2;
if (pred(mid)) {
// next range is [l, mid)
r = mid;
} else {
// next range is [mid + 1, r)
l = mid + 1;
}
}
return l;
}
아래는 나의 말로 쓴 수도코드를 그대로 c++ 코드로 옮겨적은 모습이다. evaluate 라는 람다함수를 활용하여 코드중복을 해소했다. 왜 두번이나 평가하냐고? 1번말이 첫번째 evaluate이고 2번말이 두번째 evaluate이기 때문.
/**
@return:
- first: 가장 가까운 쌍의 거리
- second: 그러한 거리를 갖는 쌍의 개수
*/
inline pair<int, int> solution(vector<int> const &cows, vector<int> &horses) {
//
std::sort(horses.begin(), horses.end());
int min_dist = std::numeric_limits<int>::max();
int count = 0;
auto evaluate = [&min_dist, &count](int dist) {
if (dist < min_dist) {
min_dist = dist;
count = 1;
} else if (dist == min_dist) {
count += 1;
}
};
for (auto const &cow : cows) {
// 어떤 소에 대하여 (소 <= 말) 조건을 만족시키는 첫번째 말을 고른다.
// 그 말의 바로 이전 말과 비교하여 더 가까운 놈을 선택한다.
auto horse_itr = first_true(horses.begin(), horses.end(),
[cow](auto itr) { return cow <= *itr; });
if (horse_itr != horses.end()) {
evaluate(abs(cow - *horse_itr));
}
if (horse_itr != horses.begin()) {
evaluate(abs(cow - *(horse_itr - 1)));
}
}
return std::make_pair(min_dist, count);
}
이렇게만 보면 참 깔끔하고 기분이 좋아지는데… 결과는 알다시피 시간초과였다.
sol2#
그래서 무지성으로 horses 벡터를 만들지 않고 메인함수에서 standard input으로 받는 족족 이분탐색을 하면 어떨까 해서 그렇게 해봤지만 마찬가지였다. 결국 나의 이분탐색 코드를 버리고 의미적으로 완전 동일한 upper_bound
함수를 사용하기로 결정했다.
cow를 먼저 입력받으니 얘를 벡터에 넣어 정렬하기로 했다. 이제 각 horse마다 가장 가까운 cow를 찾으면 되는데, 그 과정을 단순히 upper_bound
로 교체했을 뿐인데 바로 Pass가 나와버려 얼탱이 없었다.
람다함수가 병목현상을 만드는지에 관해서는 좀 더 조사할 필요가 있다.
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
freopen("sample_input.txt", "r", stdin);
int t = 0;
cin >> t;
for (int tc = 1; tc <= t; ++tc) {
int n;
int m;
int c1;
int c2;
cin >> n >> m >> c1 >> c2;
int dist_x = abs(c1 - c2);
vector<int> cows;
vector<int> horses;
for (int i = 0; i < n; ++i) {
int cow;
cin >> cow;
cows.push_back(cow);
}
// cow를 정렬하고 horse는 벡터에 넣지말고 바로 bin_search
std::sort(cows.begin(), cows.end());
int dist_z = std::numeric_limits<int>::max();
int count = 0;
auto evaluate = [&dist_z, &count](int dist) {
if (dist < dist_z) {
dist_z = dist;
count = 1;
} else if (dist == dist_z) {
count += 1;
}
};
for (int i = 0; i < m; ++i) {
int horse;
cin >> horse;
// 어떤 말에 대하여 (말 <= 소) 조건을 만족시키는 첫번째 말을 고른다.
// 그 말의 바로 이전 말과 비교하여 더 가까운 놈을 선택한다.
auto cow_itr = std::upper_bound(cows.begin(), cows.end(), horse);
if (cow_itr != cows.end()) {
evaluate(abs(horse - *cow_itr));
}
if (cow_itr != cows.begin()) {
evaluate(abs(horse - *(cow_itr - 1)));
}
}
cout << "#" << tc << " " << dist_z + dist_x << " " << count << "\\n";
}
return 0;
}