구간 합: 합 배열을 이용해 시간 복잡도를 더 줄이기 위해 사용하는 특수한 목적의 알고리즘입니다.

 

1. 합 배열 S 정의

S[i] = A[0] + A[1] + A[2] + ... + A[i-1] + A[1]

합 배열은 기존 배열을 전처리한 배열입니다.

이렇게 합 배열을 구해놓으면 기존 배열의 일정 범위 합을 구하는 시간 복잡도가 O(N)에서 O(1)로 감소합니다.

 

예를들어

인덱스 0 1 2 3 4 5
배열 A 15 13  10 7 3 12
합 배열 S 15 28 38 45 48 60

A[i]부터 A[j]까지의 배열을 합 배열 없이 구하는 경우, 최악의 경우는 i가 0이고 j가 N인 경우로 시간 복잡도는 O(N)입니다.

이런 경우 앞에서 알아본 합 배열을 사용하면 O(1)안에 답을 구할 수 있습니다.

이는 아래와같은 공식으로 만들 수 있습니다.

 

S[i] = S[i-1] + A[i]

 

아래 표에서 A[2]부터 A[5]까지의 구간 합을 합 배열을 통해 구하는 과정을 봅니다.

인덱스 0 1 2 3 4 5
배열 A 15 13 10 7 3 12
S[5]        
S[1]    

 

S[5] = A[0] + A[1] + A[2] + A[3] + A[4] + A[5]

S[1] = A[0] + A[1]

S[5] - S[1] = A[2] + A[3] + A[4] + A[5]

 

S[5] 합 배열 값에서 S[1] 값을 빼면 A[2]부터 A[5]까지의 구간 합을 구할 수 있습니다.

 

2, 구간 합 구하기 (백준 11659번)

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

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

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

 

예제 입력:

5 3
5 4 3 2 1
1 3
2 4
5 5

 

예제 출력:

12
9
1

 

N과 M의 최대가 10만이므로 시간제한 1초 내에 연산을 끝내기위해 합 배열을 사용합니다.

 

package algorithms.boj.array;

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

public class Q11659 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        //  배열길이와 몇개의 구간합을 구할지 입력받는다
        int arrayLength = Integer.parseInt(st.nextToken());
        int count = Integer.parseInt(st.nextToken());

        //  입력받은 배열길이 + 1의 길이의 합배열을 만든다.
        long[] S = new long[arrayLength + 1];
        st = new StringTokenizer(br.readLine());

        //  배열을 입력받으면 바로 합배열로 만들어 저장한다.
        for(int i=1; i<S.length; i++){
            S[i] = S[i-1] + Integer.parseInt(st.nextToken());
        }

        //  구간합을 구해야 하는 길이를 입력받고, 합배열 공식을 이용해 해당 구간합을 구한다.
        for(int i=0; i<count; i++){
            st = new StringTokenizer(br.readLine());
            int j = Integer.parseInt(st.nextToken());
            int k = Integer.parseInt(st.nextToken());

            System.out.println(S[k] - S[j-1]);
        }
    }
}

 

'Algorithm' 카테고리의 다른 글

백준 10448번: 유레카 이론  (0) 2025.04.29
백준 10158번: 개미  (0) 2025.03.04

완전 탐색에 대해 공부하기 위해 처음 주어진 문제가 백준 10448번 유레카 이론 문제였습니다.

삼각수를 구하는 공식을 이용해 입력된 K이 3개의 삼각수의 합으로 표현될 수 있는지를 확인해 1 또는 0을 출력하는 문제입니다.

 

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


 

1. 문제 풀이 접근

우선 가장 처음 문제를 접했을 때 들었던 궁금증입니다.

" 삼각수 공식을 모르는 상태로 이 문제를 받게되면, 어떻게 접근해야 하는가 ? "

다행히도 문제에서는 삼각수 공식이 T(n) = 1 + 2 + 3 + ... + n = n(n+1)/2 라고 알려주고 있습니다.

이것을 모르고 저는 처음에 삼각수 공식을 T(n) = n + T(n-1)로 접근했었는데,

이렇게 작성하고 코드를 짜려고 하니 뒤의 T(n-1)을 기억해가며 코드를 작성해야하는 상황에 놓이게 되었습니다.(재귀적 정의)

 

기본적으로 누적합 공식은 수학과정 중 수열을 배우는 파트에서 배우게 된다는데, 이를 모르고 접근했다고 가정했을 때

n = 1 → 1

n = 2 → 3

n = 3 → 6

...

n = 5 → 15

순으로 나열해가며 유추하는 방법이 있는데, 이런상황에 마주한 것 자체에서 "누적 합이고 공식이 있을 수 있겠다!"라고 앞으로 떠올릴 수 있으면 바람직하게 접근한것으로 보고 다음으로 넘어갑니다.

 

