- LIS를 \(\text{O(n log n)}\)으로 구현하는 문제이다.
- 각 항의 정보는
a b
로 주어지는데, a
가 A 전봇대와의 연결 위치, b
가 B 전봇대와의 연결 위치이다.
a
를 인덱스, b
를 값으로 보면, LIS를 구성할 수 있는 하나의 경우의 a
의 집합을 X라고 할 때, X의 여집합을 오름차순으로 출력해야 한다.
풀이
- LIS dp는 \(\text{O}( n^2 )\)이기 때문에, TLE가 난다.
- dp를 아래와 같이 정의하고, 이분 탐색을 사용해야 한다.
- lis(i)=lis의 i 번째 항에 들어갈 수 잇는 b 위치의 최솟값
- 전깃줄 정보
ar
을 a
기준으로 오름차순 정렬 후 순회하며 lis를 업데이트한다.
b
가 \(k\)일 때, \(k \lt \text{lis(i)}\)를 만족하는 가장 작은 \(i\)를 이분 탐색으로 찾고, lis[i]
를 \(k\)로 바꾼다.
lis[0...i-1]
까지는 그대로 사용하고, lis[i]
만 b
가 \(k\)인 전깃줄을 사용하는 것이다.
- 이때, 잘라야 하는 전깃줄을 출력해야 하기 때문에, 각 원소들의
lis
에서의 위치 정보를 ari
에 따로 저장한다.
ari
의 사이즈는 n
이고, 값의 범위는 [0, lis.size()-1]
이 된다.
ari
의 값을 역순으로 순회할 때, 처음 나오는 lis.size()-1, ..., 0
들의 인덱스 집합을 I
라고 하면, ar[I][0]
가 서로 교차하지 않는 전깃줄들의 a
위치이다.
- 정방향으로 순회할 때는
0, ..., lis.size()-1
을 순서대로 찾으면 된다.
코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| #include <algorithm>
#include <iostream>
#include <sstream>
#include <vector>
#define v vector
using namespace std;
using vi = vector<int>;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, aidx;
cin >> n;
vi lis, ari(n, 0);
v<vi> ar(n, vi(2, 0));
for (int i = 0; i < n; i++) {
cin >> ar[i][0] >> ar[i][1];
}
sort(ar.begin(), ar.end());
lis.push_back(0);
ari[0] = 0;
for (int i = 1; i < n; i++) {
int cur = ar[i][1];
if (ar[lis.back()][1] < cur) {
lis.push_back(i);
ari[i] = lis.size() - 1;
} else {
int s = -1, e = lis.size();
while (s + 1 < e) {
int mid = (s + e) / 2;
if (ar[lis[mid]][1] <= cur)
s = mid;
else
e = mid;
}
lis[e] = i;
ari[i] = e;
}
}
vi ans;
aidx = lis.size() - 1;
for (int i = ar.size() - 1; i >= 0; i--) {
if (ari[i] == aidx) {
ans.push_back(ar[i][0]);
aidx--;
if (aidx < 0) break;
}
}
reverse(ans.begin(), ans.end());
aidx = 0;
ostringstream oss;
oss << n - ans.size() << '\n';
for (int i = 0; i < n; i++) {
if (aidx < ans.size() && ar[i][0] == ans[aidx]) {
aidx++;
continue;
}
oss << ar[i][0] << '\n';
}
cout << oss.str();
return 0;
}
|
풀고나서
- 이분 탐색 구현이 헷갈려, 직접 구현하며 되짚었다.
그러던 중, 명확하게 정리한 글(이분 탐색(Binary Search) 헷갈리지 않게 구현하기)을 발견하고, 이해하는데 도움을 받았다.
아래는 요약이다.
- [lo, hi]가 Check(lo) != Check(hi)가 되도록 구간을 설정
- 결정문제이기 때문에,
T, ..., T, F, ..., F
나 그 반대 등으로 설정
- lo, hi는 항상 정답의 범위를 나타낼 수 있도록 해야합니다
- lo를 출력해야 하는 문제의 답이 최대 n일 때 hi = n으로 선언하거나, hi를 출력해야 하는 문제의 답이 최소 0일 때 lo = 0으로 선언하면 안됩니다. (hi = n + 1, lo = -1로 선언해야 합니다)
while (lo + 1 < hi)
, mid = (lo + hi) / 2
에서 Check(mid) = Check(lo)
라면 lo = mid
, 아니라면 hi = mid
- 구한 경계에서 답이 lo인지 hi인지 생각해보고 출력
- 처음 제출은 96ms 정도가 나왔는데, 상위권은 32ms 정도인 것을 보고, 비교하며 차이점을 찾아내려 했다.
- 처음에는 lis에 모든 전깃줄의 lis에서의 위치를 저장하려고,
vector<stack<int>>
타입으로 선언했다.
이게 문제라 생각해서 vector<int>
로 바꾸고, ari
를 따로 선언했지만, 시간이 유의미하게 달라지지 않았다.
- 컴파일러가 최적화하겠지만, if…else보다 continue를 사용하는게 이론상 더 빠르다는 것을 알게 되었다.
- 이분 탐색을 바로 실행하지 않고,
lis.back()
과 비교해서 더 크다면 바로 push_back하고, 아니면 탐색하는 방식으로 커팅이 가능하다.
ostringstream
으로 출력도 최적화했지만, 시간이 달라지진 않았다.
- 문제는 항상 넣던
sync_with_stdio
, cin.tie
구문을 넣지 않은 것이었다…
Leave a comment