[문제]

우리는 스마트폰을 사용하면서 여러 가지 앱(App)을 실행하게 된다. 대개의 경우 화면에 보이는 ‘실행 중’인 앱은 하나뿐이지만 보이지 않는 상태로 많은 앱이 '활성화'되어 있다. 앱들이 활성화 되어 있다는 것은 화면에 보이지 않더라도 메인 메모리에 직전의 상태가 기록되어 있는 것을 말한다. 현재 실행 중이 아니더라도 이렇게 메모리에 남겨두는 이유는 사용자가 이전에 실행하던 앱을 다시 불러올 때에 직전의 상태를 메인 메모리로부터 읽어 들여 실행 준비를 빠르게 마치기 위해서이다.

하지만 스마트폰의 메모리는 제한적이기 때문에 한번이라도 실행했던 모든 앱을 활성화된 채로 메인 메모리에 남겨두다 보면 메모리 부족 상태가 오기 쉽다. 새로운 앱을 실행시키기 위해 필요한 메모리가 부족해지면 스마트폰의 운영체제는 활성화 되어 있는 앱들 중 몇 개를 선택하여 메모리로부터 삭제하는 수밖에 없다. 이러한 과정을 앱의 ‘비활성화’라고 한다.

메모리 부족 상황에서 활성화 되어 있는 앱들을 무작위로 필요한 메모리만큼 비활성화 하는 것은 좋은 방법이 아니다. 비활성화된 앱들을 재실행할 경우 그만큼 시간이 더 필요하기 때문이다. 여러분은 이러한 앱의 비활성화 문제를 스마트하게 해결하기 위한 프로그램을 작성해야 한다

현재 N개의 앱, A1, ..., AN이 활성화 되어 있다고 가정하자. 이들 앱 Ai는 각각 mi 바이트만큼의 메모리를 사용하고 있다. 또한, 앱 Ai를 비활성화한 후에 다시 실행하고자 할 경우, 추가적으로 들어가는 비용(시간 등)을 수치화 한 것을 ci 라고 하자. 이러한 상황에서 사용자가 새로운 앱 B를 실행하고자 하여, 추가로 M 바이트의 메모리가 필요하다고 하자. 즉, 현재 활성화 되어 있는 앱 A1, ..., AN 중에서 몇 개를 비활성화 하여 M 바이트 이상의 메모리를 추가로 확보해야 하는 것이다. 여러분은 그 중에서 비활성화 했을 경우의 비용 ci의 합을 최소화하여 필요한 메모리 M 바이트를 확보하는 방법을 찾아야 한다.

 

[입력]

입력은 3줄로 이루어져 있다. 첫 줄에는 정수 N과 M이 공백문자로 구분되어 주어지며, 둘째 줄과 셋째 줄에는 각각 N개의 정수가 공백문자로 구분되어 주어진다. 둘째 줄의 N개의 정수는 현재 활성화 되어 있는 앱 A1, ..., AN이 사용 중인 메모리의 바이트 수인 m1, ..., mN을 의미하며, 셋째 줄의 정수는 각 앱을 비활성화 했을 경우의 비용 c1, ..., cN을 의미한다

단, 1 ≤ N ≤ 100, 1 ≤ M ≤ 10,000,000이며, 1 ≤ m1, ..., mN ≤ 10,000,000을 만족한다. 또한, 0 ≤ c1, ..., cN ≤ 100이고, M ≤ m1 + m2 + ... + mN이다.

 

[출력]

필요한 메모리 M 바이트를 확보하기 위한 앱 비활성화의 최소의 비용을 계산하여 한 줄에 출력해야 한다.

 

[예제]


>문제 풀이

 유명한 배낭 알고리즘을 이용해서 풀어야하는 문제였습니다.

* 변수 설명

N // 입력되는 앱의 개수

M //필요한 메모리의 양

App apps[]; //앱을 저장할 객체 배열 App(int memory, int cost)

int dp[]; //dp[i]: 비용 i를 사용할때 비울 수 있는 최대 메모리

