# 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();
}
}
```