그동안 중앙대학교 알고리즘 학회 ChAOS에 소속되어 백준 온라인 저지에 대회 문제를 출제해보는 귀중한 기회를 몇 번 가졌었는데, 생각해보니 대회 공식 에디토리얼 이외에 블로그에 문제에 대한 이야기를 한 적이 없었네요. 마침 생각난 김에 그동안 출제했던 문제의 출제 의도를 하나씩 풀어보려고 합니다. (정해 코드는 없습니다!)
글 작성 이후에도 dlaud5379 명의로 다른 문제를 출제할 때마다 업데이트할 예정입니다.
Windows의 파일 탐색기를 보면 파일이 정렬된 방식이 보통의 정렬 방식과는 다른 것을 알 수 있다.
보통 문자열을 정렬할 때는 맨 앞부터 한 글자씩 비교하다가 어느 한쪽이 끝나거나 일치하지 않는 글자가 있으면 그 위치의 문자를 비교한 결과가 문자열 전체를 비교한 결과가 된다. 한편 파일 탐색기는 여러 자리의 수를 한 글자로 취급해서 비교하는데, 이 때문에 "str12ing"과 "str123ing" 중 후자가 아니라 전자가 앞에 온다. 이러한 정렬 방식을 종종 "natural sort"라고 하기도 한다.
여러 개의 문자열이 주어지면 natural sort 방식으로 정렬한 결과를 출력하는 프로그램을 작성해 보자. 이 문제에서 구현할 알고리즘은 다음을 만족해야 한다.
문자열은 알파벳 대소문자와 숫자로만 이루어져 있다.
숫자열이 알파벳보다 앞에 오고, 알파벳끼리는 AaBbCc...XxYyZz의 순서를 따른다.
문자열을 비교하는 중 숫자가 있을 경우 그 다음에 오는 숫자를 최대한 많이 묶어 한 단위로 비교한다. 예를 들어 "a12bc345"는 "a", "12", "b", "c", "345"의 다섯 단위로 이루어져 있다.
숫자열끼리는 십진법으로 읽어서 더 작은 것이 앞에 온다. 이때 예제 2에서와 같이 값이 263을 초과할 수 있다.
같은 값을 가지는 숫자열일 경우 앞에 따라붙는 0의 개수가 적은 것이 앞에 온다.
입력
첫 줄에 문자열의 개수 N(2 ≤ N ≤ 10,000)이 주어진다. 그 다음 N줄에 정렬할 문자열이 한 줄에 하나씩 주어진다.
2026년 3월 21일 현재 이 문제의 난이도는 Gold III이며, 알고리즘 분류는 #구현implementation#문자열string#정렬sorting입니다. 출제 이후 거의 4년이 지난 지금(2024년 2월 8일) 구글에 백준 20210으로 검색해 봤더니 생각보다 이 문제에 대한 블로그 글이 많네요. 풀어주셔서 감사합니다 🙇
지문에서 짐작할 수 있듯이 표준 라이브러리에서 제공하는 정렬 함수에 비교 함수를 만들어 호출하면 되는 정직한 문제입니다. 물론 그 비교 함수 구현이 좀 복잡하긴 합니다. 비교 함수의 조건을 간단하게 요약하면 이렇습니다.
숫자는 무조건 문자보다 작다.
문자끼리 비교
대소문자 구분 없이 사전순으로 비교하고,
같은 문자라면 대문자가 소문자보다 작다.
숫자끼리 비교
최대한 긴 숫자열로 묶는다.
십진법으로 읽은 값으로 비교하고,
같은 값이라면 불필요한 0이 적을수록 작다.
제가 의도한 C++14 풀이에서 비교 함수의 전체적인 구조는 이렇습니다. 입력 문자열의 길이 n에 대해 비교 함수의 시간 복잡도는 입니다.
잇창명의 집에는 오래된 전자레인지가 있다. 백준 온라인 저지에서 문제를 너무 많이 푼 잇창명은 문득 이런 궁금증이 생기기 시작했다.
버튼을 최소 몇 번 눌러야 조리시간 2분을 맞출 수 있을까?
잇창명의 전자레인지에는 다음과 같이 버튼이 4개 있고, 각 버튼을 누르면 다음과 같이 작동한다. 초기 상태에는 조리시간이 0초이고, 조리 중이 아니며, 조리시작 버튼을 눌러야 조리가 시작된다.
10초: 조리시간이 10초 늘어난다.
1분: 조리시간이 1분(60초) 늘어난다.
10분: 조리시간이 10분(600초) 늘어난다.
조리시작
조리 중이 아닐 때: 조리가 시작된다. 만약에 조리시간이 0초였다면 30초로 늘어난다.
조리 중일 때: 조리시간이 30초 늘어난다.
모든 버튼은 조리 중인지의 여부와 무관하게 항상 누를 수 있으며, 별도의 언급이 없을 경우 항상 같은 동작을 한다.
예를 들어 이 전자레인지로 2분을 맞추려면 조리시작 버튼을 4번 누르면 되지만, 최적의 방법은 아니다. 그 대신 1분-1분-조리시작 순서로 버튼을 누르면 버튼을 누른 횟수가 3번이 되어 최적이다. 1분-1분의 경우에는 조리가 되지 않기 때문에 최적이 아니다. 실제로는 조리 중에는 남은 조리시간이 계속 줄어들고 중간에 조리를 취소할 수 있지만, 이 문제에서는 생각하지 않기로 한다.
잇창명은 지난 한 학기 동안 전자레인지를 이용할 때마다 매번 문제로 내고 싶은 마음이 들어서 괴로워하고 있다. 잇창명을 도와주자!
입력
첫 줄에 잇창명이 원하는 조리시간이 M:S 형태로 주어진다(0 ≤ M ≤ 60, 0 ≤ S ≤ 59). M은 분, S는 초이며, 항상 두 자리 숫자로 주어진다.
조리시간은 10초 이상 60분(3600초) 이하이며, 항상 10의 배수이다.
출력
주어진 조리시간을 맞추기 위해 버튼을 눌러야 하는 최소 횟수를 출력한다.
예제 입력 1
20:00
예제 출력 1
3
2026년 3월 21일 현재 이 문제의 난이도는 Silver II이며, 알고리즘 분류는 #그래프 이론graphs#그리디 알고리즘greedy#그래프 탐색graph_traversal#너비 우선 탐색bfs입니다.
처음 출제했을 때는 #그리디 알고리즘greedy과 #너비 우선 탐색bfs만을 정해로 생각했는데, 문제 검수 중에 #다이나믹 프로그래밍dp으로 풀 수 있다는 것을 알게 되어 결과적으로는 정해가 사실상 3개인 문제가 되었습니다. 개인적으로 좋은 문제가 되었다고 생각해 꽤 만족스럽네요.
너비 우선 탐색
가장 직관적으로 떠오를 만한 풀이입니다. 조리시간(int)과 조리 여부(bool)의 순서쌍을 정점으로, 각 버튼마다 가능한 상태 변화를 유향 간선으로 삼고 노드에서 출발하는 너비 우선 탐색을 할 수 있습니다.
그리디 알고리즘
제가 처음 의도했던 C11 풀이이기도 합니다. 당시 Gather에서 공개했던 에디토리얼에 그리디 알고리즘에 대한 증명을 실었었는데, 대중에 공개된 링크를 못 찾겠네요. 이 블로그에는 좀 더 엄밀하게 다듬은 증명을 싣겠습니다.
30초 버튼의 특수한 성질을 무시하고 문제를 풀면 30초 버튼을 정확히 0번 혹은 1번 누를 수 있습니다(30초 다음 단위가 60초이므로).
30초 버튼을 1번 누르는 경우: 30초 버튼을 가장 처음에 누르면 정상적으로 30초가 추가되면서 조리가 시작되므로 일반적인 탐욕법과 같은 해를 가집니다.
30초 버튼을 누르지 않는 경우: 이때는 조리가 시작되지 않으므로 어떻게든 30초 버튼을 추가로 눌러야 합니다. 편의상 추가로 버튼을 누르는 횟수를 이라고 하겠습니다.
모든 경우에 대해 인 알고리즘이 존재합니다.
모든 버튼을 누르고 나서 30초 버튼을 마지막에 추가로 누를 수 있는데, 아직 조리가 시작되지 않았으므로 조리가 시작되고 원래 조리시간이 0이 아니었으므로 조리시간은 유지됩니다.
탐욕법의 성질에 의해 일 수는 없습니다.
위의 논리에 의해 일반적인 탐욕법으로 구한 해에 을 더한 것이 해가 됩니다.
추가로 입력 범위가 작은 것을 이용해 너비 우선 탐색 코드와 탐욕법 코드가 모든 입력에 대해 같은 출력을 하는지 체크하는, 말 그대로 'proof by AC'1를 했습니다.
동적 계획법
위의 너비 우선 탐색 풀이를 그대로 동적 계획법으로 바꿀 수 있습니다. 조리시간 i와 조리 여부 j에 대해 dp[i][j]를 만들고 풀면 됩니다.
잇창명은 취미로 리듬 게임을 한다. 위에서 내려오는 노트를 정확한 타이밍에 처리하면 높은 점수를 얻을 수 있다.
노트를 처리하면 각각 다음 판정 중 하나를 받을 수 있다.
Perfect
Great
Good
Miss
노트를 처리하지 못하면 Miss 판정을 받는다. 위 4가지 판정 이외에 다른 판정은 없다. 노트가 N개 있는 곡에서 Perfect 판정을 a개, Great 판정을 b개, Good 판정을 c개 받았을 때의 점수는 다음 식으로 계산한다. 소숫점 아래는 버린다.
다시 말해, Good 판정의 점수인 을 기준으로 각 판정의 점수는 다음과 같다.
Perfect = 2 Good + 1
Great = 2 Good
Good = 1 Good
Miss = 0
예를 들어 노트가 100개 있는 곡에서 Perfect를 10개, Great를 20개, Good을 30개 받으면 450,000,010점을 획득한다. 이때 최고 점수는 Perfect 판정만을 N개 받았을 때 109+N점이 된다.
잇창명은 이 게임에서 정확히 123,456,789점을 획득해 사람들을 놀라게 하고 싶다. 잇창명은 초고수 플레이어라서 모든 노트의 판정을 원하는 대로 조절할 수 있다. 잇창명이 연주하려는 곡의 노트 수가 주어질 때, 잇창명이 이 곡을 어떻게 연주해야 원하는 점수를 획득할 수 있을지 계산하는 프로그램을 작성하시오.
입력
첫 줄에 테스트 케이스의 수 T(1 ≤ T ≤ 2,000)가 주어진다. 다음 줄부터는 T개의 테스트 케이스가 주어진다.
각 테스트 케이스에서는 한 줄에 노트의 개수 N(1 ≤ N ≤ 20,000)과 잇창명이 원하는 점수 S(0 ≤ S ≤ 109+N)가 공백으로 구분되어 주어진다. N과 S는 정수이다.
출력
각 테스트 케이스마다 한 줄에 걸쳐 다음을 출력한다.
잇창명이 Perfect 판정을 a개, Great 판정을 b개, Good 판정을 c개 받아서 정확히 S점을 획득할 수 있다면 2a+2b+c와 a를 공백으로 구분하여 출력한다. 가능한 (2a+2b+c, a)의 쌍이 여러 개인 입력은 없다.
잇창명이 곡을 어떻게 연주해도 정확히 S점을 획득할 수 없다면 -1을 출력한다.
예제 입력 1
31000 10000010004318 89995400020000 123456789
예제 출력 1
2000 10007772 318-1
2026년 3월 21일 현재 이 문제의 난이도는 Gold II이며, 알고리즘 분류는 #수학math입니다. 원래 를 의도한 문제라서 난이도 의견에 "이 [출제자의] 의도"라는 말을 보고 깜짝 놀랐습니다. (solved.ac 가이드라인인 '가장 쉽게 풀었을 때' 기준으로는 이 맞긴 합니다.)
소재와의 연관성
제가 남긴 solved.ac 난이도 의견에서 볼 수 있듯이 이 문제의 소재는 실존하는 리듬 게임인 Arcaea, 특히 점수 체계입니다. 특히 문제에서 제시한 판정 체계는 실제 게임의 판정 체계에서 용어만 더욱 대중적인 것으로 바꾼 것입니다.
Perfect = Shiny PURE
Great = Early/Late PURE
인게임에서는 기본적으로 세 판정이 모두 PURE로만 표시되며, Early/Late PURE는 따로 표시하도록 하는 숙련자용 설정이 있습니다.
Good = FAR
Miss = LOST
이 게임을 다루는 비공식 위키인 Arcaea Wiki의 Scoring2 문서에서 실제 게임의 점수 체계를 확인해볼 수 있는데, 이 역시 기준 점수(10'000'000점)를 제외하고 문제에서 제시하는 것과 일치합니다.
Shiny PURE = 100% + 1점
이때 100%는 기준 점수를 노트 수로 나눈 것을 기준으로 합니다.
Early/Late PURE = 100%
FAR = 50%
LOST = 0%
콤보는 점수에 전혀 영향이 없습니다. 문제에서도 혼동을 피하기 위해 콤보와 관련된 언급을 일절 하지 않았습니다.
원래는 문제에서도 기준 점수를 10,000,000점으로 두었지만, 범위가 너무 작아서 의도치 않은 풀이를 허용할 수 있다는 검수진의 의견을 수용해 1,000,000,000점으로 늘렸습니다.
이 서술에서 문제의 정해에 대한 힌트를 얻었습니다. 오래 전에 트위터에서 다른 리듬 게임인 Cytus를 소재로 목표 점수를 입력하면 그 점수를 정확히 맞추는 플레이를 출력하는 콘솔 프로그램을 봤던 (것으로 기억하는) 것도 어느 정도 영향이 있었습니다.
다 쓰고 나서 Arcaea라는 배경지식을 지우니 '그게 뭔데' 싶은 문제가 되어버렸고, 구글 검색이나 solved.ac 난이도 기여에서도 "문제의 의도를 모르겠다"는 반응이 어느 정도 보여서 정말 아쉽습니다. 노트 개수 제한을 20,000이 아니라 22,361로 뒀으면 의도가 더 드러났을까 싶네요.
출제자가 의도한 풀이
먼저 출력 설명의 "가능한 (2a+2b+c, a)의 쌍이 여러 개인 입력은 없다"와 바로 위에서 언급한 "[N의 범위 제한을] 22,361로 뒀으면 의도가 더 드러났을까 싶네요"에 주목해야 하는데, 22,361은 위에서 인용된 2,237과 수상하게 비슷해 보이는 데서 알 수 있듯이 모든 노트를 Great 이상으로 처리해야 1,000,000,000점을 달성할 수 있는 최대 노트 수입니다. 이게 풀이랑 무슨 관련이 있냐고요?
문제에서 Good 판정을 일종의 단위로 사용한 것이 또 하나의 힌트가 되는데, 최종 점수 식을 다음과 같이 두 부분으로 분리할 수 있습니다.
(Good 단위/'큰 단위')
('작은 단위')
모든 점수를 로 적고 를 특정한 값으로 고정했을 때 가능한 점수의 집합을 '띠'라고 합시다. 그러면 주어진 조건 내에서 서로 다른 어떤 띠끼리도 겹치지 않는다는 것을 알 수 있습니다.
더욱 부연 설명을 하자면, 큰 단위의 점수가 일 때 작은 단위의 점수를 받을 수 있는 유일한 판정인 Perfect의 최대 개수는 입니다. 여기서 이 띠가 가질 수 있는 작은 단위의 범위가 가 됨을 알 수 있습니다.
그런데 이므로 작은 단위의 범위가 이 값을 넘어가면 띠끼리 겹치는 상황이 생깁니다. 문제의 범위 을 대입하면 , 이므로 그런 일은 발생하지 않습니다. 그래서 이게 풀이랑 무슨 관련이 있냐고요?
이제 띠끼리 겹치지 않는다는 사실을 알았으니 점수의 작은 단위를 신경쓰지 않고 큰 단위를 먼저 구할 수 있고, 그 다음에 작은 단위를 구할 수 있습니다. 가능한 큰 단위는 최대 하나뿐이므로 테스트 케이스당 에 답을 구할 수 있습니다.
제가 의도한 C11 풀이를 보면 사칙연산만을 사용해 큰 단위와 작은 단위를 하나씩 구하고, 점수가 띠 안에 들어가는지 확인하는 로직 한 번으로 테스트 케이스가 끝나는 것을 볼 수 있습니다.
아무튼 정해가 배배 꼬인 것도, 힌트가 너무 꽁꽁 숨겨져있던 것도 부정할 수는 없을 것 같습니다. 출제를 하면서 #애드 혹ad_hoc을 붙일지 말지 고민했는데, 지금 알고리즘 분류를 봐도 의외로 #애드 혹ad_hoc은 없네요.
문제 여담
문제에 나온 것과 같이 실제로 저도 취미로 리듬 게임을 하고(휴대폰으로는 지금도 Arcaea를 하고, 오락실에서는 사운드 볼텍스를 가장 자주 합니다), "정확히 123,456,789점을 획득해 사람들을 놀라게 하고 싶"은 생각만 생각도 가끔씩 합니다. 당연히 실제 잇창명은 모든 노트의 판정을 원하는 대로 조절할 수 있는 초고수 플레이어가 아닙니다.
2026년 2월에는 원본 게임의 개발사에서 PC 리듬 게임 In Falsus의 데모를 공개했는데, 이 게임의 점수 체계도 기준 점수(100'000'000점)를 제외하고 Arcaea와 같습니다. 위의 계산식을 그대로 사용하면 노트가 개 이상일 때부터 띠가 겹치기 시작한다는 결과가 나옵니다.
주식회사 푸앙의 추종자인 잇창명은 푸앙에서 생산하는 종류의 컵을 가지고 있다. 이 회사에서 생산하는 컵은 쉽게 정리할 수 있도록 종류에 상관없이 컵의 입구 사이에 빈틈이 없도록 포개어진다는 장점이 있다. 편의상, 이 문제에서는 컵의 몸통 부분을 제외한 입구 부분만 생각하기로 하자.
가지고 있는 컵을 포개어 정리하던 잇창명은 문득 컵의 입구 부분의 높이 총합이 가 되도록 컵을 포갤 수 있는 경우의 수가 궁금해졌다. 모든 종류의 컵은 무한히 많이 있으며, 각 종류의 컵은 입구의 높이가 정해진 단위 높이 의 양의 정수배에 해당한다. 또한, 컵을 포갤 때는 입구가 위로 오도록 포개어야 한다.
아래와 같이 입구의 높이가 각각 , , , 인 컵 세트를 사용해 구체적인 예시를 들어 보자.
위의 네 종류의 컵을 입구 부분의 높이가 이 되도록 쌓는 방법은 아래의 그림 이외에도 여러 가지가 있으며, 그 경우의 수를 모두 합하면 가지이다.
잇창명을 위해 컵을 포개는 경우의 수를 구하는 프로그램을 작성해 보자.
입력
첫 번째 줄에 과 가 주어진다.
두 번째 줄에 종류의 컵의 높이 , , , , 이 공백으로 구분되어 주어진다.
출력
첫 번째 줄에 높이가 정확히 가 되도록 컵을 포개는 경우의 수를 ()로 나눈 나머지를 출력한다.
예제 입력 1
2 31 2
예제 출력 1
3
예제 입력 2
4 101 1 2 3
예제 출력 2
9003
예제 입력 3
2 1001 1
예제 출력 3
976371285
제한
, 는 정수
2026년 3월 21일 현재 이 문제의 난이도는 Silver I이며, 알고리즘 분류는 #다이나믹 프로그래밍dp입니다.
생각해보면 #다이나믹 프로그래밍dp이 정해인 문제 중 점화식이 입력 데이터에 따라 동적으로 바뀌는 문제를 거의 못 본 것 같아서 기획한 문제입니다. 실제로 그런 문제가 많은데 제가 기억을 못 하는 것일 수도 있지만 일단 출제 자체는 기획 의도에 맞게 잘 되긴 했습니다.
0개 이상의 컵을 쌓은 것을 '컵탑'이라고 할 때, 컵탑의 높이 에 따라 점화식 를 다음과 같이 구성할 수 있습니다.
: 높이가 음수가 되게 쌓는 것은 당연히 불가능하므로 가지입니다.
: 베이스 케이스에 해당합니다. 컵을 0개 쌓는 가지 방법이 있습니다.
: 기존에 쌓은 컵탑 위에 높이가 인 번 컵을 쌓을 수 있습니다.
높이가 인 컵을 추가로 쌓아서 쌓인 컵탑의 높이를 로 만들려면 쌓기 이전에 컵탑의 높이는 여야 합니다. 즉, 가지 방법이 가능합니다.
데이터에서 제시된 모든 컵에 대한 경우의 수를 합합니다.
즉, 수식으로 정리하면 다음과 같습니다.
정해에서는 인 경우를 생략하고 있으며, 추가로 최근 100개 항만 메모리에 유지하는 최적화가 가능합니다(이므로).
; if { ; } else { } if { ; } if { } else { ; } ; end
노트
사실 C에는 else if문이 따로 없다는 사실을 알고 있는가? 우리가 else if라고 알고 있는 구문은 사실 일반적인 else의 본문에서 중괄호를 생략하고 if문으로 채운 경우에 해당한다.
2026년 3월 21일 현재 이 문제의 난이도는 16Platinum V이며, 알고리즘 분류는 #파싱parsing#재귀recursion#문자열string입니다.
이 문제만큼 난이도 예측에 실패한 적이 없을 것 같습니다.4949번 '균형잡힌 세상'이나 2504번 '괄호의 값' 등의 단순 심화 버전으로 기획하고 실버 중상위권 정도의 난이도를 예상했는데, 막상 뚜껑을 열어보니 플래티넘 턱걸이를 하는 문제가 나왔습니다. 추가로 문제 자체가 검수하기 어려운 형태이고 지문도 엄밀하게 작성해야 하다 보니 '다시는 이런 문제 내지 말아야지' 하는 다짐을 유발하기도 했습니다.
위의 괄호 문자열 문제에서 현재 읽고 있는 위치(|)를 다음과 같이 열거형으로 표현할 수 있었듯이...
소괄호 안 (( ... | ... ))
대괄호 안 ([ ... | ... ])
이 문제에서도 다음과 같은 열거형을 정의해 스택으로 관리할 수 있습니다. 설명의 편의상 이름을 붙이겠습니다.
S_IF: if(-else)문의 첫 번째 본문 이전 (if | ... 혹은 if | ... else ...)
S_THEN: if(-else)문의 첫 번째 본문 이후 (if ... | 혹은 if ... | else ...)
S_ELSE: if-else의 두 번째 본문 이전 (if ... else | ...)
S_BLOCK: 중괄호 안 ({ ... | ... })
그런 다음에는 토큰을 읽을 때마다 다음과 같이 스택 갱신과 출력을 병행하면 됩니다. "스택의 가장 위에 토큰 X가 있을 때"는 글 작성의 편의상 "X 상태일 때"로 축약합니다.
용어 설명
X 상태: '스택의 가장 위에 토큰 X가 있을 때'를 글 작성의 편의상 축약한 형태입니다.
본문이 올 자리: S_IF나 S_ELSE 상태를 말합니다. 이 경우 다음 토큰으로는 본문을 구성할 수 있는 토큰(;, {, if)만 올 수 있습니다.
특히 S_BLOCK은 본문이 올 자리가 아닙니다.
if문 정리: S_THEN 상태이고 다음 토큰이 else가 아닌 경우에는 해당 토큰이 if-else문이 아닌 if문에 해당하며, 이때 else문으로 전이하지 않는 S_THEN을 뽑는 것을 일컫습니다.
처리해야 할 S_THEN이 여러 겹 있을 수 있음에 유의해야 합니다.
본문 여닫기: 본문이 열릴 때와 닫힐 때 필요한 특수한 처리를 일컫습니다. 중괄호가 생략됐는지 판단하여 추가로 출력하고, 본문이 닫힐 때 S_IF는 S_THEN으로 갱신, S_ELSE는 뽑아야 합니다.
if문을 정리하는 도중에도 본문 닫기 판정이 생길 수 있음에 유의해야 합니다.
세미콜론(;)
본문이 올 자리일 경우 본문 여닫기를 합니다.
그렇지 않으면 세미콜론이 일반 구문이므로 if문을 정리합니다.
여는 중괄호({)
이 토큰은 본문의 첫 토큰으로만 등장할 수 있습니다. 본문 열기를 하고 스택에 S_BLOCK을 넣습니다.
닫는 중괄호(})
이 토큰은 본문의 마지막 토큰으로만 등장할 수 있습니다. if문을 정리하고 스택의 S_BLOCK을 뽑은 뒤 본문 닫기를 합니다.
if
본문이 올 자리일 경우 본문 열기를 한 뒤 스택에 S_IF를 넣습니다.
그렇지 않으면 if(-else)문이 일반 구문이므로 if문을 정리하고 스택에 S_IF를 넣습니다.
else
이 토큰은 본문의 바로 다음에만 등장할 수 있습니다. 스택의 S_THEN을 S_ELSE로 갱신합니다.
end
입력의 종료를 나타내는 토큰입니다. if문을 정리합니다.
여기까지가 제가 의도한 대회 중에 작성할 수 있는 풀이였고, 모종의 방법을 이용하면 문제에서 제시하는 언어(문법이 아닙니다!)4를 LR 파싱할 수 있으니 관심이 있다면 도전해보시는 것도 좋겠습니다. 실제로 데이터 검증 코드가 이렇게 구현되어 있습니다.
문제 여담
사실 C에서는 0이 팔진수라는 것도 알고 계셨나요? C17 표준의 최종안인 ISO C N2176에서는 정수 상수의 문법이 다음과 같이 정의되어 있습니다. opt는 바로 앞의 문법 요소가 있어도 되고, 없어도 된다는 의미입니다.
integer-constant:
decimal-constantinteger-suffixopt
octal-constantinteger-suffixopt
hexadecimal-constantinteger-suffixopt
decimal-constant:
nonzero-digit
decimal-constantdigit
octal-constant:
0
octal-constantoctal-digit
(중략)
nonzero-digit: one of
123456789
십진 상수(decimal-constant)는 0으로 시작할 수 없고(nonzero-digit), 0으로 시작하는 것은 모두 팔진 상수(octal-constant)인 것으로 정의되어 있습니다. 0도 예외는 아닙니다.
이렇게 얻은 NFA는 멱집합 구성powerset construction 알고리즘으로 DFA로 변환할 수 있습니다. NFA의 모든 상태의 부분집합이 각각 DFA의 상태 하나가 되기 때문에 부분집합 구성subset construction이라고도 합니다.
DFA 최소화
멱집합 구성으로 얻은 DFA는 상태의 개수가 불필요하게 많을 수 있는데, 여기서 초기 상태에서 갈 수 없는 상태와 수용 상태로 갈 수 없는 상태를 없애고 서로 구별할 수 없는 상태를 합쳐서 상태의 개수를 최소화하는 것을 DFA 최소화DFA minimization라고 합니다.
위의 모든 과정을 직접 계산해야 하는 것은 아니고, 웹상의 도구 중 아무거나 하나를 골라잡으면 됩니다. 저는 CyberZHG님의 웹 앱을 이용했습니다.
문제에서 제시한 정규식인 (1(10)+1*|0+10)+을 최소 DFA로 변환한 결과는 다음과 같습니다. 초기 상태는 0이며, 모든 상태 번호에서 1을 빼는 가공을 거쳤습니다.
상태 번호
0
1
수용 상태?
#0
→ #2
→ #3
#1
→ #2
→ #7
✅
#2
→ #2
→ #4
#3
❌
→ #5
#4
→ #6
❌
#5
→ #1
❌
#6
→ #2
→ #3
✅
#7
→ #1
→ #7
✅
상태 전이표의 ❌ 표시는 상태 전이가 없어서 DFA가 문자열을 거부하는 경우이며, 코드로 구현할 때는 -1을 대신 사용할 수 있습니다.
상태 전이를 세그먼트 트리로 관리
함수라고 하면 정의역의 모든 원소를 공역의 원소 하나씩과 대응시킨 것인데, 위 상태 전이 함수의 경우에는 정의역의 원소가 8개뿐이므로 배열로 전부 기록할 수 있습니다. 예를 들어 비트열 0에 해당하는 상태 전이 배열은 C/C++ 문법으로 { 2, 2, 2, -1, 6, 1, 2, 1 }입니다. 추가로 길이가 2 이상인 비트열의 상태 전이 배열 역시 단순 함수 합성으로 구할 수 있습니다.
이제 상태 전이 배열에 결합법칙이 성립하는 이항 연산(함수 합성)이 있으니 문자열 전체의 상태 전이 배열을 세그먼트 트리로 관리할 수 있습니다. 문자열 인식은 문자열의 상태 전이 배열을 구하고 초기 상태 0에서의 함숫값, 즉 [0]번째 원소를 구한 뒤 그 상태가 수용 상태인지 확인하면 됩니다.
갱신 결과를 세그먼트 트리에 미리 저장
구간 갱신까지 처리하려면 추가로 한 가지 발상이 더 필요합니다. 이 문제에서는 함수 합성을 고려하더라도 가능한 갱신이 00, 01, 10, 11의 네 종류뿐인데, 상태 전이 함수의 모든 함숫값을 같이 저장했으니 가능한 모든 갱신 결과도 같이 저장하지 못할 이유가 없다는 것입니다.
구체적으로는 각 비트를 다음과 같이 저장하고...
0 비트: { ("0"의 상태 전이 배열), ("0"의 상태 전이 배열), ("1"의 상태 전이 배열), ("1"의 상태 전이 배열) }
1 비트: { ("0"의 상태 전이 배열), ("1"의 상태 전이 배열), ("0"의 상태 전이 배열), ("1"의 상태 전이 배열) }
Arcaea Wiki는 위키 호스팅 서비스인 Fandom 소속으로, 해당 서비스의 지나친 광고 등의 문제로 인해 부득이하게 미러 사이트로 링크합니다. 위키문법이 깨지거나 내용이 로딩되지 않는 등 기술적인 문제가 있을 수 있으며, 이 경우 하단의 Fandom 링크로 접속할 수 있습니다.
언어는 문자열의 집합을 형식언어론에서 다루는 대상으로서 부르는 이름이고, 문법은 이러한 언어를 기술하는 규칙이므로 둘은 다른 의미를 가지며, 구분되어야 하는 개념입니다. 문제에서 제시하는 문법은 그 자체로는 모호하지만(같은 문자열에 해당하는 구문 트리가 유일하지 않음), 같은 언어를 기술하면서도 모호하지 않은 문법을 제시할 수 있습니다.