* 로직

for ( int i: 0~N ) {
      for( int j: costSum~apps[i].cost ){
            dp[j]= Math.max( dp[j], dp[j-apps[i].cost]+apps[i].mem );
      }
}

cost로 만들 수 있는 최대 메모리를 매번 구할 수 없기 때문에 dp 배열에 memoization해줍니다.

mem / cost(→) j-i j
dp[cost]
:cost로 만들수 있는 최대 메모리
dp[j-i] dp[j]=??

dp[j]= Math.max( dp[j], dp[j-apps[i].cost] + apps[i].mem );

dp[j]에 dp[j] 값이랑 ( j비용에서 i번째 앱의 비용을 뺀 비용으로 만들 수 있는 최대 메모리) + i번째 앱의 메모리큰 값을 넣습니다.

 

>전체 코드

 

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
import java.io.IOException;

public class Main {
	
	public static void main(String [] args) throws IOException {
		int N, M, costSum=0;   //N: input 개수, M: 원하는 메모리값
		App apps[]; //input
		int dp[];   //cost가 i일때 최대 메모리
		
		BufferedReader br= new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st= new StringTokenizer(br.readLine());
		StringBuilder sb= new StringBuilder();
		
        //1. 입력받기
		N= Integer.parseInt(st.nextToken());
		M= Integer.parseInt(st.nextToken());
		apps=new App[N];
		//1-1. set Mem
		st= new StringTokenizer(br.readLine());
		for(int i=0; i<N; i++) {
			apps[i]= new App(0, 0);
			apps[i].setMemory(Integer.parseInt(st.nextToken()));
		}
		//1-2. set Cost
		st= new StringTokenizer(br.readLine());
		for(int i=0; i<N; i++) {
			apps[i].setCost(Integer.parseInt(st.nextToken()));
			costSum+= apps[i].getCost();
		}
		
		dp= new int[costSum+1];
		
		//2. napsack 알고리즘
		for(int i=0; i<N; i++) {
			for(int j=costSum; j>=apps[i].getCost(); j--) {
				dp[j]= Math.max(dp[j], dp[j-apps[i].getCost()]+apps[i].getMemory());
			}
		}
		
		//3. M이상 메모리를 확보하기 위한 비용 i 출력
		for(int i=0; i<=costSum; i++) {
			if(dp[i]>=M) {
				System.out.println(i);
				break;
			}
		}
	}
	
	static class App{
		int memory;
		int cost;
		public App(int memory, int cost) {
			this.memory = memory;
			this.cost = cost;
		}
		public int getMemory() {
			return memory;
		}
		public void setMemory(int memory) {
			this.memory = memory;
		}
		public int getCost() {
			return cost;
		}
		public void setCost(int cost) {
			this.cost = cost;
		}
	}//class App
}

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

 

7579번: 앱

입력은 3줄로 이루어져 있다. 첫 줄에는 정수 N과 M이 공백문자로 구분되어 주어지며, 둘째 줄과 셋째 줄에는 각각 N개의 정수가 공백문자로 구분되어 주어진다. 둘째 줄의 N개의 정수는 현재 활

www.acmicpc.net

 

[문제]

최대 K가지의 서로 다른 색을 표현할 수 있는 전구들이 있다. 이 전구 N개를 다음의 그림과 같이 한 줄로 배치하여 서로 연결한다. (동그라미 안의 숫자는 전구의 색을 의미한다)

각 전구는 스위치가 있어서 전구의 색을 임의의 색으로 바꿀 수 있다. 하나의 전구 색을 바꾸는 경우에는, 색이 바뀌는 전구에 인접한 전구가 같은 색이면, 이 전구의 색도 같이 바뀌게 되며 인접한 전구가 다른 색이 나올 때까지 계속 바뀌게 된다. 예를 들어, 위의 그림에서 4번 전구의 색을 2번 색으로 바꾸면, 5번 전구가 4번 전구와 같은 색이었으므로 2번 색으로 바뀌고, 6번 전구도 5번 전구와 같은 색이었으므로 2번 색으로 바뀌게 된다. 즉, 4번 전구의 색을 2번 색으로 바꾸면, 연결된 같은 색의 모든 전구인 4, 5, 6번의 전구가 2번 색으로 바뀌게 되어 아래의 그림과 같이 된다.

