0%

문제정의


1번 노드로부터 가장 멀리 떨어진 노드가 총 몇 개인지 계산하는 문제이다.

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.*;

public class FarthestNode {

//프로그래머스 level3 가장 먼 노드
public static void main(String[] args) {
int n = 6;
int[][] edge = {
{3, 6},
{4, 3},
{3, 2},
{1, 3},
{1, 2},
{2, 4},
{5, 2}
};

//adjacency list
ArrayList<ArrayList<Integer>> adj_list = new ArrayList<ArrayList<Integer>>(n+1);
for(int i = 0; i <= n; i++)
adj_list.add(new ArrayList<Integer>());

for(int i = 0; i < edge.length; i++)
{
adj_list.get(edge[i][0]).add(edge[i][1]);
adj_list.get(edge[i][1]).add(edge[i][0]);
}

//bfs
int[] distance = new int[n+1];
boolean[] visited = new boolean[n+1];

Queue<Integer> q = new LinkedList<>();
q.add(1);
while(!q.isEmpty())
{
int root = q.poll();
visited[root] = true;
int depth = distance[root]+1;
for(int i : adj_list.get(root))
{
if(!visited[i])
{
distance[i] = depth;
visited[i] = true;
q.add(i);

}
}

}

int max = 0;
for(int i = 1; i < distance.length; i++)
max = Math.max(max, distance[i]);
int answer = 0;
for(int i = 1; i < distance.length; i++)
answer = distance[i] == max ? answer+1 : answer;

System.out.println(answer);


}

}
bfs를 이용해서 문제를 풀면 된다. 각 노드의 depth(깊이)를 계산하여 제일 큰 값을 가지는 노드가 총 몇 개인지 세어본다. 이번 문제에서는 간선이 많기 때문에 행렬형태로 연결을 포함할 수 없다. 그래서 adjacency list를 통해 구현하였다.

그래프를 모두 표시하고 난 뒤, bfs를 한다. 큐를 선언하고 각 노드의 깊이를 저장할 distance를 선언한다. 1번노드부터 bfs를 시작할 것이므로, 1을 큐에 삽입한다.

이제 큐에서 1을 꺼내 루트로 생각하고 방문여부를 나타내는 visited를 true로 한다. root와 인접한 노드들은 root+1의 깊이를 가지므로 이 값을 depth로 둔다. 이제 인접 리스트를 돌면서 방문하지 않았던 노드들의 깊이를 기록하고 방문기록을 true로 한다.

이 과정을 모두 마치면 distance값 중에 가장 큰 것을 max값으로 한다. 그리고 distance중에 max값이 몇 개가 있는지 세면 정답이다.

adjacency list를 사용하기 때문에, 노드 개수를 V, 간선의 개수를 E라고 한다면 최종 시간복잡도는 \(O(V+E)\)이다.

테스트



필자는 처음에 다익스트라로 풀었는데 구현이 잘못되었는지 계속 틀려서 결국 노트북을 닫았다. 약한 멘탈이 미울 지경이다. 문제점을 못찾겠어서 고민하다가 bfs로도 될 것 같은데라는 생각이 들었다. 하지만 어제는 기력이 다해서 집에서 뻗었다. 결국 다른 사람의 풀이를 찾아보니 다 bfs로 풀었다. 다익스트라로 푼 사람은 못 봤다. bfs로 풀어서 정답을 맞았지만 다익스트라로 풀었을 때 왜 틀렸는지는 아직 의문이다.

문제정의


시작단어에서 끝단어로 변환하는 최소 횟수를 알아내려고 한다. 만약 변환할 수 없다면 0을 반환한다. 변환을 거칠 땐 알파벳 한 글자만 바꿀 수 있다.

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import java.util.*;