다음은 입력받은 수를 바탕으로 " 입력된 수가 삼각수 세 개의 합으로 표현될 수 있는가? "에 대한 접근입니다.

첫 풀이는 이렇게 접근했습니다.

 

[ 첫 접근 방식 ]

1. 삼각수가 저장된 배열 만들기

2. 입력된 k 미만 중 가장 큰 삼각수 찾기

3. k에서 해당 삼각수를 빼기

4. 2~3을 세 번 반복, 반복 도중 삼각수를 뺀 결과가 0이되면 0 출력

5. 세 번 반복 후, 삼각수를 뺀 결과가 0이면 1 출력

 

이를 바탕으로 코드를 작성하니 문제가 생겼습니다.

2-3번과정 k 미만 가장 큰 삼각수를 찾아 해당 삼각수를 빼기는 " Greedy(탐욕적) " 해결 방식으로 이 방법을 사용하면 3개의 삼각수 합을 만들지 못할 수 있다는 문제였습니다.

k=9 일 때, 9는 삼각수 1, 3, 6 세 개의 삼각수로 조합될 수 있지만,

위 접근방식으로 가장 큰 삼각수 6을 제외하면 9 - 6 = 3, 다음 가장 큰 삼각수 3, 3 - 3 = 0 으로 2개만에 끝나버렸습니다.

 

따라서 " 완전 탐색 "의 의도에 맞게 모든 경우를 확인하는 방식으로 고쳐봅니다.

 

1. 삼각수가 저장된 배열 만들기

2. 입력된 inputNum에 대해 모든 삼각수 배열을 순회하며 i, j, k의 합이 inputNum이 되는지 확인하기

3. 된다면 1을 출력, 안된다면 0을 출력

 


2. 코드 작성하기

변화된 접근방식으로 작성된 코드는 다음과 같습니다.

package algorithms.boj.bruteForce;

import java.util.Scanner;

public class Q10448_2 {

	public static void main(String[] args){
		int[] triangleNumsArray = setTriangleArray();
		Scanner scan = new Scanner(System.in);
		int T = scan.nextInt();

		for(int t = 0; t < T; t++){
			int inputNum = scan.nextInt();
			int ans = triangleTest(inputNum, triangleNumsArray);
			System.out.println(ans);
		}

	}

	//	먼저, 삼각수 배열을 만드는 코드를 작성해본다.
	//	삼각수 공식은 T(n) = n*(n+1)/2 임을 기억하자 !
	public static int[] setTriangleArray(){
		//	그런데 삼각수 배열의 최대 길이는 삼각수가 1000이 넘지 않는 T(n) n만큼의 길이, 따라서 이 n값을 먼저 찾아야 한다.
		int n = 1;
		while(true){
			int triangleNum = n * (n + 1) / 2;
			if(triangleNum > 1000) break;
			n++;
		}

		//	구한 n을 가지고 배열을 선언하고, 해당 배열 안에 삼각수를 저장한다.
		int[] triangleNumsArray = new int[n-1];
		for(int i = 1; i <= triangleNumsArray.length; i++) {
			triangleNumsArray[i-1] = i * (i + 1) / 2;
		}
		return triangleNumsArray;
	}

	public static int triangleTest(int inputNum, int[] triangleNumsArray){
		//	다음, 입력받은 K 값을 이용해 삼각수 세개로 표현이 가능한지 알아보는 완전탐색 for문을 작성한다.
		for(int i = 0; i < triangleNumsArray.length; i++) {
			//	첫번째 삼각수
			for (int j = 0; j < triangleNumsArray.length; j++) {
				//	두번째 삼각수
				for(int k = 0; k < triangleNumsArray.length; k++) {
					//	세번째 삼각수
					if((triangleNumsArray[i] + triangleNumsArray[j] + triangleNumsArray[k]) == inputNum){
						return 1;
					}

				}
			}
		}
		return 0;
	}


}

먼저 삼각수 배열을 만드는 메서드를 실행하고,

입력된 숫자와 만들어진 삼각수 배열을 이용해 삼각수 세 개의 합으로 표현이 되는지 확인 후 1 또는 0을 리턴해 이를 출력하는 방식의 코드입니다.

 

시간복잡도는 O(T(테스트 케이스 갯수) * ((삼각수의 개수) + (삼각수의 개수^3))) = O(T*(N + N^3)) 입니다.

입력값의 범위는 3~1000 입니다.

 

 

작동이 잘 되고, BOJ도 통과되었습니다.


3. 코드 개선하기(다른 풀이방법)

시간복잡도를 확인해보면 테스트 케이스 갯수가 앞에 곱해지는데

