LIS
그리디 알고리즘
배열 arr이 주어졌을 때, arr의 요소들 간의 순서는 지키면서 요소가 증가하는 배열을 증가 부분 수열이라고 함

완전 증가여야하므로, {1, 1, 2}은 안 된다.
이 증가 부분 수열 중 가장 긴 길이를 가지는 부분 수열을 최장 증가 부분 수열 (LIS)이라고 부른다
DP를 이용하여 푸는 알고리즘
배열의 앞에서부터 시작하여 각 원소로 끝나는 LIS를 저장
N번째 요소로 끝나는 LIS는

그리고 최종 답은 이 LIS들 중 가장 긴 LIS가 정답이다.
vector<int> nums = {1, 2, 1, 3, 2, 5};
vector<int> lis(nums.size(), 1);
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) { // 앞의 LIS들 중
if (nums[i] > nums[j]) // N번째 요소가 더 크면
lis[i] = max(lis[i], lis[j] + 1); // 최대값 갱신
}
}
return *max_element(lis.begin(), lis.end()); // LIS 중 최대값
이전의 LIS들을 탐색하는 과정을 이진탐색(logN)으로 줄여 NlogN으로 만든 알고리즘
LIS를 만드는 과정에서 LIS의 마지막 원소가 최대한 작아야 더 긴 LIS를 만들 수 있다
그래서 이전과는 다른 새로운 배열 lis를 사용
예제) arr = [3, 5, 2, 6, 1]
1,2,3)인 증가부분수열들의 가장 작은 마지막 요소값이 {1, 5, 6}라는 뜻.lis 배열은 실제 부분수열을 저장하는 게 아니라, "길이별 최적의 증가 수열 끝값들"을 저장
// 이진 탐색 후 원소값 교체
void change(int x, vector<int>& lis) {
int start = 0, end = lis.size() - 1;
int mid, target, dist = 100000000;
while (start <= end) {
mid = (start + end) / 2;
if (lis[mid] == x)
return;
if (lis[mid] < x) {
start = mid + 1;
continue;
}
if (dist > abs(x - lis[mid]))
{
dist = abs(x - lis[mid]);
target = mid;
}
end = mid - 1;
}
lis[target] = x;
}
int solution(const vector<int> nums) {
vector<int> lis = {nums[0]};
for (const auto& x : nums) {
if (x > lis.back()) { // 크면 넣어주고
lis.push_back(x);
}
else if (x < lis.back()) {
change(x, lis); // 아니면 바꾸기
}
}
return lis.size();
}
매 단계에서 '지금 당장 가장 좋은 선택'을 하는 방식
전체 문제를 한꺼번에 고려하지 않고, 현재 상황에서 최적의 선택을 해서 문제를 해결
문제의 해결법이 그리디 알고리즘이려면
T가 그래프 G의 모든 정점을 포함하는 부분 그래프로 트리이면(cycle 없으면), T는 G의 신장 트리이다.

신장 트리의 간선의 개수는 무조건 V - 1개. 그 이상이면 cycle이 생기고, 그 이하이면 모든 정점을 포함하지 못 한다.
이 신장 트리 중 간선의 가중치의 총합이 가장 작은 트리를 최소 신장 트리라 함

최소 신장 트리를 만드는 알고리즘(그리디) O(ElogV)
시작 정점을 신장트리에 넣고, 신장트리에 있는 정점으로부터 가중치가 가장 작은 정점들(신장트리에는 없는)을 신장트리에 넣으면 된다.

그리디로 풀리는 이유:
그리디는 현재 MST에 들어있는 정점들의 간선들 중 최소가중치인 간선을 택함
(V, To, w) 현재 w가 가장 최소 가중치.