public class TransformWord {

//프로그래머스 level3 단어 변환
public static int[][] matrix;
public static void main(String[] args) {
String begin = "hit";
String target = "cog";
String[] words = {"cog", "log", "lot", "dog", "dot", "hot"};

List<String> list = new ArrayList<String>();

list.add(begin);
for(String s : words)
list.add(s);

matrix = new int[list.size()][list.size()];

for(int i = 0; i < list.size(); i++)
{
for(int j = i+1; j < list.size(); j++)
{
if(canConnect(list.get(i), list.get(j)))
{
matrix[i][j] = 1;
matrix[j][i] = 1;
}
}
}

final int inf = 1250;
int[] distance = new int[list.size()];
boolean[] visited = new boolean[list.size()];
for(int i = 1; i < list.size(); i++)
distance[i] = inf;

Stack<Integer> stack = new Stack<>();
stack.push(0);
while(!stack.empty())
{
int root = stack.pop();
visited[root] = true;
for(int i = 0; i < list.size(); i++)
{
if(matrix[root][i] == 1 && !visited[i])
{
stack.push(i);
if(distance[i] > distance[root]+1)
distance[i] = distance[root]+1;
}
}
}

int index = list.indexOf(target);
if(index == -1 || distance[index] == inf)
System.out.println("false");
else
System.out.println(distance[index]);


}
public static boolean canConnect(String s1, String s2)
{
char[] arr1 = s1.toCharArray();
char[] arr2 = s2.toCharArray();
int wrong = 0;
for(int i = 0; i < arr1.length; i++)
{
if(arr1[i] != arr2[i])
++wrong;
}

return wrong == 1 ? true : false;
}
}
단어간의 변환 관계를 따지면 그래프를 만들 수 있다. 만든 그래프에서 begin에서 각 단어까지 변환하는데 걸리는 최소 변환 횟수를 구한다. 마지막으로 타겟 단어의 index에 해당하는 횟수를 반환하면 된다. 만약 타겟 단어가 없다면 0을 반환한다.

일단 그래프를 만들기 위해 두 단어가 변환이 되는 지 확인하는 함수 canConnect를 정의하였다. 모든 단어의 길이는 같다고 했으므로, 한쪽의 길이를 기준으로 하여 문자를 비교한다. 만약 다른 글자수(wrong)가 하나라면 true를 반환하고, 아니라면 false를 반환한다.

리스트 list를 만들어 첫시간과 words를 합친다. 반복문을 통해 모든 쌍을 canConnect를 통해 변환할 수 있는 관계인지 파악하고, 만약 변환할 수 있다면 matrix에 1이라 표시한다(18-30줄).

그래프를 matrix에 표시했으므로, dfs를 활용하여 최소 변환 횟수를 구할 차례이다. 이번 문제는 가능한 가 가능한지 않은가가 아니라, 최소를 찾아야 하므로 distance를 추가로 선언한다. 시작단어인 distance[0]은 0으로 하고 나머지는 큰 상수로 초기화한다.

이제 본격적인 dfs로 들어가보자. 스택에 0번을 넣고 vistied(방문했는지 여부)를 true로 한다. 그리고 변환할 수 있는 단어 중에 아직 방문하지 않은 단어들의 인덱스를 담는다. 이 때, 변환 횟수를 비교하는데 만약 현재 단어가 가지고 있는 distance값보다 현재 단어에서 1을 더한 값이 더 작다면 그 값으로 업데이트한다.

dfs를 모두 마치면 distance에는 시작단어에서 각각의 모든 단어로 변환할 수 있는 최소 횟수가 담기게 된다. 이제 indexOf()메소드를 이용하여 끝 단어의 인덱스를 구해 그 값을 반환하면 된다. 만약 끝단어의 distance값이 무한이거나 없을 경우에는 false를 출력하면 된다.

adjacency matrix를 사용하기 때문에, 최종 시간복잡도는 \(O(n^2)\)이다.

테스트



다른 사람의 풀이를 보니 그냥 dfs를 이용해서 풀었는데도 통과가 되었다. 그런데 그렇게 하면 최소값이 아니게 도달해도 정답이 되버릴텐데,, 이 부분에서 프로그래머스가 테스트 케이스를 추가해줬으면 좋겠다는 생각이 들었다.

이 문제를 풀면서 다익스트라를 참고도 안하고 혼자서 해보려고 했는데 나중에 다 풀고보니 다익스트라는 인접한 것 중에 가장 짧은 경로를 기준으로 가는 거라고 했다. 필자는 그냥 가장 나중에 확인한 것을 다음 노드로 지정하였는데 이렇게 해도 최단 거리를 찾을 수 있을지 궁금하다. 다음에 또 이런 문제를 만난다면 그 땐 다익스트라를 좀 더 세심하게 구현해야겠다. 덕분에 모자란 개념을 보충할 수 있었다.

문제정의


섬 연결하기

섬 사이를 연결하는 다리를 짓는 건설비용이 주어진다. 모든 섬을 연결하려고 할 때, 드는 건설비용의 최솟값을 구하라.

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.util.*;

class Island implements Comparable<Island>{
int s;
int e;
int v;
public Island(int s, int e, int v){
super();
this.s = s;
this.e = e;
this.v = v;
}
@Override
public int compareTo(Island arg0)
{
return arg0.v >= this.v ? -1 : 1;
}
}

