package 섹션1_String;

import java.io.*;

public class 유효한_팰린드롬 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        char[] arr = br.readLine().toUpperCase().toCharArray();

        int i=0;
        int j=arr.length-1;
        String answer = "YES";

        while(i<j){
            if(!Character.isAlphabetic(arr[i])){
                i++;
            }else if (!Character.isAlphabetic(arr[j])) {
                j--;
            }else if(arr[i] == arr[j]){ // 두 문자값이 일치하는 경우
                i++;
                j--;
            }else{ // 두 문자값이 일치하지 않는 경우
                answer = "NO";
                break;
            }
        }

        bw.write(answer);
        bw.flush();
        br.close();
        bw.close();

    }
}

 

 

 

✅ 문자열 정규식 

package 섹션1_String;

import java.io.*;

public class 유효한_팰린드롬 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        String answer="NO";

        // 문자열 정규식(A-Z가 아닌 것들을 전부 빈 문자로 대체)
        String str= br.readLine().toUpperCase().replaceAll("[^A-Z]","");
        String str_reverse=new StringBuilder(str).reverse().toString();

        // 두 문자열이 같은지 비교
        if(str.equals(str_reverse)){
            answer = "YES";
        }

        bw.write(answer);
        bw.flush();
        bw.close();
        br.close();


    }
}
package 섹션1_String;

import java.io.*;

public class 회문문자열 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        String answer = "YES";
        String str = br.readLine().toUpperCase();
        int len= str.length();

        for(int i=0;i<len/2;i++){
            if(str.charAt(i)!= str.charAt(len-i-1)){
                answer = "NO";
                break;
            }
        }

        bw.write(answer);
        bw.flush();
        br.close();
        bw.close();
    }
}

 

 

 

✅ equlasIgnoreCase : 대소문자 구분없이 문자열이 동일한지 비교

package 섹션1_String;

import java.io.*;

public class 회문문자열 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        String answer = "NO";

        String str= br.readLine();
        String reverse=new StringBuilder(str).reverse().toString();

        if(str.equalsIgnoreCase(reverse)){
            answer = "YES";
        }

        bw.write(answer);
        bw.flush();
        br.close();
        bw.close();
    }
}

 

 

" 문자열 문제는 StringBuilder의 내장함수를 적극 이용!" 

package 섹션1_String;

import java.io.*;

public class 중복된_문자제거 {
    public static void main(String[] args) throws IOException {



        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringBuilder sb= new StringBuilder();
        boolean[] checkArr = new boolean[26];

        String str= br.readLine();

        char [] arr= str.toCharArray();

        for(char c:arr){
            if(!checkArr[c-'a']){ // 처음 나온 문자인 경우
                checkArr[c-'a']=true;
                sb.append(c);
            }
        }

        bw.write(sb.toString());
        bw.flush();
        bw.close();
        br.close();
    }
}

 

  • boolean 배열을 이용하여 가리키는 문자가 나온 문자인지 아닌지 확인
  • 한번도 나오지 않은 문자인 경우, 배열에 방문 처리를 하고 정답에 문자 추가하기 

 


 

 

✅ IndexOf 를 이용한 중복된 문자 찾기 

 

for(int i=0;i<str.length();i++){ 
    System.out.println(str.charAt(i)+" "+i+" "+ str.indexOf(str.charAt(i)));
}

 

ksekkset // 입력한 문자열 
k 0 0 
s 1 1
e 2 2
k 3 0
k 4 0
s 5 1
e 6 2
t 7 7

 

 

📍 두 값에 차이가 발생하는 이유

 

k 0 0 
s 1 1
e 2 2
t 7 7

 

위의 네가지 경우를 보면 배열의 인덱스 값과 IndexOf의 리턴값이 동일하다. 

 

 

k 3 0
k 4 0
s 5 1
e 6 2

 

하지만 위의 경우에는 두 개의 값이 다른데, 현재 배열의 위치에 있는 문자가 중복되어 존재하기 때문이다. 

IndexOf는 동일한 문자값이 문자열 내에서 여러개 존재할 경우, 가장 앞에 있는 인덱스의 값을 리턴한다. 

 

그래서 현재 가리키고 있는 문자가 앞에서 존재하고 있었기 때문에, 배열의 인덱스 보다 작은 값으로 IndexOf의 리턴값이 나오게 된 것이다. 

 

