using System;
using System.Collections.Generic;
using System.Linq;
public class Solution
{
public int solution(int n, int[,] wires)
{
int answer = int.MaxValue;
// 트리 생성
Dictionary<int, HashSet<int>> dict = new Dictionary<int, HashSet<int>>();
for (int i = 1; i <= n; i++)
{
dict.Add(i, new HashSet<int>());
}
// 노드 연결
for (int i = 0; i < wires.GetLength(0); i++)
{
dict[wires[i, 0]].Add(wires[i, 1]);
dict[wires[i, 1]].Add(wires[i, 0]);
}
for (int i = 0; i < wires.GetLength(0); i++)
{
// 끊고
dict[wires[i, 0]].Remove(wires[i, 1]);
dict[wires[i, 1]].Remove(wires[i, 0]);
// 탐색하고
var queue = new Queue<int>();
bool[] visit = new bool[n + 1];
queue.Enqueue(1);
int count = 0;
while (queue.Count > 0)
{
var now = queue.Dequeue();
visit[now] = true;
++count;
foreach (var value in dict[now])
{
if (visit[value] == false)
{
queue.Enqueue(value);
}
}
}
int checkAbs = Math.Abs((n - count) - count);
if (answer > (checkAbs))
{
answer = checkAbs;
}
// 재연결
dict[wires[i, 0]].Add(wires[i, 1]);
dict[wires[i, 1]].Add(wires[i, 0]);
//절대값 확인 후 갱신
}
return answer;
}
}
처음에는 어떻게 최적화를 시켜 주어야 할까에 대해서 고민을 많이 했으나