그리디가 틀렸다고 가정해보자. 즉, 이 간선을 택하지 않는 것이 MST를 만드는 방법
그렇다면, To는 현재 MST에 있는 정점이 아니라 아직 MST에 포함되지 않은 다른 정점(V')으로부터 이어져야 함.
왜냐하면, MST에 있는 다른 정점에서 이어진다면 가중치가 w보다 높으므로 애초에 MST가 안 됨.

그럼 나중에 V'도 MST에 추가가 될 것이고, 그 때 To가 연결되며 MST에 들어올 것이다.
하지만 그림에서도 볼 수 있듯이, w'은 w보다 큼.
애초에 처음 MST에서 가장 작은 가중치가 w였기 때문.
즉, C->V'->To가 아니라 E->To->V'로 연결되어야 MST가 됨.
따라서 모순이 발생하므로 그리디로 푸는 것이 옳다
using namespace std;
const int MAX = 100;
const int INF = 1000000000;
int cost[MAX][MAX]; // 인접 행렬: 간선의 비용 저장
bool visited[MAX]; // 방문 여부
struct comp {
bool operator() (const vector<int>& a, const vector<int>& b) {
return a[2] > b[2];
}
};
int main() {
int V = 5; // 정점의 개수
// 간선 비용 초기화 (없는 간선은 INF)
for (int i = 0; i < V; i++) {
for (int j = 0; j < V; j++) {
cost[i][j] = INF;
}
}
// 예시 간선들 (무방향 그래프)
cost[0][1] = cost[1][0] = 2;
cost[0][3] = cost[3][0] = 6;
cost[1][2] = cost[2][1] = 3;
cost[1][3] = cost[3][1] = 8;
cost[1][4] = cost[4][1] = 5;
cost[2][4] = cost[4][2] = 7;
cost[3][4] = cost[4][3] = 9;
// {from, to, weight}. 가중치 기준 minHeap
priority_queue < vector<int>, vector<vector<int>>, comp> mQ;
for (int i = 0; i < V; i++) {
if (cost[0][i] != INF)
mQ.push({0, i, cost[0][i]}); // 0번 정점의 간선으로 시작
}
visited[0] = true;
vector<int> top;
int from, to;
int E = 0;
while (E < V - 1) { // 간선 V - 1개 생성
top = mQ.top();
mQ.pop();
if (visited[top[1]])
continue;
from = top[0];
to = top[1];
visited[to] = true;
for (int i = 0; i < V; i++) {
if (cost[to][i] != INF)
mQ.push({ to, i, cost[to][i] }); // 해당 정점의 간선들 후보로 추가
}
E++;
}
return 0;
}
마찬가지로 그리디 방식
프림 알고리즘은 정점을 기준으로 MST를 만들었다면, 크루스칼은 간선 기준.
![]() | (2, 6, 5)의 경우 이미 연결되어 있어 건너띔![]() |
|---|
cycle이 생기는 지 확인하는 법은 서로소 집합을 사용.
단순 방문 정보로는 cycle을 확인할 수 없기 때문
정점 추가 시 서로 다른 집합의 정점이면 서로소 집합 합쳐줌
시간 복잡도: 간선 정렬에 O(ElogE)가 소요되고, Union-Find 연산은 O(α(V))로 매우 빠름. O(ElogE) 수준이다.
using namespace std;
// 간선 정보를 담는 구조체
struct Edge {
int u, v, weight;
// 간선 정렬 (가중치 오름차순)
bool operator<(const Edge& other) const {
return weight < other.weight;
}
};
// 서로소 집합에서 부모 찾기 (경로 압축)
int findParent(vector<int>& parent, int x) {
if (parent[x] == x) return x;
return parent[x] = findParent(parent, parent[x]); // 부모 바로 찾기 가능
}
// 서로소 집합 합치기 (union)
bool unionParent(vector<int>& parent, int a, int b) {
a = findParent(parent, a);
b = findParent(parent, b);
if (a == b) return false; // 이미 같은 집합이라 사이클 생김
if (a < b) parent[b] = a;
else parent[a] = b;
return true;
}
// 크루스칼 알고리즘 함수
int kruskal(int n, vector<Edge>& edges) {
vector<int> parent(n);
for (int i = 0; i < n; ++i)
// 초기에는 각 정점이 자신의 서로소집합에 속함
// 그리고 그 집합의 루트는 자기 자신
parent[i] = i;
sort(edges.begin(), edges.end()); // 가중치 기준 오름차순 정렬
int mst_weight = 0; // MST 가중치 합 계산용
for (auto& edge : edges) {
if (unionParent(parent, edge.u, edge.v)) {
mst_weight += edge.weight;
}
}
return mst_weight;
}
int main() {
int n = 4; // 정점 수
vector<Edge> edges = {
{0, 1, 1},
{0, 2, 4},
{1, 2, 2},
{1, 3, 3},
{2, 3, 5}
};
int result = kruskal(n, edges);
cout << "최소 신장 트리의 가중치 합: " << result << "\n";
return 0;
}