public class ConnectingIsland {

//프로그래머스 문제풀이 level3 섬 연결하기
public static int[] parent;
public static void main(String[] args) {
int costs[][] = {
{0, 1, 5},
{1, 2, 3},
{2, 3, 3},
{3, 1, 2},
{3, 0, 4},
{2, 4, 6},
{4, 0, 7}
};
int n = 5;

int answer = 0;
parent = new int[n];
for(int i = 0; i < n; i++)
parent[i] = i;


List<Island> list = new ArrayList<>();
for(int i = 0; i < costs.length; i++)
{
Island temp = new Island(costs[i][0], costs[i][1], costs[i][2]);
list.add(temp);
}
Collections.sort(list);

//matrix 만들기
for(Island land : list)
{
if(find(land.e) == find(land.s))
continue;
union(land.e, land.s);
answer += land.v;
}

System.out.println(answer);
}
public static int find(int a)
{
if(a == parent[a])
return a;
return parent[a] = find(parent[a]);
}
public static void union(int a, int b)
{
int a_root = (find(a));
int b_root = (find(b));
if(a_root != b_root)
parent[a_root] = b;
}

}
크루스칼 알고리즘을 사용하면 된다. 필자는 분명 수업시간에 들었던 것 같은데 막상 구현하려고 하니 다 까먹었다.. 그래서 이번 문제를 통해 다시 정리할 수 있었다. 크루스칼 알고리즘을 간단히 설명하자면 간선의 비용이 가장 적은 것 부터 선택해 나가는 알고리즘이다. 다만 사이클이 형성되어서는 안된다. 그렇다면 간선을 선택할 때 마다 사이클이 발생하는지 봐야하는데 이 때 쓰이는 자료구조가 union-find이다.

union-find를 구현하기 위해 parent배열을 선언했다. parent 배열은 자신의 부모가 누구인지 저장하는 배열이다. 처음에는 자신의 위에 아무도 없으므로 배열의 인덱스로 값을 초기화한다.

union-find에선 두 가지 함수가 필요한데, 하나는 find함수이고, 하나는 union이다. find는 해당 노드의 최상단 부모를 찾아주는 함수이다. 최상단 부모는 parent값과 노드 값이 일치할 것이므로 그 때의 값을 반환하면 된다. 그렇지 않다면 재귀를 통해 부모를 찾아간다. 이 때, return문을 보면 find한 값을 parent[a]에 대입하는 것을 볼 수 있는데, 이 과정을 거치지 않는다면 \(O(n)\)으로 수행 시간이 길어지기 때문이다.

union함수는 두 노드를 합쳐주는 역할을 한다. a의 최상단 부모값과 b의 최상단 부모값을 찾아 이 둘이 같지 않다면 부모를 통일해준다. 코드 자체는 어렵지 않다.

필자는 섬을 표현하기 위한 클래스 Island를 새로 선언하였다. 시작점을 s, 끝점을 e, 비용을 v로 하였다. 정렬기준은 비용을 기준으로 하였다. 메인에서 읽어들인 값을 리스트에 저장하여 정렬하였다. 이렇게 하면, 간선비용을 기준으로 하여 오름차순으로 정렬 할 수 있다.

오름차순으로 정렬한 Island를 순회하며, 사이클이 형성될 경우 간선을 선택하지 않고 그렇지 않을 경우 그 값을 answer에 더하고 두 섬이 합해지는 걸 나타내기 위해 union함수를 사용한다. 사이클인지 아닌지 확인하는 방법은 두 섬의 최상단 부모가 같은지 확인하면 된다.

간선의 최대 개수는 \(n*(n-1) \over 2\)이므로, 최종 시간복잡도는 \(O(n^2log_2n^2)\)이다.

테스트



union-find를 연습했었는데 다시 하려니 또 까먹었다. 최적화하는 방법까지 잘 익혀야겠다. 중요한건 복습 또 복습이다.

다른 사람의 풀이를 보니 별도 클래스를 선언하지 않고 람다식으로 특정값을 기준으로 정렬하는 것을 보았다. 앞으로도 잘 쓰일 것 같으니 여기에 메모해 둔다.

Arrays.sort(costs, (a, b) -> Integer.compare(a[2], b[2]));

문제정의


