문제 링크 : https://leetcode.com/problems/erect-the-fence/
이 문제는 Convew Hull(볼록 껍질) 알고리즘으로 대표적으로 세가지 방법이 존재합니다.
1. Jarvis Algorithm
2. Graham Scan
3. MonotoChain
그 중 저는 MonotoChain 방법을 사용하였습니다.
정의
"2차원 평면 상에서 주어진 점들을 모두 포함하는 가장 작은 다각형"
이게 뜻이 모호할 수 있으니 정의를 확실히 잡고 가도록 하겠습니다. 사실 선을 어떻게 그어도 점을 다 포함할 수 있는 방법은 굉장히 많습니다.
예)
여기서 오른쪽 그림을 살펴보자.
그렇다면 왼쪽 그림을 보도록 하자.
CCW( Counter Clock Wise) 알고리즘은 3개의 점 A, B, C가 있을 때 이 점 3개를 이은 직선의 방향을 알고자 할 때 유용한 기하 알고리즘이다.
경우의 수는 총 3가지가 있으며 시계, 반시계, 직선이 있으며 이는 외적 을 통하여 구할 수 있으며 외적의 결과가 음수이면 시계 방향, 직선은 0, 반시계 방향일 경우 양수가 나오게 된다. (아래 그림 참조)
예시 : p1p2를 이은 선분벡터, p2p3를 이은 선분벡터의 CCW를 구하기
p1 = (x1,y1)
p2 = (x2,y2)
p3 = (x3,y3)
p1p2를 이은 선분벡터, p2p3를 이은 선분벡터의 외적을 구한 다음 해당 값의 결과값(음수, 양수, 0)에 따라 시계, 반시계, 직선인지 결정한다.
|(x2 - x1) (y2 - y1)|
|(x3 - x2) (y3 - y2)|
즉, (y3 - y2)*(x2 - x1) - (x3-x2) * (y2-y1)
이를 코드화 하면 다음과 같다.
/**
*
* @param {number[][]} p1
* @param {number[][]} p2
* @param {number[][]} p3
* @returns {number}
*/
function ccw(p1, p2, p3){
return (p3[1] - p2[1])*(p2[0]-p1[0]) - (p2[1]-p1[1])*(p3[0]-p2[0]);
}
위의 gif파일에서 볼 수 있듯이 세 개의 점들의 ccw 알고리즘 결과를 비교하여 일정한 방향으로 돌지 않으면 해당 점을 포함하지 않는 알고리즘이다.
Monotone Chain 알고리즘을 사용하지만 좀 더 쉬운 구현을 위해 위의 gif 파일과는 약간은 다른 방식을 사용하였다. 앞서 볼록 껍질은 원에 가까운 형태를 띈다고 했다. 그렇다면 시작점을 기준으로 아래 그림과 같은 형식이 무조건 만들어 지지 않을까라는 생각을 해보았다.
tip : 정렬된 배열에서 배열의 제일 첫번째 점을 기준으로 점들을 비교하기 때문에 시계 방향과 반시계 방향을 모두 체크해주어야 볼록껍질의 모든 점들을 가져올 수 있다.
for (let t of trees){
// upper는 반시계 방향일 때만 가져온다. -> 시계방향( ccw < 0 )일 경우 pop()
while (upper.length >= 2 && ccw(upper[upper.length - 2], upper[upper.length-1], t) < 0){
upper.pop();
}
// lower는 시계 방향일 때만 가져 온다. => 반시걔 방향 ( ccw > 0) 일 경우 pop()
while (lower.length >= 2 && ccw(lower[lower.length -2], lower[lower.length-1], t) > 0){
lower.pop();
}
upper.push(t);
lower.push(t);
}
return [...new Set([...upper,...lower])];
};
// leetcode 587 ErectTheFence
// Input: points = [[1,1],[2,2],[2,0],[2,4],[3,3],[4,2]]
// Output: [[1,1],[2,0],[3,3],[2,4],[4,2]]
/**
*
* @param {number[][]} p1
* @param {number[][]} p2
* @param {number[][]} p3
* @returns {number}
*/
// ccw 알고리즘 함수
function ccw(p1, p2, p3){
// 외적을 구하는 함수 - 외적을 통해 해당 선분이 어떤 방향으로 돌아가고 있는지 판단
// 음수 : 시계 방향
// 0 : 직성
// 양수 : 반시계 방향
return (p3[1] - p2[1])*(p2[0]-p1[0]) - (p2[1]-p1[1])*(p3[0]-p2[0]);
}
/**
* @param {number[][]} trees
* @return {number[][]}
*/
const outerTrees = function(trees) {
// 정렬
trees.sort((a,b) => (a[0] - b[0]) || (a[1] - b[1]));
let upper = [];
let lower = [];
for (let t of trees){
// upper는 반시계 방향일 때만 가져온다. -> 시계방향( ccw < 0 )일 경우 pop()
while (upper.length >= 2 && ccw(upper[upper.length - 2], upper[upper.length-1], t) < 0){
upper.pop();
}
// lower는 시계 방향일 때만 가져 온다. => 반시걔 방향 ( ccw > 0) 일 경우 pop()
while (lower.length >= 2 && ccw(lower[lower.length -2], lower[lower.length-1], t) > 0){
lower.pop();
}
upper.push(t);
lower.push(t);
}
return [...new Set([...upper,...lower])];
};
[ccw 알고리즘] https://snowfleur.tistory.com/98
[해설 유투브]https://www.youtube.com/watch?v=wmZfH6l_gPw
[해설 leetcode]https://leetcode.com/problems/erect-the-fence/solution/
[Convex hull(볼록 껍질 알고리즘이란?)] https://kbw1101.tistory.com/50