전구의 수 N과 N개의 전등에 대한 초기 색이 주어질 때, 모든 전구의 색이 하나로 같아질 때까지 최소 몇 번 전구의 색을 바꾸어야 하는지를 구하는 프로그램을 작성하시오. 단, 전구의 각 색은 1부터 K까지의 정수로 나타낸다.

 

[입력]

입력의 첫 번째 줄에는 전구의 수를 나타내는 양의 정수 N과 전구가 표현할 수 있는 색의 수 K가 주어진다. 단, N은 1이상 200이하의 정수이며, K는 1이상 20이하의 정수이다. 두 번째 줄에는 N개 전구의 색이 전구번호의 순서대로 하나의 정수로 하나의 빈칸을 사이에 두고 주어진다.

 

[출력]

첫째 줄에 모든 전구의 색이 하나로 같아질 때까지 전구의 색을 바꾸는 횟수의 최솟값을 하나의 정수로 출력한다.

 

[예제]


>문제 풀이

 전구의 색을 바꿔서 모든 전구가 같은 색이 되게끔 만들건데, 같은 색의 전구가 붙어있는 경우 하나의 전구 색을 바꾸면 연속되는 같은 색의 전구 역시 색이 바뀝니다.

"앞에서부터 색을 바꿔간다." 혹은 "뒤에서 부터 색을 바꿔간다." 이런 패턴이 아니라 중간의 어디의 값에서 부터 바꿔나간 횟수가 최소일 수 있습니다.

그래서 이 문제를 풀기 위해 기저 부분까지 들어가서 dp[i][j]의 값을 채워나가는 분할정복 방법을 사용하였습니다.

Q. 완전탐색 가능??

A. N이 200까지 K는 20까지 들어오기 때문에 시간초과가 납니다.

>> 완전탐색의 시간 복잡도는 O(N!)인데, 분할정복을 사용하면 O(N^3)으로 해결할 수 있습니다.

Q. 왜 O(N!) 인가요?

A. 최악의 경우: 입력으로 들어온 N개의 색이 모두 다르다고 생각해보면, N개중에 하나가 전구 색이 바뀌고, 이제 남은 N-1개의 전구 중 하나를 색을 바꿀 것이고 , 남은 N-2의 전구 중 하나의 색을 바꾸고 ~~

결국 N * (N-1) * ... * 2 * 1= N! 을 해야합니다.

저도 처음에 공부할 때 분할정복으로 전구 색이 바뀌는게 한번에 이해가 안되어서, 그림으로 표현해보았습니다. 아래 코드의 3번이 어떻게 동작하는지 그림과 함께 보면 이해가 쉬울수도..!

	public static int func(int left, int right) {
		//1. 기저점=전구 자기자신
		if(left==right) return 0;
		
        //2. 이미 구한 dp값 존재
		if(dp[left][right]!=0) {
			return dp[left][right];
		}
		
        //3. dp[][] 값을 찾아서 넣어준다.
		int tmp=0;
		dp[left][right]=Integer.MAX_VALUE;
		for(int i=left; i<right; ++i) {
            //3-1. 양쪽 구간의 색이 같은 경우: 0 vs 다른 경우: 1
			tmp= bulb[left]!=bulb[i+1]? 1 : 0;
			dp[left][right]= Math.min(dp[left][right], func(left, i)+func(i+1, right)+tmp);
		}
		return dp[left][right];
	}

 

 

>전체 코드

 

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
import java.io.IOException;

public class Main {
	static int N, K;
	static int bulb[], dp[][];
	
