본문 바로가기
알고리즘/메모

Segment Tree, LIS

by sun__ 2019. 8. 18.

https://www.acmicpc.net/problem/2042

 

2042번: 구간 합 구하기

첫째 줄에 수의 개수 N(1 ≤ N ≤ 1,000,000)과 M(1 ≤ M ≤ 10,000), K(1 ≤ K ≤ 10,000) 가 주어진다. M은 수의 변경이 일어나는 회수이고, K는 구간의 합을 구하는 회수이다. 그리고 둘째 줄부터 N+1번째 줄까지 N개의 수가 주어진다. 그리고 N+2번째 줄부터 N+M+K+1번째 줄까지 세 개의 정수 a, b, c가 주어지는데, a가 1인 경우 b번째 수를 c로 바꾸고 a가 2인 경우에는 b번째 수부터 c번째 수까지의

www.acmicpc.net

가장 기본문제.

 

구간의 합,최댓값,최소값 등, 원소간 결합,교환법칙이 성립하는 연산에대해 구간의 연산을 구할 수 있는 알고리즘.

LIS(최소증가하는수열)을 구할때도 O(nlogn)에 가능함.

 

필요한 것:

 : 원소의 수보다 큰 2의 거듭제곱 값의 두배의 크기를 갖는 세그먼트트리 배열. (루트 idx=1부터)

 ex)원소의 수가 10이면 이것보다 큰 2의 거듭제곱 값은 16이므로 배열의 크기는 32가 된다.

 

update : O(log n)

construct : O(n)

val : O(log n) 

//원소의 수가 1000000이라면 트리배열 가장 마지막줄은 1<<20(1248576)개의 원소가 들어가면 됨.
const int MAX = 1 << 21; //트리배열의 크기는 그것의 두배

int arr[MAX];

void update(int i, int val) {
	i += (MAX / 2);
	arr[i] = val;
	while (i > 1) {
		i /= 2;
		//구간 최대값이면 max(arr[i*2],arr[i*2+1])
		arr[i] = arr[i * 2] + arr[i * 2 + 1];
	}
}

void construct() {
	for (int i = MAX / 2 - 1; i > 0; --i)
		arr[i] = arr[i * 2] + arr[i * 2 + 1];
}

int val(int s, int e, int node, int ns, int ne) {
	if (e < ns || ne < s) return 0;
	if (s <= ns && ne <=eR) return arr[node];
	int mid = (ns + ne) / 2;
    //구간최댓값이나 최소값이면 밑 문장이 바뀌겠져
	return val(s, e, node * 2, ns, mid) + val(s, e, node * 2 + 1, mid + 1, ne);
}
int val(int s, int e){
	return val(s,e,1,0,MAX/2-1);
}    

 

 

 

LIS(가장긴증가하는부분수열)의 길이는 다음과 같이 구한다.

같은 값을 같는 원소들에 대해 나중에 입력된 값을 먼저 처리하는 이유는,  먼저 입력된 값을 먼저 처리하면 값을 갱신해서 원하는 값보가 큰 값이 나오게 되기 때문임.

//update, construct, val함수를 구간 최댓값으로 변경한 상태에서

int main() {
	int n; cin >> n;

	pair<int, int> p[1000001];
	for (int i = 0,a; i < n; i++) {
		cin >> a;
		p[i] = { a,i };
	}
    
    //가장 작은 값을 앞에 오도록 정렬, 같은 값이면 나중에 입력된 값이 앞에 오도록 정렬
	sort(p, p + n, [](pair<int, int> p, pair<int, int> q) {
		if (p.first != q.first) return p.first < q.first;
		return p.second > q.second;
	});

	//정렬한 순서대로 원소들을 방문하면서 전체 구간의 최대값을 갱신시켜주면
	for (int i = 0; i < n; i++) {
		int temp = val(0, p[i].second, 1, 0, = MAX/2 - 1) + 1;
		update(p[i].second, temp);
	}
    
    //루트노드에 LIS의 길이가 저장된다.
	cout << arr[1] << '\n';
}

 


좀더 간단히 구하는 방법

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
	int n;
	while (scanf("%d",&n)>0) {
		vector<int> last(1, 0);
		for (int i = 0, cur; i < n; ++i) {
			scanf("%d", &cur);
			if (last.back() < cur) last.push_back(cur);
			else *lower_bound(last.begin(), last.end(), cur) = cur;
		}
		printf("%d\n", last.size() - 1);
	}
}

 

 

<설명>

{5,6,7,1,2}의 LIS는 {5,6,7}이다. 반면에 길이가 2인 부분 증가 수열은 {5,6}, {5,7}, {1,2}이다.

이 셋 중 다음 LIS의 후보로 유력한 것은 {1,2}이다.

 

c[i] = 지금까지 만든 부분 배열이 갖는 길이 i인 증가 부분수열 중 최소의 마지막 값.

이걸 유지하면

O(n*k),,(k는 LIS의 길이)로 구현할 수 있다.

 

위 코드는 c[i]가 순증가한다는 사실을 이용해서 이분탐색을 한 것.

'알고리즘 > 메모' 카테고리의 다른 글

벨만포드, SPFA  (0) 2019.08.18
다익스트라  (0) 2019.08.18
에라토스테네스, 소수  (0) 2019.08.18
Union Find (=disjoint set)  (0) 2019.08.18
GCD 함수  (0) 2019.08.18