IndexOf는 동일한 문자가 여러 개 존재할 경우, 가장 작은 값을 리턴하니까 문자가 중복된 문자가 존재하는 경우 indexOf의 값이 배열의 인덱스 보다는 큰 값을 가질 수 없다. 
( 배열의 인덱스 값 ≥ IndexOf ) 

 

 

즉, 두 값이 일치하지 않는다는 것은 이전에 중복된 문자가 존재한다는 것이므로 두 값이 일치할때만 정답 문자열에 추가하자. 

 

 

package 섹션1_String;

import java.io.*;

public class 중복된_문자제거 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringBuilder sb= new StringBuilder();

        String str= br.readLine();

        for(int i=0;i<str.length();i++){
           if(str.indexOf(str.charAt(i))==i){
               sb.append(str.charAt(i));
           }
        }
        
        bw.write(sb.toString());
        bw.flush();
        bw.close();
        br.close();
    }
}

 

 


 

📌 중요한 코드 

 

for(int i=0;i<str.length();i++){
           if(str.indexOf(str.charAt(i))==i){
               sb.append(str.charAt(i));
           }
        }

 

✅Character.isAlphabetic 

package 섹션1_String;

import java.io.*;

public class 특정문자뒤집기 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

      char[] arr= br.readLine().toCharArray();

      int l=0;
      int r=arr.length-1;

      while(l<r){

          if(Character.isAlphabetic(arr[l]) && Character.isAlphabetic(arr[r])){
              char temp=arr[l];
              arr[l]=arr[r];
              arr[r]=temp;

              l++;
              r--;
          } else if (Character.isAlphabetic(arr[l])) {
              r--;
          } else if(Character.isAlphabetic(arr[r])){
              l++;
          }else{
              l++;
              r--;
          }

      }
        bw.write(String.valueOf(arr));
        bw.flush();
        br.close();
        bw.close();

    }
}

 

 

 

📌중요한 코드 

 

if~else if~else 구조에서 "어떤 조건으로 구분"을 해야 식을 더 간단하게 작성할 수 있을지 고민해보기

while(l<r){

    if(!Character.isAlphabetic(arr[l])){ // l-> 특수문자인 경우
       l++;
    } else if(!Character.isAlphabetic(arr[r])){ // r -> 특수문자인 경우
        r--;
    }else{
        char temp=arr[l];
        arr[l]=arr[r];
        arr[r]=temp;

        l++;
        r--;
    }

}

 

 

문제 포인트

  • 문장 속 각 단어는 공백으로 구분, 단어 중에서 가장 긴 단어를 출력
  • 가장 긴 단어가 여러개인 경우, 문장 속에서 가장 앞쪽에 위치한 단어를 출력 

 

풀이 

  1. 입력받은 문자열을 공백 단위로 구분하기
  2. 구분된 단어들의 길이를 비교하여,  최댓값 업데이트 && 해당 문자열을 저장 

 

✅ split

package 섹션1_String;

import java.io.*;

public class 가장긴단어 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        
        int max=Integer.MIN_VALUE; // 가장 작은 문자열 길이 값으로 0으로 초기화 해도 상관없음
        String answer = "";

        // 입력받은 문자열 -> 공백을 기준으로 분리
        String[] s = br.readLine().split(" ");

        for (String x : s) {
            if(x.length()>max){
                max=x.length();
                answer=x;
            }
        }

        bw.write(answer);
        bw.flush();
        br.close();
        bw.close();




    }
}

 

 

 

✅ IndexOf, subString

 

package 섹션1_String;

import java.io.*;

public class 가장긴단어 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        int max=Integer.MIN_VALUE; // 가장 작은 문자열 길이 값으로 0으로 초기화 해도 상관없음
        int pos=0; // 공백이 위치한 곳 
        String answer = "";

        // 입력받은 문자열 -> 공백을 기준으로 문자열을 분리
        String str= br.readLine();
        
        while((pos=str.indexOf(" "))!= -1){
            String tmp = str.substring(0, pos);
            int len=tmp.length();
            if(len > max){
                max=len;
                answer=tmp;
            }
            🌀str = str.substring(pos + 1); 
        }

        // 마지막 단어는 공백이 존재하지 않으므로 따로 check 
       🌀if(str.length()>max) answer=str; 

        bw.write(answer);
        bw.flush();
        br.close();
        bw.close();
        
    }
}

