Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions studies/week-01/DevJunz/1767-프로세서연결하기.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# [SWEA 1767] 프로세서 연결하기

| 항목 | 내용 |
|:---|:---|
| 🔗 문제 | https://swexpertacademy.com/main/code/problem/problemDetail.do?contestProbId=AV4suNtaXFEDFAUf |
| 📚 주제 | 완전탐색 · 백트래킹(DFS) |
| 🎚️ 난이도 | D4 |
| ⏱️ 소요 시간 | 1h 30min |
| 🔁 다시 풀기 | 예 |

---

## 1. 접근 방법
`N×N` 격자에서 값이 `1`인 칸이 코어다. **가장자리(테두리)에 있는 코어는 이미 전원에 연결된 것으로 보고 무시**하고, **내부 코어만** 상·우·하·좌 중 한 방향으로 전선을 격자 밖까지 직선으로 빼야 전원에 연결된다. 전선은 다른 코어나 이미 놓인 전선과 겹칠 수 없다.

목표는 두 단계다.
1. **연결한 코어 수를 최대화**
2. 그 최대 조건 안에서 **전선 길이 합을 최소화**

내부 코어 하나하나에 대해 "4방향 중 하나로 연결" 또는 "연결하지 않음"을 고르는 완전탐색(백트래킹)으로 풀었다. 전선을 놓을 땐 격자에 `2`를 칠하고, 재귀에서 빠져나올 때 다시 `0`으로 되돌린다.

처음엔 모든 코어를 무조건 연결하려 했지만, **한 코어를 연결하면 그 전선이 다른 코어의 길을 막아 전체 연결 수가 오히려 줄 수 있다.** 그래서 각 코어에서 "연결하지 않는" 분기를 반드시 함께 둬야 한다는 점이 핵심이었다.

## 2. 시간 복잡도
**O(N · 5^K)** — `K` = 내부 코어 수(최대 12), 각 코어에서 최대 5분기(4방향 + 연결 안 함).

- 각 분기에서 `check_path` / `set_path`가 전선을 격자 끝까지 훑으므로 분기당 `O(N)`.
- 따라서 최악 `O(N · 5^K)`. 다만 `can_go_dir` 사전 가지치기와 경로 충돌(`length == 0`) 덕분에 실제 탐색량은 이보다 크게 작다. `N ≤ 12`라 제한 시간 안에 통과.
Comment on lines +24 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

전체 시간 복잡도에 전처리 비용을 포함해 주세요.

현재 O(N · 5^K)는 DFS 탐색 비용만 설명합니다. 입력 스캔 O(N²)can_go_dir의 전체 코어 탐색 비용 O(K·C)(C = 전체 코어 수)가 누락되어, 전체 복잡도는 O(N² + K·C + N·5^K)로 적는 편이 정확합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@studies/week-01/DevJunz/1767-프로세서연결하기.md` around lines 24 - 28, 시간 복잡도 항목에서
DFS 비용만 제시하지 말고 입력 스캔 비용 O(N²)과 can_go_dir 전처리의 전체 코어 탐색 비용 O(K·C)를 포함하세요. 최종 전체
시간 복잡도를 O(N² + K·C + N·5^K)로 수정하고, 기존 분기별 DFS 설명은 유지하세요.


## 3. 공간 복잡도
**O(N²)** — 격자 `cells`가 `O(N²)`로 지배적. 재귀 호출 스택 깊이는 `O(K)`, 코어 목록·방향 목록은 `O(코어 수)`.

## 4. 핵심 풀이 방법 ⭐
- **가장자리 코어 스킵**: `c[0]`이나 `c[1]`이 `0` 또는 `N-1`이면 연결 대상에서 제외. 이걸 놓치면 불필요한 분기가 폭발한다.
- **"연결 안 함" 분기 필수**: `dfs` 마지막의 `dfs(idx+1, connected, total_length)` 한 줄. 최대 연결 수를 위해 일부 코어를 일부러 포기해야 할 수 있으므로 반드시 있어야 한다.
- **최댓값 우선, 동률이면 최소 길이**: 리프에서 `connected > max_core`면 `min_length`를 새 길이로 **리셋**하고, `connected == max_core`면 `min`으로 갱신한다. 이 두 조건을 섞으면 틀린다.
- **백트래킹 복원**: `set_path(..., 2)`로 전선을 놓고 재귀한 뒤 반드시 `set_path(..., 0)`으로 되돌린다. 복원을 빠뜨리면 이후 탐색이 오염된다.
- **방향 사전 가지치기(`can_go_dir`)**: 같은 행/열에 다른 코어가 직선 경로를 막고 있으면 그 방향은 (전선 상태와 무관하게) 영구히 불가능하므로 미리 제거해 분기를 줄인다. 코어는 절대 통과할 수 없기 때문에 안전한 가지치기다.

## 5. 실제 코드
```python
T = int(input())
for t in range(1,T+1):