	public static void main(String [] args) throws IOException {
		BufferedReader br= new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st= new StringTokenizer(br.readLine());
		
        //1. 입력받기
		N= Integer.parseInt(st.nextToken());
		K= Integer.parseInt(st.nextToken());
		bulb=new int[N];
		dp= new int[N][N];
		
		st= new StringTokenizer(br.readLine());
		for(int i=0; i<N; i++) {
			bulb[i]= Integer.parseInt(st.nextToken());
		}
		
		System.out.println(func(0, N-1));
	}
	
	public static int func(int left, int right) {
		//1. 기저점=전구 자기자신
		if(left==right) return 0;
		
        //2. 이미 구한 dp값 존재
		if(dp[left][right]!=0) {
			return dp[left][right];
		}
		
        //3. 기저부터 dp[][] 값을 찾아서 넣어준다.
		int tmp=0;
		dp[left][right]=Integer.MAX_VALUE;
		for(int i=left; i<right; ++i) {
            //3-1. 양쪽 구간의 색이 같은 경우: 0 vs 다른 경우: 1
			tmp= bulb[left]!=bulb[i+1]? 1 : 0;
			dp[left][right]= Math.min(dp[left][right], func(left, i)+func(i+1, right)+tmp);
		}
		return dp[left][right];
	}
}

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

 

2449번: 전구

입력의 첫 번째 줄에는 전구의 수를 나타내는 양의 정수 N과 전구가 표현할 수 있는 색의 수 K가 주어진다. 단, N은 1이상 200이하의 정수이며, K는 1이상 20이하의 정수이다. 두 번째 줄에는 N개 전

www.acmicpc.net

[문제]

수 N개가 주어졌을 때, i번째 수부터 j번째 수까지 합을 구하는 프로그램을 작성하시오.

 

[입력]

첫째 줄에 수의 개수 N과 합을 구해야 하는 횟수 M이 주어진다. 둘째 줄에는 N개의 수가 주어진다. 수는 1,000보다 작거나 같은 자연수이다. 셋째 줄부터 M개의 줄에는 합을 구해야 하는 구간 i와 j가 주어진다.

 

[출력]

총 M개의 줄에 입력으로 주어진 i번째 수부터 j번째 수까지 합을 출력한다.

 

[제한사항]

1 ≤ N ≤ 100,000

1 ≤ M ≤ 100,000

1 ≤ i ≤ j ≤ N

[예제]

예제 입력1 예제 입력2
5 3
5 4 3 2 1
1 3
2 4
5 5
12
9
1

 


>문제 풀이

시간 복잡도를 먼저 살펴보겠습니다.

입력 N개에 대한 구간합을 구하려면 O(N)

구간이 M개가 입력되면 O(N*M)

입력되는 N과 M의 범위 1~100,000

그러면 백억...!ㄷㄷ

>>그래서 이렇게 풀면 안됩니다.

이 문제를 해결하기 위해 dp를 사용할건데, dp[N+1]배열에 구간합을 저장할 것입니다.

dp[k]= arr[1]~arr[k]까지 저장,

이렇게 되면 i~j까지의 구간합을 구하려면 dp[j]-dp[i-1]을 계산해주면 됩니다.

시간복잡도는 한번만 계산해서 저장해놓고 사용하면 되기 때문에 O(N)

ex) a~b 구간합을 구한다고 할 때, = dp[b] - dp[a-1];

 

>전체 코드

 

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.StringTokenizer;

public class Main {
	
	public static void main(String[] args) throws IOException {
		BufferedReader br= new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st= new StringTokenizer(br.readLine());
		
		//1. 입력
		int N= Integer.parseInt(st.nextToken());
		int M= Integer.parseInt(st.nextToken());
		int[] arr=new int[N+1];
		int[] dp= new int[N+1];
		
		st= new StringTokenizer(br.readLine());
		for(int i=1; i<=N; i++) {
			arr[i]= Integer.parseInt(st.nextToken()); // arr배열 값을 입력받음과 동시에
			dp[i]=dp[i-1]+arr[i];  // dp[]에 누적합 저장
		}
		
		int a, b;
		StringBuilder sb= new StringBuilder();
		for(int i=0; i<M; i++) {
			st= new StringTokenizer(br.readLine());
			a= Integer.parseInt(st.nextToken());
			b= Integer.parseInt(st.nextToken());
			sb.append(dp[b]-dp[a-1]+"\n");
		}
		System.out.println(sb.toString());
	}//main	
}

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

 

