문제 설명


0과 1로 이루어진 어떤 문자열 x에 대해서, 당신은 다음과 같은 행동을 통해 x를 최대한 사전 순으로 앞에 오도록 만들고자 합니다.

  • x에 있는 "110"을 뽑아서, 임의의 위치에 다시 삽입합니다.

예를 들어, x = "11100" 일 때, 여기서 중앙에 있는 "110"을 뽑으면 x = "10" 이 됩니다. 뽑았던 "110"을 x의 맨 앞에 다시 삽입하면 x = "11010" 이 됩니다.

변형시킬 문자열 x가 여러 개 들어있는 문자열 배열 s가 주어졌을 때, 각 문자열에 대해서 위의 행동으로 변형해서 만들 수 있는 문자열 중 사전 순으로 가장 앞에 오는 문자열을 배열에 담아 return 하도록 solution 함수를 완성해주세요.


제한사항

  • 1 ≤ s의 길이 ≤ 1,000,000
  • 1 ≤ s의 각 원소 길이 ≤ 1,000,000
  • 1 ≤ s의 모든 원소의 길이의 합 ≤ 1,000,000

입출력 예

sresult

["1110","100111100","0111111010"] ["1101","100110110","0110110111"]

입출력 예 설명

입출력 예 #1

  • 다음 그림은 "1110"을 "1101"로 만드는 과정을 나타낸 것입니다.

  • "1101"보다 사전 순으로 더 앞에 오는 문자열을 만들 수 없으므로, 배열에 "1101"을 담아야 합니다.
  • 다음 그림은 "100111100"을 "100110110"으로 만드는 과정을 나타낸 것입니다.

  • "100110110"보다 사전 순으로 더 앞에 오는 문자열을 만들 수 없으므로, 배열에 "100110110"을 담아야 합니다.
  • 그림에 나온 방식 말고도 다른 방법으로 "100110110"을 만들 수 있습니다.
  • 다음 그림은 "0111111010"을 "0110110111"로 만드는 과정을 나타낸 것입니다.

  • "0110110111"보다 사전 순으로 더 앞에 오는 문자열을 만들 수 없으므로, 배열에 "0110110111"을 담아야 합니다.
  • 그림에 나온 방식 말고도 다른 방법으로 "0110110111"을 만들 수 있습니다.

 

접근 방법


문제의 풀이를 알아도 신경써야될 부분이 많았던 문제였습니다.

1. 문자열의 앞부터 110이 있는지 탐색

2. 110을 발견한다면 110을 문자열에서 제거하고 그 자리부터 다시 110을 찾아나가며 제거

3. 문자열의 가장 뒤부터 0이 발견되는 곳까지 탐색

4. 0이 발견되었다면 0뒤에 삭제된 110의 갯수만큼 110을 붙힘

 

1번, 2번 과정은 Stack의 자료구조를 사용해야합니다. 

* 1번 과정에서 IndexOf를 사용할 시 선형의 시간복잡도가 110의 갯수만큼 생기므로 시간초과 발생

* 2번 과정에서 substring 또는 delete를 사용할 시 선형의 시간복잡도가 110의 갯수만큼 생기므로 시간초과 발생

 

4번 과정에 있어서 string끼리 문자열 연산을 하면 시간초과가 발생합니다.

stringBuilder를 사용하여 string연산의 시간을 줄여줘야 합니다.

 

소스 코드


import java.util.*;

class Solution {
    
    
    public String solve(String s){
        
        int cnt110 = 0;
        
        Stack<Character> st = new Stack<Character>();
        
        for(int i=0;i<s.length();i++){
            
            st.push(s.charAt(i));
            
            if(st.size() >= 3){
                //시간복잡도를 조금이라도 줄이고자 3개를 순차적으로 꺼내며 검사
                char first = st.pop();
                if(first != '0'){
                    st.push(first);
                    continue;
                }
                char second = st.pop();
                if(second != '1'){
                    st.push(second);
                    st.push(first);
                    continue;
                }
                char third = st.pop();
                if(third != '1'){
                    st.push(third);
                    st.push(second);
                    st.push(first);
                    continue;
                }
                cnt110++;
            }
        }
        
        //string으로 +연산시 시간초과 발생
        StringBuilder sb = new StringBuilder();
        int pos = st.size();
        boolean flag = false;
        
        //0을 발견할때까지 idx조정
        while(!st.isEmpty()){
            char pop = st.pop();
            if(!flag && pop == '1') pos--;
            if(pop == '0') flag = true;
            sb.insert(0, pop);
        }
        
        //110의 갯수만큼 110을 idx자리에 붙힘
        String fix = "";
        for(int i=0;i<cnt110;i++)
            sb.insert(pos, "110");
    
        return sb.toString();
    }
    
    public String[] solution(String[] s) {
        
        String[] answer = {};
        answer = new String[s.length];
        
        //1케이스씩 해결
        for(int i=0;i<s.length;i++)
            answer[i] = solve(s[i]);
        
        return answer;
    }
}

문제 설명


단체사진 찍기

가을을 맞아 카카오프렌즈는 단체로 소풍을 떠났다. 즐거운 시간을 보내고 마지막에 단체사진을 찍기 위해 카메라 앞에 일렬로 나란히 섰다. 그런데 각자가 원하는 배치가 모두 달라 어떤 순서로 설지 정하는데 시간이 오래 걸렸다. 네오는 프로도와 나란히 서기를 원했고, 튜브가 뿜은 불을 맞은 적이 있던 라이언은 튜브에게서 적어도 세 칸 이상 떨어져서 서기를 원했다. 사진을 찍고 나서 돌아오는 길에, 무지는 모두가 원하는 조건을 만족하면서도 다르게 서는 방법이 있지 않았을까 생각해보게 되었다. 각 프렌즈가 원하는 조건을 입력으로 받았을 때 모든 조건을 만족할 수 있도록 서는 경우의 수를 계산하는 프로그램을 작성해보자.

입력 형식