삼각형의 꼭대기부터 바닥까지 가는 경로 중에서 경로의 합이 가장 큰 것을 찾는 문제이다. 아래쪽으로 갈 때는 자신의 바로 왼쪽 아래나 오른쪽 아래로만 이동할 수 있다.

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class IntegerTriangle {

//프로그래머스 문제풀이 level3 정수 삼각형
public static void main(String[] args) {
int[][] triangle = {
{7},
{3,8},
{8,1,0},
{2,7,4,4},
{4,5,2,6,5}
};

int answer = 0;
int[][] sum = new int[triangle.length][triangle.length];
int len = 2;

if(triangle.length == 1)
System.out.println(triangle[0][0]);

sum[0][0] = triangle[0][0];
for(int i = 1; i < triangle.length; i++)
{
sum[i][0] = triangle[i][0] + sum[i-1][0];
sum[i][len-1] = triangle[i][len-1] + sum[i-1][len-2];
for(int j = 1; j < len-1; j++)
{
int max = Math.max(sum[i-1][j-1], sum[i-1][j]);
sum[i][j] = max + triangle[i][j];
}
len++;
}
len--;
for(int i = 0; i < len; i++)
answer = Math.max(answer, sum[triangle.length-1][i]);
System.out.println(answer);
}

}
아이디어는 이렇다. 삼각형의 밑변을 길이로 하여 2차원 배열을 생성한다. 그리고 여기에 해당 위치에 오기까지의 합 중 가장 큰 것을 넣을 것이다. 필자는 이를 sum이라 하였다. sum[0][0]은 맨 꼭대기이므로 triangle[0][0]을 넣는다. 꼭대기에서 갈 수 있는 곳은 3과 8이다. 3의 위치에서 최대값은 3+7=10, 8위치에서 합의 최대는 7+8=15이다. 3번째 줄로 내려가보자. 8은 3의 위치에서 내려올 수 밖에 없으므로 최대값은 10+8=18이다. 1의 경우 2번째 줄의 3과 8에서 둘 다 내려올 수 있다. 이 문제는 최대를 찾는 문제이므로 10과 15중 15를 선택하여 15+1=16을 기록하면 된다.

이런식으로 sum을 채워나가고 가장 아래층의 수 중 가장 큰 수를 찾으면 된다. 이를 코드로 그대로 구현하였다. sum을 정사각형으로 선언했기 때문에 각 층에 있는 숫자의 개수를 len으로 알려주었다. 가장 꼭대기를 채워넣고 그 아래부터는 for문의 영향을 받는다. 각 층에서 맨 왼쪽과 오른쪽 원소는 내려오는 경로가 하나밖에 없기 때문에 이는 따로 처리한다. 이를 제외한 원소는 위층의 왼쪽 대각선과 오른쪽 대각선의 sum값을 비교 한뒤 큰 값을 취한다. 각 층에서의 연산이 끝나면 len을 증가한다.

반복이 끝나면 맨 아래층의 원소에서 최대값을 찾아 반환한다.

연산 횟수는 삼각형의 밑변을 n이라 할 때, \(n^2 \over 2\)이므로, 최종 시간복잡도는 \(O(n^2)\)이다.

테스트



다이나믹 프로그래밍에 약한 편이다. 이건 기초문제라 쉽게 해결했지만, 이 유형에 좀 더 익숙해져야겠다.

문제정의


열쇠의 돌기부분이 자물쇠의 홈에 맞는지 판별하는 문제이다. 열쇠를 자물쇠에 맞출 때, 열쇠가 자물쇠 바깥으로 삐져나올 수 있다. 하지만 자물쇠와 열쇠의 돌기부분이 서로 맞물리면 안된다. 자세한 사항은 아래 링크에서 확인하길 바란다.

문제 링크

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import java.util.Arrays;

public class LockandKey {

//프로그래머스 문제풀이 level3 자물쇠와 열쇠
public static void main(String[] args) {
int[][] key = {
{0,0,0},
{0,0,0},
{0,0,0}
};
int[][] lock = {
{1,1,1},
{1,1,0},
{1,0,1}
};

boolean answer = false;

//get rotation
int[][][] keys = new int[4][key.length][key.length];
//make keys
for(int i = 0; i < key.length; i++)
{
for(int j = 0; j < key.length; j++)
{
keys[0][i][j] = key[i][j];
keys[1][i][j] = key[key.length-1-j][i];
keys[2][i][j] = key[key.length-1-i][key.length-1-j];
keys[3][i][j] = key[j][key.length-1-i];
}
}
//count zero
int zero = 0;
for(int i = 0; i < lock.length; i++)
{
for(int j = 0; j < lock.length; j++)
{
if(lock[i][j] == 0)
zero++;
}
}

//find match key
int m = key.length;
int n = lock.length;
for(int i = 0; i < 4; i++) //4keys
{
for(int c = 0; c < m+n-1; c++)
{
for(int r = 0; r < m+n-1; r++)//right corner position
{
//check if key is matched
boolean match = true;
int zero_cnt = zero;
for(int y = c-m+1; y <= c; y++)
{
for(int x = r-m+1; x <= r; x++)
{
if(y < 0 || x < 0 || y >= lock.length || x >= lock.length)
continue;
int v = keys[i][y-(c-m+1)][x-(r-m+1)];
if(v == 1 && lock[y][x] == 1)
{
match = false;
break;
}
else if(v == 1 && lock[y][x] == 0)
zero_cnt--;
}
if(!match)
break;
}
//if answer found
if(zero_cnt == 0)
{
answer = true;
break;
}

}
if(answer)
break;
}
if(answer)
break;
}
System.out.println(answer);
}

}
코드가 긴편이다. 전체적인 과정을 생각해보면 다음과 같다. 1. 열쇠는 회전이 가능하다 90도씩 회전한 4개 열쇠를 만든다. 2. 자물쇠에서 홈이 몇 개 있는지 센다. 3. 자물쇠에 각 열쇠를 끼워본다. 4. 자물쇠에 열쇠가 맞고 모든 홈을 채웠다면 답을 true로 한다. 5. 그렇지 않다면 다음 자리로 열쇠를 이동해본다.