11659번: 구간 합 구하기 4

첫째 줄에 수의 개수 N과 합을 구해야 하는 횟수 M이 주어진다. 둘째 줄에는 N개의 수가 주어진다. 수는 1,000보다 작거나 같은 자연수이다. 셋째 줄부터 M개의 줄에는 합을 구해야 하는 구간 i와 j

www.acmicpc.net

 

[시간 제한]

Java 8: 2.5 초

Java 8 (OpenJDK): 2.5 초

Java 11: 2.5 초

Kotlin (JVM): 2.5 초

 

[문제]

N(2 ≤ N ≤ 100,000)개의 정점으로 이루어진 트리가 주어진다. 트리의 각 정점은 1번부터 N번까지 번호가 매겨져 있으며, 루트는 1번이다.

두 노드의 쌍 M(1 ≤ M ≤ 100,000)개가 주어졌을 때, 두 노드의 가장 가까운 공통 조상이 몇 번인지 출력한다.

 

[입력]

첫째 줄에 노드의 개수 N이 주어지고, 다음 N-1개 줄에는 트리 상에서 연결된 두 정점이 주어진다. 그 다음 줄에는 가장 가까운 공통 조상을 알고싶은 쌍의 개수 M이 주어지고, 다음 M개 줄에는 정점 쌍이 주어진다.

 

[출력]

M개의 줄에 차례대로 입력받은 두 정점의 가장 가까운 공통 조상을 출력한다.

 

[예제]

예제 입력1 예제 출력1
15
1 2
1 3
2 4
3 7
6 2
3 8
4 9
2 5
5 11
7 13
10 4
11 15
12 5
14 7
6
6 11
10 9
2 6
7 6
8 13
8 15
2
4
2
1
3
1

 


>문제 풀이

LCA: Lowest Common Ancestor

최소 공통 조상 알고리즘에 대한 문제입니다.

문제의 예제를 트리로 만들어 놓고 이 트리를 기준으로 LCA에 대해 설명을 하자면,

어떤 노드 u, v에 대해 depth가 더 깊은 노드를 조상노드로 올리고 올려 depth를 맞추고, 그 위에 공통으로 가지는 부모 노드를 찾는 것입니다.

말로 하는 것보다 그림으로 보는게 더 직관적으로 이해가 가기 때문에, 그림으로 살펴보겠습니다.

u와 v를 보면 depth가 같지 않습니다.

u를 v의 depth와 같아질 때까지 부모노드를 타고 올라갑니다.

그러면, parent[u]= 4 와 v를 비교하게 됩니다.

이 두 정점의 공통 부모는 2번째 노드가 되는걸 알 수 있습니다.

그럼 u와 v를 기준으로 한 층씩 올라가며 부모노드를 찾아주면 될까요??

문제에서는 N이 100,000 M이 100,000까지 들어올 수 있습니다.

만약 트리가 한쪽으로 치우치기라도 한다면 정점 u, v에 대해, 한번씩 부모 노드로 옮겨가며 lca를 찾게되는데, 최악의 경우 O(N)의 연산이 필요합니다.

그리고 M개의 케이스가 들어오면 O(M*N) 이 되어버려서 시간초과가 발생할 수 있습니다.

이를 해결하기 위해 탐색의 방법을 살펴봐야하고, dp로 정점 V에 대한 K번째 부모노드 값을 메모이제이션을 해줄 필요가 있습니다.

탐색의 시간을 줄이는 것은 어떻게 효과적으로 트리를 접어갈 수 있냐는 것입니다.

이 코드에서는 부모 노드를 나타내기 위해 parent [ ] [ ] 2차원 배열을 사용했습니다.

* parent[K][V]: 노드 V의 2^K번째 부모 노드의 번호