입력은 조건의 개수를 나타내는 정수 n과 n개의 원소로 구성된 문자열 배열 data로 주어진다. data의 원소는 각 프렌즈가 원하는 조건이 N~F=0과 같은 형태의 문자열로 구성되어 있다. 제한조건은 아래와 같다.

  • 1 <= n <= 100
  • data의 원소는 다섯 글자로 구성된 문자열이다. 각 원소의 조건은 다음과 같다.
    • 첫 번째 글자와 세 번째 글자는 다음 8개 중 하나이다. {A, C, F, J, M, N, R, T} 각각 어피치, 콘, 프로도, 제이지, 무지, 네오, 라이언, 튜브를 의미한다. 첫 번째 글자는 조건을 제시한 프렌즈, 세 번째 글자는 상대방이다. 첫 번째 글자와 세 번째 글자는 항상 다르다.
    • 두 번째 글자는 항상 ~이다.
    • 네 번째 글자는 다음 3개 중 하나이다. {=, <, >} 각각 같음, 미만, 초과를 의미한다.
    • 다섯 번째 글자는 0 이상 6 이하의 정수의 문자형이며, 조건에 제시되는 간격을 의미한다. 이때 간격은 두 프렌즈 사이에 있는 다른 프렌즈의 수이다.

출력 형식

모든 조건을 만족하는 경우의 수를 리턴한다.

예제 입출력

ndataanswer

2 ["N~F=0", "R~T>2"] 3648
2 ["M~C<2", "C~M>1"] 0

예제에 대한 설명

첫 번째 예제는 문제에 설명된 바와 같이, 네오는 프로도와의 간격이 0이기를 원하고 라이언은 튜브와의 간격이 2보다 크기를 원하는 상황이다.

두 번째 예제는 무지가 콘과의 간격이 2보다 작기를 원하고, 반대로 콘은 무지와의 간격이 1보다 크기를 원하는 상황이다. 이는 동시에 만족할 수 없는 조건이므로 경우의 수는 0이다.

 

 

 

접근 방법


백트래킹 기법을 사용하여 해결할 수 있었습니다.

 

1. 백트래킹을 사용하여 8명이 서있을 수 있는 모든 경우의 수에 대한 순열을 구합니다. (이 문제의 경우 8!개)

 

2. 구해진 한 조합에 대해 각각의 프렌즈 들이 원하는 조건(data)가 전부 만족하는지 확인합니다.

 

3. 한 조건에 대해 검사를 하는 방법은 아래와 같습니다.

 

3-1. 조건의 0번째 인덱스와 2번째 인덱스인 두 명과, 3번째 인덱스인 오퍼레이션(<, >, =) 마지막으로 4번째 인덱스인  오퍼레이션에 대한 수를 parsing합니다.

 

3-2. 아래와 같은 코드로 각 조건이 만족하는지 검사합니다.

if(operation == '='){
	if(Math.abs(first - second) == num + 1)
		return true;
}
else if(operation == '>'){
	if(Math.abs(first-second) > num + 1)
		return true;
}
else{
	if(Math.abs(first-second) < num + 1)
		return true;
}
return false;

 

 

소스 코드


import java.util.*;
import java.lang.*;

class Solution {
    
    boolean visited[] = new boolean[8];
    String friends = "ACFJMNRT";
    String batch = "";
    String[] data;
    int ans = 0;
    
    public boolean checkOne(String condition){
        
        // 2명의 위치, 연산자, 숫자
        int first = batch.indexOf(condition.substring(0, 1)); 
        int second = batch.indexOf(condition.substring(2, 3));
        char operation = condition.charAt(3);
        Integer num = Integer.parseInt(condition.substring(condition.length() - 1));
        
        if(operation == '='){
            if(Math.abs(first - second) == num + 1)
                return true;
        }
        else if(operation == '>'){
            if(Math.abs(first-second) > num + 1)
                return true;
        }
        else{
            if(Math.abs(first-second) < num + 1)
                return true;
        }
        return false;
    }
    
    //전체 데이터에 대한 검사
    public boolean check(){
        
        for(int i=0;i<data.length;i++){
            if(!checkOne(data[i])) //하나라도 만족하지 않는다면
                return false;
        }
        return true;
    } 
    
    public void backTracking(int cnt){
        
        //8명의 순열이 완성되었을 때
        if(cnt == 8){
            if(check()) //전체 조건을 검사
                ans++;  //만족한다면 ans++
            return;
        }
        
        for(int i=0;i<8;i++){
            if(visited[i] == false){
                visited[i] = true;
                batch += friends.substring(i,i+1);
                backTracking(cnt + 1);
                batch = batch.substring(0, batch.length() - 1);
                visited[i] = false;
            }    
        }
    }
    
    public int solve(int n, String[] data){
        
        this.data = data;
        backTracking(0);
        return ans;
    }
    
    public int solution(int n, String[] data) {
        int answer = 0;
        return answer = solve(n, data);
    }
}

문제 설명


후보키

프렌즈대학교 컴퓨터공학과 조교인 제이지는 네오 학과장님의 지시로, 학생들의 인적사항을 정리하는 업무를 담당하게 되었다.

그의 학부 시절 프로그래밍 경험을 되살려, 모든 인적사항을 데이터베이스에 넣기로 하였고, 이를 위해 정리를 하던 중에 후보키(Candidate Key)에 대한 고민이 필요하게 되었다.

후보키에 대한 내용이 잘 기억나지 않던 제이지는, 정확한 내용을 파악하기 위해 데이터베이스 관련 서적을 확인하여 아래와 같은 내용을 확인하였다.

  • 관계 데이터베이스에서 릴레이션(Relation)의 튜플(Tuple)을 유일하게 식별할 수 있는 속성(Attribute) 또는 속성의 집합 중, 다음 두 성질을 만족하는 것을 후보 키(Candidate Key)라고 한다.
    • 유일성(uniqueness) : 릴레이션에 있는 모든 튜플에 대해 유일하게 식별되어야 한다.
    • 최소성(minimality) : 유일성을 가진 키를 구성하는 속성(Attribute) 중 하나라도 제외하는 경우 유일성이 깨지는 것을 의미한다. 즉, 릴레이션의 모든 튜플을 유일하게 식별하는 데 꼭 필요한 속성들로만 구성되어야 한다.

