반응형
반응형
해당 글에서는 알고리즘 중 ‘완전 탐색’에 대해서 다양한 문제를 통해서 이해를 돕기 위해 작성한 글입니다.
💡 [참고] 완전 탐색(Exhaustive Search)의 이론에 대해 궁금하시면 하단의 글을 참고하시면 크게 도움이 됩니다.
1) 완전 탐색(Exhaustive Search)
💡 완전 탐색(Exhaustive Search)이란?
- 알고리즘에서 사용되는 기법 중 하나로 ‘모든 가능한 경우의 수를 탐색’하여 ‘최적의 결과를 찾는 방법’을 의미합니다.
알고리즘 종류 | 설명 |
부르트 포스 | ‘모든 경우의 수를 탐색’하면서 원하는 결과를 얻는 알고리즘을 의미합니다. ex) 좌물쇠를 푸는 방법과 같이 모든 경우를 대입하면서 비밀번호를 찾아가는 방법이 있습니다. |
비트마스크 | ‘모든 경우의 수’를 이진수료 표현하고 ‘비트 연산’을 통해 원하는 결과를 빠르게 얻는 알고리즘을 의미합니다. |
백트래킹 | 결과를 얻기 위해 진행하는 도중에 ‘막히게 되면’ 그 지점으로 다시 돌아가서 ‘다른 경로를 탐색’하는 방식을 의미합니다. 결국 모든 가능한 경우의 수를 탐색하여 해결책을 찾습니다. |
순열 탐색 | ‘순열’을 이용하여 모든 경우의 수를 탐색하는 방법입니다. 순열은 서로 다른 n개 중에서 r개를 선택하여 나열하는 방법을 의미합니다 |
재귀함수 | 자기 자신을 호출하여 모든 가능한 경우의 수를 체크하면서 최적의 해답을 얻는 방식을 의미합니다. |
DFS/BFS | 1. 깊이 우선 탐색(DFS: Depth-First Search) - 루트 노드에서 시작하여 다음 분기로 넘어가기 전에 해당 분기를 완벽하게 탐색하는 방법을 의미합니다. 2. 너비 우선 탐색(Breadth-First Search: BFS) - 루트 노드에서 시작하여 인접한 노드를 먼저 탐색하는 방법을 의미합니다. |
1. 문제 - 브루트 포스 : 최소 직사각형
💡 문제 설명
- 명함 지갑을 만드는 회사에서 지갑의 크기를 정하려고 합니다. 다양한 모양과 크기의 명함들을 모두 수납할 수 있으면서, 작아서 들고 다니기 편한 지갑을 만들어야 합니다. 이러한 요건을 만족하는 지갑을 만들기 위해 디자인팀은 모든 명함의 가로길이와 세로 길이를 조사했습니다. 아래 표는 4가지 명함의 가로길이와 세로 길이를 나타냅니다.
<하단의 표 1을 참고합니다>
- 가장 긴 가로 길이와 세로 길이가 각각 80, 70이기 때문에 80(가로) x 70(세로) 크기의 지갑을 만들면 모든 명함들을 수납할 수 있습니다. 하지만 2번 명함을 가로로 눕혀 수납한다면 80(가로) x 50(세로) 크기의 지갑으로 모든 명함들을 수납할 수 있습니다. 이때의 지갑 크기는 4000(=80 x 50)입니다.
- 모든 명함의 가로 길이와 세로 길이를 나타내는 2차원 배열 sizes가 매개변수로 주어집니다. 모든 명함을 수납할 수 있는 가장 작은 지갑을 만들 때, 지갑의 크기를 return 하도록 solution 함수를 완성해 주세요.
💡 제한사항
- sizes의 길이는 1 이상 10,000 이하입니다.
- sizes의 원소는 [w, h] 형식입니다.
- w는 명함의 가로 길이를 나타냅니다.
- h는 명함의 세로 길이를 나타냅니다.
- w와 h는 1 이상 1,000 이하인 자연수입니다.
💡 출처
- https://school.programmers.co.kr/learn/courses/30/lessons/86491
[문제 설명] - 표 1
명함 번호 | 가로 길이 | 세로 길이 |
1 | 60 | 50 |
2 | 30 | 70 |
3 | 60 | 30 |
4 | 80 | 40 |
💡 입출력 예
sizes | result |
[[60, 50], [30, 70], [60, 30], [80, 40]] | 4000 |
[[10, 7], [12, 3], [8, 15], [14, 7], [5, 15]] | 120 |
[[14, 4], [19, 6], [6, 16], [18, 7], [7, 11]] | 133 |
💡 해당 문제의 요점은 ‘브루트 포스’ 알고리즘을 통해 ‘카드의 사이즈를 구하기 위해서 모든 경우의 수를 다 확인’하여서 결과를 찾는다는 점입니다.
[STEP1] 2차원 배열을 1차원 배열로 분해하여 순회합니다(* 카드의 모든 경우의 수를 순회합니다)
[STEP2] 카드의 가로와 세로의 길이 중 ‘최대값’을 구하고 이전 카드의 너비와 비교하여 ‘최대값’을 구합니다.
[STEP3] 카드의 가로와 세로의 길이 중 ‘최소값’을 구하고 이전 카드의 높이와 비교하여 ‘최대값’을 구합니다.
[STEP4] 최종적으로 계산된 너비와 높이값을 구하여 최종 카드 사이즈를 구한다.
/**
* [완전탐색-1 : 브루트 포스] 13. 최소 직사각형
* 조건에 만족하는 모든 경우를 탐색하여 원하는 결과를 얻는 브루트 포스를 이용한 문제
*
* @return ResponseEntity
* @link ...
* @since 2023.07.23
*/
@GetMapping("/exhausSearch1")
public ResponseEntity<apiresponse> question13() {
int answer = 0;
int[][] size = {{60, 50}, {30, 70}, {60, 30}, {80, 40}};
int width = 0; // 총 계산된 카드의 너비
int height = 0; // 총 계싼된 카드의 높이
// [STEP1] 2차원 배열을 1차원 배열로 순회합니다. (* 카드의 모든 경우를 순회합니다.)
for (int[] card : size) {
int cardItemWidth = card[0]; // n번 명함의 가로 길이
int cardItemHeight = card[1]; // n번 명함의 세로 길이
// [STEP2] 카드의 기로와 세로의 길이 중 최대 값과 이전 카드 사이즈를 비교하여 최종 너비값을 구합니다.
width = Math.max(width, Math.max(cardItemWidth, cardItemHeight));
// [STEP3] 카드의 가로와 세로의 길이 총 최소 값과 이전에 카드 사이즈를 비교하여 최종 높이값을 구합니다.
height = Math.max(height, Math.min(cardItemWidth, cardItemHeight));
}
// [STEP4] 최종적으로 계산된 너비와 높이 값을 곱하여 결과값을 구합니다.
answer = width * height;
ApiResponse ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
</apiresponse
2. 문제 - 부르트 포스 : 모의고사
💡 문제 설명
- 수포자는 수학을 포기한 사람의 준말입니다. 수포자 삼인방은 모의고사에 수학 문제를 전부 찍으려 합니다. 수포자는 1번 문제부터 마지막 문제까지 다음과 같이 찍습니다.
1번 수포자가 찍는 방식: 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...
2번 수포자가 찍는 방식: 2, 1, 2, 3, 2, 4, 2, 5, 2, 1, 2, 3, 2, 4, 2, 5, ...
3번 수포자가 찍는 방식: 3, 3, 1, 1, 2, 2, 4, 4, 5, 5, 3, 3, 1, 1, 2, 2, 4, 4, 5, 5, ...
1번 문제부터 마지막 문제까지의 정답이 순서대로 들은 배열 answers가 주어졌을 때, 가장 많은 문제를 맞힌 사람이 누구인지 배열에 담아 return 하도록 solution 함수를 작성해주세요.
💡 제한 조건
- 시험은 최대 10,000 문제로 구성되어 있습니다. 문제의 정답은 1, 2, 3, 4, 5중 하나입니다. 가장 높은 점수를 받은 사람이 여럿일 경우, return 하는 값을 오름차순 정렬해 주세요.
💡 출처
- https://school.programmers.co.kr/learn/courses/30/lessons/42840?language=java
💡 입출력의 예
answers | return |
[1,2,3,4,5] | [1] |
[1,3,2,4,2] | [1,2,3] |
💡 해당 문제의 요점은 ‘브루트 포스’ 알고리즘을 통해 ‘수포자의 패턴’을 통해 결과를 얻기 위해 모든 경우의 수를 다 확인’하여서 결과를 찾는다는 점입니다.
[STEP1] 수포자들의 문제를 찍는 패턴 정의합니다.
[STEP2] 문제 배열을 순회하면서 각각의 값을 Counting 합니다. (* 모든 경우의 수를 순회합니다)
[STEP3] 구성한 리스트의 최대값을 구합니다.
[STEP4] 최대값인 사용자를 구합니다.
[STEP5] 최종 배열로 구성합니다.(List to Array[])
/**
* 14. [완전탐색-2 : 브루트 포스] 14. 모의고사
*
* @return ResponseEntity
* @link ...
* @since 2023.07.23
*/
@GetMapping("/exhausSearch2")
public ResponseEntity<apiresponse> question14() {
// int[] answers = new int[]{1, 3, 2, 4, 2};
int[] answers = new int[]{1, 2, 3, 4, 5};
List personScoreList = Arrays.asList(0, 0, 0);
// [STEP1] 수포자들의 문제를 찍는 패턴 정의
int[] person1Pattern = new int[]{1, 2, 3, 4, 5};
int[] person2Pattern = new int[]{2, 1, 2, 3, 2, 4, 2, 5};
int[] person3Pattern = new int[]{3, 3, 1, 1, 2, 2, 4, 4, 5, 5};
// [STEP2] 문제 배열을 순회하면서 각각의 값을 Counting 합니다. (* 모든 경우의 수를 순회합니다)
for (int i = 0; i < answers.length; i++) {
if (answers[i] == person1Pattern[i % 5]) personScoreList.set(0, personScoreList.get(0) + 1);
if (answers[i] == person2Pattern[i % 8]) personScoreList.set(1, personScoreList.get(1) + 1);
if (answers[i] == person3Pattern[i % 10]) personScoreList.set(2, personScoreList.get(2) + 1);
}
// [STEP3] 구성한 리스트의 최대값을 구합니다.
int max = Collections.max(personScoreList);
// [STEP4] 최대값인 사용자를 구합니다.
List maxPersonList = new ArrayList<>();
for (int i = 0; i < personScoreList.size(); i++) {
if (max == personScoreList.get(i)) maxPersonList.add(i + 1);
}
// [STEP5] 최종 배열로 구성합니다.(List to Array[])
int[] answer = maxPersonList.stream().mapToInt(i -> i).toArray();
ApiResponse ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
</apiresponse
3. 문제 - 비트 마스크 : [1차] 비밀지도
💡 문제 설명
- 네오는 평소 프로도가 비상금을 숨겨놓는 장소를 알려줄 비밀지도를 손에 넣었다. 그런데 이 비밀지도는 숫자로 암호화되어 있어 위치를 확인하기 위해서는 암호를 해독해야 한다. 다행히 지도 암호를 해독할 방법을 적어놓은 메모도 함께 발견했다.
지도는 한 변의 길이가 n인 정사각형 배열 형태로, 각 칸은 "공백"(" ") 또는 "벽"("#") 두 종류로 이루어져 있다. 전체 지도는 두 장의 지도를 겹쳐서 얻을 수 있다. 각각 "지도 1"과 "지도 2"라고 하자. 지도 1 또는 지도 2 중 어느 하나라도 벽인 부분은 전체 지도에서도 벽이다. 지도 1과 지도 2에서 모두 공백인 부분은 전체 지도에서도 공백이다."지도 1"과 "지도 2"는 각각 정수 배열로 암호화되어 있다. 암호화된 배열은 지도의 각 가로줄에서 벽 부분을 1, 공백 부분을 0으로 부호화했을 때 얻어지는 이진수에 해당하는 값의 배열이다.
<하단의 사진 확인>
네오가 프로도의 비상금을 손에 넣을 수 있도록, 비밀지도의 암호를 해독하는 작업을 도와줄 프로그램을 작성하라.
[문제 설명] 하단의 사진
💡 입력 형식
- 입력으로 지도의 한 변 크기 n과 2개의 정수 배열 arr1, arr2가 들어온다.
1. 1 ≦ n ≦ 16
2. arr1, arr2는 길이 n인 정수 배열로 주어진다.
3. 정수 배열의 각 원소 x를 이진수로 변환했을 때의 길이는 n 이하이다. 즉, 0 ≦ x ≦ 2n - 1을 만족한다.
💡 출력 형식
- 원래의 비밀지도를 해독하여 '#', 공백으로 구성된 문자열 배열로 출력하라.
💡 입출력 예제
<하단의 표 참고>
매개변수 | 값 |
n | 5 |
arr1 | [9, 20, 28, 18, 11] |
arr2 | [30, 1, 21, 17, 28] |
출력 | ["#####","# # #", "### #", "# ##", "#####"] |
매개변수 | 값 |
n | 6 |
arr1 | [46, 33, 33 ,22, 31, 50] |
arr2 | [27 ,56, 19, 14, 14, 10] |
출력 | ["######", "### #", "## ##", " #### ", " #####", "### # "] |
💡 해당 문제의 요점은 '비트 마스크' 알고리즘을 통해 숫자 이진수로 변환 후 연산을 통해 최종 결과값을 찾는다는 점입니다.
[STEP1] 결과 값을 넣을 배열의 사이즈를 구성합니다.
[STEP2] arr1와 arr2의 비트 연산자로 'OR 연산자' 계산을 하고 이진수로 변환한 값을 결과 배열에 넣어둡니다.(비트마스크 - 이진수로 변환하여 연산합니다)
[STEP3] 구성한 이진수 배열을 순회하며 0인경우 " " 공백으로 변환하고 1인 경우 "#"으로 변환합니다.
[STEP4] 결과값을 도출합니다.
/**
* 15. [완전탐색-3 : 비트마스크] [1차] 비밀지도
*
* @return ResponseEntity
* @link ...
* @since 2023.07.23
*/
@GetMapping("/exhausSearch3")
public ResponseEntity<apiresponse> question15() {
int n = 5;
int[] arr1 = new int[]{9, 20, 28, 18, 11};
int[] arr2 = new int[]{30, 1, 21, 17, 28};
// [STEP1] 결과값을 넣을 배열의 사이즈를 구성합니다.
String[] answer = new String[n];
// [STEP2] arr1와 arr2의 비트 연산자로 'OR 연산자' 계산을 하고 이진수로 변환한 값을 결과 배열에 넣어둡니다.(비트마스크 - 이진수로 변환하여 연산합니다)
for (int i = 0; i < arr1.length; i++) {
answer[i] = Integer.toBinaryString(arr1[i] | arr2[i]);
System.out.println(answer[i]);
}
// [STEP3] 구성한 이진수 배열을 순회하며 0인경우 " " 공백으로 변환하고 1인 경우 "#"으로 변환합니다.
for (int i = 0; i < answer.length; i++) {
answer[i] = String.format("%" + n + "s", answer[i]); // n 길이에 맞게 문자열을 맞춥니다.
answer[i] = answer[i].replace("1", "#"); // "1"의 값인 경우 "#"로 치환합니다.
answer[i] = answer[i].replace("0", " "); // "0"의 값인 경우 " "로 치환합니다.
}
ApiResponse ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
</apiresponse
💡 [참고] 비트 마스크 알고리즘을 이용하기 위한 비트 연산에 대해 궁금하시면 아래의 링크를 참고하시면 도움이 됩니다
4. 문제 - 브루트 포스 : 카펫
💡 문제 설명
- Leo는 카펫을 사러 갔다가 아래 그림과 같이 중앙에는 노란색으로 칠해져 있고 테두리 1줄은 갈색으로 칠해져 있는 격자 모양 카펫을 봤습니다.
<아래의 사진 참고>
Leo는 집으로 돌아와서 아까 본 카펫의 노란색과 갈색으로 색칠된 격자의 개수는 기억했지만, 전체 카펫의 크기는 기억하지 못했습니다.
Leo가 본 카펫에서 갈색 격자의 수 brown, 노란색 격자의 수 yellow가 매개변수로 주어질 때 카펫의 가로, 세로 크기를 순서대로 배열에 담아 return 하도록 solution 함수를 작성해주세요.
💡 제한사항
- 갈색 격자의 수 brown은 8 이상 5,000 이하인 자연수입니다.
노란색 격자의 수 yellow는 1 이상 2,000,000 이하인 자연수입니다.카펫의 가로 길이는 세로 길이와 같거나, 세로 길이보다 깁니다.
💡 출처
- https://school.programmers.co.kr/learn/courses/30/lessons/42842
💡 입출력 예
brown | yellow | return |
10 | 2 | [4, 3] |
8 | 1 | [3, 3] |
24 | 24 | [8, 6] |
💡 해당 문제의 요점은 ‘브루트 포스’ 알고리즘을 통해 역순으로 배열을 순회하면서 모든 경우의 수를 다 확인하여 결과를 찾는다는 점입니다.
[STEP1] 갈색 격자와 노란 격자를 합하여 전체의 개수를 구합니다.
[STEP2] 그 합의 제곱근부터 1이 될 때까지 반복 수행합니다. (브루트 포스)
[STEP3] 반복 수행을 하면서 그 중 합의 약수인 경우를 찾습니다.
[STEP4] 약수 중 합에서 가로의 길이를 나누어서 세로(j)의 길이를 계산합니다.
[STEP5] 가로(i)와 세로(j)의 길이를 이용하여 노란색 타일의 개수가 맞는지 확인하고, 맞을 경우에는 해당 크기를 반환합니다.
/**
* 16. [완전탐색 -4 : 브루트 포스] 카펫
*
* @return ResponseEntity
* @link ...
* @since 2023.07.24
*/
@GetMapping("/exhausSearch4")
public ResponseEntity<apiresponse> question16() {
int[] answer = new int[2];
int brown = 8;
int yellow = 1;
// [STEP1] 갈색 격자와 노란 격자를 합하여 전체의 개수를 구합니다.
int sum = brown + yellow;
// [STEP2] 그 합의 제곱근부터 1이 될 때까지 반복 수행합니다. (브루트 포스)
for (int i = (int) Math.sqrt(sum); i >= 1; i--) {
// [STEP3] 그 중 합의 약수인 경우를 찾습니다.
if (sum % i == 0) {
// [STEP4] 약수 중 합에서 가로의 길이를 나누어서 세로의 길이를 계산합니다.
int j = sum / i;
// [STEP5] 가로와 세로의 길이를 이용하여 노란색 타일의 개수가 맞는지 확인하고, 맞을 경우에는 해당 크기를 반환합니다.
if ((i - 2) * (j - 2) == yellow) {
answer = new int[]{j, i};
}
}
}
ApiResponse ar = ApiResponse
.builder()
.result(answer)
.resultCode(SUCCESS_CODE)
.resultMsg(SUCCESS_MSG).build();
return new ResponseEntity<>(ar, HttpStatus.OK);
}
💡 [참고] 좀 더 다양한 알고리즘에 대해 알고 싶으시면 아래의 글을 참고하시면 도움이 됩니다.
분류 | 알고리즘 | 링크 |
알고리즘 구현시간 | 시간 복잡도, 공간 복잡도, 빅오 표기법 | https://adjh54.tistory.com/186 |
탐색 알고리즘 | 선형탐색 | https://adjh54.tistory.com/193 |
탐색 알고리즘 | 이진탐색 | https://adjh54.tistory.com/187 |
탐색 알고리즘 | 완전탐색 : 기본구조 | https://adjh54.tistory.com/196 |
탐색 알고리즘 | 완전탐색 : 종류 | https://adjh54.tistory.com/197 |
탐색 알고리즘 | 완전탐색: 문제로 이해하기 | https://adjh54.tistory.com/227 |
알고리즘 설계 방법 | 동적 계획법 알고리즘 | https://adjh54.tistory.com/201 |
알고리즘 설계방법 | 그리디 알고리즘 | https://adjh54.tistory.com/212 |
알고리즘 설계방법 | 분할정복 알고리즘 | https://adjh54.tistory.com/219 |
기타 알고리즘 | 재귀 함수 | https://adjh54.tistory.com/194 |
기타 알고리즘 | 피보나치 수열: 경우의 수 | https://adjh54.tistory.com/183 |
기타 알고리즘 | 유클리드 호제법: 최대공약수, 최대공배수 | https://adjh54.tistory.com/179 |
오늘도 감사합니다. 😀
반응형
'Java > 알고리즘 & 자료구조' 카테고리의 다른 글
[Java/자료구조] 선형구조 - 연결 리스트 이해하기 : 단순, 이중, 순환 연결리스트 (3) | 2023.11.19 |
---|---|
[Java/자료구조] 선형구조 - 순차 리스트(Sequential List) 이해하기 : 배열, 리스트 (0) | 2023.11.18 |
[Java/자료구조] 선형구조 - 큐(Queue) 이해하기: 일반 큐, 우선순위 큐(Priority Queue) 이해하기 (2) | 2023.07.22 |
[Java/알고리즘] 분할정복(Divide and Conquer Algorithm) 이해하기 (0) | 2023.07.08 |
[Java/알고리즘] 그리디 알고리즘(탐욕법, Greedy Algorithm) 이해하기 (6) | 2023.06.24 |