개발저장소
[백준 실버 2] 12891번: DNA 비밀번호 본문
문제
https://www.acmicpc.net/problem/12891
12891번: DNA 비밀번호
평소에 문자열을 가지고 노는 것을 좋아하는 민호는 DNA 문자열을 알게 되었다. DNA 문자열은 모든 문자열에 등장하는 문자가 {‘A’, ‘C’, ‘G’, ‘T’} 인 문자열을 말한다. 예를 들어 “ACKA”
www.acmicpc.net
풀이
- 문자열에 ‘A’, ‘C’, ‘G’, ‘T' 만 등장하는 문자열을 DNA 문자열이라고 한다. 예를 들어 “ACKA”는 DNA 문자열이 아니지만 “ACCA”는 DNA 문자열이다.
- 길이 S의 임의의 DNA 문자열을 만들고, 이 DNA 문자열의 길이가 P인 부분문자열을 비밀번호로 사용하려고 한다.
- "AAAA" 같은 취약한 비밀번호가 만들어 질 수도 있기 때문에 문자의 개수가 일정 수 이상이 되는 부분문자열만 비밀번호로 채택한다.
- 예를 들어, “AAACCTGCCAA”에서 4자리의 부분문자열을 비밀번호로 사용하고 ‘A’ 는 1개 이상, ‘C’는 1개 이상, ‘G’는 1개 이상, ‘T’는 0개 이상이라는 조건을 단다면 “ACCT”는 조건에 불만족하므로 비밀번호로 사용할 수 없고, “GCCA”는 사용할 수 있다. 만들 수 있는 비밀번호의 종류의 수를 구해야 한다.
- 부분문자열이 만들어지는 위치가 다르다면 같은 내용의 부분문자열이라도 다르게 취급한다.
- S와 P의 크기는 최대 백만이다.
조건이 뭔가 많아 보이지만 길이가 P인 슬라이딩 윈도우를 다루는 문제라고 볼 수 있다.
슬라이딩 윈도우는 2개의 포인터로 범위를 지정한 다음 범위를 유지한 채로 이동하며 문제를 해결하는 알고리즘이다.
입력으로는 S와 P, 그리고 DNA 문자열과 부분문자열에 포함되어야 할 ‘A’, ‘C’, ‘G’, ‘T’ 의 최소 개수가 주어진다. 최소 개수로 주어지는 ‘A’, ‘C’, ‘G’, ‘T’ 를 크기가 4인 정수형 배열에 담아 각각 순서대로 0부터 3까지의 인덱스를 활용하여 사용하기로 하였다.
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int S = Integer.parseInt(st.nextToken());
int P = Integer.parseInt(st.nextToken());
String inputStr = br.readLine();
checkArr = new int[4]; // static으로 선언
st = new StringTokenizer(br.readLine());
for (int i = 0; i < 4; i++) {
checkArr[i] = Integer.parseInt(st.nextToken());
}
처음에는 substring을 사용해 부분문자열을 만들어 준 뒤, 부분문자열을 처음부터 탐색하며 최소 개수를 만족하는지 체크하는 방식으로 문제를 풀었다.
하지만 이 방법은 한칸씩 이동하는 슬라이딩 윈도우에서 부분문자열의 처음과 끝을 제외한 나머지 부분은 중복된채로 계산되어 비효율적이라는 사실을 깨달았다.
// 대충 이런 식으로 풀었지만 바로 시간초과
for (int i = 0; i <= S - P; i++) {
String subStr = inputStr.substring(i, i + P);
for (int j = 0; j < P; j++) {
char c = subStr.charAt(j);
...
}
}
}
그래서 슬라이딩 윈도우의 맨 처음이 i 라고 할 때, i - 1 위치의 문자와 i + P - 1 위치의 문자만 반복해서 판단해주면 된다고 생각하였다.
이 방법으로 풀기 위해서는 맨 처음 인덱스가 0부터 시작하는 초기 부분문자열을 기준으로 만들어진 초기값 배열이 필요하였다.
int cnt = 0;
int subArr[] = new int[4];
String firstStr = inputStr.substring(0, P);
for (int k = 0; k < P; k++) {
char c = firstStr.charAt(k);
subArr[findIndex(c)]++;
}
이때 findIndex()는 부분문자열의 한 문자가 배열의 어떤 인덱스에 해당하는지 판별하기 위해 정의한 메서드이다.
private static int findIndex(char c) {
int ans = 0;
switch (c) {
case 'A':
ans = 0;
break;
case 'C':
ans = 1;
break;
case 'G':
ans = 2;
break;
case 'T':
ans = 3;
break;
}
return ans;
}
이렇게 subArr를 checkArr와 비교하여 조건을 만족하는지 판단해야 한다. checkDNA() 메서드를 정의하여 비교하는 과정을 만들었다.
private static boolean checkDNA(int[] arr) {
if (arr[0] < checkArr[0] || arr[1] < checkArr[1] || arr[2] < checkArr[2] || arr[3] < checkArr[3]) {
return false;
}
return true;
}
초기 부분문자열으로 subArr를 만들었다면 이 다음은 슬라이딩 윈도우를 사용해 길이 P의 부분문자열을 반복하여 검사하는 과정이 필요하다. 새로운 부분문자열을 모두 검사할 필요는 없고 이전의 부분문자열과 비교하였을 때 달리진 부분만 판단하면 된다. 즉, 이전 부분문자열의 맨 끝 인덱스와 새로운 부분문자열의 맨 끝 인덱스만 검사하면 된다.
두 부분에 만족하는 인덱스는 현재 인덱스 (슬라이딩 윈도우가 시작하는 인덱스) i 를 기준으로 i - 1과 i + P -1이다.
for (int i = 1; i <= S - P; i++) {
char outChar = inputStr.charAt(i - 1);
char inChar = inputStr.charAt(i + P - 1);
subArr[findIndex(outChar)]--;
subArr[findIndex(inChar)]++;
if (checkDNA(subArr)) {
cnt++;
}
}
전체 코드 (Java)
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
public class Main {
static int checkArr[];
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int S = Integer.parseInt(st.nextToken());
int P = Integer.parseInt(st.nextToken());
String inputStr = br.readLine();
checkArr = new int[4];
int subArr[] = new int[4];
st = new StringTokenizer(br.readLine());
for (int i = 0; i < 4; i++) {
checkArr[i] = Integer.parseInt(st.nextToken());
}
int cnt = 0;
String firstStr = inputStr.substring(0, P);
for (int k = 0; k < P; k++) {
char c = firstStr.charAt(k);
subArr[findIndex(c)]++;
}
if (checkDNA(subArr)) {
cnt++;
}
for (int i = 1; i <= S - P; i++) {
char outChar = inputStr.charAt(i - 1);
char inChar = inputStr.charAt(i + P - 1);
subArr[findIndex(outChar)]--;
subArr[findIndex(inChar)]++;
if (checkDNA(subArr)) {
cnt++;
}
}
System.out.println(cnt);
}
private static int findIndex(char c) {
int ans = 0;
switch (c) {
case 'A':
ans = 0;
break;
case 'C':
ans = 1;
break;
case 'G':
ans = 2;
break;
case 'T':
ans = 3;
break;
}
return ans;
}
private static boolean checkDNA(int[] arr) {
if (arr[0] < checkArr[0] || arr[1] < checkArr[1] || arr[2] < checkArr[2] || arr[3] < checkArr[3]) {
return false;
}
return true;
}
}
후기
문제를 풀기 전에 시간 초과를 염두하여 설계하는 습관을 들여야 한다는 생각이 들었다.
'Coding Test > Baekjoon' 카테고리의 다른 글
[백준 실버 4] 2567번: 색종이 - 2 (0) | 2024.01.14 |
---|---|
[백준 플레티넘 5] 11003번: 최솟값 찾기 (0) | 2024.01.14 |
[백준 골드 4] 1253번: 좋다 (0) | 2023.12.07 |
[백준 골드 3] 10986번: 나머지 합 (0) | 2023.11.28 |
[백준 실버 2] 1699번: 제곱수의 합 (0) | 2023.11.21 |