1번 과정 코드부터 살펴보자

1
2
3
4
5
6
7
8
9
10
11
12
13
//get rotation
int[][][] keys = new int[4][key.length][key.length];
//make keys
for(int i = 0; i < key.length; i++)
{
for(int j = 0; j < key.length; j++)
{
keys[0][i][j] = key[i][j];
keys[1][i][j] = key[key.length-1-j][i]; //90
keys[2][i][j] = key[key.length-1-i][key.length-1-j]; //180
keys[3][i][j] = key[j][key.length-1-i]; //270
}
}
회전 각에 따라 총 4개의 열쇠가 나올 수 있다. 회전을 구현하는 방법은 읽는 순서를 달리하면 된다. 이는 코드에 나와있으니 코드를 스스로 공부하고 그림을 그려가면서 알아보길 바란다. 이렇게 하면 4개의 열쇠꾸러미를 손에 쥘 수 있다.

2번 과정은 2차원 배열을 순회하면서 0인 부분을 세면 된다. 이 부분은 간단하므로 넘어가자.

3,4,5과정은 한 코드로 나타나 있다. 범위 설정에 주의하면서 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//find match key
int m = key.length;
int n = lock.length;
for(int i = 0; i < 4; i++) //4keys
{
for(int c = 0; c < m+n-1; c++)
{
for(int r = 0; r < m+n-1; r++)//right corner position
{
//check if key is matched
boolean match = true;
int zero_cnt = zero;
for(int y = c-m+1; y <= c; y++)
{
for(int x = r-m+1; x <= r; x++)
{
if(y < 0 || x < 0 || y >= lock.length || x >= lock.length)
continue;
int v = keys[i][y-(c-m+1)][x-(r-m+1)];
if(v == 1 && lock[y][x] == 1)
{
match = false;
break;
}
else if(v == 1 && lock[y][x] == 0)
zero_cnt--;
}
if(!match)
break;
}
//if answer found
if(zero_cnt == 0)
{
answer = true;
break;
}

}
if(answer)
break;
}
if(answer)
break;
}
첫번째 반복은 열쇠 꾸러미에서 몇 번째 열쇠를 쓸 것인지 나타낸다. 열쇠는 총 4개이므로 4개의 열쇠에 대해 각각 수행해본다.

c와 r의 반복은 열쇠를 이동해보는 반복이다. c와 r은 열쇠의 오른쪽 아래 모서리가 위치하는 점을 기준으로 하였다. 모든 경우의 수를 해보려면 오른쪽 아래 모서리가 (0,0)에서 (m+n-2, m+n-2)까지 움직여야 한다(m은 열쇠의 가로, n은 자물쇠의 가로이다).

열쇠를 이동했다면 이제 홈에 들어맞는지 확인해야 한다. 이는 y와 x에 대한 반복문으로 나타냈다. 오른쪽 아래 모서리를 기준으로 움직였기 때문에, 열쇠가 들어맞는지 검사하기 위해선 위쪽 모서리 값을 알아야 한다. 이는 (c-m+1, r-m+1)이다. 그럼 여기부터 열쇠와 자물쇠가 들어맞는지 확인하면 된다.

일단 x나 y가 자물쇠 범위 밖을 벗어났다면 비교할 필요가 없으므로 continue를 한다. 만약 범위 이내라면 둘을 비교한다. 이때 열쇠는 [0][0]부터 비교하는 것이기 때문에, 현재 y와 x에서 왼쪽 모서리 값을 빼준다.

만약 비교하는 열쇠와 자물쇠가 모두 돌기부분이라면 들어맞지 않으므로 break를 통해 탈출한다. 이 때 match를 이용해 한 번더 반복을 탈출하여 열쇠가 바로 이동할 수 있도록 한다.

만약 열쇠는 돌기이고 자물쇠가 홈부분이면, zero_cnt를 하나씩 줄인다. 모든 검사를 마칠 때 이 값이 0이라면 열쇠가 자물쇠에 들어맞다는 뜻이므로, 모든 반복을 탈출하고 답을 출력한다.