그리고 이 parent[i][j] 배열을 채워줘야 하는데, 아래의 표를 보면 알 수 있듯이

parent[i][j]= parent[i-1][parent[i-1][j]] 의 점화식이 성립합니다.

*** 로직 ***

1) 들어오는 두 정점의( u, v ) depth를 비교하기 위해

dfs로 depth를 구하여 depth[N+1] 배열에 담아주었습니다.

2) 2^K 점프를 통해 두 정점의 depth를 맞춰주고

더 깊은 depth를 가진 정점 u를 2^k점프하여 (depth[u]- depth[v]) 보다 작거나 같게 맞춰줍니다.

3) 공통 부모 바로 아래까지 올려줍니다.

그러면 정점 u와 v의 부모는 lca(최소 공통 부모) 니까

return parent[0][u];

 

>전체 코드

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.StringTokenizer;


public class Main {
	static int N, M, K; //N:정점개수, M:TC개수, K
	
	//LCA 관련 변수
	static int[] depth;
	static int[][] parent; // parent[K][V] 정점 V의 2^K번째 조상 정점 번호
						   // parent[K][V]= parent[K-1][parent[K-1][V]];
	
	//Tree 변수
	static ArrayList[] tree;
	
	public static void main(String[] args) throws IOException {
		BufferedReader br= new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st;
		
		//1. 입력
		N= Integer.parseInt(br.readLine());
		
		//2^K>N인 K찾기
		K=0;
		for(int i=1; i<=N; i*=2) {
			K++;
		}
		
		//LCA 관련 변수 초기화
		depth= new int[N+1];
		parent= new int[K][N+1];
		
		//tree 변수 초기화
		tree= new ArrayList[N+1];
		for(int i=1; i<=N; i++) {
			tree[i]= new ArrayList<Integer>();
		}
		
		int a, b;
		for(int i=1; i<=N-1; i++) { //입력받고 트리 만들기
			st= new StringTokenizer(br.readLine());
			a= Integer.parseInt(st.nextToken());
			b= Integer.parseInt(st.nextToken());
			
			//양방향 간선
			tree[a].add(b);
			tree[b].add(a);
		}
		
		//2. Depth 확인
		dfs(1, 1);
		
		//3. 2^N 까지 parent 채우기
		fillParent();
		
		//4. LCA 진행
		M= Integer.parseInt(br.readLine());
		StringBuilder sb= new StringBuilder();
		for(int i=1; i<=M; i++) {
			st= new StringTokenizer(br.readLine());
			a= Integer.parseInt(st.nextToken());
			b= Integer.parseInt(st.nextToken());
			
			sb.append(lca(a, b)+"\n");
		}
		
		System.out.println(sb.toString());
		
	}// main
	
	//Depth를 구함
	public static void dfs(int node, int cnt) {
		//1. depth를 기록
		depth[node]= cnt;
		
		//2. 자식들의 depth를 기록
		int len= tree[node].size(); //자식 몇개인지
		for(int i=0; i<len; i++) {
			int next= (Integer)tree[node].get(i);
			//아직 깊이를 모르면 -> 미방문 노드
			if(depth[next]==0) {
				dfs(next, cnt+1);
				parent[0][next]= node; //next 노드의 2^0= 첫번째 부모를 기록
			}
		}
	}
	
	//부모 채우기
	static void fillParent() {
		for(int i=1; i<K; i++) {
			for(int j=1; j<=N; j++) {
				//j번 노드의 i번째 부모= j의 i-1번째 부모의 i-1번째 부모 
				parent[i][j]= parent[i-1][parent[i-1][j]];
			}
		}
	}
	
