알고리즘/백준

[백준] 16139. 인간-컴퓨터 상호작용 (자바 JAVA)

spacelife 2024. 12. 29. 22:27
728x90
반응형

문제

승재는 인간-컴퓨터 상호작용에서 생체공학 설계를 공부하다가 키보드 자판이 실용적인지 궁금해졌다. 이를 알아보기 위해 승재는 다음과 같은 생각을 했다. 

'문자열에서 특정 알파벳이 몇 번 나타나는지 알아봐서 자주 나타나는 알파벳이 중지나 검지 위치에 오는 알파벳인지 확인하면 실용적인지 확인할 수 있을 것이다.'

승재를 도와 특정 문자열 $ S\mathit{} $, 특정 알파벳 $\alpha$와 문자열의 구간 $[l,r]$이 주어지면 $ S\mathit{} $의 $ l\mathit{} $번째 문자부터 $ r\mathit{} $번째 문자 사이에 $\alpha$가 몇 번 나타나는지 구하는 프로그램을 작성하여라. 승재는 문자열의 문자는 $0$번째부터 세며, $ l\mathit{} $번째와 $ r\mathit{} $번째 문자를 포함해서 생각한다. 주의할 점은 승재는 호기심이 많기에 (통계적으로 크게 무의미하지만) 같은 문자열을 두고 질문을 $ q\mathit{} $번 할 것이다.

 

입력

첫 줄에 문자열 S가 주어진다. 문자열의 길이는 200,000자 이하이며 알파벳 소문자로만 구성되었다. 두 번째 줄에는 질문의 수 $  q\mathit{}  $가 주어지며, 문제의 수는 $1\leq q\leq 200,000$을 만족한다. 세 번째 줄부터 $  q+2\mathit{}  $번째 줄에는 질문이 주어진다. 각 질문은 알파벳 소문자 $\alpha_i$와 $0\leq l_i\leq r_i<|S|$를 만족하는 정수 $ l_i\mathit{}, r_i\mathit{} $가 공백으로 구분되어 주어진다.

 

출력

각 질문마다 줄을 구분해 순서대로 답변한다. $  i\mathit{}  $번째 줄에 $ S $의 $ l_i\mathit{} $번째 문자부터 $ r_i\mathit{} $번째 문자 사이에 $ \alpha_i $가 나타나는 횟수를 출력한다.

 

서브태스크 1 (50점)

문자열의 길이는 2,000자 이하, 질문의 수는 2,000개 이하이다.

 

서브태스크 2 (50점)

추가 제한 조건이 없다.

 

예제 입력 1

seungjaehwang
4
a 0 5
a 0 6
a 6 10
a 7 10

 

예제 출력 1 

0
1
2
1

 

 

1차 시도

가장 먼저 생각했던 접근법은 S l번째 문자부터 r번째 인덱스 사이의 문자가 α와 같은지 확인하고 개수를 세는 것이었고 다음과 같이 풀이했다.

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

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

        String S = br.readLine();
        long q = Integer.parseInt(br.readLine());

        while (q-- > 0) {
            int count = 0;
            StringTokenizer st = new StringTokenizer(br.readLine());

            char alpha = st.nextToken().charAt(0);
            int l = Integer.parseInt(st.nextToken());
            int r = Integer.parseInt(st.nextToken());

            for (int i = l; i <= r; i++) {
                char ch = S.charAt(i);
                if (ch == alpha) {
                    count++;
                }
            }

            System.out.println(count);
        }
    }
}

 

하지만 50점이 나오고야 말았다..

 

문제 접근법

문자열의 길이 𝑆 ≤ 200,000, 쿼리의 개수  𝑞 ≤ 200,000 이므로,

총 연산 횟수는 200,000×200,000=40,000,000,000가 되어 1초에 1억 번의 연산을 처리할 수 있는 기준을 크게 초과한다.

따라서, 단순 for문 접근으로는 시간 초과가 발생한다.

 

 

범위 [l, r]에서 인덱스  문자가 α와 같은지 확인해야 하므로, 누적합으로 풀면 된다.

 

 

100점 풀이

문자열 S의 각 문자에 대해, 현재까지의 알파벳 등장 횟수를 계산해서 sums 배열에 저장한다.

이 때 sums[i][j]는 S의 1번째 문자부터 i번째 문자까지 알파벳 j (즉, 'a' + j)가 등장한 누적 횟수.
예를 들어, sums[3][0]은 문자열의 1번째 문자부터 3번째 문자까지 'a'가 등장한 횟수를 의미한다.

j번째 알파벳에 대해 이전 값 sums[i - 1][j]를 복사하고, j가 현재 알파벳 alpha와 같으면 +1을 추가한다.

 

예를 들어 S = "abac"일 때

첫번째 문자 a를 탐색할 때 sums[0] = 1이 되고,

두번째 문자 b를 탐색할 때 sums[1] = 1이 되고,

세번째 문자 a를 탐색할 때 sums[0] = 2이 되고,

네번째 문자 c를 탐색할 때 sums[2] = 1이 되는 식으로...

$i$ 문자 $S[i-1]$ $sums[i]$ (a, b, c, ...)
0 - [0, 0, 0, ..., 0]
1 'a' [1, 0, 0, ..., 0]
2 'b' [1, 1, 0, ..., 0]
3 'a' [2, 1, 0, ..., 0]
4 'c' [2, 1, 1, ..., 0]

 

이 연산을 수행하는데 시간복잡도는 O(N×26)을 해서 O(26N)이 되고,

영어 알파벳의 개수(26)는 상수이므로 최종 시간 복잡도는 O(N)이 된다.

 

이렇게 해서 구한 누적합 배열로 구간의 합을 구했더니 100점이 나왔다.

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

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

        String S = br.readLine();

        int[][] sums = new int[S.length() + 1][26];

        for (int i = 1; i < S.length() + 1; i++) {
            int alpha = S.charAt(i - 1) - 'a'; // i번째 문자의 알파벳 인덱스

            for (int j = 0; j < sums[i].length; j++) {
                sums[i][j] = sums[i - 1][j] + (alpha == j ? 1 : 0);
            }
        }

        int q = Integer.parseInt(br.readLine());

        StringBuilder sb = new StringBuilder();

        while (q-- > 0) {
            StringTokenizer st = new StringTokenizer(br.readLine());

            int alphaIndex = st.nextToken().charAt(0) - 'a';
            int l = Integer.parseInt(st.nextToken());
            int r = Integer.parseInt(st.nextToken());

            sb.append(sums[r + 1][alphaIndex] - sums[l][alphaIndex]).append("\n"); // 문자열의 1번째 문자부터 r번째 문자까지의 누적합
        }

        System.out.println(sb);
    }
}

 

 

후기

배열이 변하지 않으니 구간의 합도 변하지 않는다는 점을 바탕으로 앞에서부터 차례대로 누적된 합을 구하고 이걸 이용해서 구간의 합을 구하면 효율성이 올라간다.

 

 

구독과 공감은 블로그 운영에 큰 힘이 됩니다!
긍정적인 댓글 남겨주시면 감사드리며,
보완해야 할 점이 있으면 댓글로 남겨주셔도 좋습니다!

728x90
반응형