본문 바로가기

카테고리 없음

NYPC 2023 Round 2A

728x90

2023년 NYPC의 2라운드 첫 번째 대회의 풀이이다. 2023년 8월 13일 14시부터 17시까지 진행되었고, 네 문제가 출제되었다. 네 문제는 난이도순이다.

 

1. 오솔길

N개의 점이 주어지고, 점들의 $x$좌표와 $y$좌표는 모두 다르다. 두 점 $(x_1, y_1)$과 $(x_2, y_2)$가 있을 때, 두 점을 $(x_1, y_2)$, $(x_2, y_1)$으로 바꾸는 연산을 할 수 있다. 모든 점 쌍에 대해서 x좌표가 더 큰 점이 y좌표도 더 크도록 하기 위해 최소 몇 번의 연산이 필요한지를 계산하는 문제이다.

 

$N \le 200000$

 

풀이

더보기

우선 점들을 $x$좌표 오름차순으로 정렬하자. 여기서 두 점에 대한 연산을 하는 것은 두 점의 $y$좌표를 맞바꾸는 것과 같다. 따라서 문제를 최소 교환을 통해 $y$좌표를 정렬하는 문제로 바꿀 수 있다.

 

우선 $y$좌표가 모두 다르므로, 좌표 압축을 해서 1이상 $N$이하 범위의 숫자로 바꿔주자. 그러면 $Y$ 배열은 순열이 되는데, 순열 사이클 분할을 통해 순열을 여러 사이클로 분할하면, 각 사이클마다 (사이클 크기)-1이 그 사이클을 정렬하는데 필요한 교환 횟수가 된다. 따라서 순열 사이클 분할을 하고, 각 사이클의 크기를 구하면 $O(N)$에 풀 수 있다.

코드

더보기
#include <bits/stdc++.h>
#include <cassert>
#pragma GCC optimize("O3")
#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC target("avx,avx2,fma")
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef pair<int, int> pii;
#define MAX 201010
#define MAXS 20
#define INF 1000000000
#define bb ' '
#define ln '\n'
#define Ln '\n'
#define MOD 1000000007
pii arr[MAX];
int Y[MAX];
int vis[MAX];
signed main() {
	ios::sync_with_stdio(false), cin.tie(0);
	int N;
	cin >> N;
	int i;
	for (i = 1; i <= N; i++) cin >> arr[i].first >> arr[i].second;
	sort(arr + 1, arr + N + 1);
	vector<int> v;
	for (i = 1; i <= N; i++) v.push_back(arr[i].second);
	sort(v.begin(), v.end());
	for (i = 1; i <= N; i++) Y[i] = lower_bound(v.begin(), v.end(), arr[i].second) - v.begin() + 1;
	int cnt = 0;
	for (i = 1; i <= N; i++) cnt += Y[i] != i;
	for (i = 1; i <= N; i++) {
		if (vis[i]) continue;
		if (Y[i] != i) {
			cnt--;
			int v = i;
			while (!vis[v]) {
				vis[v] = 1;
				v = Y[v];
			}
		}
	}
	cout << cnt << ln;
}

2. 주사위 대체

$m$개의 수가 균등하게 나오는 주사위가 있고, 이 주사위를 적당한 횟수로 던져서 나오는 결과들을 분류하여 $n$개의 숫자가 나오는 주사위처럼 사용할 수 있는지 판별하는 문제이다. 아래는 문제에 나와 있는 $m=6, n=4$일 경우의 예시이다.

  • 1, , 3, 4가 나오면, 그대로 1, 2, 3, 4
  • 5가 나오면, 다시 굴려서 , , 이면 로, , 5, 이면
  • 이 나오면, 다시 굴려서 , , 이면 으로, , , 이면 4

$m, n \le 10^{18}$

풀이

더보기

관찰 : 어떤 정수 $k$가 있어서 $m^k$가 $n$의 배수라면 답은 YES이고, 그렇지 않으면 NO이다.

 