N = int(input())

cells = [list(map(int,input().split())) for _ in range(N)]

answer = 0



core = []
for n in range(N):
for m in range(N):
if cells[n][m] == 1:
core.append((n,m))

target_core = []
for c in core:
# 벽에 붙어 있는 코어 스킵
if c[0]==0 or c[0]==N-1 or c[1]==0 or c[1]==N-1:
continue
# 0부터 3까지 상우하좌라고 가정한다.
can_go_dir = []

for i in range(4):
#하나의 코어당 다른 모든 코어에 대해서 검사해야한다
flag = True
for c2 in core:
#자기 자신은 스킵
if c == c2:
continue
#위로 이동하는 경우만 검사
if i == 0:
if c[1]==c2[1] and c[0]>c2[0]:
flag = False
break
elif i == 1:
if c[0]==c2[0] and c[1]<c2[1]:
flag = False
break
elif i == 2:
if c[1]==c2[1] and c[0]<c2[0]:
flag = False
break
elif i == 3:
if c[0]==c2[0] and c[1]>c2[1]:
flag = False
break


if flag:
can_go_dir.append(i)
# target_core 데이터 형태 (n좌표,m좌표,(이동가능한 방향 좌표))
target_core.append((c,tuple(can_go_dir)))

dn = [-1,0,1,0]
dm = [0,1,0,-1]

# 현재 경로가 연결 가능한 경우 인지 판단하는 함수
def check_path(n,m,dir):
nn = n + dn[dir]
nm = m + dm[dir]
length = 0

while 0<=nn < N and 0 <= nm < N:
if cells[nn][nm] != 0 :
return 0
length+=1
nn += dn[dir]
nm += dm[dir]
return length

# value가 0일 때는 제거 2일 떄는 전선을 놓는 경우
def set_path(n,m,dir,value):
nn = n + dn[dir]
nm = m + dm[dir]

while 0<= nn <N and 0 <= nm <N :
cells[nn][nm] = value

nn += dn[dir]
nm += dm[dir]

max_core = 0
min_length = float('inf')

def dfs(idx, connected, total_length):
global max_core, min_length

if idx == len(target_core):
if connected>max_core:
max_core = connected
min_length = total_length

elif connected == max_core:
min_length = min(min_length,total_length)

return

(n,m),directions = target_core[idx]

for dir in directions:
length = check_path(n,m,dir)

if length == 0:
continue

set_path(n,m,dir,2)

dfs(idx+1,connected +1,total_length+length)

set_path(n,m,dir,0)
dfs(idx+1,connected,total_length)

dfs(0,0,0)

answer = min_length




print(f"#{t} {answer}")
```

---

### 🧠 회고 (선택)
- "가장자리 스킵 → 각 코어를 놓거나 포기 → 리프에서 (최대 연결, 최소 길이) 갱신"은 전형적인 배치형 백트래킹 골격이라 비슷한 유형(예: N개 대상 배치 최적화)에 그대로 재사용할 수 있다.
- `can_go_dir` 사전 가지치기는 `check_path`가 코어(`cells != 0`)도 어차피 걸러내므로 **기능상 중복**이긴 하다. 다만 분기 자체를 미리 줄여주는 최적화라 남겨둘 가치는 있다. 리뷰어에게: 이 사전 필터 없이 순수 `check_path`만으로도 제한 시간 안에 통과하는지 궁금하다.