내가 생각했을때 문제에서 원하는부분
첫 줄에 52글자의 문자열이 주어진다.
각 글자는 알파벳 대문자이며, 각 알파벳이 정확히 두 번씩 나타난다.
경로가 무조건 만나는 소가 몇 쌍인지 출력한다.
내가 이 문제를 보고 생각해본 부분
BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); : System.in은 표준 입력 스트림(보통 키보드)인데, 이를 InputStreamReader로 감싸 문자 스트림으로 만들고, 다시 BufferedReader로 감싸서 한 줄 단위로 효율적으로 읽을 수 있도록 준비한다.
String s = br.readLine(); : 문제에서 52글자의 문자열이 한 줄로 주어진다고 했으므로, readLine() 메서드를 사용하여 이 문자열 전체를 s 변수에 저장한다.
String[] s = new String[52];라고 작성하신 부분은 하나의 문자열을 읽는 것이 아니라 문자열 배열을 선언하신 것이라, 문제의 입력 방식과는 맞지 않는다.
이처럼 단일 문자열로 읽어들이는 것이 올바른 처리 방식이다.
int[][] positions = new int[26][2]; : 이 2차원 배열은 각 알파벳의 진입점과 이탈점 인덱스를 저장하는 데 사용된다.
26 : 알파벳 A부터 Z까지 총 26개이기 때문에 행의 크기가 26이다. (positions[0]는 'A'의 위치, positions[1]은 'B'의 위치 등)
각 알파벳이 문자열에서 두 번 등장하므로, 첫 번째 등장이 positions[알파벳 인덱스][0]에, 두 번째 등장이 positions[알파벳 인덱스][1]에 저장된다.
for(int i = 0; i < 26; i++) { positions[i][0] = -1; positions[i][1] = -1; } : 배열의 모든 값을 -1로 초기화한다.
이는 해당 알파벳이 아직 문자열에서 등장하지 않았다는 것을 나타내기 위함이다.
문자열 인덱스는 0부터 시작하기 때문에, 0이 실제 인덱스 값이 될 수 있으므로, -1을 사용하여 "아직 기록되지 않음"을 표현하는 것이 중요하다.
for(int i = 0; i < s.length(); i++) { ... } : 입력받은 52글자의 문자열 s를 처음부터 끝까지 반복하며 각 문자를 하나씩 확인한다.
여기서 i는 현재 문자의 인덱스(위치)를 나타낸다.
char c = s.charAt(i); : 현재 인덱스 i에 있는 문자를 char 타입 변수 c에 저장한다.
int alphaIndex = c - 'A'; : 현재 문자 c를 positions 배열의 인덱스로 변환한다.
예를 들어, c가 'A'이면 'A' - 'A'는 0이 되고, 'B'이면 1이 된다.
이렇게 하면 'A'부터 'Z'까지를 0부터 25까지의 정수 인덱스로 매핑할 수 있다.
if(positions[alphaIndex][0] == -1) { positions[alphaIndex][0] = i; } else { positions[alphaIndex][1] = i; } : 이 조건문은 현재 문자가 해당 알파벳의 첫 번째 등장인지, 아니면 두 번째 등장인지를 판단한다.
만약 positions[alphaIndex][0]이 -1이라면 (즉, 아직 해당 알파벳의 첫 등장이 기록되지 않았다면), 현재 인덱스 i를 positions[alphaIndex][0] (첫 등장 위치)에 저장한다.
else(그렇지 않다면, 즉 이미 첫 등장이 기록되었다면), 현재 인덱스 i는 두 번째 등장 위치이므로 positions[alphaIndex][1]에 저장한다.
이 과정을 통해 positions 배열에는 모든 알파벳의 진입점과 이탈점 인덱스가 정확하게 기록된다.
int crossingPairs = 0; : 경로가 서로 교차하는 소 쌍의 총 개수를 저장할 변수입니다. 0으로 초기화한다.
for(int i = 0; i < 26; i++) { for (int j = i + 1; j < 26; j++) { ... } } : 중첩된 두 개의 for 반복문으로 모든 가능한 서로 다른 소 쌍을 검사한다.
바깥쪽 i 루프는 첫 번째 소(알파벳 A부터 Z-1까지)를 선택한다.
안쪽 j 루프는 i 다음 알파벳부터 Z까지 선택한다.
이렇게 j = i + 1로 시작하면 (A,B)는 검사하지만 (B,A)처럼 중복되거나 (A,A)처럼 자기 자신을 검사하는 것을 피할 수 있다.
int i_start = positions[i][0]; , int i_end = positions[i][1]; : i에 해당하는 알파벳 소의 진입점과 이탈점 인덱스를 가져온다.
int j_start = positions[j][0]; , int j_end = positions[j][1]; : j에 해당하는 알파벳 소의 진입점과 이탈점 인덱스를 가져온다.
핵심 교차 조건: if ((i_start < j_start && j_start < i_end && i_end < j_end) || (j_start < i_start && i_start < j_end && j_end < i_end))
이 조건은 두 소의 경로([i_start, i_end]와 [j_start, j_end])가 서로 교차하는지 (즉, 괄호 쌍이 ( [ ) ]처럼 얽히는지) 확인한다.
i_start < j_start && j_start < i_end && i_end < j_end : i 소가 시작 → j 소가 시작 → i 소가 끝남 → j 소가 끝남 순서입니다. 예를 들어, A B A B 같은 형태이다.
j_start < i_start && i_start < j_end && j_end < i_end : j 소가 시작 → i 소가 시작 → j 소가 끝남 → i 소가 끝남 순서입니다. 예를 들어, B A B A 같은 형태이다.
둘 중 하나라도 참이면 crossingPairs를 1 증가시킨다.
이 조건은 한 경로가 다른 경로를 완전히 포함하는 경우 (예: A B B A는 i_start < j_start < j_end < i_end)는 crossingPairs에 포함시키지 않는다.
System.out.println(crossingPairs); : 모든 소 쌍을 확인한 후, 최종적으로 계산된 crossingPairs 값을 표준 출력으로 내보낸다.
br.close(); : 사용했던 BufferedReader 객체를 닫아 시스템 자원을 올바르게 해제한다.
코드로 구현
package baekjoon.baekjoon_30;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
// 백준 14468번 문제
public class Main1165 {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String s = br.readLine(); // 52글자의 입력 문자열을 읽습니다.
// 각 알파벳(A-Z)의 첫 등장 위치와 두 번째 등장 위치를 저장할 2차원 배열입니다.
// 0번 인덱스: 첫 등장 위치, 1번 인덱스: 두 번째 등장 위치
// 알파벳은 26개이므로 26행, 각 위치 2개이므로 2열입니다.
int[][] positions = new int[26][2];
// 초기값 -1로 설정하여 아직 등장하지 않은 문자를 구분합니다.
// '0'이 실제 위치 인덱스가 될 수 있으므로, 초기값은 -1이 적합합니다.
for(int i = 0; i < 26; i++) {
positions[i][0] = -1; // 첫 등장 위치 초기화
positions[i][1] = -1; // 두 번째 등장 위치 초기화
}
// 입력 문자열을 순회하며 각 알파벳의 등장 위치를 기록합니다.
for(int i = 0; i < s.length(); i++) {
char c = s.charAt(i); // 현재 문자를 가져옵니다.
int alphaIndex = c - 'A'; // 'A'는 0, 'B'는 1 등으로 배열 인덱스로 변환합니다.
if(positions[alphaIndex][0] == -1) {
// 해당 알파벳이 첫 번째 등장이라면, 첫 등장 위치에 현재 인덱스를 저장합니다.
positions[alphaIndex][0] = i;
} else {
// 두 번째 등장이라면, 두 번째 등장 위치에 현재 인덱스를 저장합니다.
positions[alphaIndex][1] = i;
}
}
int crossingPairs = 0; // 경로가 교차하는 소 쌍의 수를 저장할 변수
// 모든 가능한 소 쌍 (i, j)에 대해 경로가 교차하는지 확인합니다.
// i는 'A'부터 'Y'까지, j는 i+1부터 'Z'까지 반복하여 중복 계산을 피합니다.
for(int i = 0; i < 26; i++) { // 첫 번째 소 (알파벳 A부터)
for(int j = i + 1; j < 26; j++) { // 두 번째 소 (첫 번째 소 다음 알파벳부터)
// 각 소의 진입점과 이탈점 인덱스를 가져옵니다.
int i_start = positions[i][0];
int i_end = positions[i][1];
int j_start = positions[j][0];
int j_end = positions[j][1];
// 경로가 교차하는지 확인하는 조건:
// Case 1: (i_start < j_start < i_end < j_end)
// 첫 번째 소의 진입점 -> 두 번째 소의 진입점 -> 첫 번째 소의 이탈점 -> 두 번째 소의 이탈점
// 예: A ( B A ) B
// Case 2: (j_start < i_start < j_end < i_end)
// 두 번째 소의 진입점 -> 첫 번째 소의 진입점 -> 두 번째 소의 이탈점 -> 첫 번째 소의 이탈점
// 예: B ( A B ) A
// 이 두 가지 경우가 바로 괄호가 서로 겹치지만 완전히 포함하지는 않는 '교차'하는 형태입니다.
if((i_start < j_start && j_start < i_end && i_end < j_end) ||
(j_start < i_start && i_start < j_end && j_end < i_end)) {
crossingPairs++; // 조건을 만족하면 교차하는 쌍 수를 1 증가시킵니다.
}
}
}
System.out.println(crossingPairs); // 최종 결과 출력
br.close(); // 사용한 BufferedReader를 닫아서 자원을 해제합니다.
}
}
코드와 설명이 부족할수 있습니다. 코드를 보시고 문제가 있거나 코드 개선이 필요한 부분이 있다면 댓글로 말해주시면 감사한 마음으로 참고해 코드를 수정 하겠습니다.