위와 같은 관찰을 사용하면 풀 수 있다. $n, m$의 제한에 의해 두 수에는 소인수가 최대 60번 곱해져 있다. ($2^60>10^{18}$이므로) 따라서, $m^k$가 $n$의 배수가 되는 $k$가 존재한다면, $k<=60$을 가정해도 좋다. 따라서, $m^60$이 $n$의 배수인지만 확인하면 된다. 큰 수 연산을 활용해도 통과는 할 것 같지만, 중요한 것은 $n$으로 나눈 나머지이므로, $m$의 거듭제곱을 구할 때 계속 $n$으로 나눠주면서 구해도 된다. 나의 경우 long long 범위를 초과할 것 같아서 파이썬으로 풀었다.

코드

더보기
T = int(input())
for t in range(T):
	m, n = input().split()
	m = int(m)
	n = int(n)
	mul = m
	for c in range(100) :
		mul = mul % n
		if n == 0:
			break
		mul = mul * m
	if mul == 0:
		print("YES")
	else:
		print("NO")

 

3. 창수의 고민

길이 $N$의 배열이 주어지고, $1 \le i < j  \le N$인 $i, j$에 대해 배열의 $i$번째 원소가 $j$번째 원소보다 크거나 같은 쌍의 갯수를 만족 점수라 한다. $S, E$를 잘 정해서 배열의 $S$번 원소부터 $E$번 원소까지 $K$를 더했을 때 구해지는 만족 점수를 최대화해야 한다.

 

풀이

더보기

문제에서 주어진 배열을 $A_i$라 하자.

 

우선 $K$를 더하지 않았을 경우의 답은 수열에서 inversion을 세는 문제이므로 세그먼트 트리를 사용해서 풀 수 있다.(값의 범위가 커서 값 압축이 필요하다)

 

일단 $K$가 양수임을 가정하자. 그러면 $S=1$인 최적해가 반드시 존재한다. $S>1$인 해가 있을 때, 이 배열의 $S-1$번 이전 원소에 전부 $K$를 더해 $S=1$인 해를 만들었다고 생각해 보자. 이렇게 하면 원래 배열에서 조건을 만족하는 쌍은 반드시 조건을 만족한다. 원래 배열에서 조건을 만족했던 $(i, j)$ 쌍에 대하여, $j$에 $K$가 더해지면 반드시 $i$에도 $K$가 더해지기 때문이다.

 

따라서 각 $E$에 대해 $E$번 이전 원소에 $K$가 더해졌을 때 inversion이 어떻게 변하는지만 확인하면 된다. 여러 가지 방법이 있겠지만, 가장 간단한 것은 $E$를 하나씩 늘려가면서 보는 것이다.

 

현재 배열에 $E-1$번 원소까지는 전부 $K$가 더해졌고, 이제 $E$번 원소에 $K$가 더해질 때 inversion이 어떻게 되는지를 구해야 한다고 하자. $E-1$의 경우의 답을 알고 있으니, 답이 어떻게 변하는지만 확인하면 된다. $(i, E)$ ($i<E$)와 같은 쌍의 경우, $A_i$가 $A_E$이상 $A_E + K$ 미만일 때 값에 영향을 준다. $E$번 원소에 $K$가 더해지기 이전에는 조건을 만족하다가, 더해진 이후에는 더 이상 조건을 만족하지 않기 때문이다. $(E, j)$ ($E<j$)와 같은 쌍의 경우, $A_j$가 $A_E$초과 $A_E + K$이하일 경우 답에 영향을 준다. 이 쌍들은 더해지기 이전에는 조건을 만족하지 않다가, 이후에 조건을 만족하게 된다.

 

다음으로 $K$가 음수인 경우, 마찬가지로 $E=N$이라는 관찰을 할 수 있다. 이때 $[S, N]$에 $K$를 더하나 $[1, S-1]$에 $-K$를 더하나 답은 같으므로, $K$가 양수일 때와 정확히 같은 방식으로 풀 수 있다.

 

위 과정은 세그먼트 트리를 사용해서 빠르게 할 수 있다. 시간 복잡도는 $O(NlgN)$.

 

내 코드의 경우, 배열을 뒤집은 후 풀었다. 풀이 자체는 크게 다르지 않다.

코드