제이지를 위해, 아래와 같은 학생들의 인적사항이 주어졌을 때, 후보 키의 최대 개수를 구하라.

위의 예를 설명하면, 학생의 인적사항 릴레이션에서 모든 학생은 각자 유일한 "학번"을 가지고 있다. 따라서 "학번"은 릴레이션의 후보 키가 될 수 있다.
그다음 "이름"에 대해서는 같은 이름("apeach")을 사용하는 학생이 있기 때문에, "이름"은 후보 키가 될 수 없다. 그러나, 만약 ["이름", "전공"]을 함께 사용한다면 릴레이션의 모든 튜플을 유일하게 식별 가능하므로 후보 키가 될 수 있게 된다.
물론 ["이름", "전공", "학년"]을 함께 사용해도 릴레이션의 모든 튜플을 유일하게 식별할 수 있지만, 최소성을 만족하지 못하기 때문에 후보 키가 될 수 없다.
따라서, 위의 학생 인적사항의 후보키는 "학번", ["이름", "전공"] 두 개가 된다.

릴레이션을 나타내는 문자열 배열 relation이 매개변수로 주어질 때, 이 릴레이션에서 후보 키의 개수를 return 하도록 solution 함수를 완성하라.

제한사항

  • relation은 2차원 문자열 배열이다.
  • relation의 컬럼(column)의 길이는 1 이상 8 이하이며, 각각의 컬럼은 릴레이션의 속성을 나타낸다.
  • relation의 로우(row)의 길이는 1 이상 20 이하이며, 각각의 로우는 릴레이션의 튜플을 나타낸다.
  • relation의 모든 문자열의 길이는 1 이상 8 이하이며, 알파벳 소문자와 숫자로만 이루어져 있다.
  • relation의 모든 튜플은 유일하게 식별 가능하다.(즉, 중복되는 튜플은 없다.)

입출력 예

relationresult

[["100","ryan","music","2"],["200","apeach","math","2"],["300","tube","computer","3"],["400","con","computer","4"],["500","muzi","music","3"],["600","apeach","music","2"]] 2

입출력 예 설명

입출력 예 #1
문제에 주어진 릴레이션과 같으며, 후보 키는 2개이다.

 

 

접근 방법


 

문제 풀이 순서

1. 크기가 1인 조합부터 ~ n인 조합까지 순차적으로 진행

2. backTracking을 통해 크기가 i인 한 조합이 완성될 때, 최소성과 유일성을 검사한다.

3. 최소성을 보장하려면 자신보다 크기가 작은 후보키 중 자신의 부분집합이 존재하면 안된다.

4. 유일성을 보장하려면 중복되는 tuple이 존재해선 안된다.

5. 3,4번을 모두 만족한다면 후보키 목록에 현재 조합을 추가한다. 동시에 return할 값을 1증가 시킴문제 설명

후보키

프렌즈대학교 컴퓨터공학과 조교인 제이지는 네오 학과장님의 지시로, 학생들의 인적사항을 정리하는 업무를 담당하게 되었다.

 

그의 학부 시절 프로그래밍 경험을 되살려, 모든 인적사항을 데이터베이스에 넣기로 하였고, 이를 위해 정리를 하던 중에 후보키(Candidate Key)에 대한 고민이 필요하게 되었다.

 

후보키에 대한 내용이 잘 기억나지 않던 제이지는, 정확한 내용을 파악하기 위해 데이터베이스 관련 서적을 확인하여 아래와 같은 내용을 확인하였다.

 

관계 데이터베이스에서 릴레이션(Relation)의 튜플(Tuple)을 유일하게 식별할 수 있는 속성(Attribute) 또는 속성의 집합 중, 다음 두 성질을 만족하는 것을 후보 키(Candidate Key)라고 한다.

유일성(uniqueness) : 릴레이션에 있는 모든 튜플에 대해 유일하게 식별되어야 한다.

최소성(minimality) : 유일성을 가진 키를 구성하는 속성(Attribute) 중 하나라도 제외하는 경우 유일성이 깨지는 것을 의미한다. 즉, 릴레이션의 모든 튜플을 유일하게 식별하는 데 꼭 필요한 속성들로만 구성되어야 한다.

제이지를 위해, 아래와 같은 학생들의 인적사항이 주어졌을 때, 후보 키의 최대 개수를 구하라.

 

위의 예를 설명하면, 학생의 인적사항 릴레이션에서 모든 학생은 각자 유일한 "학번"을 가지고 있다. 따라서 "학번"은 릴레이션의 후보 키가 될 수 있다.

그다음 "이름"에 대해서는 같은 이름("apeach")을 사용하는 학생이 있기 때문에, "이름"은 후보 키가 될 수 없다. 그러나, 만약 ["이름", "전공"]을 함께 사용한다면 릴레이션의 모든 튜플을 유일하게 식별 가능하므로 후보 키가 될 수 있게 된다.

물론 ["이름", "전공", "학년"]을 함께 사용해도 릴레이션의 모든 튜플을 유일하게 식별할 수 있지만, 최소성을 만족하지 못하기 때문에 후보 키가 될 수 없다.

따라서, 위의 학생 인적사항의 후보키는 "학번", ["이름", "전공"] 두 개가 된다.

 

릴레이션을 나타내는 문자열 배열 relation이 매개변수로 주어질 때, 이 릴레이션에서 후보 키의 개수를 return 하도록 solution 함수를 완성하라.

 

제한사항

 

relation은 2차원 문자열 배열이다.

relation의 컬럼(column)의 길이는 1 이상 8 이하이며, 각각의 컬럼은 릴레이션의 속성을 나타낸다.

relation의 로우(row)의 길이는 1 이상 20 이하이며, 각각의 로우는 릴레이션의 튜플을 나타낸다.

relation의 모든 문자열의 길이는 1 이상 8 이하이며, 알파벳 소문자와 숫자로만 이루어져 있다.

relation의 모든 튜플은 유일하게 식별 가능하다.(즉, 중복되는 튜플은 없다.)

