코드로 문제해결 연습 > 프로그래머스 > 당구 연습

주싱·2023년 3월 19일
0

1. 문제

링크 : 프로그래머스 > Level 2 > 당구 연습

2. 해결 시간

1시간 이내에 문제 해결이 안되었다. 오랜 시간 고민하고 학습하며 문제를 해결했다.

3. 문제 해결 코드

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;

public class PracticeBilliardsTest {
    static Stream<Arguments> testCaseSupplier() {
        return Stream.of(
                Arguments.arguments(10, 10, 3, 7, new int[][]{{7, 7}, {2, 7}, {7, 3}}, new int[]{52, 37, 116})
        );
    }

    @ParameterizedTest
    @MethodSource("testCaseSupplier")
    void testSolution(int m, int n, int startX, int startY, int[][] balls, int[] expected) {
        int[] result = solution(m, n, startX, startY, balls);
        Assertions.assertArrayEquals(expected, result);
    }

    public int[] solution(int m, int n, int startX, int startY, int[][] balls) {
        return Arrays.stream(balls)
                .mapToInt(target -> minDistance(new Point(startX, startY), new Point(target[0], target[1]), m, n))
                .toArray();
    }

    public int minDistance(Point start, Point target, int width, int height) {
        return Stream.of(
                        distanceBounceLeft(start, target, width, height),
                        distanceBounceRight(start, target, width, height),
                        distanceBounceBottom(start, target, width, height),
                        distanceBounceTop(start, target, width, height))
                .filter(Optional::isPresent)
                .mapToInt(Optional::get)
                .min()
                .orElse(-1);
    }

    Optional<Integer> distanceBounceLeft(Point start, Point target, int width, int height) {
        if (blockedToLeft(start, target)) {
            return Optional.empty();
        }

        int xSum = sumOfDistanceXFromBase(start, target, 0);
        int ySum = distanceY(start, target);
        int distance = sumOfPowerEachDistance(xSum, ySum);
        return Optional.of(distance);
    }

    Optional<Integer> distanceBounceRight(Point start, Point target, int width, int height) {
        if (blockedToRight(start, target)) {
            return Optional.empty();
        }

        int xSum = sumOfDistanceXFromBase(start, target, width);
        int ySum = distanceY(start, target);
        int distance = sumOfPowerEachDistance(xSum, ySum);
        return Optional.of(distance);
    }

    Optional<Integer> distanceBounceBottom(Point start, Point target, int width, int height) {
        if (blockedToBottom(start, target)) {
            return Optional.empty();
        }

        int xSum = distanceX(start, target);
        int ySum = sumOfDistanceYFromBase(start, target, 0);
        int distance = sumOfPowerEachDistance(xSum, ySum);
        return Optional.of(distance);
    }

    Optional<Integer> distanceBounceTop(Point start, Point target, int width, int height) {
        if (blockedToTop(start, target)) {
            return Optional.empty();
        }

        int xSum = distanceX(start, target);
        int ySum = sumOfDistanceYFromBase(start, target, height);
        int distance = sumOfPowerEachDistance(xSum, ySum);
        return Optional.of(distance);
    }

    private boolean blockedToLeft(Point start, Point target) {
        return start.sameY(target) && start.greaterThanX(target);
    }

    private boolean blockedToRight(Point start, Point target) {
        return start.sameY(target) && target.greaterThanX(start);
    }

    private boolean blockedToTop(Point start, Point target) {
        return start.sameX(target) && target.greaterThanY(start);
    }

    private boolean blockedToBottom(Point start, Point target) {
        return start.sameX(target) && start.greaterThanY(target);
    }

    private int sumOfDistanceXFromBase(Point start, Point target, int base) {
        return Math.abs(start.x - base) + Math.abs(target.x - base);
    }

    private int sumOfDistanceYFromBase(Point start, Point target, int base) {
        return Math.abs(start.y - base) + Math.abs(target.y - base);
    }

    private int distanceX(Point start, Point target) {
        return Math.abs(start.x - target.x);
    }

    private int distanceY(Point start, Point target) {
        return Math.abs(start.y - target.y);
    }

    private int sumOfPowerEachDistance(int xDistance, int yDistance) {
        return (int) Math.pow(xDistance, 2) + (int) Math.pow(yDistance, 2);
    }

    class Point {
        int x;
        int y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        boolean sameX(Point other) {
            return x == other.x;
        }

        boolean sameY(Point other) {
            return y == other.y;
        }

        boolean greaterThanX(Point other) {
            return x > other.x;
        }

        boolean greaterThanY(Point other) {
            return y > other.y;
        }
    }
}

4. 회고

예외적인 케이스들

