Eat Study Love

먹고 공부하고 사랑하라

Coding_Practice

Target Sum(Array,Dynamic Programming,Backtracking)

eatplaylove 2024. 12. 26. 12:38

https://leetcode.com/problems/target-sum/description/?envType=daily-question&envId=2024-12-26

You are given an integer array nums and an integer target.

You want to build an expression out of nums by adding one of the symbols '+' and '-' before each integer in nums and then concatenate all the integers.

  • For example, if nums = [2, 1], you can add a '+' before 2 and a '-' before 1 and concatenate them to build the expression "+2-1".

Return the number of different expressions that you can build, which evaluates to target.

 

Example 1:

Input: nums = [1,1,1,1,1], target = 3
Output: 5
Explanation: There are 5 ways to assign symbols to make the sum of nums be target 3.
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

Example 2:

Input: nums = [1], target = 1
Output: 1

 

Constraints:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

싫어하지만, 피하기 힘든 DP category 문제이다.

 

한 번 부딛쳐보자.

 

DP문제 특징이, 이해는 쉬운데 이게 문제 해결 implementation이 어렵다 -_-

 

그래도 코테까지 내 인생 마지막 코딩문제(?) 공부란 생각으로 함 해보자..

 

아오!!

 

1. Python

 

실패했다. DP 카테고리를 봤음에도, 어떻게 Memoization을 써야하는 지 모르겠다.

아니 정확히 말하면 머릿속으로는 알겠는데 이걸 어떻게 구현하는 지 모르겠다 ㅠㅠ

 

위 Example 기준으로 머리를 굴려봐도 대충 특정 계산방법이 반복되는 구조라는 건 쉽게 알 수 있는데 이걸 코딩으로 구현하는게 넘 힘들다.

 

진짜 Dynamic Programming 손쉽게 푸는 사람들 Respect.. + 신기하다

 

일단 나는 개떡같이 시도하다가 코딩완성에 실패했는데,

 

내가 하려던 것은 Memoization이 없는 Recursive Solution이다. GPT도움을 받아 코드를 작성해봤다.

 

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        
        def recursive(index:int, curr_sum:int) -> int:
            if index == len(nums):
                if curr_sum == target : return 1
                else : return 0
            add_case = recursive(index+1, curr_sum + nums[index])
            sub_case = recursive(index+1, curr_sum - nums[index])
            return add_case + sub_case
        
        return recursive(0,0)

 

GPT 도움을 받아서 코드를 썼음에도, 좀 긴 Test case에 대해선 time-limit이 걸린다..

 

C++로 코드를 짜면 성능 꼴등... 으로 PASS되긴 한다...

class Solution {
public:
    int recur(int idx, int sum, vector<int>& nums, int target){
        if(idx==nums.size()){
            if(sum==target) return 1;
            else return 0;
        }
        int add = recur(idx+1, sum + nums[idx], nums, target);
        int sub = recur(idx+1, sum - nums[idx], nums, target);
        
        return add+sub;
    }

    int findTargetSumWays(vector<int>& nums, int target) {
        return recur(0,0,nums,target);
    }
};

 

Python + Memoization을 쓴 case는 아래와 같다.

 

# Python with Memoization
class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        # 메모이제이션을 위한 캐시
        memo = {}

        def dfs(index: int, current_target: int) -> int:
            # 이미 계산한 상태라면 캐시에서 결과 반환
            if (index, current_target) in memo:
                return memo[(index, current_target)]

            # 기저 조건: 모든 숫자를 사용한 경우
            if index == len(nums):
                return 1 if current_target == 0 else 0

            # 현재 숫자를 + 또는 -로 사용한 결과를 재귀 호출
            positive = dfs(index + 1, current_target - nums[index])
            negative = dfs(index + 1, current_target + nums[index])

            # 메모이제이션 저장
            memo[(index, current_target)] = positive + negative
            return memo[(index, current_target)] # 이 Return 부분이 햇갈릴 수 있는데 걍 return pos + neg 이다

        # 탐색 시작
        return dfs(0, target)

 