'알고리즘 > 자바(Java) 알고리즘 문제풀이 입문: 코딩테스트 대비' 카테고리의 다른 글

1-7 회문 문자열  (0) 2025.05.01
1-6 중복 문자 제거  (1) 2025.04.09
1-5 특정 문자 뒤집기  (0) 2025.04.09
1-2 대소문자 변환  (1) 2025.04.07
1-1 문자 찾기  (0) 2025.04.07

문제 포인트 

  • 대문자는 소문자로, 소문자는 대문자로 변환한다. 
  • 따라서 어떤 기준에 따라 대문자와 소문자로 구분할 수 있는지 생각해보면 된다. 

 

풀이 

아스키 코드표에 따르면 영어 대문자는 65~90, 소문자는 97~122 의 값을 가진다. 

따라서 대소문자의 차이값은 32이다. 

 

입력받은 문자열을 하나하나 확인해보면서, 대문자의 범위 or 소문자의 범위에 들어가는지 확인하여 대소문자를 구분한다. 

이후 대문자인 경우 소문자로, 소문자인 경우 대문자로 변환한다. 

 

 

 

package 섹션1_String;

import java.io.*;

public class 대소문자변환 {
    public static void main(String[] args) throws IOException {


        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringBuilder newStr = new StringBuilder();

        String str = br.readLine();


        int diff='a'-'A'; // 대문자와 소문자의 차이

        for(char c:str.toCharArray()){
            if(c>=65 && c<=90){ // 대문자인 경우 소문자로 변환 
                newStr.append((char) (c + diff)); 
            }else{ // 소문자인 경우 대문자로 변환 
                newStr.append((char) (c - diff));
            }
        }

        bw.write(newStr.toString());
        bw.flush();
        bw.close();
        br.close();

    }
}

 

 

 

✅ Character static 메서드 이용하기 

for(char c:str.toCharArray()){
    if(Character.isLowerCase(c)){
        newStr.append(Character.toUpperCase(c));
    } else {
        newStr.append(Character.toLowerCase(c));
    }
}

문제 포인트 

  • 문자열은 영어 알파벳으로만 구성되어있다. 
  • 영어 대소문자를 구분하지 않는다 

 

풀이 

대소문자를 구분하지 않는다면 첫번째 줄에서 입력받은 문자열과, 두번째 줄에서 입력받은 문자를 모두 대문자 or 소문자로 바꿔서 통일시켜줘야한다. 

 

 

 

package 섹션1_String;

import java.io.*;

public class 문자찾기 {
    public static void main(String[] args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        String str= br.readLine().toUpperCase(); // 문자열
        char c= br.readLine().toUpperCase().charAt(0); // 찾고자 하는 문자

        int answer=0; // 찾고자하는 문자가 나온 횟수

        for(char strChar:str.toCharArray()){
            if(strChar==c){
                answer++;
            }
        }
        System.out.println(answer);
        br.close();
    }
}

 

 

 

for문을 이용한 문자열 비교 

for(int i=0;i<str.length();i++){ // 문자열의 길이만큼 반복
    if(c==str.charAt(i)){
        answer++;
    }
}

 

 

 


 

✅ == 연산 

 

 

 

String이나 Character 처럼 객체를 ==로 비교하게 되면, 값 뿐만 아니라 인스턴스의 주소도 비교하기 때문에 값이 같아도 다른 객체인 경우(인스턴스의 주소가 다른경우) false 값이 나오게 된다. 따라서 문자열에서 단순히 값만 비교하고 싶은 경우 eqauls( ) 를 사용해야한다. 

 

하지만 int,boolean,char 과 같은 원시타입의 경우 == 를 사용하게 된다면 단순히 값만 비교한다. 

 

유니온 파인드 알고리즘은 두 노드가 하나의 집합에 속하는지 판별하고 싶을때 매우 유용하게 사용되는 방법이다. 

  • 노드를 합치는 Union 연산과 노드의 루트 노드를 찾는 Find 연산으로 이루어진다.

 

 

 

이렇게 복잡한 그래프에서는 빠른 시간 안에 두 노드가 연결되어 있는지 판별하는 게 힘들다.

바로 이런 경우에 두 노드가 연결되어 있는지, 끊어져 있는지를 판별하는 방법이  "유니온 파인드" 이다. 

 

