매칭 점수
문제 설명
https://programmers.co.kr/learn/courses/30/lessons/42893
코딩테스트 연습 - 매칭 점수
매칭 점수 프렌즈 대학교 조교였던 제이지는 허드렛일만 시키는 네오 학과장님의 마수에서 벗어나, 카카오에 입사하게 되었다. 평소에 관심있어하던 검색에 마침 결원이 발생하여, 검색개발팀
programmers.co.kr
프렌즈 대학교 조교였던 제이지는 허드렛일만 시키는 네오 학과장님의 마수에서 벗어나, 카카오에 입사하게 되었다.
평소에 관심있어하던 검색에 마침 결원이 발생하여, 검색개발팀에 편입될 수 있었고, 대망의 첫 프로젝트를 맡게 되었다.
그 프로젝트는 검색어에 가장 잘 맞는 웹페이지를 보여주기 위해 아래와 같은 규칙으로 검색어에 대한 웹페이지의 매칭점수를 계산 하는 것이었다.
- 한 웹페이지에 대해서 기본점수, 외부 링크 수, 링크점수, 그리고 매칭점수를 구할 수 있다.
- 한 웹페이지의 기본점수는 해당 웹페이지의 텍스트 중, 검색어가 등장하는 횟수이다. (대소문자 무시)
- 한 웹페이지의 외부 링크 수는 해당 웹페이지에서 다른 외부 페이지로 연결된 링크의 개수이다.
- 한 웹페이지의 링크점수는 해당 웹페이지로 링크가 걸린 다른 웹페이지의 기본점수 ÷ 외부 링크 수의 총합이다.
- 한 웹페이지의 매칭점수는 기본점수와 링크점수의 합으로 계산한다.
예를 들어, 다음과 같이 A, B, C 세 개의 웹페이지가 있고, 검색어가 hi라고 하자.
이때 A 웹페이지의 매칭점수는 다음과 같이 계산할 수 있다.
- 기본 점수는 각 웹페이지에서 hi가 등장한 횟수이다.
- A,B,C 웹페이지의 기본점수는 각각 1점, 4점, 9점이다.
- 외부 링크수는 다른 웹페이지로 링크가 걸린 개수이다.
- A,B,C 웹페이지의 외부 링크 수는 각각 1점, 2점, 3점이다.
- A 웹페이지로 링크가 걸린 페이지는 B와 C가 있다.
- A 웹페이지의 링크점수는 B의 링크점수 2점(4 ÷ 2)과 C의 링크점수 3점(9 ÷ 3)을 더한 5점이 된다.
- 그러므로, A 웹페이지의 매칭점수는 기본점수 1점 + 링크점수 5점 = 6점이 된다.
검색어 word와 웹페이지의 HTML 목록인 pages가 주어졌을 때, 매칭점수가 가장 높은 웹페이지의 index를 구하라. 만약 그런 웹페이지가 여러 개라면 그중 번호가 가장 작은 것을 구하라.
제한사항
- pages는 HTML 형식의 웹페이지가 문자열 형태로 들어있는 배열이고, 길이는 1 이상 20 이하이다.
- 한 웹페이지 문자열의 길이는 1 이상 1,500 이하이다.
- 웹페이지의 index는 pages 배열의 index와 같으며 0부터 시작한다.
- 한 웹페이지의 url은 HTML의 <head> 태그 내에 <meta> 태그의 값으로 주어진다.
- 예를들어, 아래와 같은 meta tag가 있으면 이 웹페이지의 url은 https://careers.kakao.com/index 이다.
- <meta property=og:url content=https://careers.kakao.com/index />
- 한 웹페이지에서 모든 외부 링크는 <a href=https://careers.kakao.com/index>의 형태를 가진다.
- <a> 내에 다른 attribute가 주어지는 경우는 없으며 항상 href로 연결할 사이트의 url만 포함된다.
- 위의 경우에서 해당 웹페이지는 https://careers.kakao.com/index 로 외부링크를 가지고 있다고 볼 수 있다.
- 모든 url은 https:// 로만 시작한다.
- 검색어 word는 하나의 영어 단어로만 주어지며 알파벳 소문자와 대문자로만 이루어져 있다.
- word의 길이는 1 이상 12 이하이다.
- 검색어를 찾을 때, 대소문자 구분은 무시하고 찾는다.
- 예를들어 검색어가 blind일 때, HTML 내에 Blind라는 단어가 있거나, BLIND라는 단어가 있으면 두 경우 모두 해당된다.
- 검색어는 단어 단위로 비교하며, 단어와 완전히 일치하는 경우에만 기본 점수에 반영한다.
- 단어는 알파벳을 제외한 다른 모든 문자로 구분한다.
- 예를들어 검색어가 aba 일 때, abab abababa는 단어 단위로 일치하는게 없으니, 기본 점수는 0점이 된다.
- 만약 검색어가 aba 라면, aba@aba aba는 단어 단위로 세개가 일치하므로, 기본 점수는 3점이다.
- 결과를 돌려줄때, 동일한 매칭점수를 가진 웹페이지가 여러 개라면 그중 index 번호가 가장 작은 것를 리턴한다
- 즉, 웹페이지가 세개이고, 각각 매칭점수가 3,1,3 이라면 제일 적은 index 번호인 0을 리턴하면 된다.
입출력 예#1
-
word : blind
-
pages :
["<html lang=\"ko\" xml:lang=\"ko\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n <meta charset=\"utf-8\">\n <meta property=\"og:url\" content=\"https://a.com\"/>\n</head> \n<body>\nBlind Lorem Blind ipsum dolor Blind test sit amet, consectetur adipiscing elit. \n<a href=\"https://b.com\"> Link to b </a>\n</body>\n</html>", "<html lang=\"ko\" xml:lang=\"ko\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n <meta charset=\"utf-8\">\n <meta property=\"og:url\" content=\"https://b.com\"/>\n</head> \n<body>\nSuspendisse potenti. Vivamus venenatis tellus non turpis bibendum, \n<a href=\"https://a.com\"> Link to a </a>\nblind sed congue urna varius. Suspendisse feugiat nisl ligula, quis malesuada felis hendrerit ut.\n<a href=\"https://c.com\"> Link to c </a>\n</body>\n</html>", "<html lang=\"ko\" xml:lang=\"ko\" xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n <meta charset=\"utf-8\">\n <meta property=\"og:url\" content=\"https://c.com\"/>\n</head> \n<body>\nUt condimentum urna at felis sodales rutrum. Sed dapibus cursus diam, non interdum nulla tempor nec. Phasellus rutrum enim at orci consectetu blind\n<a href=\"https://a.com\"> Link to a </a>\n</body>\n</html>"]
-
pages는 다음과 같이 3개의 웹페이지에 해당하는 HTML 문자열이 순서대로 들어있다.
<html lang="ko" xml:lang="ko" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta property="og:url" content="https://a.com"/>
</head>
<body>
Blind Lorem Blind ipsum dolor Blind test sit amet, consectetur adipiscing elit.
<a href="https://b.com"> Link to b </a>
</body>
</html>
<html lang="ko" xml:lang="ko" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta property="og:url" content="https://b.com"/>
</head>
<body>
Suspendisse potenti. Vivamus venenatis tellus non turpis bibendum,
<a href="https://a.com"> Link to a </a>
blind sed congue urna varius. Suspendisse feugiat nisl ligula, quis malesuada felis hendrerit ut.
<a href="https://c.com"> Link to c </a>
</body>
</html>
<html lang="ko" xml:lang="ko" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta property="og:url" content="https://c.com"/>
</head>
<body>
Ut condimentum urna at felis sodales rutrum. Sed dapibus cursus diam, non interdum nulla tempor nec. Phasellus rutrum enim at orci consectetu blind
<a href="https://a.com"> Link to a </a>
</body>
</html>
위의 예를 가지고 각각의 점수를 계산해보자.
-
기본점수 및 외부 링크수는 아래와 같다.
- a.com의 기본점수는 3, 외부 링크 수는 1개
- b.com의 기본점수는 1, 외부 링크 수는 2개
- c.com의 기본점수는 1, 외부 링크 수는 1개
-
링크점수는 아래와 같다.
- a.com의 링크점수는 b.com으로부터 0.5점, c.com으로부터 1점
- b.com의 링크점수는 a.com으로부터 3점
- c.com의 링크점수는 b.com으로부터 0.5점
-
각 웹 페이지의 매칭 점수는 다음과 같다.
- a.com : 4.5 점
- b.com : 4 점
- c.com : 1.5 점
따라서 매칭점수가 제일 높은 첫번째 웹 페이지의 index인 0을 리턴 하면 된다.
풀이
문제 자체의 난이도가 어렵진 않지만, c++로 문자열 처리를 해야 한다는 점에서 매우매우 어려웠던 문제이다. 문제에서 요구하는 바는 결국 주어진 page 문자열에서 해당 페이지의 링크, 내용 속에서 word가 반복되는 횟수, 그리고 퍼져나가는 외부링크의 개수와 해당 외부 링크 주소이다. 이처럼 요구하는 바도 명확하고 이를 바탕으로 계산하는 것도 간단하지만, 문자열 처리를 통해 이것들을 정제해야 한다는 점이 매우 난이도를 높게 만들었다.
우선 반복문을 통해 현재 pages 벡터 속 각 page들의 링크(string)와 page의 인덱스(int)를 하나의 key, value로 가질 수 있도록 unordered_map 자료형의 urlToInex라는 것을 만들어 저장한다. 이 자료구조를 통해 특정 페이지의 기본 점수를 계산하는 것과 해당 페이지 속 외부 링크들에게 링크 점수 분배하는 것을 동시에 할 수 있다.
이후 다시 pages 벡터를 반복문을 통해 접근하여, 본문 속 word가 반복되는 횟수를 구하여 기본점수를 저장한다. 이 경우 제한 사항에서 언급되었던 것처럼 문자 단위여야 하고 문자 단위라는 것은 알파벳으로 이루어진 문자열이어야 하기에, find 함수를 통해 리턴된 위치의 앞과 word 길이만큼의 인덱스 이후 뒤가 확실히 끊어져 있는지 확인하기 위해 조건문을 추가했다.
그리고 본문 속 외부 링크들의 개수와 해당 링크 주소들을 새로운 벡터에 저장한다. 이 때 앞선 unordered_map 자료구조에 이를 탐색하여 해당 링크가 단순히 개수에만 필요한 건지 아니면 특정 page의 링크 중 하나인지 확인하고 저장한다. 이후 벡터 속 링크들을 key값으로 urlToIndex에서 해당 page의 인덱스를 찾아내어 링크 점수를 저장한다.
이후 점수들이 모여있는 벡터를 탐색하여 해당 점수가 높은 인덱스를 반환한다. 이 때 pages의 인덱스 순으로 점수 벡터를 만들어 놓았기 때문에, 자동으로 최고 점수가 동일할 땐 인덱스가 작은 걸 반환할 수 있다.
유의할 점/수정한 부분
본문 속 word를 찾는 과정에서 대문자, 소문자를 신경쓰지 않고 모두 찾아야 했기에, page 속 본문과 word를 소문자 혹은 대문자 한 가지 경우로 모두 변환할 필요가 있었다. 이 경우 검색을 통해 찾았는데, https://stackoverflow.com/questions/313970/how-to-convert-stdstring-to-lower-case 과 같이 transform() 이라는 함수를 통해 전체적인 문자열을 소문자 또는 대문자로 한번에 변환할 수 있었다.
#include <string>
#include <vector>
#include <algorithm>
#include <unordered_map>
using namespace std;
int solution(string word, vector<string> pages) {
int answer = 0;
double bestScore = 0;
vector<double> scoreBoard(pages.size(), 0);
unordered_map<string, int> urlToIndex;
transform(word.begin(), word.end(), word.begin(), ::tolower);
for (int i = 0; i < pages.size(); i++) {//각 페이지 url 추출 후 저장
string url = "";
transform(pages[i].begin(), pages[i].end(), pages[i].begin(), ::tolower);
for (int j = pages[i].find("<meta property=\"og:url\" content=\"") + 33; pages[i][j] != '\"'; j++)
url += pages[i][j];
urlToIndex[url] = i;
}
for (int i = 0; i < pages.size(); i++) {//점수 계산
double basicScore = 0;
int pos = 0;
int bodyStart = pages[i].find("<body>") + 6;
int linkCount = 0;
vector<string> linkList;
//기본 점수 계산
pos = pages[i].find(word, bodyStart);
while (pos != -1) {
if (!('a' <= pages[i][pos - 1] && pages[i][pos - 1] <= 'z') && !('a' <= pages[i][pos + word.size()] && pages[i][pos + word.size()] <= 'z'))
basicScore++;
pos = pages[i].find(word, pos + 1);
}
scoreBoard[i] += basicScore;
//각 연결된 링크 점수 분배
pos = pages[i].find("<a href=\"", bodyStart);
while (pos != -1) {
string link = "";
linkCount++;
for (int j = pos + 9; pages[i][j] != '\"'; j++)
link += pages[i][j];
//해당 링크가 page 중에 있는 링크인지 확인
if (urlToIndex.find(link) != urlToIndex.end())
linkList.push_back(link);
pos = pages[i].find("<a href=\"", pos + 1);
}
for (int j = 0; j < linkList.size(); j++) {//해당 page 점수 추가
scoreBoard[urlToIndex[linkList[j]]] += basicScore / linkCount;
}
}
//최종 점수와 인덱스 확인
for (int i = 0; i < scoreBoard.size(); i++) {
if (scoreBoard[i] > bestScore) {
bestScore = scoreBoard[i];
answer = i;
}
}
return answer;
}