배열 기반 DP 코드는 아래와 같다.

사실 Time complexity 기준 성능은 DP나 Memoization이나 중복계산을 방지하는 건 똑같아서 비슷하다고 보면 된다.

 

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total_sum = sum(nums)
        if abs(target) > total_sum:  # target이 만들 수 있는 범위를 벗어난 경우
            return 0
        
        # DP 테이블 초기화
        dp = [0] * (2 * total_sum + 1)
        offset = total_sum  # 음수 인덱스를 처리하기 위해 offset 추가
        dp[nums[0] + offset] += 1  # 첫 번째 숫자를 +로 사용하는 경우
        dp[-nums[0] + offset] += 1  # 첫 번째 숫자를 -로 사용하는 경우
        
        # 숫자 배열 순회
        for i in range(1, len(nums)):
            next_dp = [0] * (2 * total_sum + 1)
            for current_sum in range(-total_sum, total_sum + 1):
                if dp[current_sum + offset] > 0:
                    # 현재 숫자를 +로 사용하는 경우
                    next_dp[current_sum + nums[i] + offset] += dp[current_sum + offset]
                    # 현재 숫자를 -로 사용하는 경우
                    next_dp[current_sum - nums[i] + offset] += dp[current_sum + offset]
            dp = next_dp  # 업데이트된 dp 테이블로 이동
        
        # 결과 반환
        return dp[target + offset]

 

2. C

C로도 풀어봅시다

 

솔직히 Memoization 없는 건 가볍고 할만한데,

 

있는건 코드가 넘 빡세다

 

# C로도 똑같이 Recursive
int recursive(int idx, int curr_sum, int* lst, int lstsize, int target){
    if(idx==lstsize){
        return curr_sum == target ? 1 : 0;
    }
    int add = recursive(idx+1,curr_sum + lst[idx],lst,lstsize,target);
    int sub = recursive(idx+1,curr_sum - lst[idx],lst,lstsize,target);

    return add+sub;
}

int findTargetSumWays(int* nums, int numsSize, int target) {
    return recursive(0,0,nums,numsSize,target);
}
# C Memoization
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// DP 캐시 초기화 (-1로 초기화)
int memo[21][2001];

int dfs(int nums[], int n, int index, int current_sum, int target) {
    // Base case: 모든 숫자를 사용한 경우
    if (index == n) {
        return current_sum == target ? 1 : 0;
    }

    // 메모이제이션 캐시 확인
    if (memo[index][current_sum + 1000] != -1) {
        return memo[index][current_sum + 1000];
    }

    // 재귀 호출: +와 - 두 가지 경우
    int add = dfs(nums, n, index + 1, current_sum + nums[index], target);
    int subtract = dfs(nums, n, index + 1, current_sum - nums[index], target);

    // 결과 저장
    memo[index][current_sum + 1000] = add + subtract;
    return memo[index][current_sum + 1000];
}

int findTargetSumWays(int* nums, int numsSize, int target) {
    // 메모이제이션 배열 초기화 (-1로 설정)
    memset(memo, -1, sizeof(memo));

    // DFS 호출
    return dfs(nums, numsSize, 0, 0, target);
}

int main() {
    int nums[] = {1, 1, 1, 1, 1};
    int target = 3;
    int numsSize = sizeof(nums) / sizeof(nums[0]);

    printf("Number of ways: %d\n", findTargetSumWays(nums, numsSize, target));
    return 0;
}

 

3. C++

 

Memoization 사용해보기

 

근데 Python처럼 마냥 쉬운게 아니다.. 뭐지 이거

# C++
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;

