알고리즘/문제풀이

[백준] 1208번 부분수열의 합 2 - 파이썬(Python)

SeongOnion 2021. 9. 30. 00:18
728x90

문제 (링크)

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

 

1208번: 부분수열의 합 2

첫째 줄에 정수의 개수를 나타내는 N과 정수 S가 주어진다. (1 ≤ N ≤ 40, |S| ≤ 1,000,000) 둘째 줄에 N개의 정수가 빈 칸을 사이에 두고 주어진다. 주어지는 정수의 절댓값은 100,000을 넘지 않는다.

www.acmicpc.net

 

나의 풀이

import sys
from itertools import combinations
input = sys.stdin.readline

n, s = map(int, input().split())
arr = list(map(int, input().split()))

# meet in the middle
arr_1 = arr[:n//2]
arr_2 = arr[n//2:]

subsum_arr_1 = []
subsum_arr_2 = []

for i in range(len(arr_1) + 1):
    # arr_1에서 0 ~ len(arr_1) + 1개만큼 뽑아 만들 수 있는 부분집합의 합을 구한다.
    comb_1 = combinations(arr_1, i)
    for comb in comb_1:
        subsum_arr_1.append(sum(comb))

for i in range(len(arr_2) + 1):
    # arr_2에서 0 ~ len(arr_2) + 1개만큼 뽑아 만들 수 있는 부분집합의 합을 구한다.
    comb_2 = combinations(arr_2, i)
    for comb in comb_2:
        subsum_arr_2.append(sum(comb))

subsum_arr_1.sort()
subsum_arr_2.sort()

left_pointer = 0
right_pointer = len(subsum_arr_2) - 1
ans = 0

while left_pointer < len(subsum_arr_1) and right_pointer >= 0:
    tmp = subsum_arr_1[left_pointer] + subsum_arr_2[right_pointer]

    # 두 포인터가 가르키는 값의 합이 s와 같다면
    if tmp == s:

        # 부분집합의 합이 같은 경우를 예외처리
        same_count_left = 1
        same_count_right = 1

        same_left_idx = left_pointer
        same_right_idx = right_pointer

        left_pointer += 1
        right_pointer -= 1

        while left_pointer < len(subsum_arr_1) and subsum_arr_1[left_pointer] == subsum_arr_1[same_left_idx]:
            same_count_left += 1
            left_pointer += 1
        
        while right_pointer >= 0 and subsum_arr_2[right_pointer] == subsum_arr_2[same_right_idx]:
            same_count_right += 1
            right_pointer -= 1
        
        ans += same_count_left * same_count_right
    
    elif tmp < s:
        left_pointer += 1
    
    else:
        right_pointer -= 1

# 아무것도 뽑지 않는 경우는 고려하지 않으므로, s가 0이라면 해당 경우의 수 1개를 빼준다
if s == 0:
    ans -= 1
    
print(ans)

코드 설명 및 풀이법

이전에 게시했던 부분수열의 합 문제와는 다른 버전이다.

https://seongonion.tistory.com/98

 

[백준] 1182번 부분수열의 합 - 파이썬(Python)

문제 N개의 정수로 이루어진 수열이 있을 때, 크기가 양수인 부분수열 중에서 그 수열의 원소를 다 더한 값이 S가 되는 경우의 수를 구하는 프로그램을 작성하시오. 입력 첫째 줄에 정수의 개수

seongonion.tistory.com

위의 1182번 문제는 시간제한이 2초이지만, 이번 문제는 시간제한이 1초로, 기존의 방법을 사용해서는 시간초과가 난다.

 

이번 문제를 풀기 위해선 meet in the middle 방식과 투 포인터 방식을 함께 사용해야한다.

 

meet in the middle은 큰 문제를 두 개로 나눈다는 점에서 분할정복 방식과 유사한 것처럼 보이지만, 재귀적으로 Base Case까지 나누지는 않고, 배열을 절반으로 나누어 처리해 연산 횟수를 줄인다.

 

만약 배열을 나누지 않고 그냥 진행한다면, 각 원소의 부분수열의 합을 구하는 과정에서 N이 최대값인 40일 경우 2^40번의 연산이 필요하다.

 

하지만, 이를 반으로 나누면 2^20 + 2^20번으로 줄일 수 있다.

 

위 문제에서 처리해줘야할 중요한 예외처리는 두 가지이다.

 

첫 번째로, 맨 마지막 부분에 적힌 주어진 입력값 s가 0일때는 구했던 정답에서 1을 빼줘야한다는 것이다.

 

테스트케이스에서 확인할 수 있듯, 부분수열을 뽑는데 아무것도 뽑지 않는 경우는 존재하지 않는다.

 

하지만 combinations 함수로 부분수열의 합을 구해 저장하는 과정에서는 어떠한 원소도 뽑지 않는 경우도 포함된다.

 

따라서, 해당 경우의 수 한 가지를 빼준다.

 

두 번째로, 두 부분수열의 합이 s가 되었을 때, 해당 부분수열과 중복되는 경우가 존재하는지 확인하는 것이다.

 

만약 두 개의 부분수열의 합이 [2, 3, 3, 10], [1, 4, 9] 이고, s가 7이라고 가정했을 때,

 

left_pointer가 인덱스 1을 가리키고 right_pointer가 인덱스 1을 가르킨 상황에서 두 원소를 더한 값이 7이라고 경우의 수를 1만 더해주고 포인터를 이동시키게 되면 left_pointer가 인덱스 2를 가르키고 right_pointer가 1을 가르킨 경우의 수를 세주지 못한다.

 

따라서, 해당 경우를 모두 찾아준 후 곱해주면 중복되는 경우의 예외처리도 해줄 수 있다.