아무래도 적품 전체를 관통하는 테마가 “왜 그래야 하는데?” 라고 느꼈기 때문인 것 같다. 탈로스가 했던 “모든 것을 의심해라” 와 함께. (이건 이미 대사 자체가 클리셰가 되어 버린 것 같지만..) 나아가 “변할 필요가 없다. 있는 그대로도 충분히 좋다”고, 너(남)에게 증명해야 할 필요가 없다는 대사는 크.. 심쿵! 정말 멋졌다.
걱정 아닌 걱정이라면 이 대로는 아이언맨은 캡마 하위 호환의 쩌리가 되는건 아닌가.. ㅋㅋㅋㅋ 기계도 잘 다루고, 짱짱 쎄니까.
여튼 아싸 감성 터져서(좋은 단어가 생각나질 않는다ㅠㅠ) 멋진 캡마가 앞으로도 활약하면 좋겠다
'바보'란 스스로 지옥을 만들어 내는 사람들을 말합니다. 그런 '바보'의 특징으로서, 우선 "나는 행복해질 수 없다."라고 강하게 믿고 있다, 라는 점을 들 수 있습니다. 보다 증세가 심해지면 그 믿음은 "나는 행복해져서는 안 된다." 까지 확장되어, 최종적으로는 "나는 행복해지고 싶지 않다."라는 파멸적인 오해에 이릅니다.
이렇게 되면 더 이상 무서울 것이 없습니다. 그들은 불행해질 수단을 숙지하고 있으며, 아무리 축복받은 환경이더라도 반드시 샛길을 찾아내서 능숙하게 행복을 회피해 보입니다. 일련의 과정은 전부 무의식중에 이루어지기 때문에, 그들은 이 세상 전부가 지옥이라고 생각하고 있습니다만... 실제로는 그들 스스로가 자신이 있는 그곳을 지옥으로 만들고 있는 것뿐입니다.
(중략)
이 작품을 통해 목숨의 가치라든가 사랑의 힘 같은 것에 대해 이야기하려는 마음은 사실은, 전혀 없습니다.
오랜만에 찾아간 도서관에서 보게 된 책. 내용보다 작가의 말이 인상깊었다.
아마 가끔 과거를 돌아보면서 알고보면 그 때가 좋았다고 생각하는 것도 비슷한 이유 때문인 것은 아닐까 하는 생각도 들고, 이런 이야기를 오랜만에 보니까 조금 찔리기도 하고, 쓸데없는 고민좀 덜 하고 살아야겠단 생각도 하게 됐다.
코딩을 너무 안 하는것 같아서 가끔 Codeforces를 풀고있는데, 오랜만에 좋은 문제를 찾았다.
문제를 설명하면 간단하게 N개의 정점과 M개의 가중치를 갖는 무향 간선으로 이루어진 그래프가 주어졌을 때, 모든 정점이 연결돼있도록 N-1개의 간선만 남기되(즉, 스패닝 트리를 만들면 된다), 간선의 가중치의 합이 최소가 되도록 하는 방법을 출력하는 것이다. 여기서 각 간선에는 Wi라는 가중치와 Ci라는 속성이 있는데, 가중치를 원하는 만큼 줄일 수 있지만(0이나 음수로도 줄일 수 있다), 1씩 줄일 때마다 Ci의 비용이 소요되고, 지불할 수 있는 자금의 한계 S가 주어진다.
우선, N-1개의 간선을 선택했다고 할 때, Ci가 가장 작은 간선의 가중치만 최대한 줄이는게 최대인 것은 자명하다.
이를 바탕으로 간단한 해법을 생각해보면, 모든 간선에 대해 해당 간선을 최소간선(Ci가 최소인 간선 - 즉, 가중치를 줄일 간선)으로 고정하고 최소 스패닝 트리를 만들어서 가중치의 합을 계산하는 방식으로 해결할 수 있다는 생각이 든다. 하지만 이는 O(m^2logm)이란 끔찍한 복잡도를 갖기 때문에 아직 문제를 해결하기엔 개선이 필요하다.
하지만 좀 더 생각해보면, 매번 스패닝 트리를 다시 만들지 않고도 각 간선이 최소간선인 최소 스패닝 트리를 전부 탐색해볼 수 있음을 알 수 있다.
1. Wi만 가지고 최소 스패닝 트리를 만든다.
2. 이 스패닝 트리의 Wi의 합을 T라고 하자. 우선 답을 구한다: (답) = T - S / C, 여기서 C는 스패닝 트리의 간선들의 Ci값중 최솟값이다.
3. 해당 스패닝 트리에서 두 정점을 잇는 경로상에서 가장 큰 Wi값을 갖는 간선을 찾는 쿼리(LCA)를 빠르게 할 수 있도록 전처리를 해 둔다. *[LCA]
4. 처음 스패닝 트리에 들어있지 않은 간선들을 모두 순회하면서, 다음과 같은 계산을 한다.
4a. 이 간선을 추가하면 사이클이 생기게 됨이 자명하기 때문에, 이 간선 대신 지워질 간선을 찾아야 한다.
4b. 이 때, 지워지는 간선은 Ai - Bi 경로 상에 있는 간선 중, Wi가 가장 큰 간선이 되어야 함 또한 자명하다(사이클을 이루고 있으니까 어느 간선을 지우든 상관 없다).
4c. 이를 위해 아까 전처리해둔 LCA 쿼리를 한다(이 간선의 인덱스를 j라 하자).
4d. 이 간선이 최소간선일 경우의 답을 계산한다: T - Wj + Wi - S / Ci.
4e. 이게 지금까지의 최솟값보다 더 작은 답이라면 답을 갱신한다.
트리에 한 개씩 간선을 더 붙여보면서 각 경우의 MST를 중복되는 계산 없이 구해본다는 아이디어가 재미있는 해법이다.
Since Android 5.1 (API Level 22), AlarmManager's setRepeating method forcefully raise intervals under 1 minute (60000 msec) to 1 minute. Although it is completely right decision because waking up phone every 15 seconds or 30 seconds is crazy, but SOMETIMES (for testing, researching, ...) we need such kind of 'bad' alarms. However, we can overcome this shortcoming by setting once-only alarm repetitively(not explicitly calling setRepeating but call set or setExact again and again, like a chain).
귀찮아서 안 쓰고 있었는데, O(nlogn)임에도 계속 시간초과를 받았던게 자꾸 생각나서 늦게나마 해법(결국 Fast I/O까지 써서 시간초과 X)을 올린다 -_-..
문제
철광석과 구리 광석이 묻혀있는 땅이 있다. 여기서 N(1 ≤ N ≤ 100,000)개의 광물(점으로 간주한다)의 좌표가 주어질 때, 어떤 점 P를 원점으로 잡고 1사분면의 구리 광석 개수, 2사분면의 철광석 개수, 3사분면의 구리 광석 개수, 그리고 4사분면의 철광석 개수를 셌을 때의 값을 최대화하도록 P를 정하고, 이때의 합을 출력하자.
파란색 점을 철광석, 갈색 점을 구리 광석이라고 생각하자.
P의 위치를 위와 같이 잡을 경우 합은 3(1사분면) + 3(2사분면) + 3(3사분면) + 2(4사분면) = 11이 된다.
*각 광물의 좌표는 모두 1 ≤ X,Y ≤ N 범위의 정수(자연수)이고, 모든 광물의 X좌표는 서로 다르고, Y좌표도 서로 다르다. 즉, 어떤 수평선 위에도 광물은 최대 한 개 존재하고, 어떤 수직선 위에도 광물은 최대 한 개 존재한다. 그리고, 점 P의 좌표는 실수를 가질 수 있다(즉, 경계선에 놓이는 광물은 없다고 보아도 된다).
풀이
우선 이 풀이를 이해하기 위해선 <Segment Tree> 라는 자료구조에 대한 이해가 필요하다 [링크1] [링크2].
Segment Tree를 처음 배울 때 보통 구간의 합을 구하는 예제를 통해 배우게 되는데, 이를 조금 확장하면 어떤 배열의 Prefix sum, 즉 A[1]+A[2]+...+A[i] (1 ≤ i ≤ N) 의 합이 최대가 되는 i와 그때의 합을 찾을 수 있다. 리프 노드의 최대 접두사 합은 자기 자신의 값으로 하고, 리프가 아닌 노드의 최대 접두사 합은 "왼쪽 자식의 최대 접두사 합"과 "왼쪽 자식의 합 + 오른쪽 자식의 최대 접두사 합" 중 큰 것임이 자명하기 때문에 접두사 합을 저장하는 배열을 하나 더 만들거나, 구조체를 이용해서 세그먼트 트리를 만들면 된다.
#include<cstdio>
#include<algorithm>
usingnamespacestd;
constintMAXN(131072);
structPrefixSumTree {
structNode {
int sum, max_prefix;
inline Node(): sum(0), max_prefix(0) {}
inline Node(int a): sum(a), max_prefix(a) {}
inlinevoidCombine(const Node& le, const Node& ri) {
sum = le.sum + ri.sum;
max_prefix = max(le.max_prefix, le.sum+ri.max_prefix);
}
};
int n;
Node *nodes;
inlinePrefixSumTree(int *arr, int n): n(n) {
nodes = new Node[n << 1];
for (int i = 0; i < n; i++) {
nodes[i+n] = Node(arr[i]);
}
for (int i = n - 1; i > 0; --i) {
nodes[i].Combine(nodes[i<<1], nodes[i<<1|1]);
}
}
inlinevoidUpdate(int p, int value) {
nodes[p+n] = Node(value);
for (p += n; p > 1; p >>= 1) {
int parent = p >> 1, left = p, right = p;
if (p&1) left ^= 1;
else right ^= 1;
nodes[parent].Combine(nodes[left], nodes[right]);
}
}
inlineintQueryMaximum() {
return nodes[1].max_prefix;
}
inline~PrefixSumTree() {
delete[] nodes;
}
};
이제 위의 자료구조를 이용해서 문제를 해결하는 방법에 대해 살펴보자.
점 P의 X 좌표가 특정한 값(이 값을 Px라고 하자)로 고정되어 있을 경우, 최적의 Y 좌표를 쉽게 구하는 방법은 무엇일까?
직관적으로 쉽게 알 수 있는데, 아래 그림과 같이 배열을 하나 만들고, X좌표가 Px보다 작은 철광석이 있는 Y좌표에는 -1, 구리 광석이 있는 Y좌표에는 +1, 그리고 X좌표가 Px보다 크거나 같은 철광석이 있는 Y좌표에는 +1, 구리 광석이 있는 좌표에는 -1을 준 다음, 접두사 합이 최대가 되는 지점이 바로 Px의 Y좌표가 된다(증명은 귀류법으로 간단히 할 수 있다).
맨 아래에서부터 (-1) + 1 + (-1) + (-1) + 1 + 1 + 1 = 1
이를 처음에 설명한 트리를 응용하여 모든 X좌표(N개(정확힌 N+1개)의 X좌표)에 대해 빠르게(O(logN)) 최적의 Y좌표를 구할 경우, 모든 P의 후보 지점을 살펴보는 데에 O(NlogN)의 시간이 걸리므로 최대 100,000개의 점을 처리하기엔 부족함이 없다.
우선 Px를 나타내는 가상의 수직선을 상상해보고, 다음 그림들을 보자.
->
이런 상황으로 초기화를 한다(px = 0) - O(NlogN) -> 여기서 최적의 y를 찾는다 - O(1) -> 합을 구한다 - O(1). (합은 생각해보면, 간단하게 (최대 접두사 합) + (P 오른쪽의 구리 개수) + (P 왼쪽의 철 개수)이다)
이제 수직선을 한 칸 이동시켜서(Px = 1) 반복해보자.
수직선을 이동할 때 갱신해야 하는 것은 세 가지이다:
1. 세그먼트 트리에서 해당 X좌표에 있는 점의 부호를 바꿔준다 - O(logN).
2. 해당 점이 철광석일 경우 P 왼쪽에 있는 철 개수를 저장하는 변수(LEFT_IRON) - O(1)
3. 해당 점이 구리 광석일 경우 P 오른쪽에 있는 구리 개수를 저장하는 변수(RIGHT_COPPER) - O(1).
다시 같은 방식으로 Px=2, ...., Px = N까지 모두 확인해가며 합의 최댓값을 갱신해주면 정답을 구할 수 있다.
#include<cstdio>
#include<algorithm>
usingnamespacestd;
constintMAXN(131072);
structPrefixSumTree {
structNode {
int sum, max_prefix;
inline Node(): sum(0), max_prefix(0) {}
inline Node(int a): sum(a), max_prefix(a) {}
inlinevoidCombine(const Node& le, const Node& ri) {
sum = le.sum + ri.sum;
max_prefix = max(le.max_prefix, le.sum+ri.max_prefix);
}
};
int n;
Node *nodes;
inlinePrefixSumTree(int *arr, int n): n(n) {
nodes = new Node[n << 1];
for (int i = 0; i < n; i++) {
nodes[i+n] = Node(arr[i]);
}
for (int i = n - 1; i > 0; --i) {
nodes[i].Combine(nodes[i<<1], nodes[i<<1|1]);
}
}
inlinevoidUpdate(int p, int value) {
nodes[p+n] = Node(value);
for (p += n; p > 1; p >>= 1) {
int parent = p >> 1, left = p, right = p;
if (p&1) left ^= 1;
else right ^= 1;
nodes[parent].Combine(nodes[left], nodes[right]);
}
}
inlineintQueryMaximum() {
return nodes[1].max_prefix;
}
inline~PrefixSumTree() {
delete[] nodes;
}
};
structOre {
int x, y, z;
inline Ore () {}
inlineOre(int x, int y, int z): x(x), y(y), z(z) {}
inlinebooloperator < (const Ore& j) const {
return x < j.x;
}
};
int n;
Ore total[MAXN];
PrefixSumTree *prefixsum_tree;
inlineintProcess(int base)
{
int ans = 0;
for (int i = 0; i <= n; i++) {
if (i > 0) {
int j = i-1;
if (total[j].z) {
prefixsum_tree->Update(total[j].y, -1);
++base;
} else {
--base;
prefixsum_tree->Update(total[j].y, 1);
}
}
int sum = base + prefixsum_tree->QueryMaximum();
ans = max(ans, sum);
}
return ans;
}
inlinevoidreadn(register int *n)
{
register char c;
(*n) = 0;
while (~(c = getc_unlocked(stdin))) {
if ('0' <= c && c <= '9') break;
}
do {
if (c < '0' || c > '9') break;
(*n)*=10, (*n)+=c&~48;
} while (~(c = getc_unlocked(stdin)));
}
intmain()
{
int T, TC = 1;
setbuf(stdout, NULL);
for (readn(&T); T--; TC++) {
int arr[MAXN] = {}, base = 0;
readn(&n);
printf("Case #%d\n", TC);
for (int i = 0; i < n; i++) {
readn(&total[i].x);
readn(&total[i].y);
readn(&total[i].z);
if (total[i].z) {
arr[total[i].y] += 1;
} else {
arr[total[i].y] -= 1;
++base;
}
}
sort(total, total+n);
prefixsum_tree = new PrefixSumTree(arr, MAXN);
printf("%d\n", Process(base));
delete prefixsum_tree;
}
return 0;
} /* syntax highlighter: http://markup.su/ */
Lua를 연습해볼 겸 이전에 PacMan에 사용했던 유전 프로그래밍을 약간 변형해서 적용해봤다. 너무 단순한 방법이라 그런가 성능이 좋은 편은 아니지만, Lua의 coroutine을 연습해보는 정도로 만족. 애초에 더 좋은 알고리즘을 만들려면 이전 상태를 고려하고 탄막의 속도를 계산할 줄 아는 알고리즘을 생각해야겠지(일단 지금 드는 생각은, 사람이 직접 이런 feature를 만드는건 귀찮고 재미도 없을 것 같으니 (귀찮아서 안 할듯 하지만)만약 시도한다면 NEAT나 변형된 Deep Q-Learning을 구현할 생각).