// unordered_map의 key를 위한 hash function
struct pair_hash {
    template <class T1, class T2>
    size_t operator()(const pair<T1, T2>& p) const {
        return hash<T1>()(p.first) ^ hash<T2>()(p.second);
    }
};

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        unordered_map<pair<int, int>, int, pair_hash> memo; // pair를 key로 사용

        // 재귀 함수 구현
        function<int(int, int)> dfs = [&](int index, int current_target) -> int {
            // 메모이제이션 확인
            pair<int, int> key = {index, current_target};
            if (memo.find(key) != memo.end()) {
                return memo[key];
            }

            // 기저 조건: 모든 숫자를 사용한 경우
            if (index == nums.size()) {
                return current_target == 0 ? 1 : 0;
            }

            // 현재 숫자를 + 또는 -로 사용한 결과 계산
            int positive = dfs(index + 1, current_target - nums[index]);
            int negative = dfs(index + 1, current_target + nums[index]);

            // 결과를 메모이제이션에 저장
            memo[key] = positive + negative;
            return memo[key];
        };

        // 탐색 시작
        return dfs(0, target);
    }
};

 

OFFSET을 이용해서는 아래와 같이 Memoization을 이용한다

 

class Solution {
public:
    int solve(vector<int>& nums, int target, int ind, int  sum, vector<vector<int>>& dp, int offset){

        if(ind == nums.size())
            return sum == target ? 1 : 0;

        if(dp[ind][sum+offset] != -1) return dp[ind][sum+offset];

        int inc = solve(nums,target,ind+1,sum+nums[ind],dp,offset);
        int exc = solve(nums,target,ind+1,sum-nums[ind],dp,offset);

        dp[ind][sum+offset] = inc+exc;
        return inc+exc;

    }

    int findTargetSumWays(vector<int>& nums, int target) {
        int n = nums.size();
        int sum = accumulate(nums.begin(),nums.end(),0);
        int offset = sum; // 젤 큰놈으로 offset 해놓기구나

        vector<vector<int>> dp (n,vector<int>(2*sum+1,-1));
        return solve(nums,target,0,0,dp,offset);
    }
};

offset을 sum으로 정한 이유

  1. 인덱스 변환:
    • 배열 nums의 원소를 모두 더한 최대 값 sum을 offset으로 설정하면, [-sum, sum] 범위의 모든 값을 양수 인덱스로 변환할 수 있습니다.
    • 변환된 범위:
  2. DP 배열 크기:
    • DP 배열의 열 크기는 [-sum, sum] 범위를 포함해야 하므로, 크기가 2*sum + 1이어야 합니다.
    • 각 sum 값을 sum + offset으로 변환하여 DP 배열의 유효한 인덱스로 사용할 수 있습니다.

 

이 개념을 잘 이해했는지 확인하려면 그냥 이걸 C로 코딩해보면 된다.

 

백지에서 C로 이 문제를 코딩해보자..

 

고단해도 걸어가야할 길이다!

 

최대한.. solution을 안 보려고 했는데, C에서 int ** dp 초기화 하는 과정에서 좀 참고했다

 

ㅠㅠ 따흑 졌다...

int helper(int idx, int sum, int* nums,int numsSize, int target, int** dp, int offset){
    if(idx==numsSize){
        return sum == target ? 1 : 0;
    }
    if(dp[idx][sum+offset] != -1) return dp[idx][sum+offset];

    int plus = helper(idx+1,sum + nums[idx],nums,numsSize,target,dp,offset);
    int minus = helper(idx+1,sum - nums[idx],nums,numsSize,target,dp,offset);

    dp[idx][sum+offset] = plus+minus;
    return plus+minus;
}

int findTargetSumWays(int* nums, int numsSize, int target) {
    int offset = 0;
    for(int i=0;i<numsSize;i++){
        offset += nums[i];
    }
    
    int** dp = (int**)malloc(numsSize*sizeof(int*));
    for(int j=0;j<numsSize;j++){
        dp[j] = (int *)malloc((offset*2+1)*sizeof(int));
        for(int k=0;k<offset*2+1;k++){
            dp[j][k] = -1;
        }
    }
//    return helper(0,0,nums,numsSize,target,dp,offset);
	int ans =helper(0,0,nums,numsSize,target,dp,offset);
    for(int i=0;i<numsSize;i++){
    	free(dp[i]);
        }
    free(dp);
    
    return ans;
}