반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리

Leetcode 119. Pascal's Triangle II (Easy)

2022. 9. 29. 04:04 | Posted by 솔웅


반응형

이번 문제를 풀기 전에 118번 문제를 먼저 풀면 도움이 된다.

118번 문제는 아래 글에 정리해 놓았다.

 

https://coronasdk.tistory.com/1150

 

Leetcode 118. Pascal's Triangle (Easy)

오늘의 Leetcode 문제는 118번 Pascal’s Triangle 이다. 난이도는 Easy. 전혀 Easy 하지 않은 것 같은데. 아래가 풀어야 할 문제이다. 우선 패턴을 분석하면 첫줄은 숫자가 한개이고 그 값은 1이다. 두번째

coronasdk.tistory.com

Pascal's Triangle에 대한 패턴은 118번을 다룬 글에 정리해 두었다.

 

그 패턴들을 기반으로 구한 공식은 아래와 같다.

Triangle[row][col] = triangle[row-1][col-1] + triangle[row-1][col-1]

 

자세히 알기를 원하면 위에 있는 링크를 따라 가면 된다.

 

우선 이 결과를 얻으려면 아래와 같은 코딩으로 할 수 있다.

 

class Solution {
    
    private int getNum(int row, int col) {
        if(row == 0 || col == 0 || row == col) {
            return 1;
        }
        
        return getNum(row -1, col -1) + getNum(row-1, col);
    }
    
    public List<Integer> getRow(int rowIndex) {
        List<Integer> ans = new ArrayList<>();
        
        for(int i=0; i<= rowIndex; i++) {
            ans.add(getNum(rowIndex, i));
        }
        
        return ans;
    }
}

 

getNum() 메소드를 보면 첫번째 row 이거나 첫번째 col이거나 row와 col이 같다면 즉 맨 마지막 row 이면 값이 1이 주어진다.

getRow() 메소드에서 위 함수를 콜 한 부분을 보면 ans.add(getNum(rowIndex,i)); 이다.

즉 해당 값을 ans 에 넣는 것이다.

위에 설명한 1이 되는 경우 즉 맨 처음과 맨 마지막을 제외하면 getNum(row -1, col -1) + getNum(row-1, col) 로 계산된 값이 들어간다.

이는 재귀 함수 호출로 for 문과 같은 효과를 내는 방식인데 getNum() 함수 내에서 다시 getNum() 함수를 호출하는 방식이다.

 

이렇게 하면 원하는 답을 얻을 수 있다.

 

 

조금 다른 방법도 있다.

 

매번 재귀 함수를 호출하는 것이 아니라 이전에 호출했다 값은 미리 저장해 두고 계속 사용하는 것이다.

 

class Solution {
  private final class RowCol {
    private int row, col;

    public RowCol(int row, int col) {
      this.row = row;
      this.col = col;
    }

    @Override
    public int hashCode() {
      int result = (int) (row ^ (row >>> 32));
      return (result << 5) - 1 + (int) (col ^ (col >>> 32)); // 31 * result == (result << 5) - 1
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null) return false;
      if (this.getClass() != o.getClass()) return false;

      RowCol rowCol = (RowCol) o;
      return row == rowCol.row && col == rowCol.col;
    }
  }

  private Map<RowCol, Integer> cache = new HashMap<>();

  private int getNum(int row, int col) {
    RowCol rowCol = new RowCol(row, col);

    if (cache.containsKey(rowCol)) {
      return cache.get(rowCol);
    }

    int computedVal =
        (row == 0 || col == 0 || row == col) ? 1 : getNum(row - 1, col - 1) + getNum(row - 1, col);

    cache.put(rowCol, computedVal);

    return computedVal;
  }

  public List<Integer> getRow(int rowIndex) {
    List<Integer> ans = new ArrayList<>();

    for (int i = 0; i <= rowIndex; i++) {
      ans.add(getNum(rowIndex, i));
    }

    return ans;
  }
}

 

But, it is worth noting that generating a number for a particular row requires only two numbers from the previous row. Consequently, generating a row only requires numbers from the previous row.

Thus, we could reduce our memory footprint by only keeping the latest row generated, and use that to generate a new row.

그러나 특정 행에 대한 숫자를 생성하려면 이전 행에서 두 개의 숫자만 필요하다는 점에 유의할 가치가 있습니다. 결과적으로 행을 생성하려면 이전 행의 숫자만 필요합니다.

따라서 생성된 최신 행만 유지하여 메모리 공간을 줄이고 이를 사용하여 새 행을 생성할 수 있습니다.

 

이 세가지 코드로 돌린 결과 입니다. 속도 면에서 아주 현격한 차이를 볼 수 있습니다.

 

이와는 아주 다른 접근법이 있을 수 있습니다.

이는 메모리를 절약할 수 있는 방법인데요.

 

여기서 행이 바뀔때마다 생기는 변화에 어떤 규칙성을 찾을 수 있습니다.

세번째줄 1,3,3,1에서 네번째 줄 1,4,6,4,1로 변화할 때의 규칙성을 보죠.

 

4번째 줄 맨 오른쪽은 1이 들어갑니다.

오른쪽에서 두번째 숫자는 3+1인 4가 됩니다.

그리고 오른쪽에서 세번째 숫자는 3+3 인 6이 됩니다. 오른쪽에서 네번째 숫자는 1+3인 4가 되구요. 

맨 왼쪽은 1이되구요.

이 규칙을 가지고 코딩을 하면 아래와 같이 할 수 있습니다.

 

class Solution {
  public List<Integer> getRow(int rowIndex) {
    List<Integer> row =
        new ArrayList<>(rowIndex + 1) {
          {
            add(1);
          }
        };

    for (int i = 0; i < rowIndex; i++) {
      for (int j = i; j > 0; j--) {
        row.set(j, row.get(j) + row.get(j - 1));
      }
      row.add(1);
    }

    return row;
  }
}

 

그 다음은 수학인데요.

이 부분은 제가 잘 이해를 못했습니다.

 

소스 코드는 아래와 같구요.

 

class Solution {
  public List<Integer> getRow(int n) {
    List<Integer> row =
        new ArrayList<>() {
          {
            add(1);
          }
        };

    for (int k = 1; k <= n; k++) {
      row.add((int) ((row.get(row.size() - 1) * (long) (n - k + 1)) / k));
    }

    return row;
  }
}

 

실행 결과를 보면 아래와 같습니다.

 

역시 수학 공식을 사용한 것이 Runtime과 메모리면에서 가장 효율적이네요.

 

20년동안 IT 분야에서 survival했지만 이렇게 까지 수학을 이용해서 코딩 해 본 적은 솔직히 없습니다.

 

좋은 걸 배웠지요.

반응형