티스토리 뷰
BOJ 13874. Internet Trouble
문제
- 수직선 상에 $N$개의 마을이 있고, $K$개의 마을에 인터넷을 설치할 것이다. 하나의 마을에 인터넷을 설치하는 데는 $B$의 비용이 든다.
- $i$번째 마을에는 $A_i$개의 집이 있으며, 각 집과 인터넷을 연결하는 케이블을 설치해야 한다. 이때 $i$번 마을의 집 하나와 $j$번 마을의 인터넷을 연결하는 비용은 $|i-j|\times C$이다.
- $1 \le K \le N$인 모든 $K$에 대해, $K$개의 마을에 인터넷을 설치할 때 드는 최소 비용을 구하시오.
- $1 \le N \le 6000$
풀이
일단 문제의 생김새를 보아 DP로 푸는 것 같다. 그러나 상태 전이를 어떻게 시켜야 할지가 잘 보이지 않는다. 모든 구간 $[l, r]$을 하나의 인터넷으로 덮는 비용을 미리 계산해 둔다고 하더라도, DP 전이를 단순히 하면 $O(N^3)$이 될 것이고, 여기서 $N$을 하나 떼어야 한다. 언뜻 봤을 때는 D&C Optimization이 통할 것 같아 보이는데, $N$이 워낙 크기 때문에 $O(N^2 \log N)$이 돌아가기도 어려워 보인다.
Strictly $O(N^2)$에 이를 처리할 수 있는 DP 최적화 기법이 뭐가 있을까? 현재 점화식의 꼴은
$$
DP[i][cnt]=\min_{j<i} \left( DP[j][cnt-1]+cost(j+1, i) \right)
$$의 형태이다. 여기서 $cost$ 함수를 계산하는 방법이 어떻게 될까? 인터넷을 설치하는 위치는 중간값으로 두는 게 이득이다. 그런데 이 형태로는 뭔가 최적화가 잘 될 것 같지 않다. 설치 위치(피봇) 왼쪽 부분과 오른쪽 부분에 대해 비용 함수식이 다르기 때문이다. 그렇다면 아예 이 둘을 두 개의 상태로 나눠서 계산하면 어떨까?
이 아이디어를 조금 더 자세히 기술하자면, 구간을 총 $2K$개로 나눌 것이다. 여기서 홀수번째 구간들은 오른쪽 끝에 인터넷을 설치하고, 짝수번째 구간들은 왼쪽 끝에 인터넷을 설치한다. 구간의 끝점 처리 관련해서 조금 조심해 줘야 한다. 어떤 구간 $[l, r]$에 대해 오른쪽 끝 구간에 인터넷을 설치하는 비용을 $c_r(l, r)$, 왼쪽 끝에 설치하는 비용을 $c_l(l, r)$이라고 하자.
다음과 같이 점화식을 세워 보자.
$$
DP[i][cnt] = \begin{cases}
\min_{j<i} \left( DP[j][cnt-1] + c_r(j+1, i) \right) & (cnt\text{ is odd}) \
\min_{j \le i} \left( DP[j][cnt-1] + c_l(j, i) \right) & (cnt\text{ is even})
\end{cases}
$$
식의 형태가 비슷해 보이지만 $c_l$과 $c_r$은 그 형태가 훨씬 간단하기 때문에 위 점화식은 계산할 수 있는 꼴이 된다. $c_l(l, r)$은 $\sum_{l \le i \le r} iH_i - l \sum_{l \le i \le r} H_i$와 같이 계산할 수 있다. 여기서 $\sum_{1 \le i \le x} H_i = S_x$, $\sum_{1 \le i \le x} iH_i = P_x$로 미리 초기화를 해 둔다면, 저 식은 $c_l(l, r) = (P_r - P_l) - l(S_r - S_l)$로 나타낼 수 있다. 마찬가지로 $c_r(l, r) = r(S_r-S_{l-1})-(P_r-P_{l-1})$로 나타낼 수 있다.
이제 거의 다 왔다. $c_l(l, r)$과 $c_r(l, r)$의 식 형태를 관찰하면 CHT를 쓸 수 있음을 직감할 수 있다. $cnt$의 홀짝 여부로 케이스를 나눠 보자.
- $cnt-1$이 짝수, $cnt$가 홀수일 때:
$$
\begin{align}
DP[i][cnt] &= \min_{j<i} \left( DP[j][cnt-1] + iS_i - iS_j - P_i + P_j \right) \
&= \min_{j<i} \left(DP[j][cnt-1] + P_j - iS_j\right) + iS_i - P_i
\end{align}
$$
가 되어 CHT를 적용할 수 있다. 이때 기울기 $-S_j$가 점점 감소하고 쿼리 포인트 $i$가 점점 증가하며 min 쿼리이므로, 덱을 이용해 step마다 선형에 처리 가능하다. - $cnt-1$이 홀수, $cnt$가 짝수일 떄:
$$
\begin{align}
DP[i][cnt] &= \min_{j \le i} \left( DP[j][cnt-1] + P_i - P_j - jS_i + jS_j \right) \
&= \min_{j \le i} \left( DP[j][cnt-1] + jS_j - P_j - jS_i \right) + P_i
\end{align}
$$
가 되어 CHT를 이용할 수 있다. 이때 기울기 $-j$가 점점 감소하고 쿼리 포인트 $S_i$가 점점 증가하며 min 쿼리이므로, 덱을 이용해 step마다 선형에 처리 가능하다.
따라서 전체 문제를 $O(N^2)$에 풀 수 있다.
CHT를 오랜만에 짜서 잘못 짰더니 많이 틀렸다.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll divFloor(ll a, ll b){
if(b<0) a=-a, b=-b;
return a>=0 ? a/b : -((-a+b-1)/b);
}
inline ll divCeil(ll a, ll b){
if(b<0) a=-a, b=-b;
return divFloor(a+b-1, b);
}
struct LineContainer{
ll a, b, st;
LineContainer(ll a, ll b): a(a), b(b){st=0;}
ll get(ll x){
return a*x+b;
}
ll cross(LineContainer &r){
return divCeil(r.b-b, a-r.a);
}
};
int n; ll b, c;
ll arr[6002], S[6002], P[6002];
ll DP[3][6002];
ll ans[6002];
vector<LineContainer> vec;
void line_insert(LineContainer p){
while((int)vec.size() >= 2 && vec.back().st >= vec.back().cross(p)){
vec.pop_back();
}
if(!vec.empty()) p.st = vec.back().cross(p);
vec.push_back(p);
}
int main(){
scanf("%d %lld %lld", &n, &b, &c);
for(int i=1; i<=n; i++) scanf("%lld", &arr[i]);
for(int i=1; i<=n; i++){
S[i] = S[i-1] + arr[i];
P[i] = P[i-1] + arr[i] * i;
}
DP[0][0] = 0;
for(int cnt=1; cnt<=n; cnt++){
int pnt = 0;
vec.clear();
for(int i=cnt; i<=n; i++){
if(cnt>1 || i==1) line_insert(LineContainer(-S[i-1], DP[0][i-1] + P[i-1]));
pnt = min(pnt, (int)vec.size()-1);
while(pnt+1 < (int)vec.size() && vec[pnt+1].st <= i) pnt++;
DP[1][i] = vec[pnt].get(i) + i * S[i] - P[i];
}
pnt = 0;
vec.clear();
for(int i=cnt; i<=n; i++){
line_insert(LineContainer(-i, DP[1][i] + i * S[i] - P[i]));
pnt = min(pnt, (int)vec.size()-1);
while(pnt+1 < (int)vec.size() && vec[pnt+1].st <= S[i]) pnt++;
DP[2][i] = vec[pnt].get(S[i]) + P[i];
}
assert(DP[2][n] <= 3e16);
ans[cnt] = DP[2][n] * c + b * cnt;
for(int i=0; i<=n; i++){
DP[0][i] = DP[2][i];
}
}
for(int i=1; i<=n; i++) printf("%lld ", ans[i]);
}
BOJ 20217. Eightgon
문제
- 2차원 격자 상에 $N$개의 점이 있다.
- $N$개의 점 여덟 개를 골라 팔각형을 만들 것이다. 이때, 모든 내각이 135도여야 하고, x축에 평행한 변이 존재해야 한다. (즉, 모든 변은 +x 방향 기준으로 0, 45, 90, 135, 180, 225, 270, 315도 회전된 방향으로 놓여 있다.)
- 위 조건을 만족하도록 팔각형을 만드는 방법의 수를 구하시오.
풀이
위 그림과 같이 여덟 개의 점에 $A \cdots H$로 이름을 붙이자.
먼저 각 $(A, D)$ 쌍에 대해 가능한 $(B, C)$ 쌍의 개수를 찾을 것이다. 기울기가 $1$인 모든 직선과 기울기가 $-1$인 모든 직선을 보며, 왼쪽부터 투 포인터 느낌으로 sweep해주면서 답을 찾을 수 있다. 이때 $B$의 $y$좌표가 $C$의 $y$좌표보다 더 높아야 한다는 조건을 주의하자.
비슷한 느낌으로 모든 $(E, H)$ 쌍에 대해 가능한 $(F, G)$ 쌍의 수를 찾을 수 있다.
이제 이 두 개를 합쳐야 한다. 이것 역시 가능한 모든 기울기 $0$인 직선의 쌍을 보며 문제를 해결해 주면 된다.
시간 복잡도가 문제라고 생각할 수 있겠지만, 두 직선 쌍을 고르고 그 안에서 두 점 쌍을 고르는 방법의 수는 $n^2$이라서 $O(n^2)$에 해결할 수 있다.
메모리가 512MB로, 2차원 배열을 마구 선언하다가 MLE를 당할 수 있음에 조심하자.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct dat{
int x, y, idx;
dat(){}
};
int n;
dat arr[5002];
int AD[5002][5002], HE[5002][5002];
ll DP[5002][5002];
ll ans;
int main(){
scanf("%d", &n);
for(int i=1; i<=n; i++){
scanf("%d %d", &arr[i].x, &arr[i].y);
arr[i].idx = i;
}
vector<dat> v1 (arr+1, arr+n+1), vm1 (arr+1, arr+n+1), vr (arr+1, arr+n+1);
sort(v1.begin(), v1.end(), [&](dat A, dat B){
if(A.x-A.y != B.x-B.y) return A.x-A.y < B.x-B.y;
return A.x<B.x;
});
sort(vm1.begin(), vm1.end(), [&](dat A, dat B){
if(A.x+A.y != B.x+B.y) return A.x+A.y < B.x+B.y;
return A.x<B.x;
});
sort(vr.begin(), vr.end(), [&](dat A, dat B){
if(A.y != B.y) return A.y < B.y;
return A.x<B.x;
});
vector<pair<int, int> > vr1, vrm1, vrr;
for(int i=0; i<n; i++){
int j = i;
while(j+1 < n && v1[i].x-v1[i].y == v1[j+1].x-v1[j+1].y) j++;
vr1.push_back(make_pair(i, j));
i=j;
}
for(int i=0; i<n; i++){
int j = i;
while(j+1 < n && vm1[i].x+vm1[i].y == vm1[j+1].x+vm1[j+1].y) j++;
vrm1.push_back(make_pair(i, j));
i=j;
}
for(int i=0; i<n; i++){
int j = i;
while(j+1 < n && vr[i].y == vr[j+1].y) j++;
vrr.push_back(make_pair(i, j));
i=j;
}
/// A-D
for(auto [l, r]: vr1){
for(auto [s, e]: vrm1){
vector<pair<int, int> > match;
for(int i=l; i<=r; i++) for(int j=s; j<=e; j++){
if(v1[i].x == vm1[j].x && v1[i].y > vm1[j].y) match.push_back(make_pair(i, j));
}
for(int i=l; i<=r; i++){
int pnt = -1;
for(int j=s; j<=e; j++){
if(pnt+1 < (int)match.size() && match[pnt+1].first < i && match[pnt+1].second < j) pnt++;
AD[v1[i].idx][vm1[j].idx] = pnt + 1;
}
}
}
}
/// E-H
for(auto [l, r]: vr1){
for(auto [s, e]: vrm1){
vector<pair<int, int> > match;
for(int i=r; i>=l; i--) for(int j=e; j>=s; j--){
if(v1[i].x == vm1[j].x && v1[i].y < vm1[j].y) match.push_back(make_pair(i, j));
}
for(int i=r; i>=l; i--){
int pnt = -1;
for(int j=e; j>=s; j--){
if(pnt+1 < (int)match.size() && match[pnt+1].first > i && match[pnt+1].second > j) pnt++;
HE[vm1[j].idx][v1[i].idx] = pnt + 1;
}
}
}
}
/// AD + HE
for(int pi=0; pi<(int)vrr.size(); pi++){
int l = vrr[pi].first, r = vrr[pi].second;
for(int pj=0; pj<pi; pj++){
int s = vrr[pj].first, e = vrr[pj].second;
for(int i=l; i<=r; i++){
for(int j=s; j<=e; j++){
DP[i][j] = HE[vr[i].idx][vr[j].idx];
}
}
for(int i=r-1; i>=l; i--) for(int j=s; j<=e; j++) DP[i][j] += DP[i+1][j];
for(int j=e-1; j>=s; j--) for(int i=l; i<=r; i++) DP[i][j] += DP[i][j+1];
for(int i=l; i<r; i++) for(int j=s; j<e; j++){
ans += AD[vr[i].idx][vr[j].idx] * DP[i+1][j+1];
}
}
}
// for(int i=1; i<=n; i++){
// printf("%d: ", i);
// for(int j=1; j<=n; j++) printf("%lld ", AD[i][j]);
// puts("");
// }
//
// for(int i=1; i<=n; i++){
// printf("%d: ", i);
// for(int j=1; j<=n; j++) printf("%lld ", HE[i][j]);
// puts("");
// }
printf("%lld", ans);
}
BOJ 32063. Contigency Plan
문제
- $n$개의 정점을 가진 트리가 있고, 각 간선에는 $1$번부터 $m-1$번까지의 번호가 붙어 있다.
- 이 $n$개의 정점을 가지는 트리를 또 하나 만들어야 한다. 이 간선들에는 $m$번부터 $2m-2$번까지의 번호가 붙어 있고, $2m-2$개의 간선이 모두 서로 달라야 하며, $1 \le i \le m$인 모든 $i$에 대해 $i$번 간선부터 $i+m-2$번 간선까지를 모으면 트리가 되어야 한다.
풀이
자명하게도 기존 트리가 스타 그래프인 경우에는 불가능하다. 이미 한 정점과 연결된 간선이 모두 선택되었으므로, 남은 간선들은 해당 정점과 연결된 간선이 없기 때문이다.
그렇다면 나머지 경우에는 모두 가능할까? 임의의 정점 $p$를 하나 잡고, 이 정점을 기준으로 생각해 보자. 각 간선이 끊어짐에 따라, 정점 $p$와의 연결이 끊어지는 연결 성분이 하나 생긴다. 이때 만약 해당 연결성분에 기존에 $p$와 연결되지 않은 점이 있었다면, 간선을 $p$에서 해당 정점으로 연결해 다시 트리를 만들 수 있다. $p$를 리프 노드 비슷한 걸로 잡으면 이게 될 것 같다는 아이디어를 얻을 수 있다.
다음과 같은 알고리즘을 이용하자.
정점 $p$를 $1$번 간선에 연결되지 않은 임의의 리프 노드로 고르자. 이러한 리프 노드가 반드시 존재함이 보장된다. 또한 $p$가 연결된 유일한 점을 $q$라고 하자.
- 처음 $1$번 간선을 끊을 때, $1$번 간선의 양끝점 $a$와 $b$ 중 $q$에서 더 먼 것을 $b$라고 하자. $p$와 $b$를 연결한다.
- 이후 이런 식으로 어떤 간선이 끊어지면, $q$에서 먼 점과 $p$를 연결하는 것을 반복하자. 이것을 $p-q$ 간선이 끊어질 때까지 반복한다.
- 만약 $p-q$ 간선이 끊어질 차례가 되면, $q-b$를 연결하면 된다. 이후에는 앞의 과정을 반복한다.
그런데 이 알고리즘이 통하지 않는 경우가 딱 하나 있다. 그 조건은,
- $p-q$ 간선의 번호가 $k$번일 때, $1$번부터 $k-1$번까지의 모든 간선이 $q$번 정점과 연결되어 있다.
이 경우에는 모든 $a=q$라서, $b$들이 초기에 모두 $q$와 연결되어 있기 때문에 저 과정이 불가능하다. 하지만 저 경우 $k < l$인 어떤 $l$번 간선과 연결되어 있고 $q$와는 연결되지 않은 리프 노드가 반드시 하나는 존재해야 한다. 해당 리프 노드를 $p$번 노드로 잡으면 위와 같은 방식으로 문제를 해결할 수 있다.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
int ex[100002], ey[100002];
vector<pair<int, int> > link[100002];
int deg[100002], dist[100002];
void dfs(int x, int p=-1){
for(auto [y, z]: link[x]){
if(y==p) continue;
dist[y] = dist[x] + 1;
dfs(y, x);
}
}
int main(){
scanf("%d", &n);
for(int i=1; i<n; i++){
int x, y;
scanf("%d %d", &x, &y);
link[x].push_back(make_pair(y, i));
link[y].push_back(make_pair(x, i));
deg[x]++, deg[y]++;
ex[i] = x, ey[i] = y;
}
if(count(deg+1, deg+n+1, n-1)){
puts("-1");
return 0;
}
int p = 1;
for(; p<=n; p++){
if(deg[p] > 1 || link[p][0].second == 1) continue;
bool bad = 1;
int q = link[p][0].first, k = link[p][0].second;
for(int r=1; r<k && r<=3; r++){
if(ex[r] != q && ey[r] != q) {bad = 0; break;}
}
if(bad) continue;
else break;
}
int q = link[p][0].first;
dfs(p);
int key = -1;
for(int i=1; i<n; i++){
if(ex[i] != p && ey[i] != p){
int pnt = dist[ex[i]] > dist[ey[i]] ? ex[i] : ey[i];
printf("%d %d\n", p, pnt);
bool linked = 0;
for(auto [a, b]: link[pnt]) if(a==q) linked = 1;
if(!linked) key = pnt;
}
else{
assert(key != -1);
printf("%d %d\n", q, key);
}
}
}
'문제풀이 > Problem Solving Diary' 카테고리의 다른 글
Problem Solving Diary #28 (0) | 2024.12.17 |
---|---|
Problem Solving Diary #27 (0) | 2024.11.26 |
Problem Solving Diary #25 (0) | 2024.10.13 |
Problem Solving Diary #24 (0) | 2024.07.08 |
Problem Solving Diary #23 (0) | 2024.06.20 |