728x90
728x170
부분집합의 합 문제(Subset Sum Problem)
- 부분집합의 합 문제를 한 문장으로 표현하면 다음과 같음.
- 음수가 아닌 정수로 이루어진 집합
S
와 임의의 정수x
가 주어질 때,S
의 부분집합 중에서 그 원소의 합이x
와 같은 부분집합이 존재하는가?
- 음수가 아닌 정수로 이루어진 집합
부분집합의 합 문제의 예
S = { 13, 79, 45, 29 }
x = 42 → True (13 + 29)
x = 25 → False
- 집합
S
가S = { 13, 79, 45, 29 }
형태로 주어질 경우,S
로부터 다음과 같은 16개의 부분집합을 추출할 수 있음.
{ }
{ 13 }
{ 79 }
{ 45 }
{ 29 }
{ 13, 79 }
{ 13, 45 }
{ 13, 29 }
{ 79, 45 }
{ 79, 29 }
{ 45, 29 }
{ 13, 79, 45 }
{ 13, 79, 29 }
{ 13, 45, 29 }
{ 79, 45, 29 }
{ 13, 79, 45, 29 }
- 집합
S
의 원소 개수와 집합S
로부터 조합 가능한 부분집합의 개수를 함께 나열하면 다음과 같음.- 콜론(
:
) 왼쪽 : 집합S
의 원소 개수 - 콜론(
:
) 오른쪽 : 집합S
의 부분집합 개수
- 콜론(
0: 1
1: 2
2: 4
3: 8
4: 16
5: 32
6: 64
7: 128
...
- 위에 나열된 숫자를 살펴보면, 원소 개수가
n
인 집합S
의 전체 부분집합의 개수는2ⁿ
개임을 알 수 있음.n
이 증가함에 따라 부분집합의 개수가 기하급수적으로 증가함.n
이10
이하의 숫자인 경우- 모든 부분집합을 다 검사하는 방법도 그리 나쁘지 않음.
- 그러나
100
일 경우- 전체 1,267,650,600,228,229,401,496,703,205,376가지의 부분집합을 검사해야 함.
코드
- 다음과 같이 동적 계획법(Dynamic Programming) 의 총 4가지 방법을 사용하여 코드를 작성하였음.
- 전수 조사(Brute Force)
- 백트래킹(Back Tracking)
- 메모이제이션(Memoization)
- 타뷸레이션(Tabulation)
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
#include <time.h>
#include <iomanip>
using namespace std;
#define DEBUG 0
#if DEBUG
#define PRINT(x) cerr << x
#else
#define PRINT(x)
#endif
vector<string> types = {
"BRUTE FORCE",
"BACKTRACKING",
"MEMOIZATION",
"TABULATION"
};
const int UNKNOWN = INT_MAX;
void GetTime(clock_t& timer, string type) {
// 현재 시간에서 timer를 빼서 경과 시간을 계산
timer = clock() - timer;
// 화면에 경과 시간 출력
cout << type << " 방법 경과 시간: ";
cout << fixed << setprecision(5) << (float)timer / CLOCKS_PER_SEC << endl;
timer = clock(); // timer를 현재 시간으로 초기화
}
void GetAllSubsets(vector<int> set, vector<int> subset, int index, vector<vector<vector<int>>>& allSubsets) {
// 집합 set의 끝에 다다른 경우
if (index == set.size()) {
// 부분집합 크기를 인덱스로 사용하여 부분집합을 allSubsets에 추가
allSubsets[subset.size()].push_back(subset);
return;
}
// 원소를 추가하지 않고 재귀 호출
GetAllSubsets(set, subset, index + 1, allSubsets);
// 원소를 부분집합에 추가한 후 재귀 호출
subset.push_back(set[index]);
GetAllSubsets(set, subset, index + 1, allSubsets);
}
bool SubsetSum_BruteForce(vector<int> set, int target) {
vector<vector<vector<int>>> allSubsets(set.size() + 1);
GetAllSubsets(set, {}, 0, allSubsets);
for (int size = 0; size <= set.size(); size++) {
PRINT("부분집합의 원소 개수: " << size << endl);
for (auto subset : allSubsets[size]) {
PRINT("\t{ ");
int sum = 0;
for (auto number : subset) {
sum += number;
PRINT(number << " ");
}
PRINT("} = " << sum << endl);
if (sum == target) {
return true;
}
}
}
return false;
}
bool SubsetSum_Backtracking(vector<int> &set, int sum, int i) {
// 만약 현재 부분집합의 합이 target과 같다면
if (sum == 0) {
return true;
}
// 만약 현재 부분집합의 합이 target보다 크거나, 집합의 끝에 도달했다면
if (i == set.size() || set[i] > sum) {
return false;
}
// Case 1: sum에서 set[i]을 빼서 재귀 호출(i번째 원소를 부분집합에 추가)
// Case 2: sum을 그대로 전달하여 재귀 호출(i번째 원소를 부분집합에 추가하지 않음.)
return SubsetSum_Backtracking(set, sum - set[i], i + 1) || SubsetSum_Backtracking(set, sum, i + 1);
}
bool SubsetSum_Memoization(vector<int>& set, int sum, int i, vector<vector<int>>& memo) {
// 만약 현재 부분집합의 합이 target과 같다면
if (sum == 0) {
return true;
}
// 만약 현재 부분집합의 합이 target보다 크거나, 집합의 끝에 도달했다면
if (i == set.size() || set[i] > sum) {
return false;
}
// 현재 상태가 캐시에 있는지 확인
if (memo[i][sum] == UNKNOWN) {
// 현재 상태에 대한 솔루션을 구하여 캐시에 저장
bool append = SubsetSum_Memoization(set, sum - set[i], i + 1, memo);
bool ignore = SubsetSum_Memoization(set, sum, i + 1, memo);
memo[i][sum] = append | ignore;
}
// 캐시에 저장된 값을 반환
return memo[i][sum];
}
vector<vector<bool>> SubsetSum_Tabulation(vector<int>& set) {
int maxSum = 0;
for (int i = 0; i < set.size(); i++) {
maxSum += set[i];
}
vector<vector<bool>> DP(set.size() + 1, vector<bool>(maxSum + 1, 0));
for (int i = 0; i < set.size(); i++) {
DP[i][0] = true;
}
for (int i = 1; i <= set.size(); i++) {
for (int sum = 1; sum <= maxSum; sum++) {
if (sum < set[i - 1]) {
DP[i][sum] = DP[i - 1][sum];
}
else {
DP[i][sum] = DP[i - 1][sum] || DP[i - 1][sum - set[i - 1]];
}
}
}
return DP;
}
int main() {
vector<int> set = { 16, 1058, 22, 13, 46, 55, 3, 92, 47, 7, 98, 367, 807, 106, 333, 85, 577, 9, 3059 };
int target = 6076;
int tests = 4;
sort(set.begin(), set.end());
for (int i = 0; i < tests; i++) {
bool found;
clock_t timer = clock();
switch (i) {
case 0:
found = SubsetSum_BruteForce(set, target);
break;
case 1:
found = SubsetSum_Backtracking(set, target, 0);
break;
case 2: {
// 메모이제이션 테이블 초기화
vector<vector<int>> memo(set.size(), vector<int>(7000, UNKNOWN));
found = SubsetSum_Memoization(set, target, 0, memo);
break;
}
case 3: {
vector<vector<bool>> DP = SubsetSum_Tabulation(set);
found = DP[set.size()][target];
break;
}
/* 가능한 부분집합 전부 출력
case 3: {
int total = 0;
for (auto number : set) {
total += number;
}
vector<vector<bool>> DP = SubsetSum_Tabulation(set);
found = DP[set.size()][target];
vector<int> subsetSums;
for (int sum = 1; sum <= total; sum++) {
if (DP[set.size()][sum]) {
subsetSums.push_back(sum);
}
}
cout << "다음과 같이 " << subsetSums.size() << "가지의 부분집합의 합이 가능합니다: " << endl;
for (auto sum : subsetSums) {
cout << sum << " ";
}
cout << endl;
break;
}
*/
}
if (found) {
cout << "원소 합이 " << target << "인 부분집합이 있습니다." << endl;
}
else {
cout << "원소 합이 " << target << "인 부분집합이 없습니다." << endl;
}
GetTime(timer, types[i]);
cout << endl;
}
return 0;
}
결과
원소 합이 6076인 부분집합이 있습니다.
BRUTE FORCE 방법 경과 시간: 1.36600
원소 합이 6076인 부분집합이 있습니다.
BACKTRACKING 방법 경과 시간: 0.00200
원소 합이 6076인 부분집합이 있습니다.
MEMOIZATION 방법 경과 시간: 0.00500
원소 합이 6076인 부분집합이 있습니다.
TABULATION 방법 경과 시간: 0.02000
728x90
그리드형(광고전용)
'Computer Science > Algorithm' 카테고리의 다른 글
완전제곱수(Perfect Square Number, 제곱수, 정사각수) (0) | 2022.08.31 |
---|---|
팩토리얼(Factorial) (0) | 2022.08.31 |
완전수(Perfect Number), 부족수(Deficient Number), 과잉수(Abundant Number) (0) | 2022.08.31 |
가우스 계산법(Gaussian Calculation) (0) | 2022.08.31 |
플로이드-워셜 알고리즘(Floyd-Warshall Algorithm) (0) | 2021.07.22 |
코사라주 알고리즘(Kosaraju's Algorithm) (0) | 2021.07.04 |
존슨 알고리즘(Johnson's Algorithm) (0) | 2021.07.04 |
벨만-포드 알고리즘(Bellman-Ford Algorithm) (0) | 2021.07.02 |