2018 카카오 블라인드 뉴스 클러스터링
간단히 문제를 요약하자면…
두 문자열을 받아 영문자에 대한 자카르 유사도를 계산하기
정도로 요약이 되나, 세부적인 사항이 있다.
문제 풀이에 앞서 문제 및 요구사항 정리를 해보자
- 입력값으로는 2자 이상 1000자 이하의 두 문자열을 입력 받음
- 입력값에 대한 자카르 유사도를 출력해야함
- 입력 문자열은 두 글자씩 끊어서 연산에 사용한다.
- 이 때, 기타 공백이나 숫자, 특수 문자가 포함될 경우 연산에 사용하지 않는다
- 대소문자 구별을 하지 않는다.
그렇다면 자카르 유사도
란 무엇일까?
자카르 유사도⌗
두 집합 간의 유사도를 검사하느 여러 방법 중 하나로, 수학적 수식으로 표현하면 아래와 같다.
$$J(A,B) = \frac {|A\cap B|}{|A\cup B|} = \frac {|A\cap B|} {|A| + |B| - |A\cap B| } $$
교집합을 합집합으로 나눈 값이다.
그렇다면, 문제에서 자카르 유사도를 어떻게 계산해야 될까?
문제에서는 두 집합의 경우,
입력으로 주어지는 두 문자열의 부분 집합으로
각 문자열을 제약사항(두글자 단위로, 영문만 포함된 문자열)에 맞춰 중복을 허락 하는 집합이다.
풀이⌗
먼저 두 문자
로 이뤄진 부분집합을 다루기 위해 해당하는 객체를 선언을 해주었다
class PartStr {
private char a;
private char b;
public PartStr(char a, char b) {
this.a = a;
this.b = b;
}
// equals and hashcode 생략
}
다음으론 문자열을 PartStr
부분 집합으로 변환하는 메소드를 작성하였는데
간단하게 문자열을 순회하면서 다음 문자열까지 가져와 둘 다 영문자일 경우에만 부분집합에 추가하였다.
public List<PartStr> strToCharSet(String str) {
List<PartStr> result = new ArrayList<>(); // 결과로 나올 부분 집합
str = str.toUpperCase(); // 대소문자를 무시하고 계산하기 위해 전부 대문자로 변환
for (int i = 0; i < str.length() - 1; i++) {
char a = str.charAt(i);
char b = str.charAt(i + 1);
if (!isLetter(a) || !isLetter(b)) { // Character.isLetter 메소드를 사용하여 검사하였음
continue;
}
result.add(new PartStr(a, b));
}
return result;
}
이제 두 집합을 추출하였으니, 자카르 유사도를 검사를 해야 한다.
처음 작성시에는 중복 여부에 따른 값 변화를 깊게 생각하질 않아서,
Stream
연산을 통해 곂치는 값만 카운팅을 해주었으나,
중복을 허용하는 집합이기에 다르게 처리를 하였다.
한 집합(A)에 대해서 순차적으로 확인 후(교집합) 다른 집합(B) 에서 제거하는 형식으로 진행하였다.
합집합의 경우 $ |A| + |B| - |A\cap B| $ 이기에
교집합 연산시 처리되지 않는 요소(A)와 제거되지 않은 요소(B)를 모두 더하면 자동으로 나온다.
그리고 소숫점 5자리까지는 정확히 계산하기 위해 BigDecimal
을 통해 연산을 진행하였다.
float
를 사용할시 부정확하게 연산될 가능성이 있기에
double
이나 BigDecimal
을 통해 연산을 하는게 좋아보인다.
private float jaccard(Collection<PartStr> partStrs1, Collection<PartStr> partStrs2) {
//교집합
List<PartStr> intersection = new ArrayList<>(); // 교집합으로 나올 집합
//합집합
List<PartStr> union = new ArrayList<>(); // 합집합으로 나올 결과
for (PartStr elem : partStrs1) {
if (partStrs2.contains(elem)) {
intersection.add(elem);
partStrs2.remove(elem);
}
union.add(elem);
}
union.addAll(partStrs2);
BigDecimal intersectionCount =
BigDecimal.valueOf(intersection.size());
BigDecimal unionCount
= BigDecimal.valueOf(union.size());
try {
return intersectionCount
.divide(unionCount, 5, RoundingMode.FLOOR)
.floatValue();
} catch (Exception e) {
// 0으로 나뉘는 경우나 연산을 할 수 없는 경우, 문제 내용에 따라 1을 반환하였다.
return 1L;
}
}