---
# System prepended metadata

title: SCPC 2020 Round 1 문제 풀이 및 후기

---

# SCPC 2020 Round 1 문제 풀이 및 후기

> SCPC 삼성 프로그래밍 경진대회

![Uploading file..._agtmbqnbc]()


결과

- 다이어트: 100/100점
- 카드 게임: 150/150점
- 사다리 게임: 150/150점
- 범위 안의 숫자: 200/200점
- 우범 지역: 0/200점 

## 먼저 쓰는 느낀 점

무난하게 Round 2로 갈것같다. All solve하고 싶었지만 마지막 문제 풀이를 전혀 생각 못했다. 5번은 기하 문제였는데 기하 연습좀 해야겠다. 만점이 45명 정도인데 scpc수상하려면 5번 정도 수준의 문제를 대회시간에 풀 실력을 키워야겠다. ~~나름 할만큼 했는데 주변에 올솔이 너무 많다 ㅠ~~

디스크립션이 살짝 이상한 부분이 있었다.

---

[cgiosy](https://blog.naver.com/rla_xodbs/222081264751), [rkm0959](https://rkm0959.tistory.com/160) 님의 후기를 참고하여 작성했습니다.


## 실력 맞추기

길이 $N$인 배열 $A$와 $B$가 있다. $A$와 $B$의 순서는 마음대로 바꿀 수 있다. 이 때 $A$의 수를 단 하나 원하는 아무 수로 바꿀 수 있다. $\sum_{i=1}^{N} |A[i]-B[i]|의 최솟값을 구하시오.   

- 제한조건 
$$
1 ≤ N ≤ 200,000, 
1 ≤ K ≤ N, 1 ≤ A[i] ≤ 10^9
$$

### 풀이

- 그리디
- 정렬

1. $A$와 $B$에서 가장 작은 $K$개의 수만 사용해야 한다.
2. $A$와 $B$를 오름차순 정렬했을 때 $A[1]$은 $B[K]$와 매칭하면 답을 구할 수 있다.
    - 증명: 만약 그렇지 않다면 $A[1]$이 $B[i] (i \ne K)$ 와 매칭되고 $A[j] (j \ne 1)$ 가 $B[K]$와 매칭되는데 $A[j] + B[K] ≤ max(A[1] + B[K], A[i] + B[j])$ 이기 때문이다.
3. $A[2..K]$ 와 $B[1..K-1]$ 에 대해서도 2번을 동일하게 적용하면 된다.

시간 복잡도는 $K$개를 정렬하는데 드는 속도인 $O(NlogK)$이다.

### 여담

가장 쉬운 문제. 보자마자 풀었다. 부분 점수는 모든 경우를 다 해보면 얻을 수 있었던걸로 기억한다.

### 코드

> 실제 제출한 코드와는 다르다

```cpp=
#include <bits/stdc++.h>
using namespace std;
const int MAX = 20000;

int A[MAX], B[MAX];
void solve() {
	int N, K; scanf("%d %d", &N, &K);
	for (int i = 0; i < N; i++) 
		scanf("%d", A+i);
	for (int i = 0; i < N; i++)
		scanf("%d", B+i]);
	partial_sort(A, A+K, A+N);
	partial_sort(B, B+K, B+N);
	int ans = 0;
	for (int i = 0; i < K; i++)
		ans = max(ans, A[i]+B[K-i-1]);
	printf("%d\n", ans);
}

int main() {
	// setbuf(stdout, NULL);
	int T; scanf("%d", &T);
	for (int i = 1; i <= T; i++) {
		printf("Case #%d\n",i);
		solve();
	}
}
```

## 카드 게임

두 사람이 길이 $N$의 배열 $X$와 $Y$로 게임을 한다. 둘이 번갈아 가며 턴을 진행하며, 각 턴에선 원하는 배열 하나에서 맨 뒤의 수들을 여러 개 지울 수 있다. $X$와 $Y$의 수는 모두 $K$ 이하고 지우는 수들의 합이 $K$ 이하여야 한다. 둘은 최선의 전략으로 게임을 하며, 자신의 턴에서 남아있는 수가 없는 사람이 이긴다.

X와 Y에 남아있는 수의 개수를 $i$, $j$개라고 할 때, 해당 상태에서 선공이 이길지 후공이 이길지는 정해져 있다. 모든 가능한 상태 $(N+1)^2$ 개 중 $(i, j)$에서 선공이 이기는 상태의 수와 후공이 이기는 상태의 수를 구하시오. 단, 상태 $(0, 0)$은 선공이 이긴 상태로 본다.

- 제한 조건 
$$
1 ≤ N, K ≤ 3000\\1 ≤ A[i] ≤ K
$$

### 풀이

- DP

#### 부분 점수

1. $(i, j)$ 상태 값이 $WIN$인지 $LOSE$인지 알려면 상대방에게 $(i', j')$ 로 바뀌어 넘겨주었을 때의 값들을 이용해 알 수 있다. 
2. 만약 $(i', j') = LOSE$로 변환할 수 있다면 $(i, j) = WIN$, 없다면  $(i, j) = LOSE$ 이다.


이때 채워야할 값은 $(N+1)^2$ 이고 각 칸 마다 최대  $2N$개의 값을 확인해야하기 때문에
시간 복잡도는 $O(N^3)$이다.

#### 만점

1. 한번에 하나의 배열만 건드릴 수 있으므로 $(i', j')$ 는 $(i, j')$ 이거나, $(i', j)$이다. 이때 $i'$의 최솟값을 $a$, $j'$의 최솟값을 $b$라고 할수 있다.
2. 그렇다면 $(i, [b..j-1])$, $([a..i-1], j)$에 $LOSE$가 있는지 빠르게 판별하면 $(i, j)$의 값을 알 수 있다.
3. 연결된 구간이므로 prefix sum을 통해 $O(1)$ 만에 구할 수 있다. 

시간 복잡도는 $O(N^2)$이다.

### 여담

친구들을 보니 Grundy 게임으로 많이 접근을 해서 시간초과가 난것 같다.

### 코드

> 실제 제출한 코드와는 다르다

```cpp=
#include <bits/stdc++.h>
using namespace std;
int X[3001], Y[3001], N, K;
int xl[3001], yl[3001];
int xpsum[3001][3001], ypsum[3001][3001];

int getx(int i, int j) {
	if (i < 0) return 0;
	return xpsum[i][j];
}
int gety(int i, int j) {
	if (j < 0) return 0;
	return ypsum[i][j];
}

int go(int i, int j) {
	if (i == 0 && j == 0) return 1;
	if (gety(i, j-1) - gety(i, yl[j]-1) != j-yl[j]) 
		return 1;
	if (getx(i-1, j) - getx(xl[i]-1, j) != i-xl[i]) 
		return 1;
	return 0;
}

void solve() {
	int A = 0, B = 0;
	scanf("%d %d", &N, &K);
	int lb = 1, sum = 0;
	for (int i = 1; i <= N; i++) {
		scanf("%d", &X[i]);
		sum += X[i];
		while (sum > K) {
			sum -= X[lb];
			lb++;
		}
		xl[i] = lb - 1;
	}
	lb = 1, sum = 0;
	for (int i = 1; i <= N; i++) {
		scanf("%d", &Y[i]);
		sum += Y[i];
		while (sum > K) {
			sum -= Y[lb];
			lb++;
		}
		yl[i] = lb - 1;
	}

	for (int i = 0; i <= N; i++) {
		for (int j = 0; j <= N; j++) {
			int ret = go(i, j);
			xpsum[i][j] = ypsum[i][j] = ret;
			if (ret == 1) A++;
			else B++;
			if (i) xpsum[i][j] += xpsum[i-1][j];
			if (j) ypsum[i][j] += ypsum[i][j-1];
		}
	}
	printf("%d %d\n", A, B);
}

int main() {
	// setbuf(stdout, NULL);
	int T; scanf("%d", & T);
	for (int i = 1; i <= T; i++) {
		printf("Case #%d\n",i);
		solve();
	}
}
```

## 사다리 게임

$N$개의 세로선이 있고 $M$개의 가로선이 있다. 가로선은 $(a, b)$ 순서쌍으로 이루어지며, 세로선 $a$와 세로선 $b$를 양방향으로 연결한다. $i$번째 세로선에서 모든 가로선을 타며 내려가면 $j$번째 세로선에 도착하게 되는데, 가로선을 최소한으로 지워 원하는 세로선으로 도착하도록 만드려고 한다.

즉, 사다리가 주어지면 $Q$개의 쿼리에 대해 $i$번째 세로선에서 시작해 $j$번째 세로선에 도착하도록 만들 때 최소한으로 지워야 하는 가로선의 개수의 합을 출력해야 한다. 만약 $i$에서 $j$로 갈 수 없다면 $-1$을 더한다. 

- 제한 조건
$$
2 ≤ N ≤ 1500\\1 ≤ M ≤ 2000\\1 ≤ Q ≤ 10^5\\1 ≤ i < j ≤ N
$$

### 풀이

- DP
- 최단 경로
- 0-1 BFS

여러가지 풀이가 있는 것으로 알고 있다. 나는 DP로 풀었다.

1. $i$ 에서 $j$로 가기 위한 최소 제거 횟수를 $dp[i][j]$라 하자. dp 테이블의 초기값은 $dp[i][i] = 0$이고 $dp[i][j]=\infty (j \ne i)$
2. $(u, v)$ 간선을 만나면 값을 $1$ 올리고 그대로 있거나, 값을 그대로 하고 바꾸는 선택이 가능하다. 최솟값을 계속 저장한다. 두 값을 동시 업데이트하는거에 주의를 해야한다.
$$ 
dp[i][u] = min(dp[i][u]+1, dp[i][v])\\dp[i][v] = min(dp[i][v]+1, dp[i][u])
$$

시간 복잡도는 $O(N^2 + NM + Q)$ 이다.

### 여담

$-1$을 더하라는건 정말 :thinking_face: 이다.

## 코드 

> 실제 제출한 코드와는 다르다

구현에서는 업데이트가 필요한 것만 관리하기 위해서 큐를 사용했는데 그냥 $2N$개를 봐도 무방하다. 

```cpp=
#include <bits/stdc++.h>
using namespace std;
const int INF = 987654321;

int N, K;
queue<int> q[1500];
int ans[1500][1500];
void solve() {
	int m;
	scanf("%d %d %d", &N, &K, &m);
	memset(ans, -1, sizeof(ans));
	for (int i = 0; i < N; i++) {
		q[i] = queue<int>();
		q[i].push(i);
		ans[i][i] = 0;
	}
	while (K--) {
		int a = scanf("%d", &a);
		int b = scanf("%d", &b);
		--a; --b;
		vector<int> A(N, INF), B(N, INF);
		while (!q[a].empty()) {
			int now = q[a].front(); q[a].pop();
			A[now] = ans[now][a] + 1;
			B[now] = ans[now][a];
		}
		while (!q[b].empty()) {
			int now = q[b].front(); q[b].pop();
			B[now] = min(B[now], ans[now][b] + 1);
			A[now] = min(A[now], ans[now][b]);
		}
		for (int i = 0; i < N; i++) {
			if (A[i] != INF) {
				q[a].push(i);
				ans[i][a] = A[i];
			}
			if (B[i] != INF) {
				q[b].push(i);
				ans[i][b] = B[i];
			}
		}
	}

	int ret = 0;
	while (m--) {
		int x, y; scanf("%d %d",&x, &y);
		ret += ans[x-1][y-1];
	}
	printf("%d\n", ret);
}

int main() {
	// setbuf(stdout, NULL);
	int T; scanf("%d", &T);
	for (int i = 1; i <= T; i++) {
		printf("Case #%d\n",i);
		solve();
	}
}
```

## 범위 안의 숫자

숫자로 이루어진 길이 $N$의 문자열 $S$가 주어진다. $S$에서 임의의 숫자를 골라 1로 바꾸고, 연속한 $K$개의 숫자로 이루어진 수를 모두 나열했을 때 적절한 수 $a$를 골라 $[a, a + M]$에 있는 수의 개수를 최대화하여라.

- 제한 조건
$$
1 ≤ N ≤ 50000\\1 ≤ K ≤ min(9, N)
$$

### 풀이

- 세그먼트 트리
- 좌표 압축 

1. $K$값이 작은 것에 주목한다. 총 나올 수 있는 길이 $K$의 수는 $N(K+1)$개 보다 작다. 이를 모두 구해 $V$에 저장한다.
2. $V$를 정렬하고 좌표 압축을 한다.
3. $V$에 있는 각 원소 $a$에 대해서 $[a, a+M]$에 포함되는 $V$의 최대 인덱스 번호를 저장한다. 
4. range update를 지원하는 maximum segment tree를 만든 다음, 아무것도 바꾸지 않은 $S$에 대해서 $K$개의 연속된 숫자로 이루어진 수를 모두 업데이트를 한다. 이 때 업데이트를 할때 그 수의 인덱스 번호부터 3에서 구한 최대 인덱스 번호까지 구간 업데이트를 한다. 이를 통해 각 node의 말단에는 그 수를 마지막으로 하는 구간에 해당하는 개수를 알 수 있다.
5. $S$의 $i$번째 숫자를 $1$로 바꾸었을때 바귀는 값을 빼고 넣고하는 과정을 반복하며 최대값을 갱신한다. 

시간 복잡도는 $O(NKlog(NK))$이다.

제한시간이 10초지만 상수가 크고 테스트케이스가 있기 때문에 최적화를 조금 해야 할 수 있다.

### 여담

구현을 정말 애먹었다.무려 7번 틀렸다. 인덱스 실수 하고 시간초과 나고, 대회 때는 나만 그런줄 알았는데 끝나고 보니 다들 그래서 조금 안심이다.

세그먼트트리를 안쓰고도 풀 수 있다는데 나중에 다시 봐야겠다.

### 코드

공개하기 정말 부끄러운 코드인데(`real_solve` 등) 다시 짜기는 또 귀찮다.

```cpp=
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MAX = 50'000;

ll ten[10]={1};
char s[MAX+5];
int n, k, m, sz;
int ub[MAX*10+5];
int t[MAX*41], lazy[MAX*41];
vector<int> base, a[MAX+5], b[MAX+5];
vector<ll> nums;
unordered_map<int,int> hsh;

void push(int v) {
	t[v*2] += lazy[v];
	lazy[v*2] += lazy[v];
	t[v*2+1] += lazy[v];
	lazy[v*2+1] += lazy[v];
	lazy[v] = 0;
}

void update(int v, int tl, int tr, int l, int r, int addend) {
    if (l > r) 
        return;
    if (l == tl && tr == r) {
        t[v] += addend;
        lazy[v] += addend;
    } else {
        push(v);
        int tm = (tl + tr) / 2;
        update(v*2, tl, tm, l, min(r, tm), addend);
        update(v*2+1, tm+1, tr, max(l, tm+1), r, addend);
        t[v] = max(t[v*2], t[v*2+1]);
    }
}

void update(int l, int r, int addend) {
	update(1, 0, sz-1, l, r, addend);
}

int query(int v, int tl, int tr, int l, int r) {
    if (l > r)
        return 0;
    if (l <= tl && tr <= r)
        return t[v];
    push(v);
    int tm = (tl + tr) / 2;
    return max(query(v*2, tl, tm, l, min(r, tm)), 
               query(v*2+1, tm+1, tr, max(l, tm+1), r));
}

int query() {
	return query(1, 0, sz-1, 0, sz-1);
}

void init() {
	base.clear();
	nums.clear();
	hsh = unordered_map<int,int>();
	memset(t, 0, sizeof(t));
	memset(lazy, 0, sizeof(lazy));

}

void get_number() {
	ll val = 0;
	for (int i = 0; i < k - 1; i++) {
		val = val * 10 + s[i] - '0';
	}
	for (int i = k - 1; i < n; i++) {
		val = val * 10 + s[i] - '0';
		val %= ten[k];
		nums.push_back(val);
		base.push_back(val);
		int x = val;
		for (int j = 0; j < k; j++) {
			int digit = (val % ten[j+1]) / ten[j];
			if (digit == 1) continue;
			b[i-j].push_back(val); 
			val -= (digit - 1) * ten[j];
			nums.push_back(val);
			a[i-j].push_back(val);
			val += (digit - 1) * ten[j];
		}
	}

	sort(begin(nums), end(nums));
	nums.erase(unique(begin(nums), end(nums)), end(nums));

	sz = nums.size();
	for (int i = 0; i < sz; i++) {
		hsh[nums[i]] = i;
	}
	int idx = 0;
	for (int i = 0; i < sz; i++) {
		while (nums[i] - nums[idx] > m) {
			ub[idx] = i - 1;
			idx++;
		}
	}
	for (; idx < sz; idx++) {
		ub[idx] = sz - 1;
	}
}

void real_solve() {
	for (int i : base) {
		int idx = hsh[i];
		update(idx, ub[idx], 1);
	}
	int ans = query();
	for (int i = 0; i < n; i++) {
		for (int j : a[i]) {
			int idx = hsh[j];
			update(idx, ub[idx], 1);
		}
		for (int j : b[i]) {
			int idx = hsh[j];
			update(idx, ub[idx], -1);
		}
		ans = max(ans, query());
		for (int j : a[i]) {
			int idx = hsh[j];
			update(idx, ub[idx], -1);
		}
		for (int j : b[i]) {
			int idx = hsh[j];
			update(idx, ub[idx], 1);
		}
		a[i].clear();
		b[i].clear();
	}
	printf("%d\n", ans);
}

void solve() {
	scanf("%d %d %d %s", &n, &k, &m, s);
	init();
	get_number();
	real_solve();
}

int main() {
	// setbuf(stdout, NULL);
	for (int i = 1; i <= 10; i++) 
		ten[i] = 10 * ten[i-1];
	int T; scanf("%d", &T);
	for (int i = 1; i <= T; i++) {
		printf("Case #%d\n",i);
		solve();
	}
}
```

## 우범 지역

N개의 점 $(x_i, y_i)$들이 주어지는데, 각 점에는 선택될 확률 $p_i$이 있다. 선택된 점들의 컨벡스 헐에 점 $q$가 포함될 확률을 구하시오. 단, $q$와 임의의 서로 다른 두 점을 골랐을 때 직선 상에 놓여있지 않는다.

- 제한 조건
$$
1 ≤ N ≤ 100000\\1 ≤ x_i, y_i ≤ 10^9\\0 < p_i ≤ 1
$$

### 풀이

> 풀이를 알려주신 @Miren0923님께 감사드립니다. 

- 기하
- Sliding window

0. 컨벡스 헐에 점이 있는 경우를 생각하는 것보다 컨벡스헐에 점이 없는 경우를 생각하는 것이 더 쉽다. 
1.  "점 $q$가 컨벡스헐에 들어가지 않는다."와 "점 $q$와 나머지 선택된 점을 가르는 직선이 있다"가 동치
2. 점 $q$를 중심으로 나머지 점을 (각도를 기준으로) 원주에 올리면, 점이 하나도 없는 180도 길이의 구간이 있는 확률이 답
3. 선택된 점이 없는 경우의 확률 + 선택된 점이 있는 경우의 확률을 더하면 된다.
    - 점이 없는 경우: 모든 점마다 선택되지 않을 경우 확률 곱
    - 점이 있는 경우: 모든 점마다 (자신이 선택될 확률) * (180도 이후에 점이 선택되지 않을 확률의 곱)
4. (180도 이후에 점이 선택되지 않을 확률의 곱)을 구하려면 sliding window를 통해서 구할 수 있다.
    - 여기서 실수 오차가 나는걸 방지 하기 위해서(나눗셈 없이) sparse table을 사용하면 된다.
    - 그밖에도 세그먼트트리를 사용하거나 로그값을 이용해서 실수오차를 방지할 수 있다.

확률 자체는 빠르게 구하면 $O(N)$만에 구할 수도 있지만 결국 정렬을 해야해서 전체 시간 복잡도는 $O(NlogN)$이다. 

### 여담

못 풀었다. 전혀 접근 자체를 하지 못했고 확률 문제다 보니 [몬테 카를로](https://ko.wikipedia.org/wiki/%EB%AA%AC%ED%85%8C%EC%B9%B4%EB%A5%BC%EB%A1%9C_%EB%B0%A9%EB%B2%95)로 비벼라도 볼까 했으나 여의치 않았다. 0번을 생각하면 2번까지 생각하고, 점이 있는 경우 처리하면서 재밌게 고민햇을텐데 0번을 생각하지 못했다.

브루트 포스로 $O(NlogN * 2^N)$ 에 짜는것 밖에 생각이 안났다. 저 2^N을 줄이기 위해서 생각을 많이 했다. 들로네 삼각분할을 쓰고 전처리? meet-in-the-middle? 스위핑? 전혀 아니었다. 

### 코드

> [Juno](https://boj.kr/Juno)의 코드를 변형했다.

```C++=
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ld = long double;

const int MAX = 200000;
typedef long double ld;
struct point {
	ll x,y;ld p;
	point(ll x, ll y,ld p=0):x(x),y(y),p(p){}
	ll operator/(point b)const{return x*b.y-y*b.x;}
	inline int sgn()const{return y<0||(y==0&&x<0);}
};

int ix[MAX],iy[MAX];
ld ip[MAX];
void solve() {
	int n; scanf("%d", &n);
	for (int i=0; i<n; i++) scanf("%d", ix+i);
	for (int i=0; i<n; i++) scanf("%d", iy+i);
	for (int i=0; i<n; i++) scanf("%Lf", ip+i);
	int cx, cy;scanf("%d %d", &cx, &cy);

	point c(cx,cy);
	vector<point> v;
	for (int i=0; i<n; i++) {
		v.push_back({ix[i]-cx,iy[i]-cy,ip[i]});
	}
	sort(begin(v), end(v),[](const point& a,const point& b) {
		if(a.sgn()!=b.sgn()) return a.sgn()<b.sgn();
		return a/b>0;
	});
	ld ans=0,cur=0;
	int j=0,zero=0;
	for (int i=0; i<n; i++) {
		if(v[i].p<1)cur+=log10l(1-v[i].p);
		else zero++;
	}
	if(!zero)ans+=powl(10,cur);
	cur=0;zero=0;
	auto nxt = [&](int i){return (i+1)%n;};
	for (int i=0; i<n; i++) {
		while(v[i]/v[nxt(j)]>0) {
			j=nxt(j);
			if(v[j].p<1)cur+=log10l(1-v[j].p);
			else zero++;
		}
		if(!zero)ans+=powl(10,cur+log10l(v[i].p));
		if(i==j)j=nxt(j);
		else {
			if(v[nxt(i)].p<1)cur-=log10l(1-v[nxt(i)].p);
			else zero--;
		}
	}
	printf("%.10Lf\n", 1.L-ans);
}

int main() {
	// setbuf(stdout, NULL);
	int T; scanf("%d", &T);
	for (int i = 1; i <= T; i++) {
		printf("Case #%d\n",i);
		solve();
	}
}
```