선인장 그래프 (Cactus Graph)
정의
선인장 그래프 (Cactus Graph) 는 모든 간선이 최대 한 개의 단순 사이클에만 포함되는 무방향 연결 그래프. 트리 (tree) 의 일반화로, 사이클은 허용하되 “사이클끼리 간선을 공유하지 않음”.
형태: 여러 사이클이 점 (정점) 으로만 연결된 구조. 간선 수 E ≤ V + (사이클 수 - 1), 트리면 E = V - 1.
문제 상황과 동기
일반 그래프는 사이클이 복잡하게 엉켜 DP / 최단 경로 / 매칭이 NP-hard. 트리는 선형 시간 가능하지만 사이클 없음. 선인장은 사이클이 있으면서도 트리와 비슷한 구조적 성질을 보존.
naive: 일반 그래프 알고리즘 O(V^2) 또는 O(V E). 핵심 통찰: 각 사이클을 “super-node” 로 축약하면 트리 → DFS / DP 가능. 사이클 내부는 별도 처리.
PS 에서: “도로망에 사이클 하나”, “회로 네트워크 (공유 안 함)”, BOJ 지름 / 최단 경로 변형.
시각화
핵심 아이디어
invariant
간선 e 는 최대 하나의 사이클 C 에만 속함. 두 사이클이 간선을 공유하지 않음 → 간선 분할 (edge partition) 이 깔끔.
DFS 기반 분해
- DFS tree 를 만든다.
- back edge (u, v) 가 나오면 u → v 경로 + (u, v) 가 사이클 C.
- C 의 간선들을 “이미 사이클에 포함됨” 마킹. 다른 back edge 가 C 의 간선을 다시 쓰면 선인장 아님.
시간 O(V + E), 공간 O(V).
DP on cactus
- 각 사이클을 “구간” 으로 보고 사이클 내 DP.
- 사이클을 하나의 노드로 축약한 트리에서 tree DP.
- 둘 합치기: 사이클 DP → 트리 DP.
예: 선인장 지름 = max(트리 지름, 각 사이클 내 최대 거리).
알고리즘
is_cactus(G):
visited = [false] * V
edge_used = set()
for root in V:
if not visited[root]:
if not dfs_cactus(root, -1, visited, edge_used):
return false
return true
dfs_cactus(u, parent, visited, edge_used):
visited[u] = true
for v in adj[u]:
if v == parent: continue
edge = (min(u,v), max(u,v))
if visited[v]:
# back edge
if edge in edge_used:
return false # 이미 다른 사이클에 속함
path = trace_path(u, v)
for e in path:
if e in edge_used:
return false
edge_used.add(e)
else:
if not dfs_cactus(v, u, visited, edge_used):
return false
return true
구현
// 선인장 그래프 검증: DFS + back edge 중복 체크
#include <bits/stdc++.h>
using namespace std;
vector<vector<int>> adj;
vector<int> parent, depth;
set<pair<int,int>> cycleEdges;
bool isCactus;
void dfs(int u, int p, int d) {
parent[u] = p;
depth[u] = d;
for (int v : adj[u]) {
if (v == p) continue;
auto edge = minmax(u, v);
if (depth[v] == -1) {
dfs(v, u, d + 1);
} else if (depth[v] < depth[u]) {
// back edge u -> v
// trace cycle
for (int x = u; x != v; x = parent[x]) {
auto e = minmax(x, parent[x]);
if (cycleEdges.count(e)) { isCactus = false; return; }
cycleEdges.insert(e);
}
if (cycleEdges.count(edge)) { isCactus = false; return; }
cycleEdges.insert(edge);
}
}
}
int main() {
int n, m; cin >> n >> m;
adj.resize(n); parent.resize(n, -1); depth.resize(n, -1);
isCactus = true;
for (int i = 0; i < m; i++) {
int u, v; cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
for (int i = 0; i < n; i++)
if (depth[i] == -1) dfs(i, -1, 0);
cout << (isCactus ? "cactus" : "not cactus") << "\n";
}5 6
0 1
1 2
2 0
2 3
3 4
4 2cactus복잡도
| 항목 | 값 |
|---|---|
| 선인장 판별 | O(V + E) DFS |
| 선인장 DP | O(V + E) (사이클 + 트리 DP) |
| 지름 | O(V + E) |
| 최단 경로 | O((V + E) log V) (Dijkstra) |
| 공간 | O(V + E) |
변형 / 활용
1. 선인장 지름
각 사이클 내 최대 거리 + 트리 지름. 사이클을 “무게 중심” 으로 축약.
2. 선인장 최단 경로
Dijkstra / BFS. 사이클 구조가 트리와 비슷해 일반 그래프보다 빠름.
3. 선인장 DP
부분합 DP, 매칭, 독립 집합 모두 O(V + E). 일반 그래프는 NP.
4. 방향 선인장
각 간선이 최대 한 사이클. functional graph 의 변형.
함정
1. 간선 공유 놓침
두 사이클이 정점을 공유해도 OK. 간선을 공유하면 NO.
O---O
|\ /|
| X | ← 간선 (중앙 교차) 공유 → 선인장 아님
|/ \|
O---O
2. back edge 중복 처리
같은 back edge 를 두 번 방문하면 잘못된 판단. set 으로 관리.
3. 비연결 그래프
선인장 정의는 “연결 그래프”. 비연결이면 각 컴포넌트 따로 판별.
4. DP 순서
사이클 내부 DP → 트리 DP 순서. 역순이면 값 꼬임.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 10891 | Cactus? Not Cactus? | - | kokoa-lab |
| BOJ 13926 | gahui and sousenkyo 2 | - | kokoa-lab |
| BOJ 17409 | 트리의 외심 | - | kokoa-lab |
| BOJ 2406 | 전략적 배치 | - | kokoa-lab |
참고
이 글의 용어 (4개)
- 깊이 우선 탐색 (DFS)algorithm
- 정의 깊이 우선 탐색 (Depth-First Search, DFS) 는 그래프 G=(V, E) 에서 갈 수 있는 만큼 깊이 들어가다가 막히면 백트래킹하는 알고리즘. 스택 (LIF…
- 트리 (Trees)algorithm
- 정의 트리 (Tree) 는 사이클이 없는 연결 그래프 (acyclic connected graph). N 개 정점이면 정확히 N-1 개 간선. 임의의 두 정점 사이에 유일한 경로…
- Cycle Detection: 그래프 사이클 탐지algorithm
- 정의 그래프에 사이클이 존재하는가, 또는 어느 사이클인가 를 찾는 문제. 무향 그래프 Union-Find 접근 간선 (u, v) 삽입 시 이면 사이클. DFS 접근 부모가 아닌 …
- Graph DP: 트리/DAG 위 DPalgorithm
- 정의 Graph DP 는 그래프 구조를 이용한 DP. 사이클 없는 그래프 (트리, DAG) 에서 특히 자연스러움. Tree DP 루트 트리에서 자식 서브트리 결과를 조합. Rer…
💬 댓글