반복 횟수를 계산해보자면 \((m+n-2)(m+n-2)m*m\)이 된다. 따라서 최고차항을 따로 떼어내면 최종 시간복잡도는 \(O(m^2n^2)\)이다.

테스트



처음으로 2시간 안에 level3문제를 스스로 풀어서 기쁘다. 멘탈만 흔들리지 않으면 성장할 수 있다고 생각하는 오늘이다. 요새 네카라쿠배 2차테스트를 하는데 알고보니 실수를 해서 점수를 다 날려 먹게 생겼다 하... 2일차까지 테스트 점수가 다 꽝인데 그래도 포기하지 않고 끝까지 해보기로 했다. 학원의 힘을 빌리지 않고도 성장할 수 있으니까! 가벼운 마음으로 테스트에 임하자.

문제정의


네트워크의 개수가 몇 개인지 판별하는 문제이다. 기본적인 탐색문제이다.

문제 링크

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.util.*;

public class Network {

//프로그래머스 문제풀이 level3 네트워크
public static void main(String[] args) {
int[][] computers = {
{1, 1, 0},
{1, 1, 0},
{0, 0, 1}
};
int n = 3;
int answer = 0;
boolean[] visited = new boolean[n];
for(int i = 0 ; i < n; i++)
{
if(!visited[i])
{
answer++;
visited[i] = true;
Stack<Integer> stack = new Stack<>();
stack.push(i);
while(!stack.empty())
{
int t = stack.peek();
stack.pop();
for(int j = 0; j < n; j++)
{
if(computers[t][j] == 1 && !visited[j])
{
visited[j] = true;
stack.push(j);
}

}
}
}
}
}

}

전형적인 탐색문제라 어렵지 않다. 일단 각 컴퓨터를 한번이라도 체크했는지 확인하는 불리안 배열을 생성한다. 그리고 이 배열을 순회하며 만약 한 번도 체크하지 않은 컴퓨터라면 dfs를 실시 한다. 필자는 스택을 이용해서 dfs를 구현했다.

반복 횟수를 계산해보자면 \((m+n-2)(m+n-2)m*m\)이 된다. 따라서 최고차항을 따로 떼어내면 최종 시간복잡도는 \(O(m^2n^2)\)이다.

테스트



예전만 해도 dfs가 왜 스택이고 bfs는 왜 큐인지 이해가 잘 안갔는데 어느 새, 혼자서 아무것도 참고하지 않고 dfs를 구현할 수 있게 되었다. 늦은 감이 없지 않아 있지만, 그래도 성장했음을 느낀다.

문제정의


나란히 있는 두 풍선을 임의로 선택하여 그 중에 큰 쪽을 터트린다. 단, 한번은 더 작은 숫자의 풍선을 터트릴 수 있다. 풍선이 1개가 남을 때 까지 터트릴 때, 최후에 남을 수 있는 풍선의 갯수를 반환하라.

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.ArrayList;

public class BallonPop {

//프로그래머스 문제풀이 level3 풍선 터트리기

public static void main(String[] args) {
int[] a = {9,1,5};
int[] left = new int[a.length];
int[] right = new int[a.length];

left[0] = a[0];
right[a.length-1] = a[a.length-1];

for(int i = 1; i < a.length; i++)
left[i] = left[i-1] > a[i] ? a[i] : left[i-1];

for(int i = a.length-2; i >= 0; i--)
right[i] = right[i+1] > a[i] ? a[i] : right[i+1];

int answer = 0;
for(int i = 0; i < a.length; i++)
answer = (left[i] < a[i]) && (right[i] < a[i]) ? answer : answer+1;

System.out.println(answer);
}

}

한 풍선을 선택하고 그 풍선이 최후로 남을 수 있는 경우의 수를 생각해보면 자신을 기준으로 좌우에서 풍선의 최소값을 각각 구한다. 만약 좌우의 풍선값보다 기준 풍선값의 값이 작다면, 이 풍선은 절대 터트릴 수 없다. 이를 코드로 구현한 것이 위와 같다.

시간 복잡도는 \(O(n)\)이다.

테스트



요새 문제풀이를 통 못했는데 일단 난이도가 어려워져서 고민하는 시간이 많아 포스팅 하는 텀이 길어진다. 물론 이 풀이도 질문하기에서 본 접근방법을 참고한 거라 공부가 많이 필요하다고 느낀다. 패스트캠퍼스에서 주관하는 네카라쿠배 교육과정의 2차테스트를 치르고 있는 중인데, 혼자 공부하는 것보단 학원이 나을 것 같아서 최선을 다해 테스트에 임하려고 한다. 그래도 혼자 공부하는 시간을 줄이진 말아야지. 화이팅!

