티스토리 뷰
1. The Best Lineup
사전순으로 최대인 것을 찾는 문제는 보통 그리디한 접근 방식을 가지는 경우가 많다. 사전순으로 최대라는 개념은 정의 자체가 그리디하기 때문이다.
그래서 이 문제의 경우도 앞부터 채워나가면서 가장 큰 수를 그리디하게 넣으려는 접근이 성립한다. 문제에 주어진 연산을 최대 한 번 해서 만들 수 있는 결과 수열을 유효한 수열이라고 하자. 맨 앞 자리에서부터 최대한 큰 수 넣기를 시도해 보면서, 해당 수열이 유효한지를 계속 확인하면 된다. (어떤 수열이 유효하면 그 접두사들도 모두 유효해야 하므로...)
최종 답의 형태를 보면, 큰 수 여러 개가 같은 것이 여러 개끼리 나오면서, 점점 줄어드는 형태를 상상할 수 있다. 따라서 $v$를 $N$부터 $1$까지 내리면서, $v$를 최대 몇 개까지 넣을 수 있는지 생각해 보자. 이렇게 하면서 답을 앞부터 점점 채워나갈 것이다.
연산을 한 번도 쓰지 못한다고 가정해 보자. 이 경우 $v$들 중에서 현재 답의 마지막 원소보다 오른쪽에 있는 것들을 사용할 수 있다. 그러나, 만약 연산을 한 번 사용할 수 있다면, $v$들 중에서 현재 답의 두 번째로 마지막 원소보다 오른쪽에 있는 것들을 사용할 수 있을 것이다. 만약 지금 연산을 사용해서 넣을 수 있는 수의 개수가 더 많다면, 우리는 사전순 최대인 답을 찾고 있으므로 무조건 연산을 사용해야 한다. 그러나 만약 연산을 사용해도 지금 넣을 수 있는 $v$의 개수가 변하지 않으면, 연산은 아껴두는 게 좋을 것이다.
이것은 사전에 각 수의 위치를 모두 저장해 놓고 이분 탐색 등을 잘 활용하여 $O(N \log N)$에 해결할 수 있다.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int t;
int n;
int A[200002];
vector<int> occ[200002];
int main(){
scanf("%d", &t);
while(t--){
scanf("%d", &n);
for(int i=1; i<=n; i++) occ[i].clear();
for(int i=1; i<=n; i++){
scanf("%d", &A[i]);
occ[A[i]].push_back(i);
}
vector<int> ans; /// 위치로 저장
int used = 0; /// 마지막 위치, 스킬 사용 여부
for(int v=n; v>=1; v--){
if(occ[v].empty()) continue;
if(ans.empty()){
ans = occ[v];
continue;
}
int cnt = occ[v].end() - lower_bound(occ[v].begin(), occ[v].end(), ans.back());
int new_cnt = used ? 0 : (occ[v].end() - lower_bound(occ[v].begin(), occ[v].end(),
ans.size()==1 ? 0 : ans[ans.size()-2]));
if(cnt < new_cnt){
used = 1;
int p = (ans.size()==1 ? 0 : ans[ans.size()-2]);
for(int x: occ[v]) if(x > p) ans.push_back(x);
}
else for(int x: occ[v]){
int p = ans.back();
if(x > p) ans.push_back(x);
}
}
for(int i=0; i<(int)ans.size(); i++){
printf("%d%c", A[ans[i]], i+1==(int)ans.size() ? '\n' : ' ');
}
}
}
2. Vocabulary Quiz
문제의 상황을 트리로 생각할 수 있다. (엄밀히 말하자면 문제의 상황은 Trie(트라이)와 일치하지만, 해당 자료구조를 전혀 몰라도 푸는 데 지장이 없다.)
어떤 단어 (정점) $x$를 말할 때, $x$를 확정할 수 있을 때까지 말한다고 한다. 이때 어디까지 말해야 할까? 어떤 단어 $x$를 말하는 과정은 트리의 루트($0$번 정점)에서 $x$번 정점을 향해 내려가는 과정으로 생각할 수 있다. 그러다가 밑으로 내려가는 길에 리프 노드가 $x$번 하나밖에 안 남으면 멈춘다.
이 위치를 어떻게 찾을 수 있을까? 반대로 $x$번 정점에서 올라간다고 생각해 보자. 언제까지 올라가면 될까? $par[x]$의 자손이 $1$개인 경우 계속 올라가다가, 자식이 여러 개인 정점을 만난 경우 그 직전에 멈추면 된다.
예시를 보자.

