문제를 요약하자면 tree가 주어질 때 모든 정점으로부터의 거리가 최소가 되는 정점에서의 모든 정점으로까지의 길이의 합을 출력하는 문제가 되겠습니다.
직관
tree기 때문에 Cycle이 없습니다. 따라서 한 edge를 제거했을 때, 그래프로 나눠집니다.
A와 B를 잇는 edge가 있다고 가정해봅시다. 그리고, A쪽 그래프에있는 모든 노드로부터 A까지의 거리를 알 수 있다고 가정해봅시다.
그럼 B쪽 노드의 관점에서 봤을 때는 우리는 모든 A쪽 그래프의 노드로부터 B까지의 거리의 총합을 다음 수식으로 구할 수 있습니다.
$$TotalDist_{A side} + NodeCnt_{A side}$$
왜냐하면 A쪽 노드로부터 B쪽 노드로 오는 모든 길은 edge A->B를 지날 수 밖이 없기 때문이죠.
접근법
그러므로 우리는 DP를 계산할 수 있습니다.
노드 N과 S가 있을 때, edge (N,S)에 의해서 N쪽 graph와 S쪽 graph 로 나뉘게됩니다.
그리고 $cnt[n][s] = $ $n$ 쪽 그래프에 있는 노드의 수 $dist[n][s] =$ N쪽 그래프에서 S로 가는 총 거리의 합
라고 정의할 때 우리는 이 두 배열을 다음과 같은 방식으로 업데이트할 수 있습니다.
$G[n]$ 을 노드 N과 이어진 모든 이웃 노드들을 포함하고 있을 때
$$ dp[n][s] = sum_{i \in G[n], i \neq s}(dp[n][i] + cnt[n][i])$$ $$ cnt[n][s] = sum_{i \in G[n], i \neq s}(cnt[n][i])$$
로 계산되게 됩니다.
복잡도
이 풀이는 시간복잡도와 공간복잡도가 오직 edge의 개수의 영향받으며, edge는 총 N-1개 존재합니다.
시간 복잡도: $O(N)$
공간 복잡도: $O(N)$
코드
class Solution:
def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]:
self.G = [[] for i in range(n)]
for e in edges:
self.G[e[0]].append(e[1])
self.G[e[1]].append(e[0])
ans = []
for i in range(n):
ans.append(self.dp(i, -1)[0])
return ans
@cache
def dp(self, pnode, source):
dist = 0
cnt = 0
for n in self.G[pnode]:
if n == source:
continue
n_dist, n_cnt = self.dp(n, pnode)
dist += n_dist + n_cnt
cnt += n_cnt
cnt += 1
return dist, cnt
1B는 통과할만한 것같은데, 간만에 문제를 풀어서 그런가 문제 푸는속도도 코드도 영 맘에들지 않는다.
무엇보다 아이디어 떠오르는 속도가 정말 너무 많은 trial-and-error을 필요로한다.
Append Sort
문제는 아래에서 자세히 읽을 수 있다.
문제 설명
정수가 N개가 주어진다.
정수 N개를 정렬하고 싶은데, 정수끼리의 순서를 바꿀수 없다.
우리가수행할 수 있는 연산은 각 정수에다가 숫자들을 더하는 건데 예를 들면 123이 있다면 여기다가 4를 더해서 1234로만드는 이런연산이가능하다.
123 → 1234
이런 연산을 최소한으로 수행해서 strictly increasing order로 만들어라.
sample Input
4 3 100 7 10 2 10 10 3 4 19 1 3 1 2 3
Sample Output
Case #1: 4 Case #2: 1 Case #3: 2 Case #4: 0
설명:
첫번째는 아래처럼 추가하면 100, 700, 1000 으로 오름차순으로 만들수 있다.
100
7 → 700
10 → 1000
총 0을 4번 추가했고, 이거보다 적게 추가해서 오름차순으로 만들 수 없기 때문에 답은 4다
솔루션 코드
for tc in range(int(input())):
N = int(input())
X = list(map(int, input().split(" ")))
ans = 0
for i in range(1, N):
if X[i] > X[i-1]:
continue
s_b = list(str(X[i-1]))
s_p_max = list(str(X[i]))
s_p_min = list(str(X[i]))
count = 0
ori_len = len(s_p_max)
while int("".join(s_b)) >= int("".join(s_p_max)):
s_p_max += "9"
s_p_min += "0"
count += 1
ans += count
if int("".join(s_b)) < int("".join(s_p_min)):
X[i] = int("".join(s_p_min))
else:
# same
X[i] = X[i-1]+1
print(f"Case #{tc+1}: {ans}")
그지처럼 풀었다.
첫번째 원소는 건드리지 않아도 되니까 range(1, N) 으로 시작한다. 시작부터 크다면 신경쓸필요없으니까 그리고 뒤를 9로 일단 채워나가면서(그 자리수에서 체울 수 있는 가장 큰 수) 될때까지 자리수를 늘려나간다.
그리고 늘려나간 최종 값중에서 가장 작은 값 (예를들자면, 10에서 2개의 digit을 추가했다면, 1099이 최대값, 1000이 최솟값이 된다.)이 배열의 이전 값 (s_b, X[i-1]) 보다 크다면 그게 답이 되기 때문에 X[i]에 그 값 자체를 넣어준다. int("".join(s_p_min))
만약 최솟값이 같다면, X[i-1]의 값에 +1해주면 된다. 이부분은 +1해줌으로서 다른자리까지 전부 변화시켜주려면 9여야하는데, 모두 9로 되어있는 경우는 14번 째 줄의
while int("".join(s_b)) >= int("".join(s_p_max))
에서 컷팅당한다. 즉 +1만해줘도된다.
이해가 안가시면 질문주세요. 대충 설명드립니다.
Prime Time
문제는 아래에서 자세히 읽을 수 있다.
소수가 주어진다. (중복된 소수들도 주어진다.)
소수를 두 그룹으로 만들 것인데, 한 그룹은 덧셈으로, 한 그룹은 곱셈으로 모든 연산을 수행할것이다. 이 때 합을 한 그룹과 곱을 한 그룹의 계산 값이 같아야 한다.
예를 들자면
2 - 2개
3 - 1개
5 - 2개
7 - 1개
11 - 1개
가 주어졌을 때,
2+ 2 + 3 + 7 + 11 == 5 * 5
가 되고 두 그룹의 합은 25로 같다. 이렇게 만들 수 있는 가장 큰 값을 구하라.
중요한 인풋 설명
소수는 499 이하의 것들로만 주어진다.
소수는 서로다른 소수 95개 까지 주어질 수있으며, 각 소수는 여러개 존재할 수 있다.
Sample Input
4 5 2 2 3 1 5 2 7 1 11 1 1 17 2 2 2 2 3 1 1 2 7
Sample Output
Case #1: 25 Case #2: 17 Case #3: 0 Case #4: 8
설명:
첫번째 input은 2가 2개, 3이 1개, 5가 2개, 7이 1개 11이 1개다.
이 때, 더하기 그룹은 2, 2, 3, 7, 11을 선택하고, 나머지 곱하기 그룹은 5 5를 선택하면 최대값 25가 뽑인다. 그래서 답은 25
Test Set 1 솔루션 소스코드
$2≤N_1+N_2+⋯+N_M≤10$. (소수의 총 개수가 10개 이하)
from functools import reduce
for tc in range(int(input())):
N = int(input())
P = []
for i in range(N):
p_i, n_i = map(int, input().split())
P += [p_i for j in range(n_i)]
ans = 0
for i in range(1 << len(P)):
arr = list(enumerate([1 if i & (1 << j) else 0 for j in range(len(P))]))
arr_mul = reduce(lambda x, y: x * y, [P[i] if v == 0 else 1 for i, v in arr])
arr_sum = sum([P[i] if v == 1 else 0 for i, v in arr])
if arr_mul == arr_sum:
ans = max(ans, arr_mul)
print(f"Case #{tc+1}: {ans}")
소수의 총 개수가 10개 이하다.
잘모를 땐 다해보자. 모든 소수를 분해한다. 0번그룹 1번그룹
그래서 두개의 곱과 합을 구한 후 두개가 같다면 ans로 출력한다.
Test Set 2 솔루션 소스코드
$2≤N_1+N_2+⋯+N_M≤100$. (소수의 총 개수가 100개 이하)
from functools import reduce
for tc in range(int(input())):
N = int(input())
P = []
UP = {}
for i in range(N):
p_i, n_i = map(int, input().split())
UP[p_i] = n_i
P += [p_i for j in range(n_i)]
all_sum = sum(P)
ans = 0
unique_p = UP.keys()
for t in range(all_sum, 0, -1):
is_fail = False
may_ans = t
tot = 0
for p in unique_p:
p_count = UP[p]
while t % p == 0:
t //= p
p_count -= 1
tot += p
if p_count < 0:
is_fail = True
if is_fail:
break
if is_fail:
break
if t != 1:
is_fail = True
if not is_fail and all_sum - tot == may_ans:
ans = may_ans
break
print(f"Case #{tc+1}: {ans}")
소스코드가 개판이다.
자 이제 어떻게 줄일 수 있을 지 생각해보자. 100개니까 모두를 해보는 건 불가능하다. ($2^{100}$ 은 안봐도 TLE)
합과 곱중 하나를 고정시켜야하는데, 생각해보면 모든 수의 합은 매우 작다. 그러니까 곱하는걸 선택할 때 많은 수를 곱할 수 없다.
100개를 다합쳐보면 최대 얼마가나올까?
$499 * 100 = 49900$ 밖에 안나온다.
그럼 모든 소수를 다더하고, 1씩 빼가면서 그 수를 만들 수 있는지 확인한다. 만약 만드는 것을 실패하면, 불가능하다는 것이고, 만드는 것을 성공하면 만드는데 든 소수의 합을 뺀 것이 지금의 수인지 체크하면 된다. 그 코드는 아래와 같다.
if not is_fail and all_sum - tot == may_ans:
ans = may_ans
break
시간복잡도는 대충 49900 * len(unique_p) * log (49900) 정도 될껀데, 뒤에껀 대충 빼고, 계산해도 49,90,000 정도다. 매우적으니까 당연히 통과다. 문제는 테스트 셋 3인데...
Test Set 3 솔루션 소스코드
$2≤N_1+N_2+⋯+N_M≤10^{15}$. (소수의 총 개수가 $10^{15}$개 이하)
이거 보고 솔직히 아 당연히 수학문제겠지 하고 제꼈다. 구글 수학문제좋아하니까 그럴꺼라고 생각했다.
그런데 멍청했다.
from functools import reduce
for tc in range(int(input())):
N = int(input())
P = []
UP = {}
all_sum = 0
for i in range(N):
p_i, n_i = map(int, input().split())
UP[p_i] = n_i
all_sum += p_i * n_i
ans = 0
unique_p = UP.keys()
for t in range(all_sum, max(0, all_sum-8982), -1):
is_fail = False
may_ans = t
tot = 0
for p in unique_p:
p_count = UP[p]
while t % p == 0:
t //= p
p_count -= 1
tot += p
if p_count < 0:
is_fail = True
if is_fail:
break
if is_fail:
break
if t != 1:
is_fail = True
if not is_fail and all_sum - tot == may_ans:
ans = may_ans
break
print(f"Case #{tc+1}: {ans}")
거의 메인 로직은 똑같고 중요하게 추가된게 아래 이거 하나다.
for t in range(all_sum, max(0, all_sum-8982), -1):
생각해보면, 곱셈으로 아무리 빼고싶어도 소수를 빼는데 한계가 있다. 곱셈이 너무 커지니까. 그러니까 곱셈측에도 컷팅같은 느낌을 줄 수 있다.
잘 생각해보면 499를 $10^{15}$ 더해봤자 $499*10^{15}$ 이다. 아니 엄청 큰 숫자아니냐고? 곱셈의 세계에서는 매우 작은 숫자다.
그럼 소수 최대 몇개를 뺄 수 있을까. 를 계산해보면
$$\text{log}_2(499*10^{15})=17.xx$$
이긴한데, 사실 계산하기 귀찮으니까 아무리그래도 $499^{18}$ 보다는 작을거라고 생각할 수 있다. 그러니까 아무리 빼고싶어도 499를 18번보다 많이 뺄수없으며, 어떤 소수든 18개이상 빼면 전체합을 넘어가게된다.
즉 뺄수있는 것의 범위가 $499*18=8982$보다는 작게된다는 것이다.
그래서 정말 뺄수있는최소값을 range(all_sum-8982, all_sum) 로 잡으면 되는데, all_sum-8982 가 0보다 작을수있으니까 max를 씌워준것일 뿐이다.
이건 대회중에 떠올리지 못한 아이디어다. 답안지보고 아 수학아니었는데... 하고 너무 아쉬워서 딱 저 아이디어 추가하고 메모리문제좀개선해서 제출했떠니 바로 답이 뜨더라.
flow문제 중에서도 MCMF문제다. "동전의 교환 = 코스트" 로 두고 흰색을 모두 흰색으로 옮길때의 최소 코스트로 그래프 설계를 하면 된다.(검은색을 검은색으로 옮기는 코스트로 생각해도 무방) 흰색 정점이 있는 곳을 src와 연결하고 흰색 동전이 있는 곳을 sink로 돌렸을 때 나오는 MCMF결과를 출력하면 된다.
flow 문제 중에서도 이분 그래프로 만든 후 최대매칭하는 문제다. 익히 알려진 사실로 최대 매칭은 flow와 동치다. 각각의 그리드를 순서대로 1, 2, 3,4... 번호를 매긴 후 인접한 것 끼리 연결한다. 이때 짝수는 src에 홀수는 sink에 연결하며 각각의 캐퍼시티는 그 그리드의 값으로 하고 매칭을 찾으면 된다. 이 때 결과를 찾는 방법은 두가지 있는데 src와 sink에 남아 있는 캐퍼시티를 추적하는 방법과 전체에서 매칭의 수를 빼는 방법이 있다.(1매칭은 1코스트를 감소시켜주므로)
기하 문제다.(segment tree로도 풀린다고 한다.) kernel이 존재할 수 있는 구간을 결정하고 그 위치가 주어진 polygon사이에 존재하는 지 결정하는 문제다. 열심히 그려보다보면 어떤 방식으로 kernel이 존재할 수 있는 구간을 결정하는 지 알 수 있다.(솔직히 이런걸 대회중에 발견하기는 어려워 보인다..)
그래프 문제 중에서도 SCC를 사용하여 시간을 줄이는 문제다. 사실 진짜 별거 없고 coloring하는데 그 coloring을 SCC내에서 하면 되는 문제다. 근데 한가지 조심해야할게 그냥 돌리면 WA가 날 수 있는데 사이클이 여러개 존재할 수 있다. 따라서 coloring을 back edge만 체크하게 해야한다. 이때 잘 처리 해줘야 TLE가 안난다.
숫자로 이루어진 문자열 2개가 주어지는데 첫 번째 문자열을 두번 째 문자열로 바꾸는데 드는 최소 코스트를 구하는 문제.
문자열에 가할 수 있는 연산은 최소 1개에서 최대 3개를 선택해서 그 값을 한꺼번에 최대 1이상 3이하 증가/감소 시킬 수 있다. 물론 숫자는 0~9까지만 사용하며 0밑으로 내려가면 9로, 9 이상으로 올라가면 0으로 다시 돌아간다. 즉 숫자를 회전시키는 것이라고 볼 수 있다.
dp 상태 정의 :
dy[n][t1][t2] :: 첫번째 부터 n번째까지 자물쇠를 돌렸을 때 n번째 숫자가 t2고 n-1번째 숫자가 t1이고 1~n-2까지의 숫자는 목표 다이얼을 완성한 상태
t1과 t2는 0~9까지 값을 가진다.
전처리가 좀 필요한데 다른 분들은 수식만으로 구하신 것 같지만 나는 멍청함으로 수식따위 구하지 않고 직접 bfs를 사용해서 구했다.
가장 기초적인 전처리 방법은
pre[s1][s2][s3][t1][t2][t3] :: 세 다이얼이 s1, s2, s3일 때 t1, t2, t3로 변환하기 위한 최소 코스트
근데 이걸 전처리하기 너무 귀찮다. 일단 더럽게 배열이 6차원이다.
이 전처리를 줄일 수 있는데 s1, s2, s3, t1, t2, t3 정보가 모두 필요하지 않다는 데 핵심 아이디어가 있다. 실제로 필요한 정보는 (t1 -s1), (t2 - s2), (t3- s3) 정보다. 따라서 전처리 해야하는 내용을
pre[s1][s2][s3] 원래 다이얼로 부터 차이 값이 s1, s2, s3일 때 변환하기 위한 최소 코스트
로 정의 할 수 있다. 물론 구현의 편의상 (-1 == 9) (-2 == 8) ... 이다. 따라서 s1, s2, s3 는 모두 0에서부터 9사이의 값을 가진다.
dp 점화식 :
여기서
n2 == 원래 다이얼의 n번째 숫자
t0 == 목표 다이얼의 n-2번째 숫자
cost((s0,s1,s2) ->(t0,t1,t2)) 는 pre[t0-s0][t1-s1][t2-s2]이다.
디피 테이블 하나를 체우는데 10^2이 들고 모든 테이블을 체우기 위해서는 n*10^2이 드므로 시간복잡도는 n* 10^4
상당히 많이 틀렸던 문제.
이문제를 처음 틀리고 나는 완벽한데 틀렸다며 부들부들했던 문제다.
실수할 여지가 좀 많다.
완전 알고리즘을 못하던 시절에 손댔다가 실패했던 문젠데 생각이 나서 풀고 해설을 써본다.(실제로 BOJ 자물쇠에 대한 풀이를 원하시는 분들이 많다고 들었다)