두 노드가 연결되어있는지 판단하는 방법은 ,  두 노드가 하나의 집합 안에 포함되는지 확인하면 된다. 

그렇게 하기 위해서는 먼저 연결되어있는 노드들을  하나의 집합으로 만드는 과정이 필요하다.  바로 이 과정이 Union (유니온) 이다.

 

예를들어, 1호선이 지나가는 역들은 모두 남색선으로 표시하였다. 이것이 바로 유니온 과정이다.

그럼 어떤 임의의 역의 색깔이 남색인지 아닌지에 따라 1호선에 속하는 역인지 아닌지를 판단할 수 있는 것이다. 

 


 

 

아래와 같이 아직 연결관계가 설정되어있지 않은 노드들이 있다고 하자. 

 

처음에는 노드들이 모두 연결관계를 가지고 있지 않기 때문에 각 노드가 대표노드 가 된다. 

 

대표노드란 특정 노드 집합을 대표하는 노드 값으로, 하나의 집합에 포함되어있음을 나타내기 위해  하나의 대표값이 필요하다.

그때 사용하는 노드의 값을 대표 노드라고 하는 것이다. 

 

현재 각 노드는 모두 자기자신의 집합을 대표하는 노드이므로, 배열의 값을 index값으로 초기화해준다. 

여기서 배열의 값은 자신이 속한 집합의 대표노드 값을 의미한다. 

 

따라서 현재는 배열의 인덱스와 배열의 값이 모두 일치한다. 

이미지 출처: Do it 코딩테스트 자바편

 

 

 

이제  1과4 , 5와6 노드를 하나의 집합으로 만들어보자

 

(1,4)   →  대표값 1

(5,6)   →  대표값 5 

 

이렇게 두 노드가 합쳐져 하나의 그래프가 될 때, 자식이 되는 노드의 값에 부모 노드의 값을 적어준다. 

따라서 (1,4)의 노드 집합에서는 부모가 1이므로 A[4]=1로 나타낸다. 

 

 

A[4]=1 , A[6]=5 

 

이렇게 값을 바꾼후 배열의 값을 읽어보자.  4의 부모는 1이고, 6의 부모는 5임이 한눈에 파악된다. 

 

 

이미지 출처: Do it 코딩테스트 자바편

 

 

 

현재의 상황은 아래의 그림과 같다. 

 

이제 노드4와 노드6을 연결해보자. 

 

두 노드의 대표노드 값은 서로 다르기 때문에, 서로 다른 집합에 속하는 노드 인것을 알 수 있다. 

특정 노드의 대표노드 값을 찾는 방법은 아래와 같다. 

 

 

🐟 대표노드 값을 찾는 방법 

  • 대상 노드 배열의 index값과 value값이 동일한지 확인한다
  • 동일하지 않으면 value값이 가리키는 index로 이동한다
  • 이동위치의 index값과 value값이 동일해질때까지 1~2번 과정을 반복한다 (반복이므로 재귀함수로 구현)

 

그리고 이렇게 자신이 속한 집합의 대표노드를 찾는 연산을 find연산 이라고 한다. 

 

 

- 4번 노드의 대표노드를 찾는 방법 

  1. 배열의 4번에 있는 값을 확인한다. 1번이다.
  2. 배열의 1번에 있는 값을 확인한다. 1번이다.
  3. 배열의 인덱스와 값이 일치한다.  

 

 

- 6번의 대표 노드를 찾는 방법 

  1. 배열의 6번에 있는 값을 확인한다. 5번이다.
  2. 배열의 5번에 있는 값을 확인한다. 5번이다.
  3. 배열의 인덱스와 값이 일치한다. 

 

 

4의 대표노드값은 1이고, 6의 대표노드 값은 5이다. 

 

 

 

따라서, 대표노드끼리 연결해주면 된다. 

 

(1,5)   →  대표값 1 

 

노드 5의 부모를 노드1로  설정하기 위해 A[5]=1로 바꿔주자.

그럼 배열의 값은 [1,2,3,1,1,5] 가 된다. 

 

 1의 값을 가지고 있는 1,4,5는 같은 집합에 속하는 것이 한눈에 보인다. 하지만  노드6은 대표노드가 아닌, 인접한 부모 노드인 5의 값을 가지고 있기 때문에 1로 대표되는 집합에 속하는지 한눈에 파악하기 어렵다. 

 

 

 