이것은 두 번째 예제를 그린 것이다. 처음에 $4$번 단어를 읽을 때는 $4$번 정점의 부모인 $1$번 정점이 자식이 두 개이므로 거기까지 올라가면 안 된다. 즉, $4$번 단어를 확정지으려면 $4$번 단어에 해당하는 노드까지 모두 읽어야 한다. 이때 $4$번 단어의 깊이인 $2$를 출력하면 된다. 이후 $4 \rightarrow 1$로 가는 edge를 제거해 준다.
두 번째 케이스에서는 $3$번 정점에서 $1$번 정점으로 올라가도 된다. $1$번 정점의 자식이 하나밖에 안 남았기 때문이다. 그러나 $0$번 정점은 자식이 두 개 남아 있기 때문에, 더 이상 올라갈 수 없다. 즉, 이번 단어를 확정하려면 $1$번 단어까지는 불러야 한다. $1$번 정점의 깊이인 $1$을 출력한다. 이후 $1 \rightarrow 0$으로 가는 간선을 제거한다.
세 번째 케이스에서는 $0$번 정점의 자식이 하나밖에 안 남았으므로 $0$번 정점까지 올라갈 수 있다. 따라서 $0$번 정점의 깊이인 $0$이 답이 된다.
이제 이 알고리즘의 시간 복잡도를 분석해 보자. 처음에는 이 알고리즘의 시간 복잡도가 $O(N^2)$이라고 생각할 수도 있다. 그러나, 각 간선을 정확히 한 번씩 확인하므로, 실제 시간 복잡도는 $O(N)$이다.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
int par[1'000'005];
int deg[1'000'005], depth[1'000'005];
int main(){
scanf("%d", &n);
for(int i=1; i<=n; i++){
scanf("%d", &par[i]);
deg[par[i]]++;
depth[i] = depth[par[i]] + 1;
}
int m = 0;
for(int i=1; i<=n; i++) if(!deg[i]) m++;
vector<int> vec (m);
for(int i=0; i<m; i++) scanf("%d", &vec[i]);
for(int x: vec){
int y = x;
while(y && deg[par[y]] == 1){
y = par[y];
}
if(y) deg[par[y]]--;
printf("%d\n", depth[y]);
}
}
3. Transforming Pairs
거꾸로 생각하자. 할 수 있는 것은 큰 쪽에서 작은 쪽을 빼는 것밖에 없다. ($a$와 $b$가 모두 $1$ 이하이므로, 두 수가 같은 경우는 더 이상 아무 action도 할 수 없다고 생각해도 된다.)
따라서 역으로 문제를 풀어주면 유클리드 호제법과 비슷한 방식으로 쉽게 해결할 수 있다. 구현 시 주의사항으로, $(c, d)$에서 $(c \bmod d, d)$로 가는 중간 과정에서 $(a, b)$가 나올 수도 있다는 사실을 유의해야 한다. 예를 들어 $(c, d) = (19, 8)$인데 $(a, b) = (11, 8)$인 상황을 놓치지 않도록 주의해야 한다. (이러한 상황은 예제 2에서도 등장한다.)
유클리드 호제법의 시간 복잡도와 같이, 각 테스트 케이스를 해결하는 복잡도는 $\log \left( \min(c, d) \right)$ 정도 수준이다.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int t;
ll a, b, c, d;
int main(){
scanf("%d", &t);
while(t--){
scanf("%lld %lld %lld %lld", &a, &b, &c, &d);
ll cnt = 0; int done = 0;
while(c && d){
if(a==c&&b==d){
printf("%lld\n", cnt);
done = 1;
break;
}
if(c<d) swap(a,b), swap(c,d);
if(d == b && c >= a){
if((c-a)%d == 0){
printf("%lld\n", cnt + (c-a)/d);
done = 1;
break;
}
}
cnt += c/d, c%=d;
}
if(!done) puts("-1");
}
}
'문제풀이 > 기출문제' 카테고리의 다른 글
| USACO 2023-2024 February (Silver) (0) | 2025.08.21 |
|---|---|
| USACO 2023-2024 January (Silver) (0) | 2025.08.18 |
| USACO 2023-2024 December (Silver) (0) | 2025.08.14 |
| USACO 2024-2025 January (Silver) (0) | 2025.08.07 |
| USACO 2024-2025 December (Silver) (0) | 2025.08.03 |
