티스토리 뷰
싱가포르 NOI 2022를 돌아 364/500점을 받았다.
[BOJ 27288] A. Voting Cities (0:13)
바로 몇 주 전에 동아리에서 랜덤 플래 풀기를 하다가 본 문제라서 바로 풀었다. 기본적인 아이디어는 $K$개의 정점을 시작점으로 두고 한번에 다익스트라를 하면 되는데, 다익스트라에 상태 $d$를 추가적으로 저장한다. $d$는 사용한 쿠폰 목록이다. 아직 쿠폰 가격은 모르므로 쿠폰 가격은 쿼리를 처리할 때 더하도록 하자. 이후 쿼리가 들어올 때는 $32$가지 $d$를 고르는 경우 중 가장 싼 것을 고르면 된다.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct dat{
int x, d; ll y;
dat(){}
dat(int x, int d, ll y): x(x), d(d), y(y){}
bool operator<(const dat &r)const{
return y>r.y;
}
};
int n, m, k, q;
int voting[5002];
vector<pair<int, ll> > link[5002]; /// 역으로 저장함
ll dist[5002][32]; /// dist[i][j]: i번까지 j번 티켓 쓰고 도착하는 경로
bool visited[5002][32];
int main(){
scanf("%d %d %d", &n, &m, &k);
for(int i=1; i<=k; i++) scanf("%d", &voting[i]);
for(int i=1; i<=m; i++){
int x, y; ll w;
scanf("%d %d %lld", &x, &y, &w);
link[y].push_back(make_pair(x, w));
}
priority_queue<dat> pq;
for(int i=0; i<n; i++) for(int j=0; j<32; j++) dist[i][j] = 1e18;
for(int i=1; i<=k; i++){
pq.push(dat(voting[i], 0, 0));
dist[voting[i]][0] = 0;
}
while(!pq.empty()){
dat tmp = pq.top(); pq.pop();
if(visited[tmp.x][tmp.d]) continue;
int x = tmp.x, d = tmp.d; ll y = tmp.y;
visited[x][d] = 1;
dist[x][d] = y;
// printf("%d %d: %lld\n", x, d, y);
for(auto [nxt, add]: link[x]){
for(int mode=0; mode<6; mode++){
if((d >> mode) & 1) continue;
int nxtD = d + (mode == 5 ? 0 : (1<<mode));
ll nxtY = y + add / 10 * (mode == 5 ? 10 : (9-mode));
pq.push(dat(nxt, nxtD, nxtY));
}
}
}
scanf("%d", &q);
while(q--){
int S; ll price[5];
scanf("%d %lld %lld %lld %lld %lld", &S, &price[0], &price[1], &price[2], &price[3], &price[4]);
ll ans = 1e18;
for(int d=0; d<32; d++){
if(dist[S][d] > 1e17) continue;
bool able = 1;
for(int i=0; i<5; i++) if(((d>>i)&1) && price[i] == -1) able = 0;
if(!able) continue;
ll option = dist[S][d];
for(int i=0; i<5; i++) if((d>>i)&1) option += price[i];
ans = min(ans, option);
}
if(ans > 1e17) puts("-1");
else printf("%lld\n", ans);
}
}
[BOJ 27291] D. Grapevine (2:40)
트리 문제를 잘 다룰 수 있는지 묻는 문제다. Centroid Decomposition과 dfs ordering을 이용한 세그먼트 트리를 잘 굴리면 된다. 이 알고리즘들을 모두 안다면 쉽게 풀 수 있다. 다만 구현이 매우 길어서 실수할 구석이 많다. 나도 이 문제에서 1시간 반 이상을 소비한 것 같다. 시간 복잡도는 $O(N \log N + Q \log^2 N)$ 쯤 일 것이다.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct segmentTree{
int SZ;
vector<ll> tree, lazy;
void init(int n){
SZ = n;
tree.resize(n*4+1), lazy.resize(n*4+1);
init(1, 1, n);
}
void init(int i, int l, int r){
tree[i] = 1e18;
lazy[i] = 0;
if(l==r) return;
int m = (l+r)>>1;
init(i*2, l, m);
init(i*2+1, m+1, r);
}
void propagate(int i, int l, int r){
tree[i] += lazy[i];
if(l!=r) lazy[i*2] += lazy[i], lazy[i*2+1] += lazy[i];
lazy[i] = 0;
}
void add(int i, int l, int r, int s, int e, ll v){
propagate(i, l, r);
if(r<s || e<l) return;
if(s<=l && r<=e){
lazy[i] = v;
propagate(i, l, r);
return;
}
int m = (l+r)>>1;
add(i*2, l, m, s, e, v);
add(i*2+1, m+1, r, s, e, v);
tree[i] = min(tree[i*2], tree[i*2+1]);
}
ll query(int i, int l, int r, int s, int e){
propagate(i, l, r);
if(r<s || e<l) return 3e18;
if(s<=l && r<=e) return tree[i];
int m = (l+r)>>1;
return min(query(i*2, l, m, s, e), query(i*2+1, m+1, r, s, e));
}
ll query(){
return query(1, 1, SZ, 1, SZ);
}
} tree[100002];
struct Line{
int nxt, idx;
Line(){}
Line(int nxt, int idx): nxt(nxt), idx(idx){}
};
int n, q;
int ex[100002], ey[100002];
ll len[100002];
vector<Line> link[100002];
bool on[100002];
namespace HLD{
struct Fenwick{
int n;
ll tree[100002];
void init(int _n){
n = _n;
for(int i=1; i<=n; i++) tree[i] = 0;
}
void _add(int x, ll v){
while(x<=n){
tree[x] += v;
x += x&-x;
}
}
void add(int s, int e, ll v){
_add(s, v);
_add(e+1, -v);
}
ll _sum(int x){
ll ret = 0;
while(x){
ret += tree[x];
x -= x&-x;
}
return ret;
}
ll sum(int x){
return _sum(x);
}
} tree;
int par[100002];
int in[100002], out[100002], depth[100002], inCnt;
int LCADB[100002][20];
void dfs(int x, int p=-1){
in[x] = ++inCnt;
for(Line y: link[x]){
if(y.nxt == p) continue;
par[y.nxt] = x;
depth[y.nxt] = depth[x] + 1;
dfs(y.nxt, x);
}
out[x] = inCnt;
}
void init(){
dfs(1);
for(int i=1; i<=n; i++) LCADB[i][0] = par[i];
for(int d=1; d<20; d++) for(int i=1; i<=n; i++) LCADB[i][d] = LCADB[LCADB[i][d-1]][d-1];
tree.init(n);
for(int i=1; i<n; i++){
int x = ex[i], y = ey[i];
if(depth[x] < depth[y]) swap(x, y);
/// x가 더 아래에 있는 정점
tree.add(in[x], out[x], len[i]);
}
}
int getLCA(int x, int y){
if(depth[x] > depth[y]) swap(x, y);
for(int d=0; d<20; d++) if((depth[y]-depth[x])&(1<<d)) y = LCADB[y][d];
if(x==y) return x;
for(int d=19; d>=0; d--) if(LCADB[x][d] != LCADB[y][d]) x = LCADB[x][d], y = LCADB[y][d];
return par[x];
}
ll query(int a, int b){
int c = getLCA(a, b);
return tree.sum(in[a]) + tree.sum(in[b]) - tree.sum(in[c])*2;
}
void update(int x, ll v){
tree.add(in[x], out[x], v);
}
}
bool centroidUsed[100002];
int centroidRoot, centroidPar[100002], centroidDepth[100002];
int subtreeSize[100002];
int sz[100002], in[100002][20], out[100002][20], depth[100002][20];
int canFindEdges[100002][20];
void getSubtreeSize(int x, int p=-1){
subtreeSize[x] = 1;
for(auto y: link[x]){
if(y.nxt==p || centroidUsed[y.nxt]) continue;
getSubtreeSize(y.nxt, x);
subtreeSize[x] += subtreeSize[y.nxt];
}
}
int getCentroid(int x, int p, int lim){
for(auto y: link[x]){
if(y.nxt==p || centroidUsed[y.nxt]) continue;
if(subtreeSize[y.nxt] >= lim) return getCentroid(y.nxt, x, lim);
}
return x;
}
void eulerTourDfs(int x, int p, int d, int c){
in[x][d] = ++sz[c];
for(auto y: link[x]){
if(y.nxt == p || centroidUsed[y.nxt]) continue;
depth[y.nxt][d] = depth[x][d] + 1;
eulerTourDfs(y.nxt, x, d, c);
}
out[x][d] = sz[c];
}
void putLengthDfs(int x, int p, int d, int c){
for(auto y: link[x]){
if(y.nxt == p || centroidUsed[y.nxt]) continue;
// printf("Put %d-%d in %d\n", ex[y.idx], ey[y.idx], c);
canFindEdges[y.idx][d] = c;
tree[c].add(1, 1, sz[c], in[y.nxt][d], out[y.nxt][d], len[y.idx]);
putLengthDfs(y.nxt, x, d, c);
}
}
int findCentroid(int x, int d = 0){
getSubtreeSize(x);
x = getCentroid(x, -1, (subtreeSize[x] + 1) / 2);
centroidUsed[x] = 1;
centroidDepth[x] = d;
depth[x][d] = 0;
eulerTourDfs(x, -1, d, x);
tree[x].init(sz[x]);
putLengthDfs(x, -1, d, x);
for(auto y: link[x]){
if(centroidUsed[y.nxt]) continue;
int tmp = findCentroid(y.nxt, d+1);
centroidPar[tmp] = x;
}
return x;
}
ll getAnswer(int x){
int c = x;
ll ret = 1e18;
while(c){
// printf("C: %d, X: %d: XtoC %lld, Cval %lld\n", c, x, HLD::query(c, x), tree[c].query());
ret = min(ret, HLD::query(c, x) + tree[c].query());
c = centroidPar[c];
}
assert(ret <= 1e17);
return ret;
}
void changeVertex(int x){
int c = x;
ll v = (on[x] ? 1e18 : -1e18);
on[x] = !on[x];
while(c){ /// 센트로이드를 돌아다니며 +, - 해주기
int d = centroidDepth[c];
tree[c].add(1, 1, sz[c], in[x][d], in[x][d], v);
c = centroidPar[c];
}
}
void changeEdge(int idx, ll v){
/// HLD 데이터 수정
assert(idx);
int x = ex[idx], y = ey[idx];
if(HLD::depth[x] < HLD::depth[y]) swap(x, y);
HLD::update(x, v - len[idx]);
for(int d=19; d>=0; d--){ /// 센트로이드 데이터 수정
if(!canFindEdges[idx][d]) continue;
int x = ex[idx], y = ey[idx], c = canFindEdges[idx][d];
if(depth[x][d] < depth[y][d]) swap(x, y); /// 이제 x가 더 아래에
tree[c].add(1, 1, sz[c], in[x][d], out[x][d], v - len[idx]);
// printf("After changing c %d: query ans %lld\n", c, tree[c].query());
}
len[idx] = v;
}
int main(){
map<pair<int, int>, int> mpEdge;
scanf("%d %d", &n, &q);
for(int i=1; i<n; i++){
int x, y; ll w;
scanf("%d %d %lld", &x, &y, &w);
ex[i] = x, ey[i] = y, len[i] = w;
link[x].push_back(Line(y, i));
link[y].push_back(Line(x, i));
mpEdge[make_pair(x, y)] = mpEdge[make_pair(y, x)] = i;
}
centroidRoot = findCentroid(1);
HLD::init();
int grapeCnt = 0;
while(q--){
int qt;
scanf("%d", &qt);
if(qt == 1){ /// Seek the nearest
int x;
scanf("%d", &x);
if(!grapeCnt) puts("-1");
else printf("%lld\n", getAnswer(x));
}
else if(qt == 2){ /// 정점 업데이트
int x;
scanf("%d", &x);
grapeCnt += (on[x] ? -1 : 1);
changeVertex(x);
}
else{
int a, b; ll w;
scanf("%d %d %lld", &a, &b, &w);
changeEdge(mpEdge[make_pair(a, b)], w);
}
}
}
[BOJ 27289] B. Gym Badges (4:01)
처음 봤을 때는 쉬울 줄 알았는데, 그렇지 않아서 당황한 문제다. 끝나고 찾아보니 BOJ 15773 (Touch The Sky)와 같은 문제라고 한다. 옛날에 풀었는데 기억이 안 났다. 일단 Full Task를 풀려면 제곱 풀이를 아는 게 좋다.
문제를 다음과 같이 바꿔 표현한다. 점수 $S$가 있고, 순서쌍 $(A_i, B_i)$가 있다. $S \le B_i$인 경우 $S += A_i$를 시행할 수 있다. 한 순서쌍은 한 번만 시행할 수 있다. 가능한 시행의 최대 횟수를 구하는 것이 목표이다.
Observation 1. 먼저 두 순서쌍 $(A_i, B_i)$와 $(A_j, B_j)$를 모두 시행에 사용할 때, 그 순서를 고려해 보자. 두 시행을 모두 하기 전의 $S$값을 $L$로 놓고, 시행이 가능할 조건에 대한 두 가지 식을 정리하고 나면, $A_i + B_i < A_j + B_j$일 경우 첫 번째 순서쌍을 먼저 쓰는 것이 이득임을 알 수 있다. 따라서 먼저 이 순서대로 모든 순서쌍을 정렬한다, 더불어, $A_i + B_i = A_j + B_j$일 경우, 무엇이 앞에 오든 큰 상관이 없다.
이제 여기서 자명한 DP 하나를 생각할 수 있다. $DP[i][j]$는 $i$번 순서쌍까지 총 $j$번의 시행을 했을 때의 최소 높이이다. 높이가 작을수록 더 좋다. 이대로 DP를 해 나가면 쉽게 $O(N^2)$로 풀 수 있다. 이렇게 하면 Subtask 1, 3을 풀 수 있다.
이제 이 DP를 최적화할 방법을 찾는다. $DP[i]$의 값들을 보면 $j$가 커질수록 증가한다. 인접한 값들의 차이에 주목하면, 이 차이 역시 증가함을 알 수 있다. 증명은 귀납법을 이용하는데, $DP[i][j+1] - DP[i][j] := D_j$로 놓고, 순서쌍이 하나 추가될 때 어떤 기준으로 이 $D_j$ 수열이 변하는지를 관찰하면 된다. $D$ 수열의 의미, 그리고 제곱 점화식을 생각해 보면 두 가지 경우가 있음을 알 수 있는데, 먼저 이번에 본 수열의 $A_i$값이 $D$ 수열의 특정 위치에 끼어들어가 오름차순이 유지되는 사건이 반드시 일어난다. 여기에 추가로 만약 시행 전 $D$값의 합, 즉 $DP[i][j]$의 최댓값이 $B_i$ 이하였다면, 마지막 $D$값이 탈락된다. 이 현상은 직접 시뮬레이션을 여러 번 해 보면 납득된다. 증명 역시 어렵지 않을 것 같다.
그래서 남은 것은 이 $D$값을 어떤 자료구조로 관리해 주면 된다. 처음에 풀이를 전부 알기 전 세그먼트 트리로 구현하는 바람에 세그를 썼는데, 그냥 힙 하나만 들고 다녀도 충분할 것 같다.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct segTree{
ll tree[2000002];
void add(int i, int l, int r, int x, ll v){
if(l==r){
tree[i] += v;
return;
}
int m = (l+r)>>1;
if(x<=m) add(i*2, l, m, x, v);
else add(i*2+1, m+1, r, x, v);
tree[i] = tree[i*2] + tree[i*2+1];
}
int rightest(int i, int l, int r){
if(l==r) return l;
int m = (l+r)>>1;
if(tree[i*2+1]) return rightest(i*2+1, m+1, r);
return rightest(i*2, l, m);
}
} tree;
int n;
pair<ll, ll> arr[500002];
pair<ll, int> datas[500002];
int loc[500002];
ll val[500002];
ll sum;
int ans;
int main(){
scanf("%d", &n);
for(int i=1; i<=n; i++) scanf("%lld", &arr[i].first);
for(int i=1; i<=n; i++) scanf("%lld", &arr[i].second);
sort(arr+1, arr+n+1, [&](pair<ll, ll> A, pair<ll, ll> B){
if(A.first + A.second != B.first + B.second) return A.first + A.second < B.first + B.second;
return A.second < B.second;
});
for(int i=1; i<=n; i++){
datas[i] = make_pair(arr[i].first, i);
}
sort(datas+1, datas+n+1);
for(int i=1; i<=n; i++){
loc[datas[i].second] = i;
val[i] = datas[i].first;
}
for(int i=1; i<=n; i++){
bool rep = (sum > arr[i].second);
sum += arr[i].first;
ans++;
tree.add(1, 1, n, loc[i], arr[i].first);
if(rep){
int r = tree.rightest(1, 1, n);
sum -= val[r];
tree.add(1, 1, n, r, -val[r]);
ans--;
}
}
printf("%d", ans);
}
[BOJ 27290] C. Towers (29점)
처음에 문제를 보고 쉽게 풀릴 것 같았는데 결국 끝까지 풀지 못했다. IOI 2021 Park가 생각나는 어려운 문제인 것 같다.
Subtask 4까지 풀었는데, Subtask 1, 2는 그냥 브루트포스고 3은 네 꼭짓점에 타워를 설치하며 사각형 크기를 점점 줄여 나가면 된다. Subtask 4는 왼쪽부터 x좌표를 보며 행 내의 모든 점을 색칠하는데, 만약 색칠 도중 한 y좌표에 세 점이 색칠되면 가운데 점 색칠을 해제해 주는 식으로 풀면 된다.
(푸는 중)
[BOJ 27292] E. Fruits (35점)
이 문제는 엄청 어려울 것 같다는 느낌이 있었고 실제로도 그런 것 같다. Subtask 1, 2는 쉽기 때문에 설명을 생략한다.
Subtask 4를 풀려면 제곱 DP를 만들면 된다. $DP[i][j]$를 $i$번 market까지 방문했고 최댓값이 $j$인 상태에서 최대 이익이라고 정의한다. $DP[i] \rightarrow DP[i+1]$의 전이를 $O(N^2)$에 하면 세제곱에 Subtask 3을 풀 수 있다. 전이를 $O(N)$에 하는 것도 조금만 머리를 써 주면 된다. $DP[i+1][j]$의 값을 고를 때 $DP[i][0] \cdots DP[i][j]$ 중 최댓값을 보면 되니까, $j$를 올려 주면서 저 최댓값을 저장해 주는 식으로 하면 된다. 이렇게 하면 $O(N^2)$이 풀린다.
(푸는 중)
'코딩 > 국대 멘토링 교육' 카테고리의 다른 글
2023 4주차 멘토링 일지 (0) | 2023.05.27 |
---|---|
2023 3주차 멘토링 일지 (2) | 2023.05.21 |
2023 2주차 멘토링 일지 (0) | 2023.05.14 |
2023 1주차 멘토링 일지 (1) | 2023.05.13 |
2021 국대 멘토링 일지 - 3주차 (0) | 2021.04.24 |