D: 현재 값을 2배로 만들고 10000으로 나눈 나머지를 저장S: 현재 값에서 1을 뺌 (0이면 9999가 됨)L: 각 자릿수를 왼쪽으로 회전R: 각 자릿수를 오른쪽으로 회전A에서 B로의 최단 경로를 찾는 문제다.
비슷한 문제를 풀어본 경험을 바탕으로 떠올린 풀이방법은 두가지였다.
DFS + 백트래킹
재귀를 활용한 깊이 우선 탐색을 할 경우, 특정 명령어가 먼저 연속으로 사용된다. 따라서 특정 숫자에 도달했을 때, 명령어가 최소 길이라는 보장이 없다. 그렇기에 명령어의 길이를 확인하면서 갱신 시켜주어야한다는 단점이 존재한다.
여러번 연산이 이루어지기에 DFS + 메모이제이션으로 연산의 최소화가 불가능하다고 판단했다.
BFS + 다익스트라
너비 우선 탐색을 할 경우, 큐에 들어가는 명령어의 숫자가 짧은 순으로 정렬할 수 있다. 따라서 특정 숫자에 도달했을 때 사용된 명령어의 길이가 최소라는 것을 보장할 수 있다.
게다가 레지스터에 들어가는 숫자가 4자리로 제한되기에 주어진 메모리 내에서 방문체크가 가능하다. 그렇기에 너비 우선 탐색의 최대 연산 회수는 4자리 수만큼인 10000회로 제한된다.
너비 우선 탐색을 사용한다고 생각했을 때, 남은 것은 명령어를 구성하는 방법이다.
D를 수행할 때, 12 -> 24로 변화된다면 24의 이전 숫자는 12가 된다.import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
int T = Integer.parseInt(in.readLine());
for(int i = 0; i < T; i++) {
input(in);
solve();
}
}
static int from;
static int to;
static void input(BufferedReader in) throws IOException {
StringTokenizer tokens = new StringTokenizer(in.readLine());
from = Integer.parseInt(tokens.nextToken());
to = Integer.parseInt(tokens.nextToken());
}
static void solve() {
boolean[] visited = new boolean[10_000];
PriorityQueue<Command> pq = new PriorityQueue<>();
visited[from] = true;
pq.add(new Command(from, ""));
while(!pq.isEmpty()) {
Command cur = pq.poll();
if(cur.num == to) {
System.out.println(cur.str);
break;
}
// D
int d = (cur.num * 2) % 10_000;
if(!visited[d]) {
visited[d] = true;
pq.add(new Command(d, cur.str + "D"));
}
// S
int s = cur.num > 0 ? cur.num-1 : 9999;
if(!visited[s]) {
visited[s] = true;
pq.add(new Command(s, cur.str + "S"));
}
// L
int top = cur.num / 1_000;
int l = ((cur.num * 10) % 10_000) + top;
if(!visited[l]) {
visited[l] = true;
pq.add(new Command(l, cur.str + "L"));
}
// R
int bottom = cur.num % 10;
int r = (cur.num / 10) + bottom * 1_000;
if(!visited[r]) {
visited[r] = true;
pq.add(new Command(r, cur.str + "R"));
}
}
}
static class Command implements Comparable<Command>{
int num;
String str;
public Command(int num, String str) {
this.num = num;
this.str = str;
}
public int compareTo(Command other) {
return Integer.compare(this.str.length(), other.str.length());
}
}
}
import java.util.*;
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
int T = Integer.parseInt(in.readLine());
for(int i = 0; i < T; i++) {
input(in);
solve();
}
}
static int from;
static int to;
static void input(BufferedReader in) throws IOException {
StringTokenizer tokens = new StringTokenizer(in.readLine());
from = Integer.parseInt(tokens.nextToken());
to = Integer.parseInt(tokens.nextToken());
}
static void solve() {
boolean[] visited = new boolean[10_000];
int[] prev = new int[10_000];
char[] cmd = new char[10_000];
ArrayDeque<Integer> pq = new ArrayDeque<>();
visited[from] = true;
pq.add(from);
prev[from] = -1;
while(!pq.isEmpty()) {
int cur = pq.poll();
if(cur == to) {
StringBuilder str = new StringBuilder();
int pivot = cur;
while(pivot != from) {
str.append(cmd[pivot]);
pivot = prev[pivot];
}
str.reverse(); // 역순으로 거슬러 올라가기에 뒤집어주어야한다.
System.out.println(str.toString());
break;
}
// D
int d = (cur * 2) % 10_000;
if(!visited[d]) {
visited[d] = true; // 방문 체크
prev[d] = cur; // 이전 숫자 저장
cmd[d] = 'D'; // 사용된 명령어 저장
pq.add(d);
}
// S
int s = cur > 0 ? cur-1 : 9999;
if(!visited[s]) {
visited[s] = true;
prev[s] = cur;
cmd[s] = 'S';
pq.add(s);
}
// L
int top = cur / 1_000;
int l = ((cur * 10) % 10_000) + top;
if(!visited[l]) {
visited[l] = true;
prev[l] = cur;
cmd[l] = 'L';
pq.add(l);
}
// R
int bottom = cur % 10;
int r = (cur / 10) + bottom * 1_000;
if(!visited[r]) {
visited[r] = true;
prev[r] = cur;
cmd[r] = 'R';
pq.add(r);
}
}
}
}
String을 사용한 코드가 수행시간이 4배 가량 더 오래 걸렸다. 자바에서 String을 연산자로 더하는 방식은 피해야하는게 맞다.