문제정의


가로 길이가 2이고 세로의 길이가 1인 직사각형 모양의 타일이 있다. 이 타일로 2*n바닥을 덮을 수 있는 경우의 수를 구하는 문제이다.

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Tiling2 {

//프로그래머스 문제풀이 Level3 2xn 타일링

public static int[] cache;
public static void main(String[] args) {
int n = 4;
cache = new int[n];
int answer = ReturnAnswer(n, 0);

}

public static int ReturnAnswer(int n, int w)
{
if(w == n)
return 1;
else if(w > n)
return 0;
else if(cache[w] != 0)
return cache[w];
else
{
int res = 0;
res = ReturnAnswer(n,w+1) % 1000_000_007;
res += ReturnAnswer(n, w+2) % 1000_000_007;
cache[w] = res%1000_000_007;
return cache[w];
}
}

}
생각해보면 세로길이2를 채울 수 있는 경우의 수는 2가지 밖에 없다. 하나는 세로로 하나의 타일이 쌓인 경우이고, 나머지 하나는 가로로 두개의 타일이 쌓인 경우이다. 첫번째 경우에는 가로로 1증가하고, 두번째 경우는 가로가 2증가한다. 이 두가지 경우를 계속 시도해보는 코드를 짜면 된다.

ReturnAnswer함수를 살펴보자. n은 주어진 가로 길이이고 w는 현재 가로 길이다. 만약 이 가로 길이에 도달하면 1을 반환하고 그렇지 않으면 0을 반환한다. 여기서 연산의 반복을 줄이기 위해 캐시도 사용한다. 만약 해당 길이에 이미 구해놓은 값이 있으면 그 값을 사용한다.

새로 계산해야 하는 경우 길이를 1늘린 것과 2늘린 값을 가져와서 나머지 연산을 한다. 이 둘은 더하는 과정에서도 값이 1,000,000,007을 넘는 경우가 있으므로 캐시에 값을 넣을 때도 나머지 연산을 한 뒤 값을 반환한다.

시간 복잡도는 \(O(log2^n)\)이다.

테스트



문제정의


N으로 표현하기

자연수 N을 최대 8개까지 써서 사칙연산으로 주어진 수를 표현할 수 있는지 보는 문제이다. 만약 표현할 수 있다면 표현할 수 있는 수 중에 가장 작은 사용횟수를 출력해야 한다.

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class NExpress {

//프로그래머스 문제풀이 level3 n으로 표현
static int answer = -1;
public static void main(String[] args) {
int N = 4, number = 17;

ReturnMin(0, 0, number, N);
System.out.println(answer);
}
public static void ReturnMin(int res, int cnt, int number, int N)
{
int temp = N;
if(cnt > 8)
answer = -1;
if(res == number)
{
if(answer == -1 || answer > cnt)
answer = cnt;
}
else
{
for(int i = 0; i < 8-cnt; i++)
{
ReturnMin(res+temp, cnt+i+1, number, N);
ReturnMin(res-temp, cnt+i+1, number, N);
ReturnMin(res*temp, cnt+i+1, number, N);
ReturnMin(res/temp, cnt+i+1, number, N);

temp = temp*10+N;
}

}
}

}
다른 사람의 코드를 찾아서 쓴 코드이다. 필자도 dfs로 풀 생각을 하였는데 temp부분에서 실수를 했다. 가령 1111+1111는 2222가 나와야 하는데 필자는 1111+1111을 계산하지 않고 끝내버리는 오류가 있었기 때문이다. 문제점을 알았으니 다행이다. 일단 코드 설명을 해보면 res는 현재까지 계산한 값을 의미하고 cnt는 사용횟수이다. number는 만들고자 하는 값이다.

재귀의 종료 조건은 사용횟수가 8을 넘어가면 답이 없는 것이므로 answer에 -1을 한다. 다른 경우는 8번안에 숫자가 만들어진다면 answer가 -1이거나 cnt보다 answer가 큰 경우 답을 cnt로 한다. 그것도 아니라면 사칙연산을 추가로 진행해야 한다. 사용할 수 있는 횟수만큼 N을 뒤에 붙여가며 더해간다. 예를들면 5를 4번쓸 수 있으면 [5,55,555,5555]로 사칙연산을 하는 것이다.

시간 복잡도는 최대 8번이라는 조건이 있기 때문에 \(O(4^8)\)이므로 \(O(1)\)로 나타낼 수 있다.

테스트