입출력 예

 

relationresult

 

[["100","ryan","music","2"],["200","apeach","math","2"],["300","tube","computer","3"],["400","con","computer","4"],["500","muzi","music","3"],["600","apeach","music","2"]] 2

입출력 예 설명

 

입출력 예 #1

문제에 주어진 릴레이션과 같으며, 후보 키는 2개이다.

 

 

 

 

 

접근 방법

문제 풀이 순서

 

1. 크기가 1인 조합부터 ~ n인 조합까지 순차적으로 진행

 

2. backTracking을 통해 크기가 i인 한 조합이 완성될 때, 최소성과 유일성을 검사한다.

 

3. 최소성을 보장하려면 자신보다 크기가 작은 후보키 중 자신의 부분집합이 존재하면 안된다.

 

4. 유일성을 보장하려면 중복되는 tuple이 존재해선 안된다.

 

5. 3,4번을 모두 만족한다면 후보키 목록에 현재 조합을 추가한다. 동시에 return할 값을 1증가 시킴

 

자세한 내용은 소스코드에 주석 처리 하겠습니다.

 

 

소스 코드


Java

import java.util.*;

class Solution {
    
    int ans = 0;
    String relation[][];
    HashMap <String, Boolean> visited = new HashMap<String, Boolean>(); //후보키 목록
    
    // 최소성을 만족하는지 검사
    public boolean checkMin(String seqstr){
        
        for(String elem : visited.keySet()){
            boolean flag = false;
            for(int i=0;i<elem.length();i++){
                if(seqstr.indexOf(elem.substring(i, i+1)) == -1)
                    flag = true;
            }
            if(flag == false) // 현재 조합이 후보키를 부분집합으로 가진다면
                return false;
        }
        return true;
    }
    
    // 유일성을 만족하는지 검사
    public boolean checkOnly(ArrayList<Integer> seq){
        
        HashMap <String, Boolean> map = new HashMap<String, Boolean>(); //튜플 목록
    
        for(int i=0;i<relation.length;i++){
            String key = "";
            
            for(int j=0;j<seq.size();j++)
                key += (relation[i][seq.get(j)] + ","); //i번째 튜플에, seq[j]번째 속성
            
            if(map.containsKey(key) == false) //해당 값이 없다면
                map.put(key, new Boolean(true));
            else  //중복되는 값이 있다면
                return false;
        }
        return true;
    }
    
    public void search(int n, int cnt, int now, int attr, ArrayList<Integer> seq){
        
        if(n == cnt){
        //indexOf 메소드로 포함여부를 검사하기 위해 String으로 변환
        //set이나 map 구조를 사용해도 상관없음
            String seqstr = "";
            for(int i=0; i<seq.size();i++)
                seqstr += seq.get(i).toString();
            
            if(checkMin(seqstr) && checkOnly(seq)){
                visited.put(seqstr, new Boolean(true));
                ans++;
            }
            return;
        }
        for(int i = now; i<attr; i++){
            seq.add(i);
            search(n, cnt+1, i+1, attr, seq);
            seq.remove(seq.size() - 1);
        }
    }
    
    public void solve(){
        
        ArrayList<Integer> seq = new ArrayList<Integer>();
        int attr = relation[0].length;
        int tup = relation.length;
        
        //속성의 수를 증가시킴, 작은 수 부터 확인해야 최소성을 확인하기 편함
        for(int i=1;i<=attr;i++){ 
            search(i,0,0,attr, seq);
        }
    }
    
    public int solution(String[][] relation) {
        int answer = 0;
        this.relation = relation;
        solve();
        return answer = ans;
    }
}

 

C++

과거에 c++로 풀었던 풀이입니다. Java와 풀이가 살짝 다릅니다.

#include <string>
#include <algorithm>
#include <string.h>
#include <vector>
#include <map>

using namespace std;

bool visited[8] = { false };
vector<vector<int>> combination;
int result = 0;

bool isCandidate(string &list, vector<vector<string>> &relation) {

	map<string, bool> duplicateCheck;
	for (int i = 0; i < relation.size(); i++) {
		vector<string> row = relation[i];
		string elem;
		for (int j = 0; j < list.size(); j++) {
			elem += (row[int(list[j]) - int('0')]+" ");
		}
		if (duplicateCheck.count(elem) == 0) {
			duplicateCheck.insert({elem, true});
		}
		else { //중복되는 값이 있다면
			return false;
		}
	}
	return true;
}

bool comp(vector<int> list1, vector<int> list2) {
	if (list1.size() < list2.size())
		return true;
	return false;
}

void solve(int size, int cnt, vector<int> &list, vector<vector<string>> &relation, int start) {
	if (size == cnt) {
		return;
	}
	for (int i = start; i < size; i++) {
		if (!visited[i]) {
			visited[i] = true;
			list.push_back(i);
			solve(size, cnt + 1, list, relation, i);
			combination.push_back(list);
			list.pop_back();
			visited[i] = false;
		}
	}
}

int solution(vector<vector<string>> relation) {
	
	int answer = 0;
	int col = relation[0].size();
	vector<int> list;

	result = 0;
	memset(visited, 0, sizeof(visited));
	
    //combination은 크기가 1~n까지 모든 조합이며, 크기가 작은 순부터 큰 순서로 정렬
	solve(col, 0, list, relation, 0);
	sort(combination.begin(), combination.end(), comp);

	vector<string> comb(combination.size());
	for (int i = 0; i < combination.size(); i++) {
		for (int j = 0; j < combination[i].size(); j++) {
			comb[i].push_back(char(int('0') + combination[i][j]));
		}
	}
	
	for (int i = 0; i < comb.size(); i++) {
		if (isCandidate(comb[i], relation)) {
			string tmp = comb[i];
			for (int j = 0; j < comb.size(); j++) {
				bool find = true;
				for (auto t : tmp) {
					if (comb[j].find(t) == string::npos) { // 모든요소가포함되는지 = 모두 찾는다면 = 하나라도 못찾는다면
						find = false;
						break;
					}
				}
				if (find) {
					comb.erase(comb.begin() + j);
                    j--;
				}
			}
			i--; //자기 자신도 삭제가 되므로
			result++;
		}
	}
	answer = result;
	return answer;
}