더보기
#include <bits/stdc++.h>
#include <cassert>
#pragma GCC optimize("O3")
#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC target("avx,avx2,fma")
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef pair<int, int> pii;
#define MAX 301010
#define MAXS 20
#define INF 1000000000
#define bb ' '
#define ln '\n'
#define Ln '\n'
#define MOD 1000000007
struct fenwick {
	int N;
	vector<int> tree;
	fenwick(int N = 0) :N(N) { tree.resize(N + 1); }
	void upd(int i, int x) { while (i <= N) { tree[i] += x, i += i & -i; } }
	int get(int i) { int ans = 0; while (i) { ans += tree[i], i -= i & -i; } return ans; }
};
ll arr[MAX];
inline int getind(vector<ll>& v, ll x) { return lower_bound(v.begin(), v.end(), x) - v.begin() + 1; }
signed main() {
	ios::sync_with_stdio(false), cin.tie(0);
	int N;
	ll K;
	cin >> N >> K;
	int c = 0;
	if (K < 0) c = 1, K *= -1;
	int i;
	vector<ll> v;
	for (i = 1; i <= N; i++) cin >> arr[i], v.push_back(arr[i]), v.push_back(arr[i] - 1), v.push_back(arr[i] + K), v.push_back(arr[i] + K - 1);
	reverse(arr + 1, arr + N + 1);
	sort(v.begin(), v.end());
	v.erase(unique(v.begin(), v.end()), v.end());
	fenwick pfen(v.size()), sfen(v.size());
	ll sum = 0;
	for (i = 1; i <= N; i++) {
		int ind = getind(v, arr[i] - 1);
		sum += pfen.get(ind);
		pfen.upd(getind(v, arr[i]), 1);
	}
	ll ans = sum;
	int mv = 0;
	for (i = N; i >= 1; i--) {
		pfen.upd(getind(v, arr[i]), -1);
		sum += pfen.get(getind(v, arr[i] + K)) - pfen.get(getind(v, arr[i]));
		sum -= sfen.get(getind(v, arr[i] + K - 1)) - sfen.get(getind(v, arr[i] - 1));
		sfen.upd(getind(v, arr[i] + K), 1);
		if (sum > ans) {
			ans = sum;
			mv = i;
		}
		ans = max(ans, sum);
	}
	if (!mv) cout << 1 << bb << N << ln;
	else {
		if (c) {
			if (mv == 1) cout << 1 << bb << N << ln;
			else cout << N + 2 - mv << bb << N << ln;
		}
		else cout << 1 << bb << N + 1 - mv << ln;
	}
}

4. 편지

풀이

더보기

여러 가지 관찰이 필요한 문제이다.

 

관찰 1. 깊이(정점 1부터의 거리)가 같은 정점들을 전부 합쳐도 답은 유지된다.

깊이가 같은 정점들을 모두 합치면 트리는 직선이 되므로, 이후의 풀이에서는 주어지는 트리는 직선으로 가정하겠다.

 

관찰 2. $v$번 정점에 $k$개의 편지가 있다면, $v$번 정점에 단 하나만의 편지를 남기고 그 외의 모든 편지를 $v+1$번 정점으로 주어도 답은 유지된다.

 

예를 들어, 직선에 편지가 각각 3, 0, 2, 0, 0, 0개 있었다고 할 때, 이의 답은 각 정점에 편지가 1, 1, 1, 1, 1, 0개 있었을 경우와 같다.

 

관찰 2처럼 편지를 뒤쪽 정점으로 넘겼다면, 모든 시점에 대해서 각 정점은 최대 하나 이하만의 편지를 가지고 있게 된다.(맨 뒤쪽 정점 제외) 따라서, 이 경우의 답은 (편지 개수의 합)+(편지를 하나도 들고 있지 않은 정점의 개수)이 된다. (편지를 가지고 있는 가장 마지막 정점 뒤쪽에 있는 정점들은 계산에서 제외해야 한다)


따라서, 다음과 같은 값을 저장하는 세그먼트 트리를 사용해서 풀 수 있다. $l, r$은 세그먼트 트리의 노드가 관리하는 구간이다.

$sum$ : $[l, r]$ 정점들을 앞에서부터 보면서, 하나의 편지만을 남기고 모든 편지를 뒤쪽으로 넘겼을 때 $r$번 정점에 남게 되는 편지

