
참가자 A, B, C, D가 시작 칸부터 지정된 칸 수 만큼 총 16칸을 돌며 소유되지 않는 칸을 얻고, 입력값이 끝나거나 모든 칸이 채워진다면 끝나는 게임입니다.
즉, 게임 보드와 참가자의 이동 및 소유 규칙을 명확히 이해하고, 종료 조건까지 확실히 끝내야 하는 알고리즘 문제입니다.
먼저 의사코드를 작성해보겠습니다. 어제와 달리 간단명료하면서 그렇게 생각했던 이유를 들어보겠습니다.
play()에 들어오는 파라미터는 숫자가 4개 씩 들어오는 배열입니다. 예시에도 "1,2,3,4" 이렇게 쓴 걸 보니, 입력값 정의를 ["1,2,3,4", "1,2,3,4"] 이런 식으로 정해보겠습니다.Map 혹은 Dict(JS-객체) 자료구조를 사용하면서, A ~ D 참가자의 소유 장소 개수를 정의해 log로 출력합니다. 직접 의사코드를 써보면서 어떤 자료구조를 쓸 지 후술하겠습니다.간단한 것만 직접 눈으로 작성해보고, 예상 출력값을 그려봤습니다.
같은 수가 연속, 임의의 난수, 2개 만 숫자가 같은 것이 연속일 때 등등을 다양한 상황을 걸러내기 위해 입력했습니다.
console.log('1.', play(['1,2,3,4', '1,1,1,1', '2,2,2,2', '3,3,3,3']));
// 예상 출력: {A: 1, B: 2, C: 3, D: 4}
console.log('2.', play(['1,1,1,1', '1,1,1,1', '1,1,1,1', '1,1,1,1']));
// 예상 출력: {A: 1, B: 2, C: 3, D: 4}
console.log('3.', play(['4,2,1,3', '2,1,2,2', '3,2,1,3', '2,1,3,2']));
console.log('4.', play(['3,3,3,3', '2,2,2,2', '1,1,1,1']));
console.log('5.', play(['1,3,2,4', '2,1,4,3']));
// 예상 출력: { A: 1, B: 1, C: 2, D: 2 }
console.log('6.', play(['1,2,1,2', '2,1,2,1', '1,2,1,2', '2,1,2,1']));
console.log('7.', play(['1,2,3,4']));
// 예상 출력: { A: 1, B: 1, C: 1, D: 1 }
console.log('8.', play(['1,4,2,1', '1,1,3,4', '2,1,1,4', '2,4,2,4']));
console.log('9.', play(['4,3,2,1', '1,2,3,4', '4,3,2,1', '1,2,3,4']));
console.log(
'10.',
play(['1,1,1,1', '2,2,2,2', '3,3,3,3', '4,4,4,4', '5,5,5,5', '6,6,6,6']),
);
boardArray)에 적합하다고 생각해 배열로 선언했습니다.null로 초기화 했습니다.partPositionboard의 선형적 구조를 탐색하고, 각각의 참가자가 서있는 위치가 필요하다고 생각해 정의했습니다.String - 참가자명, Value는 board의 인덱스 - 위치인 Number로 초기화 했습니다.partOwnedboard와 partPosition가 진행되면서 참가자가 장소를 소유하게 됐다면, 개수를 추가할 변수도 필요하다 생각해 정의했습니다.partPosition와 마찬가지로 객체로 선언했습니다.Number로 초기화 했습니다.,을 가지는 문자열로 이뤄져 있습니다.i로 입력값을 추출합니다."1,2,3,4" 이런 식으로 이뤄져 있기 때문에 split(',')을 이용해 각 문자열인 이동 숫자를 추출합니다.Number로 가공합니다.board의 해당 인덱스 - 요소가 null이면 소유 로직을 실행합니다. null이 아니라면 넘어갑니다.board에 참가자를 할당합니다.partOwned의 해당 참가자의 Value에 1을 추가합니다.partOwned를 리턴합니다.의사코드로 썼던 부분을 참고해 실제로 코드를 작성하겠습니다.
function play(param0) {
// 0. 변수명을 잘 알아볼 수 있게 파라미터 재 할당
const moves = param0;
// 1-1. board: 시작 칸 포함 16칸짜리 보드 초기화
const board = Array(16).fill(null);
// 1-2. partPosition: 각 참가자의 초기 위치
const partPosition = { A: 0, B: 0, C: 0, D: 0 };
// 1-3. partOwned: 각 참가자의 소유한 칸 수
const partOwned = { A: 0, B: 0, C: 0, D: 0 };
...
우선 의사코드 1번을 작성했습니다. 변수명과 배열, 객체 등 해당 자료구조, 초기화까지 작성했습니다.
참고로 참가자가 4명이 있어서 긴 느낌이지만 을 좋게 만드려고 생각해 moves로 재할당 해줬습니다.
// 2. 반복문 돌기
for (let i = 0; i < moves.length; i++) {
// 2-1. moves 추출하기
const move = moves[i];
// 2-2. 입력값 문자열 가공
const moveSplit = move.split(',').map(Number);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
// board 소유주 확인
if (board[partPosition.A] === null) {
// 소유 없음 확인 후 할당
board[partPosition.A] = 'A';
partOwned.A += 1;
}
/// B 참가자
// 위치 지정
partPosition.B = (partPosition.B + moveSplit[1]) % board.length;
// board 소유주 확인
if (board[partPosition.B] === null) {
// 소유 없음 확인 후 할당
board[partPosition.B] = 'B';
partOwned.B += 1;
}
/// C 참가자
// 위치 지정
partPosition.C = (partPosition.C + moveSplit[2]) % board.length;
// board 소유주 확인
if (board[partPosition.C] === null) {
// 소유 없음 확인 후 할당
board[partPosition.C] = 'C';
partOwned.C += 1;
}
/// D 참가자
// 위치 지정
partPosition.D = (partPosition.D + moveSplit[3]) % board.length;
// board 소유주 확인
if (board[partPosition.D] === null) {
// 소유 없음 확인 후 할당
board[partPosition.D] = 'D';
partOwned.D += 1;
}
console.log(
`현재 보드 상태 ${i + 1}번째: ${board.map((v) => {
if (!v) return '_';
return v;
})}`,
);
}
i를 선언해 파라미터(moves)를 분리했습니다.moves를 i로 쪼개고, split 및 Number로 쓰기 편하게 재가공했습니다.board 소유주 확인 - 확인 후 할당 순으로 구현 했습니다. 로직이 끝나면 B, C, D로 넘어가게 순서대로 작성했습니다.board의 상태를 확인하기 위해 console.log도 작성해봤습니다. return partOwned;
}
출력값이 원하는 partOwned를 리턴합니다.
테스트 케이스를 실행시켜보겠습니다.
아주 잘나옵니다. 실제로는 틀린 답이 나올 수도 있으니 이게 실제로 맞는지 디버깅 및 board 진행 상태를 log에 찍어보고 눈으로 검사 해봤을 때, 옳은 답인지 확인하겠습니다.
# node ./boost-marbel.js
현재 보드 상태 1번째: _,A,B,C,D,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,B,C,D,D,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,B,C,D,D,C,D,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,B,C,D,D,C,D,B,C,D,_,_,_,_,_
1. {A: 1, B: 2, C: 3, D: 4}
현재 보드 상태 1번째: _,A,_,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,A,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,A,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,A,A,A,_,_,_,_,_,_,_,_,_,_,_
2. {A: 4, B: 0, C: 0, D: 0}
현재 보드 상태 1번째: _,C,B,D,A,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,C,B,D,A,D,A,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,C,B,D,A,D,A,_,D,A,_,_,_,_,_,_
현재 보드 상태 4번째: _,C,B,D,A,D,A,C,D,A,D,A,_,_,_,_
3. {A: 4, B: 1, C: 2, D: 4}
현재 보드 상태 1번째: _,_,_,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,_,_,A,_,A,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,_,_,A,_,A,A,_,_,_,_,_,_,_,_,_
4. {A: 3, B: 0, C: 0, D: 0}
현재 보드 상태 1번째: _,A,C,B,D,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,C,B,D,_,C,D,_,_,_,_,_,_,_,_
5. {A: 1, B: 1, C: 2, D: 2}
현재 보드 상태 1번째: _,A,B,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,B,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,B,A,A,B,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,B,A,A,B,A,_,_,_,_,_,_,_,_,_
6. {A: 4, B: 2, C: 0, D: 0}
현재 보드 상태 1번째: _,A,B,C,D,_,_,_,_,_,_,_,_,_,_,_
7. {A: 1, B: 1, C: 1, D: 1}
현재 보드 상태 1번째: _,A,C,_,B,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,C,_,B,B,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,C,_,B,B,B,_,_,D,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,C,_,B,B,B,_,C,D,B,_,_,D,_,_
8. {A: 1, B: 4, C: 2, D: 2}
현재 보드 상태 1번째: _,D,C,B,A,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,D,C,B,A,A,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,D,C,B,A,A,D,C,B,A,_,_,_,_,_,_
현재 보드 상태 4번째: _,D,C,B,A,A,D,C,B,A,A,_,_,_,_,_
9. {A: 4, B: 2, C: 2, D: 2}
현재 보드 상태 1번째: _,A,_,_,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 2번째: _,A,_,A,_,_,_,_,_,_,_,_,_,_,_,_
현재 보드 상태 3번째: _,A,_,A,_,_,A,_,_,_,_,_,_,_,_,_
현재 보드 상태 4번째: _,A,_,A,_,_,A,_,_,_,A,_,_,_,_,_
현재 보드 상태 5번째: _,A,_,A,_,_,A,_,_,_,A,_,_,_,_,A
현재 보드 상태 6번째: _,A,_,A,_,A,A,_,_,_,A,_,_,_,_,A
10. {A: 6, B: 0, C: 0, D: 0}
각각 보드 상태를 봤을 때, 과정도 틀린 게 없고 예상 출력값도 똑같이 나오고 있습니다.
즉, 문제가 없다고 판단했습니다.
제 판단은 항상 틀릴 수도 있다는 것을 염두에 뒀지만 오늘 다시 증명이 되니 마음이 아픕니다. 테스트 케이스에 부족한 유형을 더 채우는 과정에서 문제가 발생했습니다.
바로 board가 다 채워질 때의 종료 조건이 없어 같은 결과가 여러번 반복 돼 요구사항을 충족시키지 못했습니다. 곧바로 디버깅에 들어가겠습니다.
먼저 간단히 의사 코드를 생각했습니다.
board에 소유할 장소가 없어지면, 즉, 소유할 장소가 없어지는 순간에 반복 종료 및 리턴이후 이러한 과정을 코드에 담으려고 생각했습니다.
처음에는 배열 메소드인 every를 사용해 할까 싶었지만, 순서가 끝날 때마다 한 배열을 순회하기 때문에 시간 복잡도가 엄청 증가한다고 생각해 다른 방법을 모색했습니다.
차라리 현재 전체 소유 장소 개수를 알 수 있는 변수를 추가로 선언 및 초기화 후, board의 길이랑 같다면 종료 로직이 실행되게끔 조건문을 각각 추가해야겠다 생각했습니다.
// 전체 장소 소유 개수
let ownedCount = 0;
...
console.log(`반복 ${i + 1}번째`);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
// board 소유주 확인
if (board[partPosition.A] === null) {
// 소유 없음 확인 후 할당
board[partPosition.A] = 'A';
partOwned.A += 1;
ownedCount += 1;
console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
if (ownedCount === board.length) {
break;
}
}
이런 식으로 break 문을 넣어서 전체 장소 소유 개수, ownedCount가 board.length랑 같다면 반복을 종료하고, 그 결과값을 반환하도록 했습니다.
또다른 부작용이 없는지 반복 시작할 때, log를 찍어 반복이 얼마나 됐는지 기록하게 했습니다.
이번엔 board를 더 면밀히 보기 위해 각 참가자의 소유가 진행될 때 board의 log를 찍어 또다른 버그는 없는지 확인했습니다.
사진을 보시면 쭉 반복 과정이 진행되다가 9번째에 소유된 장소가 15개가 되고, 소유되기 전까지 반복이 되다가 12번째에 16개가 되어 반복이 끝난 것을 볼 수 있습니다.
즉, param0의 길이가 15임에도 불구하고, 12번째에 반복이 끝났음을 확인 할 수 있었습니다.
제출 시 과정을 보기 위한 console.log는 다 주석 처리 했습니다.
function play(param0) {
// 0. 변수명을 잘 알아볼 수 있게 파라미터 재 할당
const moves = param0;
// 1-1. board: 시작 칸 포함 16칸짜리 보드 초기화
const board = Array(16).fill(null);
// 1-2. partPosition: 각 참가자의 초기 위치
const partPosition = { A: 0, B: 0, C: 0, D: 0 };
// 1-3. partOwned: 각 참가자의 소유한 칸 수
const partOwned = { A: 0, B: 0, C: 0, D: 0 };
// 디버깅: 전체 장소 소유 개수
let ownedCount = 0;
// 2. 반복문 돌기
for (let i = 0; i < moves.length; i++) {
// console.log(`반복 ${i + 1}번째`);
// 2-1. moves 추출하기
const move = moves[i];
// 2-2. 입력값 문자열 가공
const moveSplit = move.split(',').map(Number);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
// board 소유주 확인
if (board[partPosition.A] === null) {
// 소유 없음 확인 후 할당
board[partPosition.A] = 'A';
partOwned.A += 1;
// 디버깅: 전체 소유 장소 + 1
ownedCount += 1;
// console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
// 디버깅: 소유할 장소가 없다면 break
if (ownedCount === board.length) {
break;
}
}
/// B 참가자
// 위치 지정
partPosition.B = (partPosition.B + moveSplit[1]) % board.length;
// board 소유주 확인
if (board[partPosition.B] === null) {
// 소유 없음 확인 후 할당
board[partPosition.B] = 'B';
partOwned.B += 1;
// 디버깅: 전체 소유 장소 + 1
ownedCount += 1;
// console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
// 디버깅: 소유할 장소가 없다면 break
if (ownedCount === board.length) {
break;
}
}
/// C 참가자
// 위치 지정
partPosition.C = (partPosition.C + moveSplit[2]) % board.length;
// board 소유주 확인
if (board[partPosition.C] === null) {
// 소유 없음 확인 후 할당
board[partPosition.C] = 'C';
partOwned.C += 1;
// 디버깅: 전체 소유 장소 + 1
ownedCount += 1;
// console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
// 디버깅: 소유할 장소가 없다면 break
if (ownedCount === board.length) {
break;
}
}
/// D 참가자
// 위치 지정
partPosition.D = (partPosition.D + moveSplit[3]) % board.length;
// board 소유주 확인
if (board[partPosition.D] === null) {
// 소유 없음 확인 후 할당
board[partPosition.D] = 'D';
partOwned.D += 1;
// 디버깅: 전체 소유 장소 + 1
ownedCount += 1;
// console.log(`현재 보드 상태 ${i + 1}번째: ${board}`);
// 디버깅: 소유할 장소가 없다면 break
if (ownedCount === board.length) {
break;
}
}
// 보드 상태를 보기 위한 log
// console.log(
// `현재 보드 상태 ${i + 1}번째: ${board.map((v) => {
// if (!v) return '_';
// return v;
// })}`,
// );
}
return partOwned;
}
// 테스트 케이스 실행: 각 턴마다 4개의 이동 값 (A, B, C, D 순서)
console.log('1.', play(['1,2,3,4', '1,1,1,1', '2,2,2,2', '3,3,3,3']));
// 1. 예상 출력: {A: 1, B: 2, C: 3, D: 4}
console.log('2.', play(['1,1,1,1', '1,1,1,1', '1,1,1,1', '1,1,1,1']));
// 2. 예상 출력: {A: 4, B: 0, C: 0, D: 0}
console.log('3.', play(['4,2,1,3', '2,1,2,2', '3,2,1,3', '2,1,3,2']));
// 3. 예상 출력: {A: 4, B: 1, C: 2, D: 4}
console.log('4.', play(['3,3,3,3', '2,2,2,2', '1,1,1,1']));
// 4. 예상 출력: {A: 3, B: 0, C: 0, D: 0}
console.log('5.', play(['1,3,2,4', '2,1,4,3']));
// 5. 예상 출력: {A: 1, B: 1, C: 2, D: 2}
console.log('6.', play(['1,2,1,2', '2,1,2,1', '1,2,1,2', '2,1,2,1']));
// 6. 예상 출력: {A: 4, B: 2, C: 0, D: 0}
console.log('7.', play(['1,2,3,4']));
// 7. 예상 출력: {A: 1, B: 1, C: 1, D: 1}
console.log('8.', play(['1,4,2,1', '1,1,3,4', '2,1,1,4', '2,4,2,4']));
// 8. 예상 출력: {A: 4, B: 4, C: 4, D: 3}
console.log('9.', play(['4,3,2,1', '1,2,3,4', '4,3,2,1', '1,2,3,4']));
// 9. 예상 출력: {A: 4, B: 2, C: 2, D: 2}
console.log(
'10.',
play(['1,1,1,1', '2,2,2,2', '3,3,3,3', '4,4,4,4', '5,5,5,5', '6,6,6,6']),
);
// 10. 예상 출력: {A: 6, B: 0, C: 0, D: 0}
console.log(
'11.',
play([
'4,3,2,1',
'2,3,4,1',
'3,4,1,2',
'4,1,2,3',
'1,3,2,4',
'2,4,3,1',
'3,1,4,2',
'4,2,1,3',
'1,4,3,2',
'2,1,4,3',
'3,2,1,4',
'4,3,2,1',
'1,2,4,3',
'2,3,1,4',
'3,4,2,1',
'4,1,3,2',
]),
);
// 11. 예상 출력: {A: 7, B: 5, C: 2, D: 2}
리팩토링도 진행해보겠습니다. 제가 먼저 거슬렸던 부분은 입력값 문자열 가공 부분과 중복 코드가 너무 많다는 부분입니다.
입력값 문자열 가공 후 moveSplit[0] 이런 식으로 써서 위치 지정 계산을 썼었는데 구현 중간에 '이게 뭐였지?' 하는 생각이 들었어서 리팩토링을 진행해보려고 합니다.
...
// 2-2. 입력값 문자열 가공
const moveSplit = move.split(',').map(Number);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveSplit[0]) % board.length;
...
변수에 의미를 넣을 수 있는 직관적 방법이 const moveA = moveSplit[0] 이런 식으로 해서 의미를 부여하려고 했습니다. 하지만 코드가 더 난잡해지고, 의미 부여로 4줄이 더 생기는 거는 원치 않았습니다.
더 분해해 재사용에 용이할 수 있는 또다른 방법이 있을까 하고 생각해보니 moveSplit은 배열이라는 생각이 들었고, 구조 분해 할당 생각이 났습니다. 바로 착수했습니다.
...
// 2-2. 입력값 문자열 가공
const [moveA, moveB, moveC, moveD] = move.split(',').map(Number);
/// A 참가자
// 위치 지정
partPosition.A = (partPosition.A + moveA) % board.length;
...
전과 달리 1줄로 변수 선언이 되고, 또한 A가 움직이는 거리라고 생각돼 더욱 직관적으로 변했습니다.
나중에 알고리즘에서 1분 1초가 아까우니 구조 분해 할당을 더 활용해야겠다 생각했습니다.
참가자 관련 중복 코드가 4개나 있습니다. 중복 코드는 시간과 다투는 알고리즘 테스트에서 우선순위는 떨어지지만 참을 수 없어서 리팩토링을 진행해보겠습니다.
코드를 보면 참가자 A ~ D의 로직이 참가자 이름만 다를 뿐이지 나머지는 다 같은 로직임을 발견했습니다. 이를 줄여줄 사용자 정의 함수를 추가해보겠습니다.
// 리팩토링: 참가자 이동 및 소유 체크 함수 선언
const moveCheckOwn = (participant, move) => {
partPosition[participant] = (partPosition[participant] + move) % board.length;
if (board[partPosition[participant]] === null) {
board[partPosition[participant]] = participant;
partOwned[participant] += 1;
ownedCount += 1;
// 리팩토링: 모든 칸이 채워졌음을 나타냄
if (ownedCount === board.length) {
return true;
}
}
// 리팩토링: 아직 모든 칸이 채워지지 않음
return false;
};
'A', 'B'나 moveB처럼 참가자마다 변경되는 부분을 파라미터화 했습니다.partPosition.C => partPosition[participant]break문은 반복문 안에서만 사용 가능하기에 빼고 Boolean 데이터를 반환 시켜 호출부에서 break가 일어나도록 조정했습니다.function play(param0) {
const moves = param0;
const board = Array(16).fill(null);
const partPosition = { A: 0, B: 0, C: 0, D: 0 };
const partOwned = { A: 0, B: 0, C: 0, D: 0 };
let ownedCount = 0;
// 리팩토링: 참가자 이동 및 소유 체크 함수 선언
const moveCheckOwn = (participant, move) => {
partPosition[participant] =
(partPosition[participant] + move) % board.length;
if (board[partPosition[participant]] === null) {
board[partPosition[participant]] = participant;
partOwned[participant] += 1;
ownedCount += 1;
// 리팩토링: 모든 칸이 채워졌음을 나타냄
if (ownedCount === board.length) {
return true;
}
}
// 리팩토링: 아직 모든 칸이 채워지지 않음
return false;
};
// 2. 반복문 돌기
for (let i = 0; i < moves.length; i++) {
const move = moves[i];
const [moveA, moveB, moveC, moveD] = move.split(',').map(Number);
// 리팩토링: 새로 만든 함수로 수정 및 반환값으로 break
// A 참가자
if (moveCheckOwn('A', moveA)) break;
// B 참가자
if (moveCheckOwn('B', moveB)) break;
// C 참가자
if (moveCheckOwn('C', moveC)) break;
// D 참가자
if (moveCheckOwn('D', moveD)) break;
}
return partOwned;
}
if문의 조건이어도 함수는 실행이 되는 걸 이용해 바로 조건문의 조건으로 넣어주고, break 문도 실행되게 했습니다.
기능적으로는 달라진 부분은 없지만 훨씬 깔끔해진 모습입니다.
테스트 케이스는 항상 모자란 거 같습니다. 당연한 말이지만 사이트 이펙트나 버그는 제가 모르는 사이에 들어와 자꾸 괴롭힙니다 ㅠ
더더욱 테스트 케이스를 견고하면서 다양한 케이스를 시도할 수 있도록 노력해야겠습니다.
등등 여러가지 할 게 많다고 더욱 느낀 오늘이었습니다.