문제 설명


데이터 처리 전문가가 되고 싶은 "어피치"는 문자열을 압축하는 방법에 대해 공부를 하고 있습니다. 최근에 대량의 데이터 처리를 위한 간단한 비손실 압축 방법에 대해 공부를 하고 있는데, 문자열에서 같은 값이 연속해서 나타나는 것을 그 문자의 개수와 반복되는 값으로 표현하여 더 짧은 문자열로 줄여서 표현하는 알고리즘을 공부하고 있습니다.
간단한 예로 "aabbaccc"의 경우 "2a2ba3c"(문자가 반복되지 않아 한번만 나타난 경우 1은 생략함)와 같이 표현할 수 있는데, 이러한 방식은 반복되는 문자가 적은 경우 압축률이 낮다는 단점이 있습니다. 예를 들면, "abcabcdede"와 같은 문자열은 전혀 압축되지 않습니다. "어피치"는 이러한 단점을 해결하기 위해 문자열을 1개 이상의 단위로 잘라서 압축하여 더 짧은 문자열로 표현할 수 있는지 방법을 찾아보려고 합니다.

예를 들어, "ababcdcdababcdcd"의 경우 문자를 1개 단위로 자르면 전혀 압축되지 않지만, 2개 단위로 잘라서 압축한다면 "2ab2cd2ab2cd"로 표현할 수 있습니다. 다른 방법으로 8개 단위로 잘라서 압축한다면 "2ababcdcd"로 표현할 수 있으며, 이때가 가장 짧게 압축하여 표현할 수 있는 방법입니다.

다른 예로, "abcabcdede"와 같은 경우, 문자를 2개 단위로 잘라서 압축하면 "abcabc2de"가 되지만, 3개 단위로 자른다면 "2abcdede"가 되어 3개 단위가 가장 짧은 압축 방법이 됩니다. 이때 3개 단위로 자르고 마지막에 남는 문자열은 그대로 붙여주면 됩니다.

압축할 문자열 s가 매개변수로 주어질 때, 위에 설명한 방법으로 1개 이상 단위로 문자열을 잘라 압축하여 표현한 문자열 중 가장 짧은 것의 길이를 return 하도록 solution 함수를 완성해주세요.

제한사항

  • s의 길이는 1 이상 1,000 이하입니다.
  • s는 알파벳 소문자로만 이루어져 있습니다.

입출력 예

sresult

"aabbaccc" 7
"ababcdcdababcdcd" 9
"abcabcdede" 8
"abcabcabcabcdededededede" 14
"xababcdcdababcdcd" 17

입출력 예에 대한 설명

입출력 예 #1

문자열을 1개 단위로 잘라 압축했을 때 가장 짧습니다.

입출력 예 #2

문자열을 8개 단위로 잘라 압축했을 때 가장 짧습니다.

입출력 예 #3

문자열을 3개 단위로 잘라 압축했을 때 가장 짧습니다.

입출력 예 #4

문자열을 2개 단위로 자르면 "abcabcabcabc6de" 가 됩니다.
문자열을 3개 단위로 자르면 "4abcdededededede" 가 됩니다.
문자열을 4개 단위로 자르면 "abcabcabcabc3dede" 가 됩니다.
문자열을 6개 단위로 자를 경우 "2abcabc2dedede"가 되며, 이때의 길이가 14로 가장 짧습니다.

입출력 예 #5

문자열은 제일 앞부터 정해진 길이만큼 잘라야 합니다.
따라서 주어진 문자열을 x / ababcdcd / ababcdcd 로 자르는 것은 불가능 합니다.
이 경우 어떻게 문자열을 잘라도 압축되지 않으므로 가장 짧은 길이는 17이 됩니다.

 

 

접근 방법


1. 일단 몇개의 문자열로 잘라야 최소가 되는지 모르므로 (1개~ [문자열길이/2]개)만큼 잘라보며 비교를 해야된다.

(문자열의 길이의 절반보다 큰 수로 자르게 되는 것은 압축을 할 수 없기 때문에 의미가 없다.)

int answer = s.length();
        
for(int i=1;i <= s.length()/2; i++)
	answer = Math.min(answer, findLength(s, i));

 

2. 문자열을 i개로 자른 단위를 s라고 한다면, 문자열  s와 s가 연속적으로 반복된 수 cnt를 저장할 수 있는 구조가 필요하다. 

  class Info{
        String s;
        Integer cnt;
        
        public Info(String s, int cnt){
            this.s = s;
            this.cnt = cnt;
        }
    }

 

3-1. ArrayList<Info> arr형태의 자료구조에 전체 문자열을 i개 단위로 잘라가며 저장을한다.

3-2. 만약 arr의 가장 마지막에 저장된 문자열과 현재 잘려진 문자열이 동일하다면 arr의 가장 마지막 요소의 cnt를 증가

3-3. 만약 arr의 가장 마지막 저장된 문자열과 다르다면, arr에 add 메소드로 삽입을 한다.

 

4. 완성된 arr의 정보를 토대로 문자열의 길이를 계산한다.

 

 

소스 코드


Java

import java.util.*;

class Solution {
    
    class Info{
        String s;
        Integer cnt;
        
        public Info(String s, int cnt){
            this.s = s;
            this.cnt = cnt;
        }
    }
    
    public int findLength(String s, int n){
        
        ArrayList<Info> arr = new ArrayList<Info>();
        
        int idx = 0;
        
        //n개 단위 문자열 처리
        while(idx + n <= s.length()){
            
            String sub = s.substring(idx, idx + n);
            
            if(arr == null || arr.size() == 0)
                arr.add(new Info(sub, 1));
            else{
                
                Info back = arr.get(arr.size() - 1);
                
                if(back.s.equals(sub))
                    back.cnt++;
                else
                    arr.add(new Info(sub, 1));
            }
            
            idx += n;
        }
        
        //압축된 문자열 길이 구하기
        int result = 0;
        
        for(int i=0;i<arr.size();i++){
            Info info = arr.get(i);
            result += info.s.length();
            if(info.cnt > 1)
                result += info.cnt.toString().length();
        }
        return result + (s.length() - idx);
    }
    