처음 문제에 접근하며 몇몇 복잡한 조건들이 생각났는데 이미 문제에서 제약 조건을 주고 있었다. 한 번 문제를 읽는 것으로 완벽하게 머리에 담지 못한 것 같다.

  • 머쓱이는 리스트에 담긴 각 위치에 순서대로 공을 놓아가며 “원쿠션” 연습을 한다. 즉 타겟 공 간에 부딪힘을 고려할 필요는 없다.
  • 머쓱이가 항상 같은 위치에 공을 놓는다. 즉 공의 다음 위치를 동적으로 계산할 필요는 없다.
  • 공의 크기는 무시하며, 두 공의 좌표가 정확히 일치하는 경우에만 두 공이 맞았다고 판단한다. 즉 공이 타겟 공 곁을 살짝 맞는 케이스는 고려할 필요가 없다.

선언적이고 단계적 접근은 가독성과 눈 검증을 쉽게 한다

처음에는 통째로 ‘어떻게’ 해결하는 코드를 작성해서 코드를 검증하기 어려웠다. 차츰 문제를 해결하는 단계를 나눌 수 있었는데 코드를 읽기 쉬움은 물론이고 각 단계를 눈으로 라도 검증하기가 훨씬 좋았다.

문제의 의도 이해

처음에 문제가 왜 거리가 아닌 x 성분, y 성분 제곱의 합을 반환하는지 의문점이 있었는데 루트를 씌우면 부동소수점이 생기기 때문에 정확한 결과 확인이 복잡해 지기 때문이란 걸 문제에 접근하면서 이해하게 되었다.

불필요한 나누기

처음에 x 벡터의 크기를 계산하고 y 벡터의 크기 계산하는 과정을 쪼개고 있었는데 그러면 불필요하게 중간 데이터가 생김을 깨달았다. 함께 처리되는 것은 함께 두는 것이 좋다.

필요한 합치기

Point 같이 한 쌍으로 처리되는 데이터가 나오면 클래스로 묶어서 다루면 코드가 일단 단순해지고, 데이터 처리도 항상 함께 일어남으로 메서드로 전달하거나 반환 받을 때 좋음을 느낀다.

단계를 나누고 눈으로 보기

타겟 공이 시작 공을 가리고 있어서 벽을 맞추지 못하는 경우의 조건을 계산할 때 실수를 했다. 해당 조건을 계산하는 함수로 분리하고 그리고 직접 그림을 그려본 후 조건 계산의 오류를 발견할 수 있었다.

잘못 접근한 해결책

코너에 맞는 케이스를 생각하느라 많은 시간을 허비했다. 코너에 맞고 튕겨져 나와 타겟을 맞는 조건(기울기가 같은 조건)을 열심히 계산했는데 다시 생각해보니 코너에 맞고 나오는 경우는 항상 벽을 맞는 최소거리가 존재 했다. 따라서 코너를 맞는 경우는 생각할 필요가 없었고 문제의 난이도를 높이는 조건으로 주어진 것 같았다.

헷갈리는 이름

같은 기능인데 x, y 만 다른 함수를 distanceX, distanceY 와 같이 하면 헷갈리는 경우가 많았다. 함수 이름이 더 길어지는 sumOfDistanceFromXBase 같은 메서드는 바꿔서 사용하는 실수를 했다. x, y 보다 더 식별하기 좋은 이름을 사용하는 것이 좋겠다.

5. 개선할 점

  • 문제와 제약 조건을 한 번 읽을 때 가능하면 캐치할 수 있도록 차근히 정리하면서 읽자. 나중에 두 번 읽으면 더 많은 시간이 걸린다.
  • 다음에는 처음부터 선언적으로 문제를 나누어 가며 해결책을 찾고 단계별로 검증하며 나아가야 겠다. 그래야 코드 각 단계를 검증하기 쉽다.
  • 다음에 테스트 케이스를 먼저 추가하고 문제를 풀어보는 연습을 해야 겠다.
  • x, y 같은 짧고 혼동되는 이름 보다 더 분명한 이름을 사용해야 겠다.

6. 잘한 점

  • 차츰 문제를 해결하는 단계들을 나누고 단계 별로 눈으로 검증해 나간 점 잘했다.
  • 문제 상황을 그림으로 그려본 것 잘했다.
  • 예외적인 상태를 -1이나 Interge.MAX_VALUE로 반환하는 것보다 Optional 로 처리하고 스트림에서 empty() 를 필터링 한 것은 잘했다.
  • 공통된 코드를 묶을 수 있는 방법을 찾다가 distanceX, distanceY, sumOfDistanceFromXBase, sumOfDistanceFromYBase 같은 메서드를 만들 수 있었다. 잘한 것 같다.
  • 어려운 조건들을 함수로 추출해서 의미를 부여한 것 잘했다. (blockedLeft … )

7. 학습한 것

  • Math.pow() 메서드가 제곱, 영어로는 power를 의미함을 다시 한 번 머리에 익혔다.
  • Math.sqrt() 메서드가 제곱근, 영어로는 square root를 의미함을 다시 한 번 머리에 익혔다.

많이 배우고 성장한 시간이었습니다. 감사합니다.

profile
소프트웨어 엔지니어, 일상

0개의 댓글