그렇다면, 대표노드가 1인 집합에 속하는 모든 노드의 값을 1로 나타내면,  해당 노드들이 모두 같은 집합에 속한다는 것을 더 잘 파악 할 수 있지 않을까?  

 

 

🐟 find 연산의 작동원리

  • 대상 노드 배열의 index값과 value값이 동일한지 확인한다
  • 동일하지 않으면 value값이 가리키는 index로 이동한다
  • 이동위치의 index값과 value값이 동일해질때까지 1~2번 과정을 반복한다 (반복이므로 재귀함수로 구현)
  • 대표노드를 찾고 재귀 함수를 빠져나오면서 거치는 모든 노드의 값을 대표노드 값으로 바꿔줌 (경로 압축)

 

즉  6에서 find 연산을 수행할때 6 5 1 순으로 재귀함수를 호출했다가, 빠져나오면서  A[6]=1 로 만들어주면 된다. 

 

 

 

 

이렇게 한번의 find 연산으로 모든 노드가 대표노드와 직접적으로 연결되는 형태 로 변형 될 수 있다. 

 

이 방법을 통해, 자식 노드가 아무리 많아지더라도 자식노드에서는  대표노드의 값을 바로 구할 수 있게 된다. 

 

 

 

 

 

 

 

 

🐸 참고 

설명이 매우 잘되어있어서 한번씩 가서 읽어보면 좋을것 같다!! 

 

[알고리즘] 유니온 파인드 (Union-Find)

두 노드는 서로 같은 집합에 속할까?

velog.io

 

 

🍥 출처 

서울 지하철 노선도 이미지 : https://www.sisul.or.kr/open_content/skydome/introduce/pop_subway.jsp

그래프 이미지: https://lgphone.tistory.com/90

그 외 이미지: Do it 코딩테스트 자바편 

'알고리즘 > 개념정리' 카테고리의 다른 글

그래프를 구현하는 방법  (6) 2024.11.20
소수를 구하는 방법  (4) 2024.11.19
정렬  (0) 2024.07.10

🐙 문제 

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

 

이 문제는 " 이분 그래프 " 의 뜻을 잘 이해해야한다. 

그래프의 정점의 집합을 둘로 분할하여, 각 집합에 속한 정점끼리는 서로 인접하지 않도록 분할할 수 있을 때, 그러한 그래프를 특별히 이분 그래프 라 부른다. 

 

쉽게 설명하자면,  그래프의 노드들을 바구니 A와 바구니 B에 나눠서 담는다고 하자. 

이때 같은 바구니에 담긴 노드들끼리는 인접하면 안된다는 의미이다.

 

 

그래프가 예시로 주어졌을때,  노드를 번갈아 바구니 A,B에 나누어서 담아보면 된다. 

 

 

[입력예시] 

2   // 테스트 케이스 개수 
3 2 // case1 : 노드의 개수, 엣지의 개수 
1 3
2 3
4 4 // case2 : 노드의 개수, 엣지의 개수 
1 2
2 3
3 4
4 2

 

 

 

🐙 풀이과정 

 

이분 그래프인 case 

 

 

첫번째 케이스를 그림으로 나타내면 아래와 같다 

 

그리고 바구니 A,B 를 만든다. 

먼저 바구니 A에 노드 1을 담는다. ( A,B 아무거나 상관없음) 

 

노드3으로 이동하여, 현재 바구니의 값과 반대인 바구니 B에 담는다. ( 바구니는 A,B 두개 밖에 없기 때문에 A 가 아니라면 B에 담아야한다) 

 

방문하지 않은 노드2로 이동하여, 현재 바구니 값(B)와 반대인 바구니  A에 값을 담는다 

 

 

노드 1에서 시작하여  노드들을 바구니에 번갈아 가며 담았을때, 위의 그림과 같이 구분된다. 

 

결과적으로 같은 바구니A에 담긴 노드1,2는 인접하지 않는다. 

 

이러한 과정을 노드2에서 시작, 노드3에서 시작하더라도 항상 같은 바구니에 있는 노드들은 인접하지 않는다. 

 

  • 노드 2에서 시작한 경우 

 

  • 노드3에서 시작한 경우 

 

 

따라서 이 그래프는 이분 그래프 라고 할 수 있다. 

잘 이해가 안간다면 이분 그래프가 안되는 경우를 보고 이해해보자

 

 

이분 그래프가 아닌 case 

 