    public int solution(String s) {
        int answer = s.length();
        
        for(int i=1;i <= s.length()/2; i++)
            answer = Math.min(answer, findLength(s, i));
        
        return answer;
    }
}

 

C++

#include <string>
#include <vector>
#include <algorithm>

using namespace std;

int strLength(string s, int psize) {
	
	vector <pair<int, string>> patterns;
	int result = 0;
	int nonepattern = s.size();

	int i = 0;
	while (i < s.size() - psize){
		if (s.substr(i, psize).compare(s.substr(i + psize, psize)) == 0) {
			if (patterns.empty() || patterns.back().second.compare(s.substr(i, psize)) != 0)
				patterns.push_back({ 2, s.substr(i, psize) });
			else
				patterns.back().first += 1;

			if (s.substr(i + psize, psize).compare(s.substr(i + 2 * psize, psize)) == 0)
				i += psize;
			else
				i += 2 * psize;
		}
		else
			i+= psize;
	}
	
	for (int i = 0; i < patterns.size(); i++) {
		pair<int, string> pattern = patterns[i];
		int cnt = to_string(pattern.first).size();
		result += (cnt + psize);
		nonepattern -= (pattern.first*psize);
	}
	return result + nonepattern;
}

int solution(string s) {
	int answer = 0;
	int minimum = s.size();
	for (int i = 1; i < s.size()/2 + 1; i++) {
		minimum = min(minimum, strLength(s, i));
	}
	return answer = minimum;
}

문제 설명


레스토랑을 운영하던 스카피는 코로나19로 인한 불경기를 극복하고자 메뉴를 새로 구성하려고 고민하고 있습니다.
기존에는 단품으로만 제공하던 메뉴를 조합해서 코스요리 형태로 재구성해서 새로운 메뉴를 제공하기로 결정했습니다. 어떤 단품메뉴들을 조합해서 코스요리 메뉴로 구성하면 좋을 지 고민하던 "스카피"는 이전에 각 손님들이 주문할 때 가장 많이 함께 주문한 단품메뉴들을 코스요리 메뉴로 구성하기로 했습니다.
단, 코스요리 메뉴는 최소 2가지 이상의 단품메뉴로 구성하려고 합니다. 또한, 최소 2명 이상의 손님으로부터 주문된 단품메뉴 조합에 대해서만 코스요리 메뉴 후보에 포함하기로 했습니다.

예를 들어, 손님 6명이 주문한 단품메뉴들의 조합이 다음과 같다면,
(각 손님은 단품메뉴를 2개 이상 주문해야 하며, 각 단품메뉴는 A ~ Z의 알파벳 대문자로 표기합니다.)

손님 번호주문한 단품메뉴 조합

1번 손님 A, B, C, F, G
2번 손님 A, C
3번 손님 C, D, E
4번 손님 A, C, D, E
5번 손님 B, C, F, G
6번 손님 A, C, D, E, H

가장 많이 함께 주문된 단품메뉴 조합에 따라 "스카피"가 만들게 될 코스요리 메뉴 구성 후보는 다음과 같습니다.

코스 종류메뉴 구성설명

요리 2개 코스 A, C 1번, 2번, 4번, 6번 손님으로부터 총 4번 주문됐습니다.
요리 3개 코스 C, D, E 3번, 4번, 6번 손님으로부터 총 3번 주문됐습니다.
요리 4개 코스 B, C, F, G 1번, 5번 손님으로부터 총 2번 주문됐습니다.
요리 4개 코스 A, C, D, E 4번, 6번 손님으로부터 총 2번 주문됐습니다.

[문제]

각 손님들이 주문한 단품메뉴들이 문자열 형식으로 담긴 배열 orders, "스카피"가 추가하고 싶어하는 코스요리를 구성하는 단품메뉴들의 갯수가 담긴 배열 course가 매개변수로 주어질 때, "스카피"가 새로 추가하게 될 코스요리의 메뉴 구성을 문자열 형태로 배열에 담아 return 하도록 solution 함수를 완성해 주세요.

[제한사항]

  • orders 배열의 크기는 2 이상 20 이하입니다.
  • orders 배열의 각 원소는 크기가 2 이상 10 이하인 문자열입니다.
    • 각 문자열은 알파벳 대문자로만 이루어져 있습니다.
    • 각 문자열에는 같은 알파벳이 중복해서 들어있지 않습니다.
  • course 배열의 크기는 1 이상 10 이하입니다.
    • course 배열의 각 원소는 2 이상 10 이하인 자연수가 오름차순으로 정렬되어 있습니다.
    • course 배열에는 같은 값이 중복해서 들어있지 않습니다.
  • 정답은 각 코스요리 메뉴의 구성을 문자열 형식으로 배열에 담아 사전 순으로 오름차순 정렬해서 return 해주세요.
    • 배열의 각 원소에 저장된 문자열 또한 알파벳 오름차순으로 정렬되어야 합니다.
    • 만약 가장 많이 함께 주문된 메뉴 구성이 여러 개라면, 모두 배열에 담아 return 하면 됩니다.
    • orders와 course 매개변수는 return 하는 배열의 길이가 1 이상이 되도록 주어집니다.

[입출력 예]

orderscourseresult

["ABCFG", "AC", "CDE", "ACDE", "BCFG", "ACDEH"] [2,3,4] ["AC", "ACDE", "BCFG", "CDE"]
["ABCDE", "AB", "CD", "ADE", "XYZ", "XYZ", "ACD"] [2,3,5] ["ACD", "AD", "ADE", "CD", "XYZ"]
["XYZ", "XWY", "WXA"] [2,3,4] ["WX", "XY"]

입출력 예에 대한 설명


입출력 예 #1
문제의 예시와 같습니다.

