문제 링크: https://leetcode.com/problems/sum-of-distances-in-tree/

 

Sum of Distances in Tree - LeetCode

Sum of Distances in Tree - There is an undirected connected tree with n nodes labeled from 0 to n - 1 and n - 1 edges. You are given the integer n and the array edges where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the

leetcode.com

 

문제

문제를 요약하자면 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

 

영문 솔루션

https://leetcode.com/problems/sum-of-distances-in-tree/discuss/2939374/Python-Super-Simple-O(N)-DP-Solution 

Codejam 문제 풀이

sub Title: (1) Round1

구글 code jam은 일반적으로 난이도 순으로 나오기때문에 순서대로 풀면된다.

순위

2000등 밖으로 밀려나서, 1A 라운드 통과는 못했다.

거의 4년만에 제대로된 대회에 참가하는것 같은데 그래서 그런지 정말 머리가 안돌아간다.

1B는 통과할만한 것같은데, 간만에 문제를 풀어서 그런가 문제 푸는속도도 코드도 영 맘에들지 않는다.

무엇보다 아이디어 떠오르는 속도가 정말 너무 많은 trial-and-error을 필요로한다.

Append Sort

문제는 아래에서 자세히 읽을 수 있다.

 

 

Code Jam - Google’s Coding Competitions

Put your coding skills to the test as you work your way through multiple rounds of algorithmic coding puzzles for the title of Code Jam Champ and 15,000 USD.

codingcompetitions.withgoogle.com

문제 설명

정수가 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

 

문제는 아래에서 자세히 읽을 수 있다.

 

Code Jam - Google’s Coding Competitions

Put your coding skills to the test as you work your way through multiple rounds of algorithmic coding puzzles for the title of Code Jam Champ and 15,000 USD.

codingcompetitions.withgoogle.com

소수가 주어진다. (중복된 소수들도 주어진다.)

소수를 두 그룹으로 만들 것인데, 한 그룹은 덧셈으로, 한 그룹은 곱셈으로 모든 연산을 수행할것이다. 이 때 합을 한 그룹과 곱을 한 그룹의 계산 값이 같아야 한다.

예를 들자면

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를 씌워준것일 뿐이다.

이건 대회중에 떠올리지 못한 아이디어다. 답안지보고 아 수학아니었는데... 하고 너무 아쉬워서 딱 저 아이디어 추가하고 메모리문제좀개선해서 제출했떠니 바로 답이 뜨더라.

아쉽다아쉬워...

Hacked Exam

문제 해석을 잘못하고있다가 10분전에 해석을 완료했다;

테스트셋 1번은 그냥 긁을만했는데 긁었으면 비벼볼만했을텐데 아쉽다.

마무리

아쉽네. 다음번엔 더잘해야징. 1B에는 다음라운드갈수있지않을까.

H. Polynomial BOJ 11500

 

 문제 설명 :

 

  어떤 함수 f(k)를 아래와 같이 정의한다. 

 

 

 이 때 우리는 f에 관한 새로운 식 S(n)을 정의할 건데 그건 또 아래와 같다.

 

이럴 때 S(n)을 n에 관하여 정리해보자면 아래와 같이 정리된다.

 

 

이 때 다음을 구해서 출력하시오.

 

주어지는 것은 다음이다. 

 

 

문제 풀이 :

 

 사실 이런 종류의 수식을 처음 접하는 사람은 이게 대체 뭔 개소린가 싶은 느낌이 들 것이다. 정수론과 굉장히 밀접한 식인데 처음 읽었을 때 도무지 분석을 할 수가 없었다.

 

 S(n)을 f(k)로 풀어보자면 다음과 같다.

일단, 

이식을 보면 이걸 n에 관하여 정리해야 하는데, 정수론에 대해 기초 지식이 있는 사람들은 (사실 고딩때도 배운다) 거듭제곱의 합이 n에 관한 식으로 나온다는 사실을 알 수 있다. 

 

 

예를들면,

 

요런 것들? 

 

따라서 거듭제곱이 같은 것끼리 모아보자.

위 식을 정리하면 아래와 같이 깔끔하게 나오게 된다.

 

 

이제 필요한게 0<=t<=25 사이에 있는 모든 거듭제곱의 합의 계수들만 찾으면 답이다.

 

근데이게 쉽지가 않다. 이 걸 찾기 위해서는 가장 마지막에 주어진 식이 필요하다.

 

 

1부터 n까지 k제곱의 합을 앞으로 T(n,k)라 했을 때 다음과같다.

^증명보시려면 클릭하세요.

 

위 식을 정리하고나면 0~k-1의 거듭제곱합을 통해서 k거듭제곱합을 구할 수 있다.

따라서 0,1,2,3,4...25까지 미리 거듭제곱의 합을 구해놓고 처리하면 끝

 