문제의 예시2번째는 이분 그래프가 아닌 경우이다. 

 

왜냐하면 위의 그림에서 나와있듯이,  2 → 3 → 4 가 사이클을 형성하고 있다. 

 

그런데 4 → 2 의 경우, B에 담긴 노드4가  B에 담긴 노드2와 인접하고 있다. 즉, 같은 바구니에 담긴 노드들이 인접했다. 

따라서 이 경우는 이분 그래프가 될 수 없다. 

 

그렇다면 어떤 상황에 같은 바구니에 담긴 노드들이 서로 인접할까? 

 

정답은 다음 경로에 있는 노드가, 현재 노드의 바구니와 동일한 경우이다. 

현재 노드가 바구니 A에 담겨있는데, 다음으로 탐색할 노드도 바구니A에 담겨있다면 이 경우 이분 그래프의 정의에 위배되는 것이다. 

 

 


 

  • 임의의 노드 한점에서 DFS를 수행한다. ( 중요한 것은 모든 노드에서 수행해야한다는것!) 
  • 아직 방문하지 않은 노드라면, 현재 바구니의 값과 다른 바구니의 값으로 설정한다.
  • 이미 방문한 노드라면, 해당 노드의 바구니의 값을 확인하여 현재 바구니의 값과 일치한다면 No를 출력, 현재 바구니의 값과 일치하지 않는다면 Yes를 출력한다. 

 

 

🐙 코드 

package 고자프_수업정리.그래프;

import java.io.*;
import java.util.*;

public class P48_이분그래프 {

    static ArrayList<Integer>[] list; // 그래프 저장
    static StringBuilder sb; // 결과를 저장
    static String[] visited; // 방문 상태 저장
    static final String NOT_VISITED = "N"; // 초기값 설정
    static boolean isBipartiteGraph; // 이분 그래프 여부 확인 플래그

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st;

        int k = Integer.parseInt(br.readLine()); // 테스트 케이스 개수
        sb = new StringBuilder();

        for (int i = 0; i < k; i++) {
            st = new StringTokenizer(br.readLine());
            int V = Integer.parseInt(st.nextToken()); // 정점 개수
            int E = Integer.parseInt(st.nextToken()); // 간선 개수

            // 그래프 초기화
            list = new ArrayList[V + 1];
            visited = new String[V + 1];
            for (int j = 1; j <= V; j++) {
                list[j] = new ArrayList<>();
                visited[j] = NOT_VISITED;
            }

            // 그래프 생성
            for (int j = 0; j < E; j++) {
                st = new StringTokenizer(br.readLine());
                int u = Integer.parseInt(st.nextToken());
                int v = Integer.parseInt(st.nextToken());

                // 무방향 간선 추가
                list[u].add(v);
                list[v].add(u);
            }

            // 이분 그래프 판별
            isBipartiteGraph = true; // 기본값
            // 주어진 그래프가 연결그래프라는 보장 x -> 모든 노드에서 수행하기 
            for (int j = 1; j <= V; j++) {
                if (visited[j].equals(NOT_VISITED)) {
                    checkBipartiteGraph(j, "A");
                    if(!isBipartiteGraph) break; 
                }
            }
            
            // 결과 저장
            sb.append(isBipartiteGraph ? "YES" : "NO").append("\n");
        }

        // 결과 출력
        bw.write(sb.toString());
        bw.flush();
        bw.close();
        br.close();
    }

    static void checkBipartiteGraph(int x, String group) {
        visited[x] = group; // 현재 노드를 방문 표시
        String opposite = group.equals("A") ? "B" : "A"; // 반대 그룹 설정

        for (int num : list[x]) {
            if (visited[num].equals(NOT_VISITED)) { // 아직 방문하지 않은 노드
                checkBipartiteGraph(num, opposite);
            } else if (visited[num].equals(group)) { // 동일 그룹에 속할 경우
                isBipartiteGraph = false; // 이분 그래프가 아님
                return;
            }
        }
    }
}

 

 

🧐 무방향 그래프로 저장하는 이유? 

간선의 방향이 입력에서 명확히 주어지지 않으면 양방향 그래프로 처리하자. 

 

🧐 모든 노드에서 탐색을 시작해야하는 이유? 

문제의 조건에서 모든 노드들이 하나로 연결되어있다고 생각할 만한 부분은 없었기 때문에, 임의의 한 노드에서만 탐색을 하여서  이분 그래프인지를 따져선 안된다.  모든 노드에서 확인을 해줘야한다. 

 