위와 같은 제한 사항 특성상 완전 탐색으로도 문제를 풀이 할 수 있다는 것을 확인했습니다.
따라서, 아래와 같은 방식으로 문제를 풀었습니다.
- Dictionary 형태로 트리 구조 생성
- wires의 배열에서 순서대로 간선 제거
- BFS 로 연결된 노드의 갯수 확인 및 해답 최적화
- 간선 재연결 후 다음 배열 확인
이렇게 완전 탐색으로 문제를 풀때는 시간 복잡도에 대해서 확인해 보면 좋습니다. 이때 알아둬야 할 것이 Big_O 표기법 입니다
BIG-O 표기법은 알고리즘으 ㅣ시간복잡도를 나타내는 방법으로, 입력 크기 n에 따라 실행 시간이 어떻게 증가하는지를 표현합니다.
| 표기 | 의미 | 예시 |
|---|---|---|
| O(1) | 상수 시간 | 배열에서 인덱스로 접근 arr[i] |
| O(log n) | 로그 시간 | 이진 탐색(Binary Search) |
| O(n) | 선형 시간 | 배열 순회(단순 반복문) |
| O(n log n) | 로그 선형 시간 | 퀵 정렬, 병합 정렬 |
| O(n²) | 이차 시간 | 중첩 반복문(버블 정렬) |
| O(2ⁿ) | 지수 시간 | 피보나치 재귀(비효율적) |
| O(n!) | 팩토리얼 시간 | 브루트포스 순열 생성 |
핵심 정리
- 가장 빨리 증가하는 항만 남기고, 상수는 무시한다.
예: 3𝑛2+5𝑛+7⇒𝑂(𝑛2)3n 2+5n+7⇒O(n2)- 입력 크기 n이 커질수록 성능 차이가 극명해진다.
- 최악의 경우(Worst Case)를 기준으로 측정하는 것이 일반적이다.
이 문제는 혼자서는 풀이 할 수 없어 결국 외부에서 해답을 찾아보았습니다.
문제의 제한 조건을 보고, 완전 탐색으로는 해당 문제를 풀 수 없다는 것을 알 수 있었습니다. 특정 배열에서의 합을 구해야 한다는 점에서 "누적합" 을 먼저 떠올렸으나, 곱해지는 값이 펄스로 구성 되어 있어 단순하게 풀어지지 않았습니다.
때문에, 이를 최적화 하며 풀어주기 위해서 DP(동적 프로그래밍)이 필요했습니다.
풀이 1번 : DP 이용
using System;
public class Solution
{
public long solution(int[] sequence)
{
int n = sequence.Length;
long[,] dp = new long[n, 2];
long max = 0;
// i, k => 0 = 양수 펄스, 1 = 음수 펄스 (i번째 원소)
dp[0, 0] = sequence[0];
dp[0, 1] = -1 * sequence[0];
max = Math.Max(dp[0, 0], dp[0, 1]);
for (int i = 1; i < n; i++)
{
dp[i, 0] = Math.Max(sequence[i], dp[i - 1, 1] + sequence[i]);
dp[i, 1] = Math.Max(-1 * sequence[i], dp[i - 1, 0] - sequence[i]);
max = Math.Max(dp[i, 0], max);
max = Math.Max(dp[i, 1], max);
}
return max;
}
}
풀이 2번 : 카데인 알고리즘 사용
using System;
public class Solution
{
public long solution(int[] sequence)
{
long oddnowMax = 0;
long oddMax = long.MinValue;
for(int i =0; i < sequence.Length; i++)
{
int oddSeq = (i % 2 == 0 ? -1 : 1) * sequence[i];
oddnowMax = Math.Max(oddSeq, oddSeq + oddnowMax);
oddMax = Math.Max(oddMax, oddnowMax);
}
long evennowMax = 0;
long evenMax = long.MinValue;
for (int i = 0; i < sequence.Length; i++)
{
int evenSeq = (i % 2 == 0 ? 1 : -1) * sequence[i];
evennowMax = Math.Max(evenSeq, evenSeq + evennowMax);
evenMax = Math.Max(evenMax, evennowMax);
}
return Math.Max(evenMax, oddMax);
}
}
public int Kadane(int[] arr)
{
int currentMax = arr[0]; // 현재까지의 최대 부분합
int globalMax = arr[0]; // 전체 배열에서의 최대값
for (int i = 1; i < arr.Length; i++)
{
currentMax = Math.Max(arr[i], currentMax + arr[i]);
globalMax = Math.Max(globalMax, currentMax);
}
return globalMax;
}
현재 최대와 전체 최대를 갱신하는 방식입니다.
두 풀이 모두 DP 를 이용하지만 첫번 째 풀이는 dp 배열로 인한 메모리 사용이 추가적으로 필요하지만 시간 복잡도에서 앞서고, 카데인 알고리즘은 직관적이지만 중복 계산이 발생합니다.
결국 동적 프로그래밍으로 풀어야 하는 문제였는데, 아직까지도 가장 어려운 카테고리인 만큼 더 많은 공부와 예제 풀이가 필요해 보입니다.
가비지 컬렉터란 더이상 사용하지 않는 객체를 자동으로 식별하고 해제하는 메커니즘. C#에서의 가비지 컬렉터는 .NET 네트워크의 일부로 Heap 영역에서 동작합니다.
C# 의 가비지 컬렉터는 Mark and Sweep 알고리즘을 사용하는 세대별 가비지 컬렉터 입니다. 이는 다음과 같은 단계로 이루어집니다.
또한 세대별 가비지 컬렉션의 세부적인 내용은 아래와 같습니다.
각 세대애 대한 설명은 아래와 같습니다.
- 0세대 : gc를 한번도 겪지 않는 객체
- 1세대 : gc를 한번 겪은 객체
- 2세대 : gc를 2회 이상 겪은 객체
단, 이때 gc는 이전 세대까지 모두 부르기 때문에, 2세대 에서의 GC는 사실상 전체를 의미합니다.
이 세대를 나누는 근거는 아래와 같습니다.
- 최근에 생성된 객체일수록 생명주기가 짧을 가능성이 높고, 오래된 객체일수록 생명 주기가 길 가능성이 높다.
- 최근에 생성된 객체들 끼리는 서로 연관성이 높을 가능성이 높고, 비슷한 시점에서 자주 액세스 된다.
- 일부분 Heap에 대해서 GC를 하는 것이 전체 대상 보다 빠르다.
장점
- 자동 메모리 관리 : GC는 자동으로 메모를 해제하기 때문에 안정적인 시스템을 개발하는데 도움을 줍니다
- 메모리 누수 방지 : 개발자가 메모리를 명시적으로 해제 하지 않아도 되기 떄문에 누수가 발생할 가능성이 적습니다
- 안전성 향상 : 객체가 더이상 참조되지 않을때 삭제하므로, 잘못된 접근을 방지할 수 있습니다.
- 메모리 단편화 감소 : 힙 메모리를 재배치 하기 떄문에 단편화를 줄입니다.
단점
- 성능 오버헤드 : GC는 주기적으로 실행되는데, 이때 앱이 일시적으로 중단될 수 있습니다.
- 예측 불가능한 중단 : GC의 발생 시점을 예측 할 수 없습니다.
- 메모리 사용량 증가 : GC를 위해서는 추가적인 메모리가 필요합니다.