맞아요, 말씀 이해했어요! 😊
백준 16357 Circuits 문제 자체는 굉장히 길고 복잡하지만, 거기서 쓰인 중요한 개념들(예: 구간 압축, 세그먼트 트리, lazy 전파, 구간 최대값 쿼리)은 정보처리기사 실기 알고리즘 문제로 출제될 수 있는 형태가 있어요.
실제 실기에서는 이렇게 큰 문제 대신에 단일 개념을 확실히 묻는 짧은 문제가 나오는 경우가 많아요. 그래서 아래와 같이 “실기용 연습 문제”를 정리해 줄게요.
문제:
N개의 작업이 주어집니다. 각 작업은 시작 시각s와 종료 시각e를 가집니다.
이때 한 시각에 동시에 진행 중인 작업 수의 최댓값을 출력하는 프로그램을 작성하세요.입력:
5 1 3 2 5 4 6 7 8 5 9출력:
2※ 위 출력에서
2는 가장 많은 작업이 겹친 수입니다.
이 문제는
✅ 선형 스위핑(Sweep Line)
✅ 구간 압축(좌표 압축)
✅ 세그먼트 트리 또는 우선순위 큐
중 하나로 풀 수 있습니다.
정보처리기사의 경우 이런 문제에서 요구하는 건 대체로 아래와 같습니다.
입력을 받는다.
이벤트 정리:
+1-1시간 순 정렬 후, 한 번 훑으면서 현재 진행 작업 수를 계산하고 최댓값을 추적한다.
출력.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int time;
int delta;
} Event;
int compare(const void *a, const void *b) {
Event *ea = (Event*)a;
Event *eb = (Event*)b;
if (ea->time != eb->time) return ea->time - eb->time;
return ea->delta - eb->delta;
}
int main() {
int N;
scanf("%d", &N);
Event events[N*2];
for(int i=0;i<N;i++){
int s,e;
scanf("%d %d",&s,&e);
events[i*2].time = s;
events[i*2].delta = 1; // 시작
events[i*2+1].time = e;
events[i*2+1].delta = -1; // 종료
}
qsort(events,N*2,sizeof(Event),compare);
int cur = 0, ans = 0;
for(int i=0;i<N*2;i++){
cur += events[i].delta;
if(cur>ans) ans = cur;
}
printf("%d\n", ans); // 출력
return 0;
}
이 문제에서 설명할 때도 실기 준비 방식처럼:
Event 구조체: time, delta(+1/-1) 역할 → 작업 시작/종료 표시qsort로 정렬 기준 설명for 루프로 이벤트 하나씩 처리하면서 현재 겹친 수를 계산(1,3), (2,5), ...를 대입해 상태 변화를 단계별 디버깅이런 식으로 준비해 두면 좋아요!
좋아요! 😄 이제 int main() 안의 입력을 받는 부분을 한 줄 한 줄 디버깅하듯 자세히 설명해 드릴게요.
설명할 부분은 아래 코드예요:
int main() {
int N;
scanf("%d", &N);
Event events[N*2];
for(int i=0;i<N;i++){
int s,e;
scanf("%d %d",&s,&e);
events[i*2].time = s;
events[i*2].delta = 1; // 시작
events[i*2+1].time = e;
events[i*2+1].delta = -1; // 종료
}
...
}
int N;N이라는 정수를 선언합니다.scanf("%d", &N);📌 표준입력에서 정수 한 개를 읽어 N에 저장.
🧠 이 부분에서 일어나는 일:
5라고 치면 N=5.🧠 왜 여기 있어야 하나?
N을 알아야 이후 메모리 할당(Event events[N*2];)을 위해서.Event events[N*2];📌 이벤트 배열을 선언합니다.
🧠 왜 N*2?
🧠 events 안에는 time(언제 시작/끝나는지), delta(+1 or -1)가 담김.
for(int i=0;i<N;i++){ ... }📌 i=0부터 i=N-1까지 반복 → 작업 N개를 한 줄씩 읽을 거예요.
🧠 왜 N번 돌까?
int s,e;s)과 종료(e) 시간을 담을 지역변수를 선언.scanf("%d %d",&s,&e);1 3이 입력되면 s=1, e=3.events에 채우면 돼요.events[i*2].time = s;📌 i번째 작업의 시작 이벤트 위치를 채움.
🧠 i*2 계산:
i=0 → 0i=1 → 2i=2 → 4 ...🧠 이렇게 짝수 index를 시작 이벤트용 슬롯으로 예약.
🧠 events[0].time = 1, events[2].time = 2 ... 이렇게 차곡차곡 쌓임.
events[i*2].delta = 1; // 시작+1.time에서 작업 수가 하나 증가.events[i*2+1].time = e;📌 i번째 작업의 종료 이벤트 위치.
🧠 i*2+1 계산:
i=0 → 1i=1 → 3i=2 → 5 ...🧠 이렇게 홀수 index를 종료 이벤트용 슬롯으로 예약.
🧠 events[1].time = e, events[3].time = e ... 형태로 쌓임.
events[i*2+1].delta = -1; // 종료-1.cur--를 하여 현재 작업 수 감소.2
1 3
2 5
2*2 = 4반복문:
i=0:
events[0].time = 1; delta = +1events[1].time = 3; delta = -1i=1:
events[2].time = 2; delta = +1events[3].time = 5; delta = -1최종 events 배열 상태:
index | time | delta
0 | 1 | +1
1 | 3 | -1
2 | 2 | +1
3 | 5 | -1
이 상태에서 정렬(qsort)하면 time 순으로 [ (1,+1), (2,+1), (3,-1), (5,-1) ]가 되고, 이를 쭉 돌며 +1/-1 계산하면서 최대값을 찾습니다.
좋아요! 🕵️♂️ 이제 qsort() 호출 부분과 compare() 함수 부분을 한 줄 한 줄 디버깅하면서 아주 자세히 설명해 드릴게요.
qsort() 호출 부분qsort(events, N*2, sizeof(Event), compare);
이 한 줄의 역할을 쪼개서 설명해 보자:
events:
Event 구조체들의 시작 지점.N*2:
sizeof(Event):
Event)의 크기(바이트 단위).qsort 내부에서 인덱스를 옮기며 메모리 접근할 때 필요.compare:
🔄
qsort()가 내부에서 하는 일:
- 이벤트 배열을 섞고 비교해 가며 정렬하기 위해
compare()호출.- 정렬 기준에 맞춰
events배열의 원소들의 위치를 바꿈.- 정렬 끝나면
events가 오름차순 정리됨.
compare() 함수 부분 디버깅여기서 중요한 건 compare()가 정렬 기준을 어떻게 정하는지를 정의한다는 것.
이 함수를 한 줄 한 줄 보자:
int compare(const void *a, const void *b) {
Event *ea = (Event*)a;
Event *eb = (Event*)b;
if (ea->time != eb->time) return ea->time - eb->time;
return ea->delta - eb->delta;
}
Event *ea = (Event*)a;qsort()에서 a를 void 포인터로 넘겨주므로 우리가 원래 타입(Event *)로 변환.qsort()는 어떤 타입이든 정렬할 수 있으므로 void *를 쓰는데, 우리는 Event 정렬이므로 캐스팅 필수.Event *eb = (Event*)b;b를 Event*로 캐스팅.if (ea->time != eb->time) return ea->time - eb->time;📌 가장 먼저 **이벤트의 time(시각)**을 기준 정렬.
🧠 ea->time과 eb->time 비교:
ea가 eb보다 앞으로ea가 eb보다 뒤로🧠 왜 이렇게?
시각이 작은 이벤트부터 큰 이벤트 순서대로 정렬하기 위해.
return ea->delta - eb->delta;📌 만약 time이 같다면 delta(+1/-1) 비교.
🧠 왜 이렇게?
같은 시각에 시작(+1)과 종료(-1)가 겹치면, 시작 이벤트를 먼저 처리해야 겹침 수 계산에서 손해보지 않도록 정렬.
🧠 ea->delta - eb->delta:
시작(+1) 먼저 오고 종료(-1)가 나중에 오도록 → +1 - (-1) = +2 → 반환값 양수 → 시작이 뒤로?
여기 디버깅 포인트!
현재 조건 ea->delta - eb->delta의 결과를 보자:
1 - (-1) = 2 (>0) → ea가 뒤로 갑니다.지금 compare가 ea->delta - eb->delta면, 종료 이벤트가 시작 이벤트보다 우선 오도록 정렬됨. 우리는 시작(+1)이 우선이길 원한다면 반대로 빼야 합니다.
✅ 따라서 이 부분을:
return eb->delta - ea->delta;
이렇게 바꿔주면:
eb->delta - ea->delta = -1-1 = -2(<0) → 시작 이벤트(+1)가 정렬에서 앞으로 오게 됨.time | delta
1 | +1 (e1)
3 | -1 (e2)
2 | +1 (e3)
3 | -1 (e4)
qsort() 호출 후 compare()가 호출되는 경우예: (1,+1) vs (3,-1)
ea->time - eb->time = 1-3 = -2(<0) → (1,+1)이 앞.예: (3,-1) vs (3,-1) → time 같고 delta 같음 → 0 반환
예: (3,-1) vs (3,-1) → 같으므로 무순서.
예: (3,+1) vs (3,-1)
ea->delta - eb->delta = +1 - (-1) = 2 → 양수 반환 → ea가 뒤로 감(원치 않는 정렬)eb->delta - ea->delta = -1 - (+1) = -2 → 음수 반환 → 시작(+1)가 앞으로 옴 ✅compare() 정리int compare(const void *a, const void *b) {
Event *ea = (Event*)a;
Event *eb = (Event*)b;
if (ea->time != eb->time) return ea->time - eb->time;
return eb->delta - ea->delta; // 시작(+1) 우선 정렬
}
qsort() 호출 시:
compare() 함수가 두 원소를 받음time 오름차순 정렬이렇게 정렬된 후 스위핑하면서 cur += delta 누적 계산.
좋아요! 🎯 이제 우리가 정리한 기준(compare() 함수) 대로 정렬이 끝난 후의 최종 이벤트 배열 상태를 한 줄 한 줄 나열해 줄게요. 이렇게 정렬된 상태를 보면 이후 스위핑 부분(cur += delta)을 이해하기가 훨씬 수월해질 거예요. 🔥
입력:
5
1 3
2 5
4 6
7 8
5 9
이 작업들을 이벤트로 변환 → 10개의 이벤트:
| 작업 | time | delta | 설명 |
|---|---|---|---|
| 작업1 | 1 | +1 | 시작 |
| 작업1 | 3 | -1 | 종료 |
| 작업2 | 2 | +1 | 시작 |
| 작업2 | 5 | -1 | 종료 |
| 작업3 | 4 | +1 | 시작 |
| 작업3 | 6 | -1 | 종료 |
| 작업4 | 7 | +1 | 시작 |
| 작업4 | 8 | -1 | 종료 |
| 작업5 | 5 | +1 | 시작 |
| 작업5 | 9 | -1 | 종료 |
compare())time 오름차순time이 같으면 delta가 +1(시작) 우선 정렬이 기준을 적용하여 최종 정렬된 이벤트 배열은 아래와 같습니다:
| index | time | delta | 비고 (설명) |
|---|---|---|---|
| 0 | 1 | +1 | 작업1 시작 |
| 1 | 2 | +1 | 작업2 시작 |
| 2 | 3 | -1 | 작업1 종료 |
| 3 | 4 | +1 | 작업3 시작 |
| 4 | 5 | +1 | 작업5 시작 (종료보다 우선) |
| 5 | 5 | -1 | 작업2 종료 |
| 6 | 6 | -1 | 작업3 종료 |
| 7 | 7 | +1 | 작업4 시작 |
| 8 | 8 | -1 | 작업4 종료 |
| 9 | 9 | -1 | 작업5 종료 |
✅ 이렇게 정리된 상태에서 이후 코드를 보면:
int cur = 0, ans = 0;
for (int i = 0; i < N*2; i++) {
cur += events[i].delta; // 현재 겹친 작업 수 변동
if (cur > ans) ans = cur; // 최댓값 갱신
}
이 과정을 한 단계 한 단계 손으로 디버깅할 수 있어요.
정렬된 이벤트를 하나씩 읽으며 cur과 ans 추적:
| index | time | delta | cur 계산 | cur | ans |
|---|---|---|---|---|---|
| 0 | 1 | +1 | cur = 0+1 | 1 | 1 |
| 1 | 2 | +1 | cur = 1+1 | 2 | 2 |
| 2 | 3 | -1 | cur = 2-1 | 1 | 2 |
| 3 | 4 | +1 | cur = 1+1 | 2 | 2 |
| 4 | 5 | +1 | cur = 2+1 | 3 | 3 |
| 5 | 5 | -1 | cur = 3-1 | 2 | 3 |
| 6 | 6 | -1 | cur = 2-1 | 1 | 3 |
| 7 | 7 | +1 | cur = 1+1 | 2 | 3 |
| 8 | 8 | -1 | cur = 2-1 | 1 | 3 |
| 9 | 9 | -1 | cur = 1-1 | 0 | 3 |
✅ 최종 ans = 3 → 한 시점에 가장 많은 작업 수 = 3
맞아요, 바로 그런 느낌이에요! 🎯
이 부분을 딱 짚어주셨네요. 이건 C 표준에서 정해 놓은 규칙이고, qsort()나 sort() 함수가 이 규칙을 따릅니다. 아래에서 조금 더 원리와 함께 쉽게 설명해 줄게요.
이건 정렬 함수(qsort())가 비교 함수의 반환값을 어떻게 쓰도록 설계되었는지에 대한 규칙이에요.
C 언어의 qsort() 설명서(매뉴얼)에는 이렇게 나와요:
compare(a,b)함수 반환값 < 0 →a가b보다 "작음" → 정렬된 배열에서a가b보다 앞쪽
compare(a,b)함수 반환값 > 0 →a가b보다 "큼" → 정렬된 배열에서a가b보다 뒤쪽반환값 == 0 → 두 원소는 정렬 기준에서 같다고 취급
✅ qsort() 안의 정렬 알고리즘(퀵정렬, 병합정렬 등)은 이 규칙을 따릅니다.
즉, 반환값 음수 → 바꿀 필요 없음(이미 순서 맞음), 반환값 양수 → 두 요소 순서 바꿔라.
학교에서 줄 세우기라고 생각해봅시다.
A, B에게 말해요:A가 앞에 있어야 하면 음수를, 아니면 양수를 말해 주세요.”A가 키가 더 작다고 판단(음수 반환)하면 선생님은 “맞아, A가 B보다 앞에 와라”라고 정해요.A가 B보다 크다고 판단(양수 반환)하면 선생님은 “너네 자리 바꿔!”라고 정해요.✅ 정렬할 때 compare() 반환값 음수 → A를 B보다 앞에 두자.
✅ 반환값 양수 → A를 B보다 뒤에 두자.
이 규칙 자체를 C 표준에서 정해 놓았기 때문에, 우리가 이 규칙만 지키면 qsort()가 내부적으로 열심히 자리 바꾸며 정리해 줍니다.
맞아요!
이건 qsort() 내부 정렬 알고리즘이 미리 정해 놓은 “약속”이에요.
우리는 그냥 compare()에서 기준을 잘 정해서 음수나 양수를 반환해주기만 하면 됩니다. 그러면 qsort()가 내부적으로 이 반환값을 보고 알아서 정렬해 주니까 걱정할 게 없어요. 😄
좋아요! 😊
지금 ea->time, eb->delta 처럼 나오는 -> 연산자에 대해 아주 기초부터 하나씩 차근히 설명해 드릴게요.
-> 연산자란?C언어에서 구조체(struct)를 다룰 때 두 가지 경우가 있어요:
구조체 변수를 선언했다고 해봅시다:
struct Point {
int x;
int y;
};
struct Point p;
p.x = 10;
p.y = 20;
여기서 p는 변수이므로, 멤버(x, y)에 접근할 때 .(닷) 연산자를 씁니다.
이번엔 구조체의 주소를 담고 있는 포인터라고 생각해 볼까요?
struct Point *pp;
pp 안에 p의 주소를 담아봅시다:
pp = &p; // p의 주소를 pp가 가리킴
이제 pp를 이용해 p.x와 p.y에 접근하려면 어떻게 해야 할까요?
-> 연산자포인터를 이용해 구조체 멤버에 접근할 땐 ->를 씁니다:
pp->x = 10;
pp->y = 20;
이건 사실 아래와 완전히 같은 의미예요:
(*pp).x = 10;
(*pp).y = 20;
여기서 *pp는 포인터 pp가 가리키는 구조체이고, 그 구조체의 멤버 x를 수정하는 겁니다.
->가 동작하는 방식pp->x(*pp).xpp 안에 있는 주소를 따라가서 구조체를 찾고, 그 구조체 안의 x 멤버를 가져옴.#include <stdio.h>
struct Point {
int x;
int y;
};
int main() {
struct Point p;
p.x = 100;
p.y = 200;
struct Point *pp = &p; // pp가 p를 가리킴
printf("pp->x = %d\n", pp->x); // pp가 가리키는 구조체의 x를 출력
printf("pp->y = %d\n", pp->y); // pp가 가리키는 구조체의 y를 출력
return 0;
}
pp->x = 100
pp->y = 200
| 형태 | 의미 |
|---|---|
변수.멤버 | 구조체 변수의 멤버 접근 |
포인터->멤버 | 구조체 포인터가 가리키는 멤버 접근 |
지금 우리가 본 ea->time이나 eb->delta는:
ea와 eb가 Event 구조체를 가리키는 포인터이므로->를 이용해서 그 포인터가 가리키는 구조체 안의 time과 delta 멤버에 접근한 거예요.