소스코드 보기 <- 클릭

문제집 : https://www.acmicpc.net/category/detail/1413

풀이는 하나하나 업데이트 될 예정입니다.

간략한 문제 분류와 솔루션만 제시 해놓고 풀이가 따로 필요하다고 생각되는 문제들은 추후 링크 하도록 하겠습니다. (요청하시면 그 부분에 관한 풀이도 올려드리겠습니다. ) 


A. Coin Swap  BOJ 11493


 flow문제 중에서도 MCMF문제다.  "동전의 교환 = 코스트" 로 두고 흰색을 모두 흰색으로 옮길때의 최소 코스트로 그래프 설계를 하면 된다.(검은색을 검은색으로 옮기는 코스트로 생각해도 무방) 흰색 정점이 있는 곳을 src와 연결하고 흰색 동전이 있는 곳을 sink로 돌렸을 때 나오는 MCMF결과를 출력하면 된다.


C. Grid BOJ 11495


 flow 문제 중에서도 이분 그래프로 만든 후 최대매칭하는 문제다. 익히 알려진 사실로 최대 매칭은 flow와 동치다.
 각각의 그리드를 순서대로 1, 2, 3,4... 번호를 매긴 후 인접한 것 끼리 연결한다. 이때 짝수는 src에 홀수는 sink에 연결하며 각각의 캐퍼시티는 그 그리드의 값으로 하고 매칭을 찾으면 된다.
 이 때 결과를 찾는 방법은 두가지 있는데 src와 sink에 남아 있는 캐퍼시티를 추적하는 방법과 전체에서 매칭의 수를 빼는 방법이 있다.(1매칭은 1코스트를 감소시켜주므로) 


D. Kernel  BOJ 11496


 기하 문제다.(segment tree로도 풀린다고 한다.) kernel이 존재할 수 있는 구간을 결정하고 그 위치가 주어진 polygon사이에 존재하는 지 결정하는 문제다. 열심히 그려보다보면 어떤 방식으로 kernel이 존재할 수 있는 구간을 결정하는 지 알 수 있다.(솔직히 이런걸 대회중에 발견하기는 어려워 보인다..)


E. Log Jumping BOJ 11497


 그리디 문제다. 통나무들을 길이 순으로 정렬하고 원의 왼쪽 오른쪽 차례대로 배치해 나가면 최적이 된다. 


F. Odd Cycle BOJ 11498


 그래프 문제 중에서도 SCC를 사용하여 시간을 줄이는 문제다. 사실 진짜 별거 없고 coloring하는데 그 coloring을 SCC내에서 하면 되는 문제다. 근데 한가지 조심해야할게 그냥 돌리면 WA가 날 수 있는데 사이클이 여러개 존재할 수 있다. 따라서 coloring을 back edge만 체크하게 해야한다. 이때 잘 처리 해줘야 TLE가 안난다. 


G. Path BOJ 11499


 기하 문제 중에서도 Convex_hull을 사용하는 문제다. 사실 그냥 컨백스 홀 만드는 방식으로 그냥 쭉 훑으면서 만들면서 최대 값을 갱신만 시켜주면된다.


H. Polynomial BOJ 11500 솔루션


 그냥 수식을 정리하는 문제다. 수학적 센스가 필요한 것 같다. (이미 알고 있다면 개꿀이었을 듯...) 수식을 정리해서 그대로 대입하면 된다. (하루종일 정리하면 된다.) 거듭 제곱의 합에 관하여 정리해야 한다.


I. Stock BOJ 11501


 파라매트릭 서치 문제다. 특정 상한을 결정하고 만들 수 있는지 검사한다. 이 때 검사는 선형으로 할 수 있어서 O(n) 그리고 파라매트릭에 logn이 들어 총 O(nlogn)이 든다. 


J. 3-Primes Problem BOJ 11502

 

 추측이라는데 그런가 보다 하면 된다. 그냥 하라는데로 하면 되는데 1000밖에 안된다. 따라서 소수 a,b를 결정하면 자동으로c가 결정된다. 여러가지 방법이 있는데, 전수 검사를 해서 (a,b,결정하고 c가 소수인지 확인) 푸는 방법, 넵섹하는 법, 등등 있다.


K. Tree Edit BOJ 11503


트리 dp문제다. 아직 안풀었다.


 L. Wheel of Numbers BOJ 11504


시뮬레이션 문제다. 직접 다해보면 된다. 




원 출처 : 모름



문제 링크 : 

https://www.acmicpc.net/problem/1514


문제 설명 : 

 숫자로 이루어진 문자열 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 자물쇠에 대한 풀이를 원하시는 분들이 많다고 들었다)


소스코드는... 더럽다. 알아서 읽어보세여 ^ㅇ^ (심지어 변수명도 다름)


소스보기


+ Recent posts