$mx$ : 위 과정에서 편지를 뒤쪽으로 넘길 때, 단 하나의 편지도 가져가지 못 하게 되는 정점의 수

 

초기 상태에서 하나 이상의 편지를 가진 정점 중 가장 큰 인덱스를 가진 정점을 $v$라 하면, 세그먼트 트리에 $[1, v]$ 구간 쿼리를 날려서 편지를 하나도 들고 있지 않은 정점의 개수를 구할 수 있고, 모든 편지의 갯수의 합은 쿼리마다 선형에 관리할 수 있으므로, 총 시간 복잡도 $O(QlgN)$에 풀 수 있다.

 

내 코드의 세그먼트 트리 정의는 약간 다른데, sum을 마지막 정점이 다음 구간의 정점으로 넘기게 되는 편지의 수를 저장했다. 즉, 풀이의 정의보다 하나 작은 값을 저장했다.

코드

더보기
#include <bits/stdc++.h>
#include <cassert>
#pragma GCC optimize("O3")
#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#pragma GCC target("avx,avx2,fma")
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pll;
typedef pair<int, int> pii;
#define MAX 201010
#define MAXS 20
#define INF 1000000000
#define bb ' '
#define ln '\n'
#define Ln '\n'
#define MOD 1000000007
struct node {
	ll sum;
	ll cnt;
	node(ll x = -1) :sum(max(x - 1, 0ll)), cnt(x ? 0 : 1) {}
};
node operator+(node n1, node n2) {
	node ret;
	ll mn = min(n2.cnt, n1.sum);
	n2.cnt -= mn;
	n1.sum -= mn;
	ret.cnt = n1.cnt + n2.cnt;
	ret.sum = n2.sum + n1.sum;
	return ret;
}
int N, Q;
ll S[MAX];
namespace segtree {
	node tree[MAX * 4];
	void init(int s, int e, int loc = 1) {
		if (s == e) {
			tree[loc] = node(S[s]);
			return;
		}
		int m = s + e >> 1;
		init(s, m, loc * 2);
		init(m + 1, e, loc * 2 + 1);
		tree[loc] = tree[loc * 2] + tree[loc * 2 + 1];
	}
	void upd(int s, int e, int i, int loc = 1) {
		if (e < i || i < s) return;
		if (s == e) {
			tree[loc] = node(S[i]);
			return;
		}
		int m = s + e >> 1;
		upd(s, m, i, loc * 2);
		upd(m + 1, e, i, loc * 2 + 1);
		tree[loc] = tree[loc * 2] + tree[loc * 2 + 1];
	}
	node query(int s, int e, int l, int r, int loc = 1) {
		if (e < l || r < s) return node();
		if (l <= s && e <= r) return tree[loc];
		int m = s + e >> 1;
		return query(s, m, l, r, loc * 2) + query(m + 1, e, l, r, loc * 2 + 1);
	}
}
ll sum = 0;
ll cpy[MAX];
ll A[MAX];
int dep[MAX];
signed main() {
	ios::sync_with_stdio(false), cin.tie(0);
	cin >> N >> Q;
	int i, p, a, b;
	dep[1] = 1;
	for (i = 2; i <= N; i++) {
		cin >> p;
		dep[i] = dep[p] + 1;
	}
	for (i = 1; i <= N; i++) {
		cin >> A[i];
		S[dep[i]] += A[i];
		sum += A[i];
	}
	set<int> st;
	for (i = 1; i <= N; i++) if (S[i]) st.insert(i);
	segtree::init(1, N);
	if (st.size()) cout << segtree::query(1, N, 1, *st.rbegin()).cnt + sum << ln;
	else cout << 0 << ln;
	while (Q--) {
		cin >> a >> b;
		sum += b - A[a];
		S[dep[a]] -= A[a];
		A[a] = b;
		S[dep[a]] += A[a];
		if (S[dep[a]]) st.insert(dep[a]);
		else st.erase(dep[a]);
		segtree::upd(1, N, dep[a]);
		if (st.size()) cout << segtree::query(1, N, 1, *st.rbegin()).cnt + sum << ln;
		else cout << 0 << ln;
	}
}
728x90