모든 삼각수의 개별 합에 대한 배열을 만들어놓고 테스트 케이스가 이 합 배열 안에 있는지 확인만 하면

테스트 케이스때마다 삼각수의 갯수 합을 구하는 반복을 줄일 수 있다.

HashSet<Integer> possibleSums = new HashSet<>();

for(int i = 0; i < triangleNumsArray.length; i++){
    for(int j = 0; j < triangleNumsArray.length; j++){
        for(int k = 0; k < triangleNumsArray.length; k++){
            int sum = triangleNumsArray[i] + triangleNumsArray[j] + triangleNumsArray[k];
            possibleSums.add(sum);
        }
    }
}

for(int t = 0; t < possibleSums.size(); t++){
    int K = scan.nextInt();
    if(possibleSums.contains(K)){
        System.out.println("1");
    }else{
        System.out.println("0");
    }
}

 

코드를 다음과 같이 개선해 추가하면, O(T * N^3)의 복잡도를 O(T + N^3)으로 줄일 수 있다 !

'Algorithm' 카테고리의 다른 글

백준 11659번: 구간 합  (0) 2025.09.06
백준 10158번: 개미  (0) 2025.03.04

단순 문제풀이에 따른 시간초과와 이에 따른 시간복잡도 개선 방법에 대해 생각해볼 수 있는 문제입니다.

 

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


풀이

    public static void solve01(int w, int h, int x, int y, int t) {
        int velX = 1;
        int velY = 1;

        while (t-- > 0){
            if(x == w) velX = -1;
            else if (x == 0) velX = 1;
            if(y == h) velY = -1;
            else if (y == 0) velY = 1;
            x += velX;
            y += velY;
        }
        System.out.println(x + " " + y);
    }

단순하게 가로(w), 세로(h), 현재 좌표 x(x), 현재 좌표 y(y), 진행시간(t)를 매개변수로 받아 풀이를 진행한 코드입니다.

예제 입력에 따른 예제 출력이 맞아 정상적으로 잘 작동하는듯 보입니다.

 

하지만, 이 문제에는 0.15초의 시간 제한이 존재하고, 계산할 시간 t의 범위는 1 <= t <= 200,000,000으로 어마어마한 계산 시간을 입력받게 됩니다.

 

작성한 코드의 시간 복잡도는 반복문 하나로 입력받은 진행시간동안만 실행되면 되므로 O(T)라고 할 수 있습니다.

그런데 입력 시간이 200,000,000 ? 시간 제한 초과를 피할 수 없습니다.

 

꽤나 단순한 문제라고 생각했고, 풀이방법또한 단순해 이 코드에서 어떻게 시간 최적화를 할 수 있을까에 대한 고민이 필요합니다.

특히 어떠한 정렬 알고리즘이 이용된것도 아니기에, 단순 특정 알고리즘에 대한 시간복잡도를 외워 모든 문제를 풀이하는 접근방법으로는 어떠한 방법도 적용시킬 수 없습니다.

 

시간복잡도, 공간복잡도를 떠나 규칙(패턴)을 찾는 방식으로 접근방식을 추가해볼 필요가 있습니다.

 

첫 예제 입력인 좌표 (4, 1)에서 시작했다고 가정합니다.

T=24 지점에서 원점좌표 (4, 1)로 돌아오며, 진행방향도 같은 모습을 볼 수 있습니다.

 

그렇지만, 이런식의 문제에 대해 규칙, 패턴을 찾아야 할 때 항상 그림을 그려 확인할 수는 없습니다.

따라서, 이 또한 코드로 작성해봅니다.

boolean[][] visitedUpperRight = new boolean[h + 1][w + 1];
boolean[][] visitedUpperLeft = new boolean[h + 1][w + 1];
boolean[][] visitedLowerRight = new boolean[h + 1][w + 1];
boolean[][] visitedLowerLeft = new boolean[h + 1][w + 1];

while(t-- > 0){
    x += velX;
    y += velY;

    if(velX == 1 && velY == 1){
        if(visitedUpperRight[x][y]){

        }
    }else if(velX == -1 && velY == 1){
        if(visitedUpperLeft[x][y]){

        }
    }else if(velX == -1 && velY == 1){
        if(visitedLowerRight[x][y]){

        }
    }else if(velX == 1 && velY == -1){
        if(visitedLowerLeft[x][y]){}
    }


}

이런식으로 우상단, 좌상단, 우하단, 좌하단 진행마다 방문했던 좌표일때 특정 코드를 실행하게 하면 될 것 같습니다.

하지만, 여기에도 조심해야할 부분이 있습니다.

 