그래서 boolean isBipartiteGraph = true; 라는 변수를 이용하여 한번이라도 false 값으로 초기화 되면 , 그 그래프는 이분 그래프가 될 수 없도록 하는 것이다. 

 

 

 


🐙 정리 

이 문제는 간단히 정리하자면, 그래프를 이용한 DFS 응용문제이다.  

왜 이 문제에서 왜 DFS를 사용하였고, 왜 두 그룹으로 나누는지에 대해서 생각해보면 좋을 것 같다.

'알고리즘 > 백준' 카테고리의 다른 글

[백준] 1929 소수 구하기 자바  (1) 2024.11.19
[백준] 11047 동전0 자바  (0) 2024.11.18
[백준] 1167번 트리의 지름  (0) 2024.10.30
[백준] 11005 진법 변환2 자바  (2) 2024.10.03
그래프 정리 

 

 

그래프(Graph)에 대해 알아보자

트리와 그래프 모두 대표적인 비선형 자료구조이다. 선형자료구조와 다르게 데이터 요소들이 계층적으로 연결되어 있다. 그렇다면 비선형 자료구조란 무엇일까? 비선형자료구조란? > 비선형 자

velog.io

 

위의 벨로그에 그래프에 대한 개념정리가 매우 잘되어있어서, 읽어보면 좋을것 같다

 

  •  노드 = 정점
  •  간선 = 엣지 : 노드 사이를 연결하는 선 (두 노드 사이의 연결관계)

 

그래프를 구현하는 방법

 

1️⃣ 엣지 리스트 

 

엣지 리스트란  엣지를 중심으로 그래프를 표현하는 방법이다. 

엣지리스트는 배열에 출발노드, 도착노드를 저장하여 그래프를 표현한다. 엣지에 가중치가 있는경우 출발노드, 도착노드, 가중치를 저장하여 그래프를 표현한다. 

 

 

  • 가중치가 없는 경우

가중치가 없는 그래프인 경우,  출발노드와 도착노드만을 저장하면 되므로 배열의 열은 2개면 충분하다. 

 

이미지 출처: 두잇 코딩테스트 자바편

 

1에서 2로 뻗어나가는 엣지는 [1,2] 

2에서 5로 뻗어나가는 엣지는 [2,5] 

 

이처럼, 방향이 있는 그래프는 순서에 맞게 노드를 배열에 저장하는 방식으로 표현한다. 

각각의 행은 두 노드의 연결관계를 나타내고 있으므로 엣지를 의미한다. 

이렇게 노드를 배열에 저장하여 엣지(연결관계) 를 표현하므로 엣지 리스트라 한다. 

 

 

  • 가중치가 있는 경우

가중치가 있는 경우는 가중치를 저장하는 열을 하나 더 추가하면 된다. 

 

이미지 출처: 두잇 코딩테스트 자바편

 

 

하지만 엣지리스트의 경우, 특정 노드와 관련된 엣지를 탐색하기는 쉽지 않다. 

특정 노드와 관련된 엣지를 탐색하기 위해선 엣지 리스트 전체를 순회(출발노드와 도착노드 모두 확인해야함) 하며 특정 노드를 찾아야한다. 

 

따라서 그래프에 N개의 엣지가 있는 경우,  특정 노드와 관련된 엣지를 찾기 위해선 O(N) 이 필요하다. 

이러한 시간복잡도는 엣지의 개수가 많아질수록, 특정 노드와 관련된 엣지를 찾는것은 매우 많은 시간이 걸리게 될 것이다. 

 

 

 

2️⃣ 인접행렬

인접행렬은 엣지 리스트와 반대로 노드를 중심으로 그래프를 표현하는 방법이다. 

노드의 개수를 N 이라고 할때, NxN 크기의 2차원 배열을 이용하여 노드 사이의 관계를 나타낸다. 

 

  • 가중치가 없는 경우

이미지 출처: 두잇 코딩테스트 자바편

 

1에서 2로 향하는 엣지를 1행2열에 1을 저장하는 방식으로 표현한다. 

1을 저장하는 이유는 가중치가 없기 때문이다. 가중치가 있는 엣지라면 가중치의 값을 저장하면 된다. 

 

