일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- SDK upgrade
- linux
- Google Developer API
- Camera Movement
- css framework
- Packet Network
- Google Refund
- --watch
- java
- server
- mongoDB
- springboot
- screencapture
- Git
- nodejs
- Spring Boot
- MySQL
- Unity Editor
- express
- unity
- OverTheWire
- draganddrop
- react
- spread 연산자
- rpg server
- Camera Zoom
- Unity IAP
- Digital Ocean
- docker
- critical rendering path
- Today
- Total
우당탕탕 개발일지
[프로그래머스] Level 3. 외벽 점검 본문
https://school.programmers.co.kr/learn/courses/30/lessons/60062
프로그래머스
SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프
programmers.co.kr
해결 방법(실패, 시간초과)
- 가장 많이 걸을 수 있는 친구 순서대로 나열한다.
- 첫 번째 친구부터, 각 지점 (시계, 반시계) 를 모두 돌아보며 완전 탐색을 진행한다.
- 만약 남은 외벽이 없거나, 더이상 친구가 없다면 재귀를 종료한다.
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
vector<int> friends;
int max_friend;
int wall_length= 0;
int answer = -1;
int get_dist(int s, int e, bool clockwise){
if(clockwise){
if(e>=s) return e-s;
return e+wall_length-s;
}else
{
if(e<=s) return s-e;
else return wall_length-e+s;
}
}
vector<int> check(vector<int> remains, int start, int dist, bool clockwise){
vector<int> r ;
for(int i=0; i<remains.size(); i++){
if(get_dist(start,remains[i],clockwise) > dist){
r.push_back(remains[i]);
}
}
return r;
}
void dfs(vector<int> remains, int fcnt){ //점검완료지점. 사용한 친구 수
// cout <<"사용된 친구:"<<fcnt <<", 남은 외벽 "<<remains.size()<<endl;
if(remains.size()== 0){
if(answer == -1) answer = fcnt;
else answer = min(answer, fcnt);
return;
}
if(fcnt == max_friend ) return ; //외벽점검을 완료할 수 없음.
int dist = friends[fcnt];
//남은 외벽들을 왼/오른쪽으로 탐색
for(int i = 0; i<remains.size(); i++){
//시계방향 탐색
vector<int> r = check(remains, remains[i], dist, true);
// cout <<fcnt<<"번째 친구 사용(시계). 남은 외벽:"<<r.size();
dfs(r, fcnt+1);
r = check(remains,remains[i],dist,false);
// cout <<fcnt<<"번째 친구 사용(반시계). 남은 외벽:"<<r.size();
dfs(r,fcnt+1);
}
}
int solution(int n, vector<int> weak, vector<int> dist) {
//이동거리가 큰 친구부터 정렬
sort(dist.rbegin(), dist.rend());
wall_length =n;
friends = dist;
max_friend = dist.size();
dfs(weak, 0);
return answer;
}
이 문제는 해답은 찾을 수 있으나, 시간초과가 발생했다. 조금 더 효율적인 방법을 생각할 필요가 있었다.
개선 1 (실패, 시관초과 2개)
생각해보니, 시계, 반시계 방향을 고려할 필요가 있을까?? 친구 A가 1->5 번까지 시계방향으로 이동한 것과 5->1 번으로 시계방향 이동한 것은 동일하다. 따라서 시계방향과 관련된 정보를 제거하고, dfs 재귀도 한번으로 변경했다. 이전 코드보다는 시간초과가 훨씬 줄어들었지만, 여전히 2개의 문제에서 시간초과가 발생하였다.
#include <string>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
vector<int> friends;
int max_friend;
int wall_length= 0;
int answer = -1;
int get_dist(int s, int e){
if(e>=s) return e-s;
return e+wall_length-s;
}
vector<int> check(vector<int> remains, int start, int dist){
vector<int> r ;
for(int i=0; i<remains.size(); i++){
if(get_dist(start,remains[i]) > dist){
r.push_back(remains[i]);
}
}
return r;
}
void dfs(vector<int> remains, int fcnt){ //점검완료지점. 사용한 친구 수
// cout <<"사용된 친구:"<<fcnt <<", 남은 외벽 "<<remains.size()<<endl;
if(remains.size()== 0){
if(answer == -1) answer = fcnt;
else answer = min(answer, fcnt);
return;
}
if(fcnt == max_friend ) return ; //외벽점검을 완료할 수 없음.
//외벽 탐색 후 검사가 필요한 외벽 리스트 r을 반환
for(int i = 0; i<remains.size(); i++){
vector<int> r = check(remains, remains[i], friends[fcnt]);
dfs(r, fcnt+1);
}
}
int solution(int n, vector<int> weak, vector<int> dist) {
//이동거리가 큰 친구부터 정렬
sort(dist.rbegin(), dist.rend());
wall_length =n;
friends = dist;
max_friend = dist.size();
dfs(weak, 0);
return answer;
}
아예 다른 방법으로 접근할수밖에.. 아 한개만 시간초과인데 ㅠㅠ
해결방법(성공)
순열을 이용해서 문제를 푼다. (Brute-Force)
순열(permutation) 이란 , 주어진 요소들의 순서를 바꿔가며 가느한 모든 경우를 만들어 내는 것을 의미한다. 예를들어 dist = {1,2,3} 일 경우 아래처럼 모든 가능한 순서를 만들어서 각각의 경우에 대해 탐색한다.
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
이 문제에서 어려운 점은 원형 배열 즉, N다음이 0으로 순회한다는 점이다. 이를 단순화하기 위해, weak배열을 2배로 확장한다. 예를들어 n = 12 이고 weak = [1,5,6,10] 이라면 확장된 weak 배열 = [1,5,6,10,13,17,18,22] 이다. 이렇게 하면 시계방향으로만 순차 탐색하여도 원형 배열을 고려한 것과 같게된다.
친구들의 순서를 바꿔가면서 순서대로 벽을 점검하게 하고, 가장 최소한의 친구가 필요한 경우를 찾는다.
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
int solution(int n, vector<int> weak, vector<int> dist) {
int answer = dist.size() + 1; // 최소한의 친구 수를 찾기 위해 초기값을 큰 값으로 설정
int length = weak.size();
// 1. **배열을 2배 길이로 변환**
for (int i = 0; i < length; i++) {
weak.push_back(weak[i] + n); // 원형 구조를 직선으로 변환
}
// 2. **모든 시작점에 대해 탐색**
for (int start = 0; start < length; start++) {
// 3. **순열을 이용해 친구를 배치하는 모든 경우 탐색 (while 사용)**
sort(dist.begin(), dist.end()); // 순열을 위해 정렬
bool hasNextPermutation = true; // 순열이 있는지 확인하는 변수
while (hasNextPermutation) {
int cnt = 1; // 사용한 친구 수
int position = weak[start] + dist[cnt - 1]; // 첫 번째 친구가 도달할 수 있는 위치
// 4. **점검할 위치 탐색**
for (int index = start; index < start + length; index++) {
if (position < weak[index]) { // 친구가 점검할 수 없는 곳 발견
cnt += 1; // 새로운 친구 투입
if (cnt > dist.size()) break; // 친구를 모두 써도 점검 불가능하면 종료
position = weak[index] + dist[cnt - 1]; // 다음 친구가 도달할 수 있는 위치 업데이트
}
}
// 5. **최소한의 친구 수 갱신**
answer = min(answer, cnt);
// 6. **다음 순열 생성**
hasNextPermutation = next_permutation(dist.begin(), dist.end());
}
}
// 7. **최소 친구 수가 dist.size()보다 크면 모든 지점을 점검할 수 없다는 뜻**
return answer > dist.size() ? -1 : answer;
}
내가 처음에 이 코드를 보고 든 의문은 이거였다.
그러면 순열을 이용해서 친구들의 순서를 바꿔가며 모든 경우를 찾는거잖아, 근데 왜 모든 시작점에 대해 탐색하는거야??
친구가 3명이라 가정하면 {1,2,3}{1,3,2}.. 총 6개의 방법으로 친구들을 순서를 매기고,
그 순서대로 0번째 벽부터 검사하는 거잖아, 그러면 맨 위에있는 for문은 필요없는거아니야??
🔹 모든 시작점에 대해 탐색하는 이유
🧐 반례 생각해보기
친구가 3명(dist = {1, 2, 3})이고, 약한 지점(weak)이 {1, 5, 6, 10}이라고 가정해볼게요.
- 우리가 next_permutation()을 이용해서 {1,2,3}, {1,3,2}, {2,1,3}, ... 식으로 친구들의 순서만 바꾼다면,
어느 지점부터 시작할지는 고정된 상태가 됩니다.
→ 즉, weak[0] = 1부터만 탐색하면 출발 지점이 최적이 아닐 수도 있음. - 하지만 weak이 원형으로 이어진 벽이기 때문에, 어느 약한 지점부터 시작하느냐에 따라 점검할 수 있는 구역이 달라집니다.
- {1, 5, 6, 10}에서 1부터 시작하는 경우와 5부터 시작하는 경우의 점검 방식이 달라질 수 있음.
- 친구들의 순서가 같더라도 어느 위치에서 시작하느냐에 따라 최소 인원이 달라질 수 있음.
예제 분석:- {1,2,3} 순서로 weak[0] = 1에서 시작하면 친구 3명이 필요할 수도 있지만,
- 같은 {1,2,3} 순서로 weak[1] = 5에서 시작하면 친구 2명으로 끝낼 수도 있음.
👉 즉, 친구 순서만 바꾼다고 해서 최소 인원을 찾을 수 있는 게 아님!
👉 벽의 어느 지점에서 점검을 시작하는지도 최적의 해를 찾는 중요한 요소!
오케이! 알았따
내가 푼 DFS 방법과 순열을 이용한 방법의 시간복잡도는 어떻게 될까??
1. 순열을 이용한 방법
시간 복잡도 분석
- 친구 순열의 경우의 수
- dist의 최대 크기 = 8
- 순열의 개수 = O(8!) = 40,320 (최악의 경우)
- 모든 시작점을 고려
- weak의 최대 크기 = 15
- 모든 weak[i]를 시작점으로 고려 → O(15)
- 각 순열에 대해 O(N)의 탐색 수행
- 한 번의 순열당 weak을 검사하는 반복문 수행 → O(N)
총 시간 복잡도
O(8!×15×N)=O(40,320×15×N)O(8! \times 15 \times N) = O(40,320 \times 15 \times N)
시간 복잡도 분석
- DFS 탐색 깊이
- 최대 dist.size()까지 깊이 탐색 (O(K), 최대 K=8)
- DFS의 분기 개수
- weak의 개수만큼 분기 (O(15))
- 각 DFS 호출마다 O(N)의 연산 수행
- check() 함수에서 remains를 검사하는 연산 수행.
총 시간 복잡도
O(N×15K)O(N \times 15^K)
K=8일 때 → O(N \times 15^8) ≈ O(15^8) = O(2.5억)
차이가,.많이나네..ㅎㅎ..
✅ 순열 vs DFS: 언제 사용해야 할까?
1️⃣ 순열(Brute-force)이 적절한 경우
"몇 개의 요소를 특정 순서로 배치하는 경우의 수를 모두 확인해야 할 때"
"순서가 중요한 경우"
✅ 판단 기준
- 요소(예: 친구, 병력 등)의 순서가 바뀌면 결과가 달라지는 경우.
- 주어진 요소를 하나씩 선택하여 배치하면서 해결해야 하는 경우.
- **요소 개수(K)**가 작을 때 (K!이 감당 가능한 수준인지 확인).
- 조합이 아닌 순열이 필요한 문제(즉, 같은 요소라도 순서가 다르면 다른 경우).
✅ 예제 문제
- 외벽 점검 문제
- dist(친구들)을 어떻게 배치하느냐가 결과에 영향을 미침 → 순열
- 즉, {1,2,3}과 {3,2,1}은 서로 다른 경우이므로 순열을 고려해야 함.
- N개의 공을 특정 위치에 배치하는 문제
- 예: 3개의 공을 5개의 칸에 배치하는 모든 방법을 고려해야 한다면 → 순열
- 가장 긴 문자열을 만들 수 있는 순서 찾기
- 예: {"apple", "banana", "cherry"}를 조합하여 가장 긴 문자열을 만들 때 → 순열 탐색
📌 시간 복잡도 주의
- 요소 개수(K)가 8 이하면 K! 연산을 감당 가능 → 순열 사용
- K=10이면 10! = 3,628,800 → 가능할 수도 있음
- K=12 이상이면 순열 사용 불가능 (12! = 479,001,600) 🚨
2️⃣ DFS(백트래킹)이 적절한 경우
"완전 탐색이 필요하지만, 가지치기를 통해 탐색량을 줄일 수 있을 때"
"순서가 중요하지 않거나, 특정 조건을 만족하는 경로를 찾을 때"
✅ 판단 기준
- 요소의 순서가 중요하지 않을 때 (즉, {1,2,3}과 {3,2,1}이 같은 경우로 취급될 때).
- 완전 탐색을 하되 불필요한 탐색을 줄이는 가지치기가 가능할 때.
- 트리 형태로 탐색이 가능할 때 (예: 그래프 탐색, 조합 탐색).
- 가능한 경우의 수가 너무 많아서 모든 경우를 볼 수 없을 때 → 백트래킹(가지치기) 사용.
✅ 예제 문제
- 미로 탐색 문제 (최단 경로 찾기)
- 미로의 모든 경로를 탐색하지만 이미 최적해를 찾으면 더 이상 탐색하지 않음 → DFS+백트래킹
- N-Queens 문제 (체스판에 퀸 배치하기)
- 퀸을 배치하는 모든 경우를 탐색하지만, 불가능한 경우는 가지치기(백트래킹)로 제거
- 경로 찾기 문제
- 특정 조건을 만족하는 경로를 찾는 문제
- 예: 그래프 탐색 (DFS/BFS)
📌 시간 복잡도 주의
- DFS의 탐색 깊이(D)가 10 이하면 → DFS 사용 가능 (O(N^D))
- D=15 이상이면 DFS로 탐색할 경우 너무 많은 연산 필요 🚨
- 백트래킹을 잘 활용하면 O(N^D)에서 실제 탐색 횟수를 훨씬 줄일 수 있음.
'Algorithm(c++) > Level 3' 카테고리의 다른 글
[프로그래머스] Level 3. 표현 가능한 이진트리 (2) | 2025.02.05 |
---|---|
[프로그래머스] Level 3. 경주로 건설 (1) | 2025.02.05 |
[프로그래머스] Level 3. 부대 복귀 (2) | 2025.02.04 |
[프로그래머스] Level 3. 양과 늑대 (1) | 2025.02.04 |
[프로그래머스] Level 3. 숫자 타자 대회 (1) | 2025.02.04 |