입출력 예 #2
AD가 세 번, CD가 세 번, ACD가 두 번, ADE가 두 번, XYZ 가 두 번 주문됐습니다.
요리 5개를 주문한 손님이 1명 있지만, 최소 2명 이상의 손님에게서 주문된 구성만 코스요리 후보에 들어가므로, 요리 5개로 구성된 코스요리는 새로 추가하지 않습니다.

입출력 예 #3
WX가 두 번, XY가 두 번 주문됐습니다.
3명의 손님 모두 단품메뉴를 3개씩 주문했지만, 최소 2명 이상의 손님에게서 주문된 구성만 코스요리 후보에 들어가므로, 요리 3개로 구성된 코스요리는 새로 추가하지 않습니다.
또, 단품메뉴를 4개 이상 주문한 손님은 없으므로, 요리 4개로 구성된 코스요리 또한 새로 추가하지 않습니다.

 

 

 

접근 방법


 HashMap과 backTracking을 적절히 활용하여 해결할 수 있는 문제였습니다.

 

1. 문제를 해결하기에 앞서 orders를 정렬 해야합니다. 한 예시로 xy와 yx는 같은 조합으로 볼 수 있고, 이러한 경우를 수월하게 계산하기 위해 사전순으로 정렬합니다.

 

2. 최소 2번 이상 주문된 음식의 조합(backTracking)을 HashMap에 저장합니다. (HashMap<String 음식조합, Integer 주문횟수>) 

 

3. HashMap의 원소를 각각 탐색하면서 음식조합의 수가 course의 한 요소와 일치한다면 가장 많이 주문된 횟수를 비교 갱신하며 음식조합 또한 추가 혹은 갱신을 합니다. 이 때 아래 2개의 HashMap을 사용하였습니다.

 (1) HashMap<Integer, ArrayList<String>> maxMap = new HashMap<Integer, ArrayList<String>>(); 
 (2) HashMap<Integer, Integer> maxCnt = new HashMap<Integer, Integer>();  

 

maxMap은 음식조합의 수(course)에 따른 음식조합을 저장합니다. 만약 한 음식 조합의 주문된 횟수가 최대 주문 횟수랑 일치하는 경우 요소를 추가해야 하기 때문에 value는 List의 형태를 가져야 합니다.

maxCnt는 음식조합의 수(course)에  따른 주문된 최대 횟수를 저장합니다.

 

자세한 내용은 아래 소스코드에 주석으로 설명하겠습니다.

 

 

소스 코드


import java.util.*;
import java.lang.*;

class Solution {
    
    HashMap<String, Integer> map = new HashMap<String, Integer>();
    HashMap<Integer, ArrayList<String>> maxMap = new HashMap<Integer, ArrayList<String>>(); 
    HashMap<Integer, Integer> maxCnt = new HashMap<Integer, Integer>(); 
    
    public void backTracking(int n, int cnt, int now, String order, String comb){
        
        if(cnt == n){
            if(map.containsKey(comb)){ //이미 주문된 적 있는 조합이라면
                int orderCnt = map.get(comb);
                map.put(comb, orderCnt + 1); // 주문횟수 1증가
            }
            else
                map.put(comb, 1);
        }
        for(int i = now; i<order.length(); i++){
            comb += order.charAt(i);
            backTracking(n, cnt+1, i + 1, order, comb);
            comb = comb.substring(0, comb.length() - 1);
        }
    }
    
    public ArrayList<String> solve(String[] orders, int[] course){
        
        ArrayList<String> ans = new ArrayList<String>();
        
        //1번 과정, 수월한 풀이를 위해 문자열 사전순 정렬
        for(int i=0;i<orders.length;i++){
            char[] tmp = orders[i].toCharArray();
            Arrays.sort(tmp);
            orders[i] = new String(tmp);
        }
        
        //2번 과정, i번째 주문에 대해 ,j개(최소 2개) 이상의 조합을 구함
        for(int i=0;i<orders.length;i++)
            for(int j=2;j<=orders[i].length();j++)
                backTracking(j,0,0,orders[i], "");    
        
        //course를 map에 미리 매핑시킴, 이후 course의 요소인지 판별하기에 쉽게 이용 가능
        for(int i=0;i<course.length;i++){
            maxMap.put(course[i], new ArrayList<String>()); 
            maxCnt.put(course[i], 0); 
        }
        
        //3번 과정
        for(Map.Entry<String, Integer> elem : map.entrySet()){
            String foods = elem.getKey();
            int cnt = elem.getValue();
            if(cnt >= 2){
                if(maxMap.containsKey(foods.length())){ //course의 요소라면
                    if(maxCnt.get(foods.length()) == cnt){ //최대 주문 횟수와 일치한다면
                        maxMap.get(foods.length()).add(foods); //추가
                    }
                    else if(maxCnt.get(foods.length()) < cnt){ //최대 주문 횟수 보다 더 크다면
                        maxCnt.put(foods.length(), cnt); //최댓값 갱신
                        ArrayList<String> newOne = new ArrayList<String>(Arrays.asList(foods)); 
                        maxMap.put(foods.length(), newOne); //새롭게 갱신
                    }
                }
            }   
        }
        
        //결과들을 ArrayList에 담는다.
        for(ArrayList<String> elem : maxMap.values())
            for(int i=0;i<elem.size();i++)
                ans.add(elem.get(i));    
        
        //사전순 정렬
        Collections.sort(ans);
        
        return ans;
    }
    
    public String[] solution(String[] orders, int[] course) {
        ArrayList<String> ans = solve(orders, course);
        String[] answer = ans.toArray(new String[ans.size()]);
        return answer;
    }
}

문제 설명


자동완성

포털 다음에서 검색어 자동완성 기능을 넣고 싶은 라이언은 한 번 입력된 문자열을 학습해서 다음 입력 때 활용하고 싶어 졌다. 예를 들어, go 가 한 번 입력되었다면, 다음 사용자는 g 만 입력해도 go를 추천해주므로 o를 입력할 필요가 없어진다! 단, 학습에 사용된 단어들 중 앞부분이 같은 경우에는 어쩔 수 없이 다른 문자가 나올 때까지 입력을 해야 한다.
효과가 얼마나 좋을지 알고 싶은 라이언은 학습된 단어들을 찾을 때 몇 글자를 입력해야 하는지 궁금해졌다.