	//최소 공통 조상
	static int lca(int a, int b) {
		//1. depth[a]>=depth[b] 이도록 조정하기
		if(depth[a]<depth[b]) {
			int temp=a;
			a= b;
			b= temp;
		}
		
		//2. 더 깊은 a를 2^K 점프하여 depth를 맞추기
		for(int i=K-1; i>=0; i--) {
			if(Math.pow(2, i)<= depth[a]- depth[b]) {
				a= parent[i][a];
			}
		}
		
		//3. depth를 맞췄는데 같다면 종료
		if(a==b) return a;
		
		//4. a-b는 같은 depth이므로 2^K 점프하며 공통 부모 바로 아래까지 올리기
		for(int i=K-1; i>=0; i--) {
			if(parent[i][a]!= parent[i][b]) {
				a= parent[i][a];
				b= parent[i][b];
			}
		}
		return parent[0][a];
	}//lca	
}

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

 

11438번: LCA 2

첫째 줄에 노드의 개수 N이 주어지고, 다음 N-1개 줄에는 트리 상에서 연결된 두 정점이 주어진다. 그 다음 줄에는 가장 가까운 공통 조상을 알고싶은 쌍의 개수 M이 주어지고, 다음 M개 줄에는 정

www.acmicpc.net

 

[문제]

어떤 N개의 수가 주어져 있다. 그런데 중간에 수의 변경이 빈번히 일어나고 그 중간에 어떤 부분의 합을 구하려 한다. 만약에 1,2,3,4,5 라는 수가 있고, 3번째 수를 6으로 바꾸고 2번째부터 5번째까지 합을 구하라고 한다면 17을 출력하면 되는 것이다. 그리고 그 상태에서 다섯 번째 수를 2로 바꾸고 3번째부터 5번째까지 합을 구하라고 한다면 12가 될 것이다.

 

[입력]

첫째 줄에 수의 개수 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(1 ≤ b ≤ N)번째 수를 c로 바꾸고 a가 2인 경우에는 b(1 ≤ b ≤ N)번째 수부터 c(b ≤ c ≤ N)번째 수까지의 합을 구하여 출력하면 된다.

입력으로 주어지는 모든 수는 -263보다 크거나 같고, 263-1보다 작거나 같은 정수이다.

 

[출력]

첫째 줄부터 K줄에 걸쳐 구한 구간의 합을 출력한다. 단, 정답은 -263보다 크거나 같고, 263-1보다 작거나 같은 정수이다.

 

[예제]

 


>문제 풀이

인덱스 트리를 이용하여 풀었습니다.

처음 접하는 알고리즘으로 푸느라 사실 아직 이해를 제대로 했는지 모르겠습니다.

응용 문제가 많다고 하니 개념을 좀 더 살펴 본 후 관련 문제들을 더 풀어보면 좋을 것 같습니다.

인덱스 트리는 구간합을 구해야할 때, 중간에 인덱스의 값이 변하는 상황에서 쓰입니다.

배열로도 구간합을 저장할 수 있지만, 이 경우 특정 인덱스에 담긴 값을 바꿀 경우 배열 전체를 갈아엎어야 합니다..

그래서 이런 경우는 인덱스 트리를 사용하면 되는데, 포화 이진트리의 리프노드에 배열로 받은 값을 넣고 그 위로 구간합을 구하면서 올리는 구조입니다.

처음에 트리를 만들기 위해서는 Bottom-up으로 리프노드부터 위로 올라가는 방법을 쓰는데, 이후 query와(구간합 구하는 메소드) update에서는(특정 인덱스에 담긴 값을 변경) Top-down과 Bottom-up 2가지 방식으로 구현할 수 있습니다.

(알고리즘 수업에서 두 가지 방법으로 모두 구현해 보았지만, 이 문제에서는 top-down으로 제출하였습니다.)

**구현

1. N개의 배열값을 받았다면, 이제 인덱스 트리에 저장해야합니다.

인덱스 트리의 노드 개수는 2의 제곱수-1개 이고 리프 노드에 N개의 값을 저장해야 합니다.

근데 노드 번호는 0부터 시작하는데 연산을 0부터 하도록 구현하기 너무 번거롭습니다. 그래서 node[0]은 남겨두고 1부터 2N까지가 우리가 실제 값을 넣고 사용하는 노드입니다. 즉 트리의 인덱스 N~2N-1까지가 리프노드이고, 여기에 nums[N]을 담아야합니다.