동적계획법으로 푸는 방법을 몰라서 재귀를 사용하였는데, 이런 경우 대게 테스트 케이스가 커서 골치 아픈 경우가 많다. 하지만 이 문제는 8이라는 제한 횟수가 있어서 그런지 시간 초과가 나는 일은 없었다. 재귀함수를 짤 때 종료조건에 대해 꼼꼼히 생각해봐야겠다.

문제정의


1초 동안의 최대 트래픽이 몇개가 되는지 구하는 문제이다.

문제풀이


전체 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class test {

public static final int START = 0;
public static final int END = 1;

public class Time{
int ms;
int state;

public Time(int t, int s){
this.ms = t;
this.state = s;
}
}

public static void main(String[] args) {
String[] lines = {"2016-09-15 01:00:04.002 2.0s", "2016-09-15 01:00:07.000 2s"};
List<Time> times = new ArrayList<Time>();
for(String line : lines)
{
String[] line_info = line.split(" ");
//종료시간을 0.001초 단위로 환산하자
String[] time_str = line_info[1].split(":");
int end_t = Integer.parseInt(time_str[0])*3600000 + Integer.parseInt(time_str[1])*60000 + Integer.parseInt(time_str[2].split("\\.")[0])*1000 + Integer.parseInt(time_str[2].split("\\.")[1]);
//지속시간을 구해볼까
String last_time_str = line_info[2].substring(0, line_info[2].length()-1);
String[] last_time_arr = last_time_str.split("\\.");
int last_t = Integer.parseInt(last_time_arr[0])*1000;
if(last_time_arr.length > 1)
last_t += Integer.parseInt(last_time_arr[1]);

//이제 시작 시간을 구하자(끝시간 - 지속시간 +1)
int start_t = end_t - last_t + 1;

test obj = new test();
test.Time t = obj.new Time(start_t, START);
times.add(t);
t = obj.new Time(end_t+999, END);
times.add(t);

}

Comparator<Time> t_comparator = new Comparator<Time>(){
@Override
public int compare(Time o1, Time o2)
{
if(o1.ms != o2.ms)
return Integer.compare(o1.ms, o2.ms);
return Integer.compare(o1.state, o2.state);
}
};

Collections.sort(times, t_comparator);

int working = 0, max_cnt = 0;
for(int i = 0; i < times.size(); i++)
{
if(times.get(i).state == START)
++working;
else
--working;

max_cnt = Math.max(working, max_cnt);
}

System.out.println(max_cnt);
}

}

이 문제에서의 핵심 아이디어는 어떻게 1초 트래픽을 어디서 볼 것인가 이다. 밀리초마다 단위를 옯겨가면서 하면 연산량이 많아진다. 따라서 우리는 언제 트래픽의 개수가 변하는지 생각해봐야 한다. 생각해보면 구간의 양 끝에서 트래픽이 변할 수 있다는 것을 알 수 있다. 이 아이디어를 가지고 코드를 살펴보자.

8~16번째 라인을 보면 Time에 대한 정의를 해놓은 것을 볼 수 있다. ms는 시간을 의미하고 state는 구간의 처음에 해당하는지 끝에 해당하는지 알려주는 변수이다. state에는 START또는 END가 들어가며, 이는 각각 0과 1이다.

24~46번째는 로그에 있는 시간을 ms단위로 바꾼 뒤, 이를 리스트에 저장하는 과정이다. 첫구간과 끝구간을 저장하는데 잘 보면 end_T+999를 저장한 것을 볼 수 있다. 이는 뒤 구간을 포함해서 1초안에 해당하면 동시에 진행되는 작업물로 보기 때문이다(ex 4.001초에 완료한것과 5초에 시작하는 트래픽은 동시에 일어난 것으로 본다).

48~57번째 줄에서는 정렬을 한다. 정렬을 할 땐 기본적으로 시작 시각을 기준으로 하고 시각 기준이 같을 시 시작 구간인 부분을 우선으로 한다. 왜냐하면 동시에 시작하고 끝난 트래픽도 두 작업을 같이 했다고 보기 때문이다.

정렬이 끝나고 난 뒤 배열을 돌면서 구간이 START 이면 working에 1을 더해주고 그렇지 않으면 1을 뺀다. 구간에만 집중하여 문제를 풀면 \(O(nlogn)\)만에 문제를 풀 수 있다.

테스트



아이디어는 다 생각이 났었는데 코드로 이 아이디어를 어떻게 풀어나가야 할지 몰라 난감했다. 거의 끝까지 왔는데 미처 못 풀어서 다른 사람의 풀이를 참고해서 짰는데 속상하다. 카카오 블라인드 테스트 중에서 가장 어려운 문제라고 하던데 그래도 아이디어는 생각해 낸 것까지 올라온 걸로 위안을 삼기로 했다. 포기하지 말자.