예를 들어, 학습된 단어들이 아래와 같을 때

go gone guild

  • go를 찾을 때 go를 모두 입력해야 한다.
  • gone을 찾을 때 gon 까지 입력해야 한다. (gon이 입력되기 전까지는 go 인지 gone인지 확신할 수 없다.)
  • guild를 찾을 때는 gu 까지만 입력하면 guild가 완성된다.

이 경우 총 입력해야 할 문자의 수는 7이다.

라이언을 도와 위와 같이 문자열이 입력으로 주어지면 학습을 시킨 후, 학습된 단어들을 순서대로 찾을 때 몇 개의 문자를 입력하면 되는지 계산하는 프로그램을 만들어보자.

입력 형식

학습과 검색에 사용될 중복 없는 단어 N개가 주어진다.
모든 단어는 알파벳 소문자로 구성되며 단어의 수 N과 단어들의 길이의 총합 L의 범위는 다음과 같다.

  • 2 <= N <= 100,000
  • 2 <= L <= 1,000,000

출력 형식

단어를 찾을 때 입력해야 할 총 문자수를 리턴한다.

입출력 예제

wordsresult

["go","gone","guild"] 7
["abc","def","ghi","jklm"] 4
["word","war","warrior","world"] 15

입출력 설명

  • 첫 번째 예제는 본문 설명과 같다.
  • 두 번째 예제에서는 모든 단어들이 공통된 부분이 없으므로, 가장 앞글자만 입력하면 된다.
  • 세 번째 예제는 총 15 자를 입력해야 하고 설명은 아래와 같다.
    • word는 word모두 입력해야 한다.
    • war는 war 까지 모두 입력해야 한다.
    • warrior는 warr 까지만 입력하면 된다.
    • world는 worl까지 입력해야 한다. (word와 구분되어야 함을 명심하자)

 

 

접근 방법


 Trie 자료구조를 공부해봤다면 문제를 봤을 때 Trie를 사용해 해결할 수 있다는 것을 바로 떠올릴 수 있었을 것입니다.

 

 저는 문제를 풀기 위해 Trie의 한 노드가 가지는 자료를 3가지를 정의하였습니다.

1. child HashMap<Character, Node> child = new HashMap();

2. boolean isEnd;

3. int branchSum;

 

1번 child는 노드의 child노드를 연결하기 위한 링크 역할을 합니다.

2번 isEnd는 단어의 끝인지의 여부를 나타냅니다.

3번 branchSum은 뻗어있는 가지의 수를 의미합니다. (isEnd == true)인 구간, 즉 단어의 끝을 가지는 부분도 하나의 가지로 계산합니다.

 

만약 단어가 go, gone, guild가 주어진다면 branchSum의 상태는 아래의 그림과 같습니다.

아래와 같은 코드로 branchSum을 계산할 수 있습니다.

public int makeBranchSum(Node now){

	if(now.isEnd && now.child.isEmpty()) //단어의 끝이면서 더 나아갈 깊이가 없는 경우
		return now.branchSum = 1;

	for(Node node : now.child.values())
		now.branchSum += makeBranchSum(node);

	//단어의 끝이지만 더 나아갈 깊이가 있는 경우, 위 그림에서 노드(o)에 해당
	if(now.isEnd && !now.child.isEmpty()) 
		now.branchSum += 1;
	return now.branchSum;
} 

 

 

 isEnd와 branchSum을 적절히 활용하면 문제를 해결할 수 있습니다.

1. 일단 처음으로 (branchSum == 1)인 구간에 도달한다면 더 이상의 가지가 뻗어있지 않다는 의미가 되므로 그 구간까지의 깊이를 반환하고, 더 깊은 곳으로 진입하지 않습니다.

2. 만약 branchSum != 1이고 isEnd == true인 경우면 더 나아갈 수 있는 가지는 남아있으나, 여기가 어떠한 단어의 끝이라는 의미가 되므로 그 구간까지의 깊이를 포함하여 더해줍니다.

public int search(Node now, int cnt){ //cnt는 깊이를 의미
        
	int ret = 0;

	if(now.branchSum == 1) //1번 항목 검사
		return cnt;

	for(Node node : now.child.values())
		ret += search(node, cnt + 1);
	if(now.isEnd) //2번 항목 검사
		ret += cnt;

	return ret;
}

 

 

소스 코드


import java.util.*;

class Solution {
    
    public class Node {
        HashMap<Character, Node> child = new HashMap();
        boolean isEnd = false;
        int branchSum = 0;
    }
    
    public class Trie {
        
        Node root;
        
        public Trie(){
            root = new Node();
        }
        
        public void insert(String word){
            
            Node current = root;
            
            for(Character c : word.toCharArray()){
                if(current.child.get(c) == null){
                    Node node = new Node();
                    current.child.put(c, node);
                    current = node;
                }
                else
                    current = current.child.get(c);
            }
            current.isEnd = true;
        }
    }
    
    public int makeBranchSum(Node now){
        
        if(now.isEnd && now.child.isEmpty())
            return now.branchSum = 1;
        
        for(Node node : now.child.values())
            now.branchSum += makeBranchSum(node);
        
        if(now.isEnd && !now.child.isEmpty())
            now.branchSum += 1;
        return now.branchSum;
    } 
    
    public int search(Node now, int cnt){
        
        int ret = 0;
        
        if(now.branchSum == 1)
            return cnt;
        
        for(Node node : now.child.values())
            ret += search(node, cnt + 1);
        if(now.isEnd)
            ret += cnt;
        
        return ret;
    }
    
    public int solve(String[] words){
        
        Trie trie = new Trie();
        
        for(int i=0;i<words.length;i++){
            String word = words[i];
            trie.insert(word);
        }
        makeBranchSum(trie.root);
        return search(trie.root, 0);
    }
    
    public int solution(String[] words) {
        int answer = 0;
        return answer = solve(words);
    }
}

+ Recent posts