=> N보다 크거나 같은 2의 제곱수가 리프노드의 개수 S이고, 2*S가 트리의 노드 개수 입니다.

ex) N=5 이면 S= 8(=2^3)

2. query ( left, right, node, queryLeft, queryRight)

left: 리프노드의 첫번째 인덱스(=1)

right: 리프노드의 마지막 인덱스(=S)

node: 현재 노드의 번호(Top-down으로 구현했으니 처음에 1로 넘긴다.)

queryLeft: 구하고자하는 구간의 왼쪽 인덱스

queryRight: 구하고자하는 구간의 오른쪽 인덱스

분기점을 3개로 나눠서 구현한다.

(1) (left, right): 현재노드가 담고있는 구간합의 구간과 (queryLeft, queryRight)와 범위가 겹치지 않는 경우

(2) (left, right)와 (queryLeft, queryRight)와 딱 맞는 경우

(3) (left, right)와 (queryLeft, queryRight)이 걸쳐있는 경우

:자식 노드들로 내려가서 필요한 값만 올릴 수 있도록 해야함.

: return query(왼쪽부분) + query(오른쪽 부분)

3. update(left, right, node, target, diff)

: 타겟을 찾아가며 타겟을 포함하는 구간합을 담은 노드에 +diff 처리를 해준다.

: diff 은 c - nums[target]

(c는 업데이트 해야할 값 , nums[target]은 업데이트 전의 값)

 

>전체 코드

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;


public class Main {
	static long nums[], tree[];
	static int N, M, K, S;
	
	public static void main(String[] args) throws IOException {
		BufferedReader br= new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st= new StringTokenizer(br.readLine());
		
		N= Integer.parseInt(st.nextToken());
		M= Integer.parseInt(st.nextToken());
		K= Integer.parseInt(st.nextToken());
		nums= new long[N];
		
		for(int i=0; i<N; i++) {
			nums[i]= Long.parseLong(br.readLine());
		}
		
		S=1;
		while(S<N) {
			S*=2;
		}
		tree= new long[S*2];
		
		initBU();
		
		int a, b;
		long c;
		for(int i=0; i<M+K; i++) {
			st= new StringTokenizer(br.readLine());
			a= Integer.parseInt(st.nextToken());
			b= Integer.parseInt(st.nextToken());
			c= Long.parseLong(st.nextToken());
			
			if(a==1) { //1이면 업뎃
				update(1, S, 1, b, c-nums[b-1]);
				nums[b-1]=c;
			}else if(a==2) { //2면 구간합
				System.out.println(query(1, S, 1, b, c));
			}
		}
		
	}//main
	
	static void initBU() {
		//Leaf에 값 반영
		for(int i=0; i<N; i++) {
			tree[S+i]=nums[i];
		}
		//내부노드 채움
		for(int i=S-1; i>0; i--) {
			tree[i]= tree[i*2]+tree[i*2+1];
		}
	}//init
	
	static long query(int left, int right, int node, int queryLeft, long queryRight) {
		// 연관이 없음
		if(right<queryLeft||queryRight<left) {
			return 0;
		}
		// 현재 노드가 쿼리 범위에 포함됨
		else if(queryLeft<=left&&right<=queryRight) {
			return tree[node];
		}
		// 현재 노드가(정확히는 구간이) 쿼리 범위에 포함은 안되는데, 겹침
		else {
			int mid= (left+right)/2;
			long leftResult= query(left, mid, node*2, queryLeft, queryRight);
			long rightResult= query(mid+1, right, node*2+1, queryLeft, queryRight);
			return leftResult+rightResult;
		}
	}//query
	
	static void update(int left, int right, int node, int target, long diff) {
		//연관없음
		if(target<left||right<target) return;
		//연관 있음
		tree[node]+=diff;
		if(left!=right) {
			int mid= (left+right)/2;
			update(left, mid, node*2, target, diff);
			update(mid+1, right, node*2+1, target, diff);
		}
	}//update
}

+ Recent posts