이처럼 인접행렬로 그래프를 표현하는 것은, 엣지를 노드를 기준으로 표현한다. 

 

 

 

 

  • 가중치가 있는 경우 

이미지 출처: 두잇 코딩테스트 자바편

 

 

인접행렬로 그래프를 나타내는것은, 엣지의 여부와 가중치의 값을  배열에 접근하면 바로 확인 할 수 있다는 장점이 있다. 

예를 들어, 3에서4로 향하는 엣지의 가중치를 알고 싶다면 A[3][4] 의 값을 읽으면 되므로 시간복잡도는 O(1) 이 된다. 

 

하지만, 특정 노드와 관련되어있는 모든 엣지를 탐색하려면 엣지리스트와 동일하게  N번 접근해야하므로 시간복잡도는 O(N)이 걸린다. 

또한 노드의 개수가 엄청나게 많을 경우, 2차원 배열 선언 자체를 할 수 없는 문제점도 있다.  ( N*N개가 필요한데 N이 10만개만 되어도 엄청나게 많은 메모리공간을 필요로하게 됨) 

 

 

 

3️⃣ 인접 리스트 

 

인접 리스트는 ArrayList 자료형을 사용하여 그래프를 표현한다. 노드의 개수만큼 ArrayList를 선언한다. 

ArrayList 배열을 만들어, 특정 노드와 연결관계가 있는 노드들을  차례대로 저장한다. 

 

🧐 ArrayList를 쓰는 이유? 

특정 노드와  연결되어있는 노드들을 저장하는 방식이 인접 리스트 방식이다. 

이런 경우 2차원 배열을 이용해서도 구현 할 수 있을 것이다. 그런데 ArrayList를 이용해서 배열을 만들어 쓰는 이유가 뭘까? 

 

배열의 경우, 생성과 동시에 크기가 선언되어야한다. 그렇기 때문에 그래프 전체를 파악하여 특정 노드에 연결된 엣지의 개수(노드의 개수와 동일)를 파악 할 수 있다면 크기를 선언해줄 수 있지만, 그렇지 않은 경우 공간을 비효율적으로 사용하게 될 수 있다.

 

그래서 크기가 가변적인 ArrayList를 이용한 배열을 만들어서 노드를 추가해주는 것이다. 

 

ArrayList에 대한 내용은 아래 블로그를 읽어보면 될 것 같다

 

🧱 자바 ArrayList 구조 & 사용법 정리

ArrayList 컬렉션 자바의 컬렉션 프레임워크를 접한다면 가장 먼저 배우는 컬렉션이 ArrayList 일 것이다. 자료구조(Data Structure) 이라고 해서 무언가 방대하게 느껴져 접근이 어려울 것 처럼 느끼겠지

inpa.tistory.com

 

 

  • 가중치가 없는 경우 

 

가중치가 없는 경우, 노드와 연결되어있는 노드들만 차례대로 저장해준다. 

 

 

 

  • 가중치가 있는 경우 

가중치가 있는 경우 자료형으로 클래스를 사용한다. 

도착노드, 가중치를 갖는 클래스를 선언하여 ArrayList에 저장한다. 

public class Node{
int startNode; // 출발노드 
int value; // 가중치 

Node(int startNode,int value){
this.startNode=startNode;
this.value=value; 
}

 

 

 

 

 

인접 리스트를 이용한 그래프 구현 방법은, 위에서 제시한 두 방법들이 가지고 있는 문제점들을 보완한다. 

  • 특정 노드와 관련된 모든 엣지를 탐색하기 매우 용이하다 (1번의 값을 모두 읽으면 된다) 
  • 공간 효율이 좋아  노드의 개수가 많아지더라도 메모리 초과 에러가 발생하지 않는다

 

그래서 일반적으로 그래프를 구현할때는 인접 리스트를 이용한다. 

 

 

 

 

🐸 출처 

그래프 개념 정리 벨로그 :  https://velog.io/@kwontae1313/트리와-그래프에-대해-알아보자

노드와 엣지 이미지  : 더북 모두의 인공지능 기초 수학 https://thebook.io/080246/0171/ 

그래프 그림 : Do it! 알고리즘 코딩 테스트 자바 편 

 

'알고리즘 > 개념정리' 카테고리의 다른 글

유니온 파인드  (0) 2024.11.23
소수를 구하는 방법  (4) 2024.11.19
정렬  (0) 2024.07.10

+ Recent posts