이미 이렇게 작성한 코드에서 주기를 찾는동안 시간도 메모리도 복잡도가 O(W * H)로 가로와 세로 너비가 2<= W, H <= 40,000임을 생각해본다면 최대 40,000의 가로, 세로 넓이를 입력 예제로 사용할 때 40,000 * 40,000 * 1byte(boolean이므로) = 1,600,000,000 byte의 공간을 사용하게 되어버리고 1.6GB의 배열은 메모리 제한(256MB)을 초과해버리게 됩니다.

 

새로운 접근방법이 필요합니다.

 

어차피 가로, 세로 이동은 서로에 영향을 주지 않으니 가로, 세로를 분리하는 쪽으로 접근해봅니다.

 

가로, 세로 각각 개미는 (2*w) 만큼 움직이면 제자리로 돌아옵니다.(같은 진행방향, 같은 좌표)

그러므로, (2*w)는 계속 반복되므로 생략해서 계산을 단축하고 싶습니다.

이때 모듈로(%: 값으로 나눈 나머지)를 이용하면 됩니다.

 

즉 '0~6까지의 가로축, 0좌표에서 개미가 300시간을 움직인 뒤 어느 좌표에 위치해있나?'를 계산하고 싶다면

주기를 제외한 이동시간 = 300%(2*6) = 300%12 = 0시간 이므로 개미는 제자리인 0좌표에 있음을 알 수 있습니다. 한시간 더 움직였다면 ?

주기를 제외한 이동시간 = 301%(2*6) = 300%12 = 1시간 이므로 0좌표에서 1시간을 더 이동한 1좌표에 개미가 있음을 알 수 있습니다.

이를 x, y좌표에 대해 코드로 아래와 같이 작성할 수 있습니다.

int velX = 1;
int velY = 1;

int timeX = t%(2*w);
int timeY = t%(2*h);

while(timeX-- > 0){
    if(x == w) velX = -1;
    else if(x == 0) velX = 1;
    x += velX;
}

while(timeY-- > 0){
    if(y == h) velY = -1;
    else if(y == 0) velY = 1;
    y += velY;
}

 

주기를 구하는 과정을 생략해버림으로써 시간복잡도가 O(max(w, h)) (w나 h중 최댓값)으로 최적화 되었습니다.

이대로 작성하면 시간제한을 통과할 수 있게 됩니다.

 

추가로, 이 시간복잡도를 O(1)까지 줄여버릴 수 있습니다.

 

지금까지의 방식은 '주기를 제외한 이동시간을 구해 개미의 원래좌표에서 더 이동하는 방식'이었습니다.

이를 '애초에 원래 이동한 좌표까지의 시간까지 더한 시간으로부터 주기를 제외한 이동시간을 개미의 최종좌표로 변환하는 방식'으로 바꾸는 것입니다.

 

개미가 원래 0~6길이의 좌표 위에서 (3, 0) 좌표에 위치해있었다고 가정합니다.

301시간만큼 이동한 좌표는 '(원래 이동한 좌표까지의 시간 = 3 + 301 시간) % (2 * w)'으로 계산하는 것입니다.

304%(2*6) = 304%12 = 4

개미는 (4, 0) 좌표에 있음을 알 수 있습니다.

 

한가지 변수가 있습니다. 개미의 원래 좌표가 (6, 0) 좌표라고 가정해봅니다.

301 시간만큼 이동한 좌표는 같은 방식으로, 307%12 = 7 입니다.

그런데 좌표길이는 0~6이므로 개미가 (7, 0)좌표위에 있을 수는 없습니다.

(6, 0)좌표 보다 더 이동하면 역방향으로 이동해야 하므로

2*w-x로 수식을 고쳐주면, 12-7로 (5, 0) 추가된 1좌표만큼 역방향으로 이동한 값을 얻을 수 있습니다.

 

이를 x,y 좌표 모두 코드로 표현하면

int x = (p + t) % (2 * w);
if (x > w) x = 2 * w - x;

int y = (q + t) % (2 * h);
if (y > h) y = 2 * h - y;

이렇게 바꿔 쓸 수 있습니다.

위처럼 작성하면 최종 시간복잡도는 O(1)로 줄어들게 됩니다.

 

O(T): 최악의 경우 2억 → O(max(w, h)): 최악의 경우 8만 → O(1)으로 최적화한 것 입니다.

 


 

1번에서 2번까지는 어떻게든 시간제한안에 계산을 밀어넣어야 하므로 해결법을 찾는다고 가정해도, 3번 O(1)로 줄이는 것은 확실히 쉽지 않습니다.

우선은 패턴을 찾아 최적화 하는 방법이 있음을 숙지해두는 것이 좋을 것 같습니다.

'Algorithm' 카테고리의 다른 글

백준 11659번: 구간 합  (0) 2025.09.06
백준 10448번: 유레카 이론  (0) 2025.04.29

+ Recent posts