<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>PS하는 kdy</title>
    <link>https://kdy40929.tistory.com/</link>
    <description>PS를 취미로 하고 있는 고등학생의 블로그입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 22 Jul 2026 00:21:18 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kdy40929</managingEditor>
    <item>
      <title>DOJ - KOI 2nd Round Mock 1 High 후기</title>
      <link>https://kdy40929.tistory.com/14</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dojoi.xyz/ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://dojoi.xyz/ko&lt;/a&gt; 라는 사이트에서 KOI 2차 모의고사를 출제하였다고 하여 고등부를 참가해보게 되었다. 시험 시간은 4시간 반이고, 대략적으로 14:00 - 18:30 정도의 시간을 잡고 참가하였다. (해당 모의고사는 실제 KOI와는 전혀 무관합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고등부의 문제는 이렇게 4문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ulhe2/dJMcahEDrAc/sQxEHki6kt4E3t5BJqK5CK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ulhe2/dJMcahEDrAc/sQxEHki6kt4E3t5BJqK5CK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ulhe2/dJMcahEDrAc/sQxEHki6kt4E3t5BJqK5CK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fulhe2%2FdJMcahEDrAc%2FsQxEHki6kt4E3t5BJqK5CK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;492&quot; height=&quot;692&quot; data-origin-width=&quot;492&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;[0:00 - 0:44] A. 히스토그램 (AC, 100)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 출제자 중 한 명인 lunarlity가 저번에 만든 히스토그램 관련 PS 문제가 NM개라고 해서 문제를 보자마자 lunarlity가 냈겠다는 생각이 들었다. 일단 X번 이하 시행으로 가능한 경우를 탐색하는 것은 굉장히 어렵기 때문에 Parametric Search를 통해 문제를 풀어야겠다는 아이디어를 낼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그러면 답이 p보다 클 수 있는가?라는 판정 문제를 풀 수 있어야 한다. 하지만 이 역시 p가 가로 * 높이 꼴이라서 어렵기 때문에 가로나 높이 중 하나의 값을 고정하는 아이디어를 떠올렸다. N 범위가 7000이므로 N^2 이상의 시간복잡도가 나와도 괜찮기 때문이다. 가로를 고정할 경우 가로 길이는 고정할 수 있지만 위치를 고정하는 과정이 추가로 필요하기 때문에 높이를 고정하여 N번 탐색하는 것을 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이렇게 되면 높이를 고정했을 때 문제를 $O(N)$ 정도에 풀 수 있어야 하는데, 내가 정한 높이보다 높은 것을 1, 낮은 것을 0으로 잡을 수 있고, p와 높이가 정해지므로 연속하게 만들어야 하는 1의 개수가 정해진다. 그러므로 1이 그 정해진 개수만큼 붙을 수 있는지 생각해보면 되고, 당연히 인접한 1들끼리 붙어야 하고, 붙는 위치는 항상 붙을 1들 중에 가장 중앙 위치에 가까운 1에 붙는 것이 이득이 됨을 쉽게 알 수 있으므로 $O(N)$에 높이를 고정했을 때의 판정 문제를 풀 수 있게 된다. 따라서 전체 시간복잡도는 $O(N^2 log N)$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Python으로 $O(N^2 log N)$을 $N \le 7000$에서 돌릴 수 있느냐가 관건이었는데, 2초 제한에 1630ms로 다행히 시간 안에 돌았다. 난이도는 P3 정도인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[0:44 - 1:17] B. 경기장 (AC, 200)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 문제는 트리와 관련된 문제였다. 주어진 예제에서 3을 경기장의 한 점으로 잡고, 여기서 불편도를 가장 많이 줄일 수 있는 방향으로 간선을 뻗어 경기장을 늘리는 그리디를 반복하면 정답을 얻을 수 있음을 확인하였다. 그래서 해당 그리디가 맞을 것이라고 생각하고 처음의 한 점을 어떻게 정해야 할지에 대해 고민했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;처음의 한 점이 $X = 1$일 때의 정답일 것이라는 추론은 위 그리디가 성립한다면 꽤 자명하게 해볼 수 있다. 따라서 이 추론이 맞는지 확인하기 위해 $X = 1$ 서브태스크에 해당하는 풀이를 짜서 먼저 제출했다. 다행히 해당 섭태에서 올바른 결과가 나왔고 따라서 이 풀이에 우선순위 큐를 이용해 간선을 추가로 연결하는 부분을 구현하여 15점을 받고 3분 후 곧바로 만점을 받을 수 있었다. 최종 시간복잡도는 간단한 tree dp를 이용해 $X = 1$의 답을 찾는 과정이 $O(N)$이고 그 이후 우선순위 큐를 이용해 답을 구하는 과정이 $O(X log N)$이므로 $O(N + Xlog N)$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 난이도는 증명에 비해 발상이 Korean Well known이라 다소 낮게 책정될 수도 있을 것 같은데, P2 정도인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[1:17 - 2:21] C. 중앙값 (PAC, 273)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;문제를 보고 이걸 어떻게 풀지 생각이 들어 차분히 서브태스크를 보기로 했다. 2점짜리 서브태스크 3개는 정말 쉽고 바로 구현해서 낼 수 있는 부분이라 4, 5, 6번 서브태스크에 해당하는 $N = 2K+1$을 집중적으로 고민했다. 최종적으로 남게 되는 답은 정확히 하나이므로 이 값이 얼마일지 알아야 하는데, 이것조차 잘 모르겠어서 먼저 $N =2K+1$이고 $A_{i} \le 2$인 섭태 4부터 생각하기로 했다. $A_{i}$의 값은 1이나 2만 가능하고, 예제 몇 개를 손으로 해보다 보니 인접한 두 수가 같으면 이 값이 계속해서 끝까지 유지되는 것을 관찰할 수 있었다. 그리고 그렇지 않다면 1과 2가 번갈아 가며 등장해야 하는데 이 경우는 1, 2, 1, 2, ...이 2, 1, 2, 1, ... 으로 뒤집힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그래서 결국 인접한 두 수의 쌍 중 중앙과 가장 가까운 것을 찾으면 되는 문제가 되고 이걸 조금 응용하면 A와 마찬가지로 p보다 작으면 0, p보다 크면 1로 바꾸어서 parametric search를 할 수 있게 된다. 따라서 $N = 2K+1$을 온전히 풀 수 있게 된다. 그래서 46점을 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;문제를 더 못 풀 수도 있겠다는 생각이 들어 일단 쉬운 서브태스크인 1, 2, 3점을 긁고 52점을 확보하였다. 그리고 뒤이은 섭태를 조금 더 고민하다가 $A_i \le 2$ 서브태스크를 위에서의 아이디어를 비슷하게 이용해서 풀 수 있다는 생각이 들었다. 그리고 이걸 활용해서 0과 1을 구분하는 기준을 2, 3, 4, 5... 10으로 바꾸어 가며 10번 연산하면 $A_i \le 10$까지 풀 수 있겠다는 생각이 들어 이걸 구현했고 총 73점을 받을 수 있었다. 그리고 풀태를 풀지 못할 것 같아 D로 넘어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[2:21 - 4:07] D. 지우기 (PAC, 297)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;문제를 읽자마자 굉장히 어렵겠다는 생각이 들었다. $l_1, r_1, l_2, r_2$가 동작하는 방식이 굉장히 까다로웠다. 일단 왼쪽 끝과 오른쪽 끝이 정해지면 값이 정해지므로 dp[l][r]을 l ~ r 구간의 f 함숫값으로 정의하면 $O(N^2)$ 사이즈의 테이블을 만들 수 있다. 그리고 여기에 Segtree를 이용해서 구간 max를 관리하면 $O(N^2 + QN log N)$에 문제가 해결된다. 몇 번의 구현 실수가 있었지만 이를 통해서 섭태 1과 2를 맞힐 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;섭태 3의 경우 모든 값이 다르면 f 함수를 취할 때 지울 수 있는 값이 없으므로 구간의 길이를 각각 최소화, 최대화하는 방법만 생각하여 구현하면 되는 간단한 서브태스크였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그리고 다양한 섭태를 보다가 그나마 섭태 5가 $l_1 = r_1, l_2 = r_2$가 성립하므로 비교적 간단헤 보였다. 오프라인 쿼리 중 mo's를 이용해서 쿼리 순서를 바꾸면 문제가 $O(N \sqrt{N} + Q)$에 풀리기 때문에 12점을 획득할 수 있어서 최종적으로 24점을 얻었다. 이 문제를 풀던 도중 채점 서버에 오류가 발생했는데, 이미 문제를 풀 수 있는 만큼 대부분 푼 대회 후반부라서 그나마 다행이었던 듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kjhmx/dJMcaiDAjGv/ykzz89RHOGD6LWetmGvBN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kjhmx/dJMcaiDAjGv/ykzz89RHOGD6LWetmGvBN1/img.png&quot; data-alt=&quot;dadas08에게도 채팅을 보냈으나, dadas08의 욕설 사용으로 인해 차마 블로그에 게시할 수 없었습니다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kjhmx/dJMcaiDAjGv/ykzz89RHOGD6LWetmGvBN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKjhmx%2FdJMcaiDAjGv%2Fykzz89RHOGD6LWetmGvBN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1252&quot; height=&quot;900&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;dadas08에게도 채팅을 보냈으나, dadas08의 욕설 사용으로 인해 차마 블로그에 게시할 수 없었습니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 4시간 반에 걸친 KOI MOCK을 마쳤고, unrated 참가자 포함 6위를 기록하며 레이팅 238점을 벌게 되었다. 문제 퀄리티가 전반적으로 높았던 것 같다. 그러나 1번과 3번이 공통적으로 파라매트릭 서치를 공통적인 아이디어로 쓰는 등 상당히 유사한 문제 같다는 생각이 들기도 해서 이 부분은 조금 아쉬웠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT63Tu/dJMcaiwOBOS/SpH3j47woS4g3STOckMlo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT63Tu/dJMcaiwOBOS/SpH3j47woS4g3STOckMlo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT63Tu/dJMcaiwOBOS/SpH3j47woS4g3STOckMlo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT63Tu%2FdJMcaiwOBOS%2FSpH3j47woS4g3STOckMlo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;856&quot; height=&quot;396&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;396&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래도 4시간 반 동안 재밌게 풀어볼 수 있는 문제를 마련해준 운영 및 출제진 분들께 감사의 말씀을 드린다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;006&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/006.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/006.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <category>DoJ</category>
      <category>KOI</category>
      <category>KOI MOCK</category>
      <category>mock</category>
      <category>ps</category>
      <category>대회후기</category>
      <category>정올</category>
      <category>프로그래밍</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/14</guid>
      <comments>https://kdy40929.tistory.com/14#entry14comment</comments>
      <pubDate>Fri, 5 Jun 2026 21:29:59 +0900</pubDate>
    </item>
    <item>
      <title>2026 SCSC 프로그래밍 경시대회 Div. 1 후기</title>
      <link>https://kdy40929.tistory.com/13</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;올해도 작년처럼 서울대학교 컴퓨터 연구회 SCSC에서 주최하는 SCPC(SCSC Computer Programming Contest)가 열려 참가했습니다. 작년에는 별 생각 없이 지원해서 검수했던 대회였는데, 올해는 참가해야겠다는 생각이 들어서 신청했습니다. 닉네임을 뭘로 할지 조금 고민을 하면서 신청 폼을 작성할 당시 옆에 있던 친구한테 스코어보드 닉네임을 추천해 달라고 했는데, admin을 얘기하길래 바로 그대로 admin을 닉네임으로 해서 참가했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;제가 참여한 디비전은 Division 1입니다. Codeforces 오렌지를 찍었기 때문인데, 제가 그렇다고 수많은 GM이나 IGM들과 경쟁했을 때 경쟁력이 있는 것도 아니라서 높은 상은 받기 어려울 것이라고 생각하고 대회에 임했습니다. (Division 2였으면 거의 우승을 바라봤을 텐데 아쉽습니다. 물론 Div.1에 계신 모든 분들이 Div.2였다면 우승을 바라보셨을 것입니다..)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;같은 날 서울대학교에서 KMO 1차가 있었는데, SCPC 대회를 치는 경곽 학생들이 다들 치는 것 같아 저도 신청하고 같이 쳤습니다. 가채점 결과 66점인 것 같은데 어느 정도 상을 주는지는 잘 모르겠습니다. 중등 KMO를 준비하던 중학교 1학년 때랑 비교해서 경시 문제를 푸는 실력은 크게 변화가 없는 듯합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;12:30에 앞선 KMO가 끝나고 급하게 SCPC가 열리는 28동으로 이동했습니다. 이동하니 12:40 정도로 약 20분 정도 시간이 남아 있었습니다. 일단 점심을 먹지 못했기 때문에 급하게 간식을 챙기고 자리에 앉았습니다. 여담이긴 하지만 의자가 살짝 불편했는데, 책상과 의자 사이 거리를 마음대로 조정하지 못하는 게 조금 불편했던 것 같습니다. (그래서 자세가 꽤 불편했는지 그날 밤과 다음날 아침까지 목이 좀 뻐근했습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;대회 시작 전에는 정신없이 달려온 탓에 간식이나 좀 먹었고 사진 한 장도 찍지 못했습니다. 아무튼 서둘러 atcoder에 로그인하고 대회가 시작했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[0:00 - 1:17] A CUBRID HA Load Balance AC (4WA 5RE)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;어.. 일단 A부터 말렸습니다. 작년 대회를 검수했고 작년 대회의 div1 A번이 p3, B번이 제일 쉬운데 g1이었던 걸 기억하고 있었지만 그럼에도 A를 읽었는데 어떻게 풀지 바로 생각나지 않는 것 자체가 당황스러웠습니다. 일단 1 i가 주어지면 그 이후로는 반드시 꺼지니까 각 서버를 켜서 사용할 수 있는 시간 범위를 생각하면서 우큐 그리디 같은 느낌을 생각했습니다. 하지만 이내 O(NQ)가 도는 제한이고 그냥 역순으로 그리디하면 된다는 생각이 들어 그대로 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그런데 바로 1WA를 적립합니다. 그러고 나서 보니 1 i가 중복해서 있는 경우 가장 처음 쿼리만 생각해줘야 하는데 가장 마지막 쿼리 기준으로 꺼지는 것처럼 코드가 짜여 있었습니다. 그래서 이를 고치고 다시 제출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;또 2WA를 적립합니다. 그러고 보니 아무런 서버도 켜져 있지 않는 상태는 유효한 상태인데 이 상태를 Master 서버가 없어서 무효한 상태라고 판단하고 Master 서버를 억지로 만들어내려고 하는 상황이었습니다. 그래서 이것도 고치고 다시 제출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그런데 3WA를 적립합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2222&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cI6fw0/dJMcabjYHCM/hzVz216SsGhMHa3qAKGja0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cI6fw0/dJMcabjYHCM/hzVz216SsGhMHa3qAKGja0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cI6fw0/dJMcabjYHCM/hzVz216SsGhMHa3qAKGja0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcI6fw0%2FdJMcabjYHCM%2FhzVz216SsGhMHa3qAKGja0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2222&quot; height=&quot;384&quot; data-origin-width=&quot;2222&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이때 틀린 데이터 개수가 단 1개인 것을 확인합니다. 그래서 작은 N에 대해서 열심히 반례를 찾으려고 시도해봤지만 잘 나오지 않았습니다. 그래서 결국 저 WA 데이터 하나를 찾아서 뚫는 방향성을 생각하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rVh6R/dJMcagS5Yzr/jk4T9X2lKKrKCJNYKk0fXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rVh6R/dJMcagS5Yzr/jk4T9X2lKKrKCJNYKk0fXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rVh6R/dJMcagS5Yzr/jk4T9X2lKKrKCJNYKk0fXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrVh6R%2FdJMcagS5Yzr%2Fjk4T9X2lKKrKCJNYKk0fXk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;328&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 작은 N에서 틀리는 게 맞는지 확인하기 위해 assert를 박아서 제출합니다. (3WA 1RE) RE 덕에 작은 N에서 틀린다는 걸 확인해냈습니다. 하지만 N이 작아도 Q가 큰 상황일 수도 있기 때문에 다시 한 번 과감한 assert를 박고 제출합니다. (3WA 2RE)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1yAUX/dJMcadaUYcv/CaOIPVortXkO11Q6NuTvg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1yAUX/dJMcadaUYcv/CaOIPVortXkO11Q6NuTvg1/img.png&quot; data-alt=&quot;N &amp;amp;lt;= 4, Q &amp;amp;lt;= 4인 데이터에서 WA가 발생한다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1yAUX/dJMcadaUYcv/CaOIPVortXkO11Q6NuTvg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1yAUX%2FdJMcadaUYcv%2FCaOIPVortXkO11Q6NuTvg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;556&quot; height=&quot;362&quot; data-origin-width=&quot;556&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;N &amp;lt;= 4, Q &amp;lt;= 4인 데이터에서 WA가 발생한다..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히 N과 Q가 모두 작은 데이터에서 틀리는 상황이었기 때문에 반례를 찾을 만할 것이라고 생각하고 계속 반례를 찾습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 우연히 아래와 같은 반례를 찾아냅니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779062551293&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;4 2
2 2 1 0
2 0 1 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답은 Master 서버 2개, Slave 서버 2개가 최소한으로 필요하므로 4이지만, 코드가 3을 뱉었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 반례를 해결해서 제출했더니 아래와 같이 더 많은 테케에서 나가는 걸 확인하고 맙니다. (4WA 2RE)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgUyRc/dJMcaf0Uvr2/rpJmyLakrZokgunUB5tk70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgUyRc/dJMcaf0Uvr2/rpJmyLakrZokgunUB5tk70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgUyRc/dJMcaf0Uvr2/rpJmyLakrZokgunUB5tk70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgUyRc%2FdJMcaf0Uvr2%2FrpJmyLakrZokgunUB5tk70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1092&quot; height=&quot;322&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;322&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 모르겠어서 다시 원래 코드로 돌아가서 assert를 더 찍어봅니다. 반례가 N = 4일 때라는 걸 찾았고, 아래와 같은 코드를 제출합니다. N이 4가 아니면 아까 49개를 맞혔던 코드로 풀고, 그렇지 않은 경우는 위에서 찾은 반례와 같은 형태를 해결하는 코드를 냈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rNRcV/dJMcaffB7in/mFlrdfbpreItniQYsCcjsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rNRcV/dJMcaffB7in/mFlrdfbpreItniQYsCcjsk/img.png&quot; data-alt=&quot;데이터 뚫기..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rNRcV/dJMcaffB7in/mFlrdfbpreItniQYsCcjsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrNRcV%2FdJMcaffB7in%2FmFlrdfbpreItniQYsCcjsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;520&quot; height=&quot;180&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데이터 뚫기..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 해서 데이터를 뚫어내는 데에 성공하고, 어떻게든 1AC를 쌓습니다.. 패널티가 망해서 솔브 수가 중요하겠다는 생각을 하면서 다음 문제로 넘어갔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tuvYk/dJMcadaUYuK/gficUURZJ8MO5huRKxnGB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tuvYk/dJMcadaUYuK/gficUURZJ8MO5huRKxnGB1/img.png&quot; data-alt=&quot;힘겨웠다..&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tuvYk/dJMcadaUYuK/gficUURZJ8MO5huRKxnGB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtuvYk%2FdJMcadaUYuK%2FgficUURZJ8MO5huRKxnGB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1016&quot; height=&quot;288&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;힘겨웠다..&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[1:17 - 1:40] C. SCSC Magical Garden AC (1TLE)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A를 풀고 스코어보드를 보니 C 솔브가 꽤 많이 보였습니다. 그래서 바로 C로 넘어갔습니다. 임의의 3x3 내부에서는 S가 더 많지만 전체 NxN 격자를 보면 C가 더 많도록 격자를 구성하는 문제였습니다. 고민을 좀 해보니 N이 3의 배수면 불가능하고, N이 조금만 커져도 불가능함을 증명할 수 있었습니다. 그래서 N = 4, N = 5, N = 7 정도를 직접 구성하다 보니 N = 7에서 쉽게 구성하는 방법을 찾아냅니다. 3x3을 아래 모양으로 반복해서 채우는 것입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779063219590&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CCC
CSS
SSS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마찬가지로 N = 8에서는 아래 모양으로 반복해서 채우면 됐습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1779063251077&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CCS
CCS
SSS&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 N을 3으로 나눈 나머지에 따라 분류해서 위 모양대로 채우고 S가 더 많은지 체크해서 출력하는 코드를 냈는데, 예상 외의 TLE를 받습니다. 그러고 나서 문제를 다시 보니 출력 데이터의 크기로 입력의 크기를 제한하고 있는 걸 확인했습니다. 그래서 로컬에서 작은 N에 대해 계속 답을 찍어보며 NO가 되는 임계점을 찾아서 제출했습니다. 그래서 AC를 받습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[1:40 - 2:43] J. ChannelTalk Workflow AC (1WA)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C를 풀고 스코어보드를 보니 J가 많이 풀려 있어 또 J로 갑니다. 보니까 문제가 해구성이라서 재밌게 해볼 만하다고 생각했습니다. 일단 문제를 처음 읽고 나니 K를 늘리는 것에서 불가능한 상황은 없을 것 같고, K가 작을 때는 완전 이진 트리 형태로 구성해야 한다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일단 아래에서부터 합쳐주면서 토너먼트처럼 트리를 구성해주고 나머지 모듈은 K를 1씩 늘리는 데에 사용하는 풀이를 생각했는데, K가 조금만 커지면 사용하지 못한다는 것을 깨닫습니다. 그래서 K가 큰 경우에 사용할 수 있는 다른 풀이를 고안해냈는데, 이 역시 모든 K에 대해 구성할 수 있는 풀이는 아니라는 생각이 들어 방향성을 바꾸기로 합니다. 그러다가 맨 처음에 생각하던 사이클 구조 (빙글빙글 돌면서 긴 거리를 이동하게 만듦)가 꽤 유효하다는 생각이 들었고, 그래서 1부터 N에 순서대로 이동하는 사이클을 만들어 문제에서 주어진 K값에 맞춰지도록 이진 트리의 탐색 결과가 실제 도달해야 하는 값에서 x칸 밀리도록 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 사소한 실수를 해서 1WA를 쌓았지만, 두 번째에 AC를 받았습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[2:43 - 2:55?] D. Mobilint Tensor Scheduling (ARIES)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음으로 스코어보드에서 많이 풀린 문제가 D라서 D를 읽었습니다. 분명히 트리 dp 같긴 한데 어떤 순서로 메모리를 바꾸는 게 이득인지 잘 감이 오지 않았습니다. 그러다가 문제를 풀다가 도중에 갑자기 K번 문제의 지문에 적힌 제한과 실제 데이터의 제한이 다르다는 공지가 떠서 K번을 열어 봤습니다. K번도 D번과 비슷한 개수의 솔브 수가 있었기 때문에 K번을 풀까 고민하다가 D번이 전혀 모르겠어서 K번을 생각해보기로 넘어갑니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[2:55? - 4:30] K. Storing Roll Cake 1WA&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 읽고 조금 고민해보니 자르는 순서와 관계없이 자르는 비용이 일정하다는 것을 관찰했고, O(NK)에 해당하는 dp식이 쉽게 r구해졌습니다. 얼핏 보아하니 CHT를 쓰면 O(N)이나 O(N log N)에 풀 수 있을까 생각해서 cht를 짰는데, K 범위 제한 때문에 제가 아는 기본 CHT로 풀리지 않는다는 사실을 깨달았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 CHT에 대해 여러 블로그를 검색해보다가 결국 풀지 못했습니다. 중간에 그럴듯한 가설을 세우고 그것대로 구현해서 CHT를 제출했는데 장렬하게 WA를 받으면서 풀지 못했습니다. 그리고 마지막에 D번에 아래와 같은 제출을 하나 하고 대회를 마무리했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rPZMU/dJMcacQFoJa/K4NeKek9VOdkKYVlfA3bP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rPZMU/dJMcacQFoJa/K4NeKek9VOdkKYVlfA3bP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rPZMU/dJMcacQFoJa/K4NeKek9VOdkKYVlfA3bP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrPZMU%2FdJMcacQFoJa%2FK4NeKek9VOdkKYVlfA3bP0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;540&quot; height=&quot;166&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코어보드 프리즈 이후로 추가로 푼 문제가 없어서 프리즈 전 17? 18등이라 많이 내려가서 25위 근처일 것이라고 생각했습니다. 그런데 의외로 저뿐만 아니라 다른 분들에게도 문제 셋이 어려웠는지 20등으로 마무리하면서 기가 막히게 네이버페이 2만원을 커트라인에 걸쳐서 사수해냈습니다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5mFnE/dJMcaa6qqEq/VvEiTgpV1SBGjolGupkjzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5mFnE/dJMcaa6qqEq/VvEiTgpV1SBGjolGupkjzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5mFnE/dJMcaa6qqEq/VvEiTgpV1SBGjolGupkjzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5mFnE%2FdJMcaa6qqEq%2FVvEiTgpV1SBGjolGupkjzK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 대회가 끝난 뒤 SCSC 뒷풀이에 참여하지는 않았지만 경곽 학생들(mj1000j, swlee0202, shijun2009)과 lunarlity, dadas08까지 총 여섯 명이서 BBQ에서 치킨을 먹었습니다. 꽤 저렴한 가격에 맛있게 먹은 듯합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xYZji/dJMcacpC64v/lsAnejMyKumKuT8N18Mlmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xYZji/dJMcacpC64v/lsAnejMyKumKuT8N18Mlmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xYZji/dJMcacpC64v/lsAnejMyKumKuT8N18Mlmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxYZji%2FdJMcacpC64v%2FlsAnejMyKumKuT8N18Mlmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4032&quot; height=&quot;3024&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 SCSC div.1을 우승한 dadas08이 프리즈 이후에 다이아 4문제를 슥슥하면서 풀었다길래 mj1000j가 그 비결을 물어봤는데 자꾸 Chinese Bronze라고만 주장해서 슬픕니다. 저에게는 Korean Diamond였는데 말입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 문제를 슥슥할 수 있게 더 다양한 자료구조와 다이아 알고리즘을 공부해야 할 것 같습니다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;021&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/021.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/021.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <category>ICPC</category>
      <category>SCPC</category>
      <category>scsc</category>
      <category>서울대학교</category>
      <category>프로그래밍</category>
      <category>프로그래밍대회</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/13</guid>
      <comments>https://kdy40929.tistory.com/13#entry13comment</comments>
      <pubDate>Mon, 18 May 2026 09:59:13 +0900</pubDate>
    </item>
    <item>
      <title>2026 Kaist Run Spring Contest 후기</title>
      <link>https://kdy40929.tistory.com/12</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 5월 3일 KAIST에서 진행된 2026 Kaist Run Spring Contest에 온사이트로 참가하였다. 백준이 없어져 기록을 남기기도 어려워졌기 때문에 이렇게라도 기록을 남긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 서울에서 출발해야 했기 때문에, 오고가는 열차표를 예매했다. 서울역 -&amp;gt; 대전역 09:33 - 10:42 표와 서대전역 -&amp;gt; 용산역 20:39 - 21:54 표를 예매했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;1318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjOuYR/dJMcabRBNkd/uafvAnVCuK1j0aKWmFNYLk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjOuYR/dJMcabRBNkd/uafvAnVCuK1j0aKWmFNYLk/img.webp&quot; data-alt=&quot;서울역 도착&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjOuYR/dJMcabRBNkd/uafvAnVCuK1j0aKWmFNYLk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjOuYR%2FdJMcabRBNkd%2FuafvAnVCuK1j0aKWmFNYLk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;375&quot; height=&quot;500&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;1318&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;서울역 도착&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서울역에는 꽤 빨리 도착해서 시간이 좀 남았다. 그래서 맛있게 호두과자도 하나 사먹고 출발했다. 맛은 약간 안에 들어있는 팥이 고체보다 액체에 가깝다고나 할까..? 굉장히 물러서 평소에 먹던 것과 느낌이 조금 달랐는데 이것도 나름대로 맛있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/binUwy/dJMcabD5t1C/AuuCEPG3gWd03HuCOYPI0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/binUwy/dJMcabD5t1C/AuuCEPG3gWd03HuCOYPI0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/binUwy/dJMcabD5t1C/AuuCEPG3gWd03HuCOYPI0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbinUwy%2FdJMcabD5t1C%2FAuuCEPG3gWd03HuCOYPI0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;387&quot; height=&quot;516&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;4032&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가는 길에는 얼마 전 재미있게 본 영화인 &amp;lt;프로젝트 헤일메리&amp;gt;의 원작인 책을 조금 읽기도 하고, 리체스에서 래피드 경기도 한 판 뒀다. 대전에 내려서는 시간이 부족하지는 않은 것 같아 택시를 타지 않고 지하철을 탔다. 버스를 타는 것보다는 그냥 걷는 게 낫다고 판단해서 그냥 걸었다. 하지만 이내 비 오는 날 열심히 걷다 보니 왜 다들 대전역에서부터 택시를 타고 다니는지 깨달을 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;1066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HzyLN/dJMcaiQHUhb/X4kOJHKuojfo03QSnfQv40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HzyLN/dJMcaiQHUhb/X4kOJHKuojfo03QSnfQv40/img.png&quot; data-alt=&quot;내 걸음이 꽤 빠르기 때문에 총 걷는 데에는 30분 정도 걸렸다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HzyLN/dJMcaiQHUhb/X4kOJHKuojfo03QSnfQv40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHzyLN%2FdJMcaiQHUhb%2FX4kOJHKuojfo03QSnfQv40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;474&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;1066&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;내 걸음이 꽤 빠르기 때문에 총 걷는 데에는 30분 정도 걸렸다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&amp;nbsp;아무튼 대회장에 들어갔더니 내 자리는 맨 앞 줄이었다. 약간 당황했지만 뭐 자리가 중요한 건 아니다. 내향적인 성격이기도 하고 solved.ac 디스코드에서 본 사람들도 대부분 내가 얼굴은 전혀 모르기 때문에 대체로 조용히 앉아 있었다. 대회장에 일찍 도착한 것도 아니라서 지루하거나 그러진 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대회는 BIKO로 진행됐다. BOJ와는 다르지만, NYPC나 KOI 환경과 대체로 비슷했기 때문에 적응에 큰 어려움은 없었다. 하지만 가장 큰 충격이었던 것은 실시간 스코어보드를 유저 화면에서 볼 수 없다는 것이었다. 그래서 운영진 분들이&lt;b&gt; Zoom을 통해서 스코어보드를 공유해주셨다...!&amp;nbsp;&lt;/b&gt;새삼 백준이 참 좋은 플랫폼이라는 걸 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무튼 대회가 시작되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[0:00 - 0:12]&amp;nbsp; A. Second Run (AC, 100)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 9문제이고 난이도순으로 정렬되어 있다는 정보가 있었기 때문에 당연히 A번부터 봤다. (지금 생각해보면 C나 D 정도 문제 먼저 잡고 퍼솔을 노리는 쪽이 좋았을 것 같기도 하다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 대회 첫 문제는 문제 지문을 읽자마자 어떻게 풀지 생각이 나서 바로 코드를 구현하러 가야 하는데, 생각이 나지 않았다. 약간 코드포스 div1 A번을 열었는데 어, 이게 뭐지? 하는 느낌이 들었다. M/4나 4개의 음표 같은 부분에서 4가 중복되는 게 비둘기집의 원리를 써야 할 것 같은 느낌이 들었는데, 이것저것 아이디어를 내봤지만 잘 안 됐다. 스코어보드에는 하나둘 첫 문제를 맞힌 사람들이 생겨서 분명히 간단한 풀이가 존재할 것이라는 믿음을 가졌지만 결국 결정론적인 풀이는 찾지 못했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보니 랜덤으로 수열을 만들어도 기댓값이 $M/4$라는 생각이 들어 랜덤을 짜도 충분히 풀 수 있겠다는 생각이 들었다. 그래서 50번 랜덤으로 수열을 만들어서 성립하는지 검사하는 코드를 짰고, 처음에는 M = 0일 때 검사 코드가 잘못되어 있어서 1WA를 받았지만, 금방 틀린 부분을 찾고 12분만에 첫 AC를 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 BOJ가 서비스 종료했지만, solved.ac 기준으로 난이도를 매긴다면 G5를 줄 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[0:12 - 0:40] B. RUN Sequence (AC, 200)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 지문 자체는 굉장히 간단하다. $a_1$과 $a_2$의 범위가 각각 $1 \le a_1 \le R$과 $1 \le a_2 \le U$로 제한되어 있고 피보나치 점화식을 만족하는 수열 {$a_n$}에 대해 $a_N$으로 가능한 값의 개수를 구하는 문제였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 $a_N$의 상한과 하한을 구하는 방향성으로 접근했고, 예제 2에서 상한과 하한 사이 모든 값이 가능하다는 점을 관찰해서, 이를 바탕으로 아이디어를 전개하고 있었는데, 생각해보니 $N$이 매우 커져도 가능한 수열의 개수가 $RU$를 넘지 않는다는 것을 깨닫고 다른 방향으로 접근을 시도했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$N$의 범위가 굉장히 커서 분할 정복을 이용한 거듭제곱을 활용하는 문제라고 생각해서 일단 피보나치 수를 구할 때 활용하는 행렬 [[1, 1], [1, 0]]을 가지고 열심히 식 정리를 했고, $a_N = f_{N-1} U + f_{N-2} R$이라는 식을 얻었다. 이때 인접한 피보나치 수는 항상 서로소이므로 서로 다른 $(R, U)$ 쌍에서 같은 $a_N$이 나오려면 $(R, U) = (a + k f_{N-1}, b - k f_{N-2})$와 같이 되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 $N \le 50$만 되어도 항상 답은 $RU$이고, $N$이 이것보다 작을 때는 $RU$에서 중복되는 개수를 빼주면 된다. 따라서 정답은 꽤 간단한 꼴로 표현되고, $RU - \max(0, R-f_{N-1}) \times \max(0, U-f_{N-2})$이다. 처음에 max 함수가 들어간다는 것을 생각을 못해서 틀렸는데, 여기서 50점을 받았다. $R$ 값과 $U$ 값이 작은 데이터를 넣어봐도 max 함수를 넣지 않아서 틀리는 케이스들이 있었어서, 이건 서브태스크 데이터가 약했던 것 같다. 꽤 긴 시간이 소요되었지만, 스코어보드를 보니 B를 푼 사람이 그렇게 많지는 않아서 나만 어려운 건 아니구나 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;solved.ac 기준 난이도는 P5를 기여할 것 같다. $a_N$ 식을 물론 작은 $N$에 대해 몇 개 구해보고 찍는 것도 쉽지만, 저 식을 얻는 과정뿐 아니라 중복되는 값을 세는 부분도 그렇게 쉽지는 않았다고 생각한다. 그래도 문제는 굉장히 좋았다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[0:40 - 0:56] D. Grilled Bottle (AC, 300)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B를 풀고 나서 스코어보드를 보니 D가 C보다 훨씬 많이 풀려 있어서 C 대신 D로 넘어갔다. 아마 어떤 분이 13분만에 D 퍼솔을 드셔서 그랬던 것 다들 D로 가셨던 것 같긴 하지만, 그래도 D가 쉬울 거라고 생각해서 일단 넘어갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 읽고 나니 우큐 그리디의 느낌이 물씬 났다. 느낌 그대로 파라매트릭 서치를 통해서 구울 수 있는 병의 개수를 구하고, 개수가 정해지면 병들을 필요한 최소 화력이 큰 순서대로 정렬하고 사용 가능한 마법 중에서 만족도가 큰 것을 하나씩 배정하는 그리디로 풀 수 있었다. 이것도 $N$과 $M$을 착각해서 한 번 WA를 받았는데, RTE라 금방 오류를 찾을 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 류의 우선순위 큐 그리디 문제들이 P5 정도에 배치되어 있는데, 여기에 파라매트릭 서치를 얹어야 하는 문제라 P4 정도 난이도인 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[0:56 - 1:24] C. Dubai Chewy Cookie (AC, 400)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 그래프에서 확률을 계산하는 쿼리가 있는 문제인데, 다행히 제일 처음 떠올린 dp의 시간복잡도가 $O(N^2 + NQ)$로 문제의 제한이 너무 잘 맞아떨어져서 어렵지 않게 풀 수 있었다. 모든 쿼리에서 1, 2번 가게에 대해서만 생각하므로 나머지 가게들은 1번과만 연결된 가게, 2번과만 연결된 가게, 그리고 둘 다와 연결된 가게로 나누고, 1 - 2 간선이 있는지 없는지에 따라서 나눠서 생각하면 어렵지 않게 풀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 세 그룹으로 가게를 나눈 후 각 그룹에서 dp[i]를 $i$개 가게가 두쫀쿠를 만들 확률로 하여 냅색 dp를 돌리면 확률을 구할 수 있으므로 각 쿼리는 1번 가게와 2번 가게 각각에서 두쫀쿠 재료를 구했는지 여부와 1, 2번과 모두 연결된 가게에서 몇 개의 두쫀쿠를 만들었는지로 구분하면 최대 $4N$개 케이스로 구분되므로 $O(N)$에 각 쿼리를 처리할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제 역시 P5 정도 되는 것 같다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[1:24 - 1:53] E. Kirameki of Revue (AC, 500)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4솔을 하고 나니 10위권 근처에 있어 꽤 괜찮은 성적이라는 생각을 하면서 5번을 봤다. 문제를 읽고 예제를 제대로 이해한 게 맞는지 확인하기 위해 $O(N^2)$ 나이브를 짜서 예제를 돌려보았다. 그래서 문제를 잘 이해했음을 확인했고, 그 김에 그냥 바로 제출해서 1:27 시점에 5점을 획득했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 $K$번째 값을 구하는 문제는 $X$ 이하인 값의 개수를 세는 이분 탐색을 통해서 해결하는 경우가 많으므로 여기서도 비슷한 방식으로 생각했고, 최상위 비트부터 정하면서 fenwick tree에서 $k$번째 값을 찾는 쿼리처럼 비슷하게 진행하려고 했다. 그랬더니 주어진 수들로 Binary Trie를 구성하고, 여기서 적당히 값을 계산하면 전체 계산량이 Trie 전체 노드 수이므로 충분히 풀 수 있다는 판단이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 Binary Trie 구현을 팀노트에 가져오긴 했지만, trie가 내게 여전히 익숙하지 않은 자료구조라서 조금 더 쉽게 구현할 수 있는 방법을 고민했다. 어차피 이 문제에서는 Trie를 따라서 상하 노드로 움직일 필요가 없고, Level 단위로 순회하게 된다는 것에 착안해서 그냥 dict를 30개 만들어서 각 층에 대한 정보를 저장시켰다. 그랬더니 구현이 훨씬 깔끔해지고 코드도 짧아진 것 같다. 그래서 자랑 겸 코드도 한 번 올려본다. &lt;s&gt;처음에는 dict 대신 set만 있으면 되는 줄 알았어서 변수명이 setlst이다&lt;/s&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777907474932&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline

n, k = map(int, input().split())
arr = [*map(int, input().split())]
setlst = [dict() for _ in range(30)]
for i in range(n):
    v = arr[i]
    for j in range(29, -1, -1):
        if v in setlst[j]: setlst[j][v] += 1
        else: setlst[j][v] = 1
        v &amp;gt;&amp;gt;= 1
ans = 0
for j in range(30):
    ans &amp;lt;&amp;lt;= 1
    tmp1, tmp2 = 0, 0
    for key, val in setlst[j].items():
        if ans &amp;gt; 0 and key ^ ans in setlst[j]:
            tmp1 += val * setlst[j][key ^ ans]
        elif ans == 0:
            tmp2 += val * (val-1) // 2
    tmp = tmp1//2 + tmp2
    if tmp &amp;lt; k:
        ans += 1
        k -= tmp
print(ans)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난이도는 두 수 XOR 최댓값을 찾는 문제가 P3인데 이보다 분명히 상위호환 문제이므로 P2 정도라고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[1:53 - 2:56] F. Pragmatism (PAC, 510)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;E번을 풀고 나면 F번을 푼 사람이 최소 너댓 명은 될 줄 알았는데, 아마 기억상 F번을 푼 사람이 1명뿐이었던 것 같다. 그래서 꽤 어려운 문제라는 생각을 했다. 하지만 내가 E를 dict를 사용하는 아이디어로 상당히 구현을 빨리 끝내서 그런 것일 수도 있겠다는 생각도 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 자체는 마치 램지 수를 연상시키는 듯한 느낌이었다. $N, K$가 주어질 때 $N-2K+2$ 길이의 path를 찾거나, 둘 사이에 간선이 전혀 존재하지 않는 크기가 $K$인 정점 집합 2개를 찾아야 하고, 둘 중 하나 이상이 반드시 존재함은 보장된다고 문제에서 주어졌다. 마치 정점 수가 얼마 이상이면 m-clique이 존재하거나 n-independent set이 존재한다 뭐 이런 얘기를 하는 램지 이론과 비슷한 느낌이지 않을까 하고 램지 수의 상한 점화식 증명과 비슷한 방향으로 계속 생각했다. 하지만 잘 풀리지 않았고, 아 여기부터 다이아 이상의 문제들인가 하는 생각도 들면서 서브태스크를 긁는 쪽으로 방향을 틀어야겠다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일단 뒤쪽 문제를 읽으러 가기 전에 가장 쉬운 서브태스크인 $K = 1$만 후딱 구현해서 제출하고 10점을 챙긴 후 뒤 문제로 넘어갔다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[2:56 - 3:23] H. Hawaiki (PAC, 540)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연히 G번을 먼저 열어보았지만 G번 문제가 인터랙티브이고 수상할 정도로 복잡한 수식이 적혀 있는 것을 보고 H번으로 넘어왔다. 어차피 풀 태스크는 분명히 다이아 이상일 거라 서브태스크부터 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브태스크 중 2번과 3번에 기울기 조건이 붙어 있었는데, 이 기울기 조건이 있으면 $u_i &amp;lt; u_j &amp;lt; v_i &amp;lt; v_j$인 $(i, j)$ 쌍이 없으므로 stack을 이용해서 예쁘게 처리할 수 있다. &lt;a href=&quot;https://www.acmicpc.net/problem/33814&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/33814&lt;/a&gt; (비록 지금은 섭종으로 인해 볼 수 없지만)를 입부시험을 검수하면서 풀어본 적이 있는데, 이때 스택을 이용해서 활용했던 전략이 유효하게 먹혀서 28점을 얻을 수 있었다. 그리고 의도치 않았지만 서브태스크 1의 2점도 같이 획득해서 총 30점을 획득했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[3:23 - 3:32] I. Wizdomiot (PAC, 582)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코어보드를 봤더니 마지막 문제에 42점이 굉장히 많길래 마지막 문제의 서브태스크를 긁으러 마지막 문제로 이동했다. 42점을 계산해보니 $N \le 2^{10}$ 서브태스크까지 해결해야 했는데, 가장 직관적으로 떠오르는 풀이는 $O(N^3)$이었다. 아무리 4초긴 해도 파이썬이라 $N \le 1024$에서 $O(N^3)$을 4초 안에 돌릴 수 있을지 걱정이 조금 들었는데, 코드를 짜고 제출해보니 42점이 받아졌다. 그래서 재빠르게 9분만에 42점을 긁고 다른 문제로 이동했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[3:32 - 3:47] F. Pragmatism (PAC, 602)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코어보드에서 F 30점이 많길래 F 30점 코드를 구현하러 갔다. 30점을 받기 위해서는 $N \le 10$ 서브태스크를 해결해야 하는데, $N$이 매우 작으므로 그냥 열심히 빛의 신전으로 가능한 모든 조합과 대립의 신전으로 가능한 모든 조합을 구하면 해결할 수 있으므로 구현 자체에 시간이 엄청 오래 걸리진 않았다. 아까부터 계속 고민하던 아이디어가 있기도 했고 남은 문제 중 가장 앞쪽에 배치되어 있으니 난이도가 그나마 제일 낮을 거라는 기대가 있었지만 서브태스크 3조차 가능성이 없어 보여서 그냥 이 문제를 버리기로 했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[3:47 - 4:20] H. Hawaiki (PAC, 602)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H에서 최대 유량을 잘 활용하면 $N \le 300$을 풀 수 있을 것 같아서 이것을 풀려고 갔다. 최대 유량은 최소 컷의 값을 구하는 게 핵심이지만, 이 문제에서는 최대 컷 위치를 찾고 그 값에 맞추어서 나머지 위치에 항해사 수를 배정해야 하므로 이걸 음수로 돌려서 생각하려고 했다. 그러기 위해서는 모든 경로에 엄청나게 많은 수의 항해사를 배치해놓고 (배치된 항해사 수) - (요구하는 항해사 수)를 capacity로 잡고 최대 유량을 돌린 후, 구해진 유량 값만큼 배치된 항해사 수를 다시 줄이는 방향으로 풀려고 시도했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 무엇이 문제인지 모르는 채로 계속 서브태스크 4가 풀리지 않아서 결국 아직 손을 대지 못했던 G번을 보기로 했다. 올라온 해설을 보니 내 접근 방향이 올바른 방향이었는데, 왜 틀렸는지는 아직도 전혀 미지수이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[4:20 - 4:45] G. AiScReam (PAC, 632)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 보고 서브태스크를 읽으니 일단 $k \le 5$ 서브태스크까지 풀 수 있으면 아주 좋을 것 같아서 $k \le 5$인 경우에 대한 풀이를 떠올려봤는데 전혀 감이 오지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 그냥 깔끔하게 포기하고 위쪽 서브태스크 3개만 맞자는 심정으로 코드를 구현했다. 서브태스크 2와 3은 사람에 따른 아이스크림 맛의 선호도 차이가 없으므로 그냥 1 2 3 ... N 순서대로 배치해도 된다. 따라서 필요한 분할 위치만 1번 쿼리로 찾아주는 코드를 짜서 4:42 시점에 24점을 획득했다. 처음에 $2^30$과 $2^40$ 등 각종 지수를 잘못 봐서 스케일링이 잘못되어 계속 1번째 테스트케이스에서 틀리는 참사가 있었는데, 다행히 고쳤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 $k = 1$인 경우는 1, 2 순서와 2, 1 순서만 고려하면 되므로 어렵지 않게 짤 수 있었다. 그래서 4:45 시점에 30점을 획득했다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[4:45 - 5:00] I. Wizdomiot&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 더 이상 점수를 획득할 수 있는 시간이 안 될 것 같아 내 제출 기록을 보면서 제출 시각을 기록하고 간식을 먹으면서 쉬고 있었는데, 스코어보드에 I 50점을 받으신 분들이 몇 분 보이길래 $O(N^3)$을 잘 깎으면 $N \le 2048$까지 풀 수 있나 싶어서 C++로 코드를 변환해서 제출해보려고 했다. 최대한 빠른 속도로 Python 코드를 C++로 변환하고 마지막 2초인가를 남기고 코드를 제출했는데 아쉽게 C++ 코드가 컴파일이 안 되어서 점수를 얻지는 못하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 대회가 마무리가 되었고, 최종 결과는 632점으로 전체 10등이었다. 지난 Semi-Game Cup보다 인원수도 많고, 그 당시 14등이고 P1이었던 E번을 못 풀어서 플래 올솔을 달성하지 못해 약간 아쉬움이 남았었는데, 이번에는 플래티넘 문제까지 모두 풀어냈고 서브태스크도 알차게 긁으면서 꽤 높은 점수를 받은 것 같아 만족스러웠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;1318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qGdGQ/dJMcabD5vqw/86KwJXnpIrfXmd6qKnNlEk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qGdGQ/dJMcabD5vqw/86KwJXnpIrfXmd6qKnNlEk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qGdGQ/dJMcabD5vqw/86KwJXnpIrfXmd6qKnNlEk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqGdGQ%2FdJMcabD5vqw%2F86KwJXnpIrfXmd6qKnNlEk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;629&quot; height=&quot;472&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;1318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 순위상도 받지 못했고, 특별상도 받지 못했지만 레드만 10명 가까이 참가한 것으로 알고 있는데 그 사이에서 10등을 차지했고, &lt;s&gt;나보다 코드포스 레이팅이 높은데 이번에 나보다 못 보신 분들도 보여서&lt;/s&gt; 나름대로 잘 친 대회라고 생각한다. 그리고 모든 문제에서 꽤 서브태스크를 긁어서 점수를 벌었기 때문에 이 점도 뿌듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대회가 모두 끝나고 나서 풀이 세션과 시상 세션이 진행되었다. 백준에서 진행된 것이 아니라서 스코어보드를 아래에서부터 한 명씩 공개하면서 위로 올라가는 방식으로 스코어보드 공개를 할 수 없었다. 그래서 그냥 스크롤을 이용해서 수동으로 스코어보드 공개를 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;풀이 세션에서 운영진 분들이 문제에 대해 열성적으로 해설하시느라 시간이 꽤 많이 지체되었는데, 시상 때에 가중치 랜덤 상과 랜덤 특별상을 추첨하려고 하는데 스크린 미러링?이 잘 안 되어서 운영진들이 결국 이걸 Zoom 화면 공유를 이용해서 해결한 것으로 보인다. 이 과정에서 예상 종료 시각이었던 7시보다 30분 넘게 늦어져서 하마터면 열차 시각 때문에 저녁을 못 먹을 뻔했다...ㅋㅋ (다행히 카이스트를 다니는 친구의 도움으로 다 식은 치킨을 먹을 수 있었다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시 내가 나중에 오프라인 대회를 운영할 일이 있다면 계획표를 조금 여유 있게 짜놓는 게 좋을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 대회 성적이 꽤 만족스러웠는데, 다음 주에 있을 KOI 1차와 다다음 주에 있을 SCSC에서도 좋은 성적 거둘 수 있었으면 좋겠다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;004&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/004.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/axz_keditor/emoticon/friends1/large/004.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/12</guid>
      <comments>https://kdy40929.tistory.com/12#entry12comment</comments>
      <pubDate>Tue, 5 May 2026 00:55:14 +0900</pubDate>
    </item>
    <item>
      <title>PS 문자열 알고리즘 - 8. 1D Trie 구현</title>
      <link>https://kdy40929.tistory.com/11</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 독자가 trie 자료 구조에 대해 이미 알고 있다는 전제 하에서 시작한다. trie라는 자료 구조를 모른다면 이 글을 읽기 전에 다른 블로그를 읽고 &lt;a href=&quot;https://www.acmicpc.net/problem/14725&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/14725&lt;/a&gt;와 같은 문제를 해결해본 후 이 글을 읽는 것을 권장한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Trie라는 자료 구조를 일반적으로 구현할 때에는 Node 클래스를 만들고 여기에 딕셔너리 혹은 리스트가 들어가서 여기에 다시 Node가 저장되는 형태로 구현한다. 하지만 이렇게 리스트나 딕셔너리를 많이 만들 경우 메모리 초과가 나기 십상이며, 접근 자체에도 긴 시간이 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이를 개선하는 대표적인 방법으로, Trie 자료 구조를 1차원 배열을 이용해서 구현하는 방식이 있다. 우리가 일반적으로 알고 있는 트라이의 구조에 인덱스를 매겨서 그림으로 그려보면 아래 그림과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;1158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1ucV3/btsPRyzAa6a/jjyKrkrgCK709iNLJl3980/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1ucV3/btsPRyzAa6a/jjyKrkrgCK709iNLJl3980/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1ucV3/btsPRyzAa6a/jjyKrkrgCK709iNLJl3980/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1ucV3%2FbtsPRyzAa6a%2FjjyKrkrgCK709iNLJl3980%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;590&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;1158&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽에 적은 대로 HE, SHE, SO, SHY라는 문자열을 순서대로 저장한 트라이이다. 이때 파란색 숫자와 같이 루트 노드를 0으로 한 후 노드가 생길 때마다 1씩 증가하면서 번호를 붙여주고, 번호가 $k$인 노드의 자식 노드들에 접근하기 위한 인덱스 $c$는 1차원 리스트 내에서 $26k \le c \le 26k + 25$에 속하게 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 26은 알파벳 대문자의 개수, 즉 가능한 알파벳(character)의 개수를 의미한다. 또한 리스트의 인덱스가 초과되면 안되므로 1차원 리스트는 알파벳의 개수와 노드의 최대 개수를 곱한 만큼 필요하고, 노드의 최대 개수는 다시 추가할 문자열의 수와 문자열의 길이의 최댓값으로 구해야 한다. 또한, A ~ Z 각각을 0에서 25까지의 인덱스에 대응해야 하므로 이를 대응하기 위한 함수도 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 각 노드를 지나간 문자열의 수를 저장하는 cnt 배열이나 해당 노드를 끝으로 하는 문자열이 있는지 저장하는 end 배열 등을 추가로 선언하여 Trie의 init 함수를 구현할 수 있다. 이를 구현한 것이 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1755082161819&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def __init__(self, alpha, leng, maxword, func):
    # alpha: the number of characters
    # max_node: the maximum number of nodes
    # func: function that translates characters to index num
    self.alpha = alpha
    self.maxnode = leng * maxword
    self.trie = [0] * (alpha * self.max_node)
    self.cnt = [0] * self.max_node
    self.end = [0] * self.max_node
    self.num = 0
    self.f = func&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 가장 중요한 insert 함수를 구현할 것이다. 먼저, 구현한 코드를 살펴본 후 이에 대한 해설을 덧붙이겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1755082214809&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def insert(self, word):
    i = 0
    self.cnt[i] += 1
    for ch in word:
        idx = self.f(ch)
        pt = self.alpha * i + idx
        if self.trie[pt] == 0:
            self.num += 1
            self.trie[pt] = self.num
        i = self.trie[pt]
        self.cnt[i] += 1
    self.end[i] += 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$i$라는 변수가 가장 핵심적인 역할을 하는 변수로서 현재 지나고 있는 노드의 번호를 저장하고 있다. 처음에는 루트 노드에서 출발하므로 $i = 0$이며, word의 앞 글자부터 살피며 ch를 idx로 trie 생성 시 만든 함수를 이용해서 변환하고, pt가 다음 자식 노드의 번호를 가리킨다. 만약, 이미 지난 자식 노드가 아닌 경우 self.trie[pt]의 값이 0이 되고, 이때는 self.num을 1 증가시켜서 trie라는 1차원 배열에 26칸의 공간을 추가로 이 노드에 할당해준다. 그렇게 해서 i를 갱신하는 식으로 insert를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;self.cnt[i]는 i값이 바뀔 때마다 1씩 더해주면 되고, self.end[i]는 i가 마지막에 도착했을 때 1을 더해주면 되므로 위와 같이 구현하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 1차원으로 trie를 구현하면 종합적으로 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1755082433509&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Trie:
    def __init__(self, alpha, leng, maxword, func):
        # alpha: the number of characters
        # max_node: the maximum number of nodes
        # func: function that translates characters to index num
        self.alpha = alpha
        self.maxnode = leng * maxword
        self.trie = [0] * (alpha * self.max_node)
        self.cnt = [0] * self.max_node
        self.end = [0] * self.max_node
        self.num = 0
        self.f = func

    def insert(self, word):
        i = 0
        self.cnt[i] += 1
        for ch in word:
            idx = self.f(ch)
            pt = self.alpha * i + idx
            if self.trie[pt] == 0:
                self.num += 1
                self.trie[pt] = self.num
            i = self.trie[pt]
            self.cnt[i] += 1
        self.end[i] += 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용하면 메모리를 효율적으로 사용하면서 트라이 문제를 해결할 수 있다. 트라이는 문자열을 다루는 자료구조를 넘어서 이진수를 비트 단위로 트라이에 저장하는 아이디어를 활용해서 푸는 XOR 문제도 있을 정도로 범용성이 높은 자료구조이므로 1차원으로 구현하는 방법을 알아두면 상당히 편리할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;search prefix count나 search word 등의 함수도 구현해야 할 때가 있지만, 이는 cnt 또는 end 배열을 이용해서 들어가서 탐색하면 되므로 생략하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 다음 글에서는 kmp를 확장하여 일대다 매칭이 가능한 아호 코라식을 살펴보겠다. 트라이 관련 문제를 몇 가지 추천하니 연습이 필요한 독자는 아래 문제들을 해결해보기를 바란다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;BOJ 14725 개미굴&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/14725&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/14725&lt;/a&gt;: 트라이 기본 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;BOJ 13505 두 수 XOR&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/13505&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/13505&lt;/a&gt;: 이진수를 트라이를 이용해 관리하여 풀 수 있는 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;BOJ 28122 아이템&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/28122&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/28122&lt;/a&gt;: 트라이에서 dfs를 돌리는 문제&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;004&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/004.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/004.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/11</guid>
      <comments>https://kdy40929.tistory.com/11#entry11comment</comments>
      <pubDate>Wed, 13 Aug 2025 20:00:17 +0900</pubDate>
    </item>
    <item>
      <title>PS 문자열 알고리즘 - 7. 접미사 배열과 lcp 배열 응용</title>
      <link>https://kdy40929.tistory.com/10</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 지난 글에서 구현하는 방법을 알아본 접미사 배열과 lcp 배열을 어떻게 활용하는지 알아볼 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 풀어보기 전에는 suffix array와 lcp array가 도대체 어떤 의미를 가지나.. 싶기도 한데, 우리는 보통 접두사, 접미사보다도 부분 문자열에 관련된 문제를 많이 해결하게 된다. 이때, 우리가 주목해서 기억해야 하는 것은 &lt;b&gt;부분 문자열은 결국 접미사의 접두사&lt;/b&gt;라는 것이다. 그러면 suffix array를 구했고, suffix array에서 lcp를 구한 것은 공통된 접미사의 접두사, 즉 공통된 부분 문자열에 관한 정보를 얻었음을 의미한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 1605 반복 부분문자열&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/1605&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1605&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 지난번 라빈 카프를 설명했을 때에도 다루었던 문제로, 부분 문자열 내에서 최대 길이를 갖는 공통된 문자열을 찾는 문제이다. lcp 배열 각각은 공통된 접미사의 접두사, 즉 공통된 부분 문자열의 길이를 담고 있으므로, lcp 배열의 최댓값이 최대 길이의 공통된 부분 문자열이 된다. 이는 조금만 생각해보아도 자명하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, lcp 배열의 크기를 n-1로 설정했기 때문에 n = 1인 경우 lcp 배열이 비어 있게 된다. 따라서 이때는 그냥 0을 출력하도록 예외처리만 조심하면 된다. 코드는 아래와 같다. 라빈 카프보다 전체 길이는 더 길지만 suffix array와 lcp array를 구하고 나면 바로 값을 얻을 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1755002837954&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline
from bisect import bisect_left

def count_sort(arr, key, k):
    # count sorting by key[idx], range: 0 - k
    n = len(arr)
    cnt = [0] * (k+1)
    for val in arr:
        cnt[key[val]] += 1
    pfs = [0] * (k+1)
    for i in range(k):
        pfs[i+1] = pfs[i] + cnt[i]
    out = [0] * n
    for val in arr:
        out[pfs[key[val]]] = val
        pfs[key[val]] += 1
    return out

def make_suffix(s):
    n = len(s)
    if n == 0: return []
    sa = [*range(n)]
    uniq = []
    for ch in sorted(s):
        if not uniq or ch != uniq[-1]:
            uniq.append(ch)
    rank = [bisect_left(uniq, ch) for ch in s]
    k = 1
    while k &amp;lt; n:
        key1 = [r+1 for r in rank]
        key2 = [rank[i+k]+1 if i+k &amp;lt; n else 0 for i in range(n)]
        sa = count_sort(sa, key2, max(key2))
        sa = count_sort(sa, key1, max(key1))
        cnt = 0
        new_rank = [0] * n
        new_rank[sa[0]] = 0
        for i in range(1, n):
            a, b = sa[i], sa[i-1]
            ra, rb = rank[a], rank[b]
            rka = rank[a+k] if a+k &amp;lt; n else -1
            rkb = rank[b+k] if b+k &amp;lt; n else -1
            if ra != rb or rka != rkb:
                cnt += 1
            new_rank[a] = cnt
        rank = new_rank[:]
        if cnt == n-1: break
        k *= 2
    return sa

def makelcp(s, sa):
    n = len(s)
    if n &amp;lt;= 1: return []
    rev = [0] * n
    for i, p in enumerate(sa):
        rev[p] = i
    lcp = [0] * (n-1)
    prvl = 0
    for i in range(n):
        r = rev[i]
        if r == 0: prvl = 0; continue
        j = sa[r-1]
        while i+prvl &amp;lt; n and j+prvl &amp;lt; n and s[i+prvl] == s[j+prvl]:
            prvl += 1
        lcp[r-1] = prvl
        if prvl: prvl -= 1
    return lcp

n = int(input())
s = input().rstrip()
if n &amp;gt; 1: print(max(makelcp(s, make_suffix(s))))
else: print(0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 9249 최장 공통 부분 문자열&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/9249&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/9249&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 앞서 풀어본 문제와 약간 다른데, 공통 부분 문자열을 같은 문자열 내에서 찾는 것이 아니라 다른 문자열 내에서 찾는 것이다. 이 경우에도 여전히 라빈 카프 알고리즘과 매개변수 탐색을 이용한 풀이가 가능하지만, 여기서는 접미사 배열과 lcp 배열을 활용해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제 역시 같은 문자열 내에서 반복되는 문자열을 찾는 문제로 환원하고 싶으므로 두 문자열을 연결하고 싶은 욕망이 생긴다. 그런데 두 가지 문제점이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 두 문자열을 이어붙이면서 생기는 부분 문자열 (즉, 두 문자열에 걸쳐 있는 부분 문자열)을 어떻게 제거할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 같은 문자열에서 여러 번 반복되는 문자열을 어떻게 제거할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 1번 문제는 두 문자열을 이어붙일 때 두 문자열 모두에서 사용하지 않는 문자 (ex: '#')를 끼워넣음으로서 방지할 수 있다. 2번 문제는 그냥 suffix array 상에서 인접한 두 개를 반복해서 확인하되 같은 문자열에서 온 값이라면 배제하는 방식으로 풀 수 있다. 왜일까? 두 문자열 $A$, $B$가 있고 이의 최대 공통 부분 문자열이 $C$라고 하자. 그런데 $C$가 suffix array 상에서 인접하지 않는다고 가정하면 suffix array 상에서 $A$에서 유래한 $C$로 시작하는 접미사와 $B$에서 유래한 $C$로 시작하는 접미사 사이에 다른 문자열이 존재해야 한다. 그러면 이 문자열 역시 $C$라는 문자열을 접두사로 가지는 접미사여야 하고, 그러면 이는 1번 문제를 제거했으므로 여전히 $A$와 $B$ 중 하나에 속한 문자열이면서 $C$를 문자열로 가지는 접두사이다. 따라서 $C$가 최대 공통 부분 문자열이라는 조건에 모순이 되거나 결국 언젠가는 인접하면서 공통 부분 문자열이 $C$가 되는 경우가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이를 구현하면 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1755004939141&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
from bisect import bisect_left
input = sys.stdin.readline

def count_sort(arr, key, k):
    # count sorting by key[idx], range: 0 - k
    n = len(arr)
    cnt = [0] * (k+1)
    for val in arr:
        cnt[key[val]] += 1
    pfs = [0] * (k+1)
    for i in range(k):
        pfs[i+1] = pfs[i] + cnt[i]
    out = [0] * n
    for val in arr:
        out[pfs[key[val]]] = val
        pfs[key[val]] += 1
    return out

def make_suffix(s):
    n = len(s)
    if n == 0: return []
    sa = [*range(n)]
    uniq = []
    for ch in sorted(s):
        if not uniq or ch != uniq[-1]:
            uniq.append(ch)
    rank = [bisect_left(uniq, ch) for ch in s]
    k = 1
    while k &amp;lt; n:
        key1 = [r+1 for r in rank]
        key2 = [rank[i+k]+1 if i+k &amp;lt; n else 0 for i in range(n)]
        sa = count_sort(sa, key2, max(key2))
        sa = count_sort(sa, key1, max(key1))
        cnt = 0
        new_rank = [0] * n
        new_rank[sa[0]] = 0
        for i in range(1, n):
            a, b = sa[i], sa[i-1]
            ra, rb = rank[a], rank[b]
            rka = rank[a+k] if a+k &amp;lt; n else -1
            rkb = rank[b+k] if b+k &amp;lt; n else -1
            if ra != rb or rka != rkb:
                cnt += 1
            new_rank[a] = cnt
        rank = new_rank[:]
        if cnt == n-1: break
        k *= 2
    return sa

def makelcp(s, sa):
    n = len(s)
    if n &amp;lt;= 1: return []
    rev = [0] * n
    for i, p in enumerate(sa):
        rev[p] = i
    lcp = [0] * (n-1)
    prvl = 0
    for i in range(n):
        r = rev[i]
        if r == 0: prvl = 0; continue
        j = sa[r-1]
        while i+prvl &amp;lt; n and j+prvl &amp;lt; n and s[i+prvl] == s[j+prvl]:
            prvl += 1
        lcp[r-1] = prvl
        if prvl: prvl -= 1
    return lcp

s1 = input().rstrip()
s2 = input().rstrip()
s = s1 + '#' + s2
sa = make_suffix(s)
lcp = makelcp(s, sa)
ans = ''
for i in range(1, len(sa)):
    if (sa[i] - len(s1)) * (sa[i-1] - len(s1)) &amp;lt; 0:
        if lcp[i-1] &amp;gt; len(ans):
            ans = s[sa[i]:sa[i]+lcp[i-1]]
print(len(ans))
print(ans)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 11479 서로 다른 부분 문자열의 개수 2&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/11479&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/11479&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 제목 그대로 서로 다른 부분 문자열의 개수를 구하는 문제이다. 이 역시 공통된 부분 문자열에 대한 정보를 lcp 배열에서 얻을 수 있다는 점을 생각하면 쉽게 풀 수 있는데, lcp 배열의 한 값이 3이라는 것의 의미는 연속한 suffix 2개는 길이 3의 공통 접두사를 가지므로 부분 문자열이 3번 중복됨을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 중복되는 문자열의 전체 개수는 lcp 배열의 전체 총합과 같게 되며, 이를 코드로 구현하면 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1755008229755&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from bisect import bisect_left

def count_sort(arr, key, k):
    # count sorting by key[idx], range: 0 - k
    n = len(arr)
    cnt = [0] * (k+1)
    for val in arr:
        cnt[key[val]] += 1
    pfs = [0] * (k+1)
    for i in range(k):
        pfs[i+1] = pfs[i] + cnt[i]
    out = [0] * n
    for val in arr:
        out[pfs[key[val]]] = val
        pfs[key[val]] += 1
    return out

def make_suffix(s):
    n = len(s)
    if n == 0: return []
    sa = [*range(n)]
    uniq = []
    for ch in sorted(s):
        if not uniq or ch != uniq[-1]:
            uniq.append(ch)
    rank = [bisect_left(uniq, ch) for ch in s]
    k = 1
    while k &amp;lt; n:
        key1 = [r+1 for r in rank]
        key2 = [rank[i+k]+1 if i+k &amp;lt; n else 0 for i in range(n)]
        sa = count_sort(sa, key2, max(key2))
        sa = count_sort(sa, key1, max(key1))
        cnt = 0
        new_rank = [0] * n
        new_rank[sa[0]] = 0
        for i in range(1, n):
            a, b = sa[i], sa[i-1]
            ra, rb = rank[a], rank[b]
            rka = rank[a+k] if a+k &amp;lt; n else -1
            rkb = rank[b+k] if b+k &amp;lt; n else -1
            if ra != rb or rka != rkb:
                cnt += 1
            new_rank[a] = cnt
        rank = new_rank[:]
        if cnt == n-1: break
        k *= 2
    return sa

def makelcp(s, sa):
    n = len(s)
    if n &amp;lt;= 1: return []
    rev = [0] * n
    for i, p in enumerate(sa):
        rev[p] = i
    lcp = [0] * (n-1)
    prvl = 0
    for i in range(n):
        r = rev[i]
        if r == 0: prvl = 0; continue
        j = sa[r-1]
        while i+prvl &amp;lt; n and j+prvl &amp;lt; n and s[i+prvl] == s[j+prvl]:
            prvl += 1
        lcp[r-1] = prvl
        if prvl: prvl -= 1
    return lcp

s = input().rstrip()
n = len(s)
print(n*(n+1)//2 - sum(makelcp(s, make_suffix(s))))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 10413 반복되는 부분 문자열&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/10413&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/10413&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 반복되는 부분 문자열의 개수를 중복을 허락하지 않고 세는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 문자열 ABRACADABRA의 lcp 배열을 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1755008937335&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;                lcp
          A 10   X
       ABRA 7    1  (A, ABRA)
ABRACADABRA 0    4  (ABRA, ABRACADABRA)
   ACADABRA 3    1  (ABRACADABRA, ACADABRA)
     ADABRA 5    1  (ACADABRA, ADABRA)
        BRA 8    0  (ADABRA, BRA)
 BRACADABRA 1    3  (BRA, BRACADABRA)
    CADABRA 4    0  (BRACADABRA, CADABRA)
      DABRA 6    0  (CADABRA, DABRA)
         RA 9    0  (DABRA, RA)
  RACADABRA 2    2  (RA, RACADABRA)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복되는 부분 문자열은 lcp 배열의 값이 3인 경우 그 위치에서 3만큼 생긴다. 하지만, 여기서 반복되는 부분 문자열에서 중복을 고려해야 한다는 점이 큰 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A에서 ABRA로 넘어갈 때 $lcp[0] = 1$이고, 여기서 중복되는 문자열은 A이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ABRA에서 ABRACADABRA로 넘어갈 때 $lcp[1] = 4$이고, 여기서 중복되는 문자열은 A, AB, ABR, ABRA 4개인데, A는 이미 센 문자열이므로 여기서 추가되는 문자열의 수는 $lcp[1] - lcp[0]$이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ABRACADABRA에서 ACADABRA로 넘어갈 때 $lcp[2] = 1$인데, 여기서 중복되는 문자열은 A이고 이미 세었으므로 추가되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 과정을 일반화하면, $lcp[i-1] &amp;lt; lcp[i]$인 경우 $lcp[i] - lcp[i-1]$개의 부분 문자열이 새롭게 반복되는 부분 문자열이고, $lcp[i-1] \ge lcp[i]$인 경우 $lcp[i]$에서 중복되는 부분 문자열은 $lcp[i-1]$에서 이미 중복된 부분 문자열이므로 더하지 않아도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래와 같은 코드로 문제를 해결할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1755010028434&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline
from bisect import bisect_left

def count_sort(arr, key, k):
    # count sorting by key[idx], range: 0 - k
    n = len(arr)
    cnt = [0] * (k+1)
    for val in arr:
        cnt[key[val]] += 1
    pfs = [0] * (k+1)
    for i in range(k):
        pfs[i+1] = pfs[i] + cnt[i]
    out = [0] * n
    for val in arr:
        out[pfs[key[val]]] = val
        pfs[key[val]] += 1
    return out

def make_suffix(s):
    n = len(s)
    if n == 0: return []
    sa = [*range(n)]
    uniq = []
    for ch in sorted(s):
        if not uniq or ch != uniq[-1]:
            uniq.append(ch)
    rank = [bisect_left(uniq, ch) for ch in s]
    k = 1
    while k &amp;lt; n:
        key1 = [r+1 for r in rank]
        key2 = [rank[i+k]+1 if i+k &amp;lt; n else 0 for i in range(n)]
        sa = count_sort(sa, key2, max(key2))
        sa = count_sort(sa, key1, max(key1))
        cnt = 0
        new_rank = [0] * n
        new_rank[sa[0]] = 0
        for i in range(1, n):
            a, b = sa[i], sa[i-1]
            ra, rb = rank[a], rank[b]
            rka = rank[a+k] if a+k &amp;lt; n else -1
            rkb = rank[b+k] if b+k &amp;lt; n else -1
            if ra != rb or rka != rkb:
                cnt += 1
            new_rank[a] = cnt
        rank = new_rank[:]
        if cnt == n-1: break
        k *= 2
    return sa

def makelcp(s, sa):
    n = len(s)
    if n &amp;lt;= 1: return []
    rev = [0] * n
    for i, p in enumerate(sa):
        rev[p] = i
    lcp = [0] * (n-1)
    prvl = 0
    for i in range(n):
        r = rev[i]
        if r == 0: prvl = 0; continue
        j = sa[r-1]
        while i+prvl &amp;lt; n and j+prvl &amp;lt; n and s[i+prvl] == s[j+prvl]:
            prvl += 1
        lcp[r-1] = prvl
        if prvl: prvl -= 1
    return lcp

t = int(input())
for _ in range(t):
    s = input().rstrip()
    sa = make_suffix(s)
    lcp = makelcp(s, sa)
    tmp = 0; ans = 0
    for i in range(len(s)-1):
        if lcp[i] &amp;gt; tmp:
            ans += lcp[i] - tmp
        tmp = lcp[i]
    print(ans)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 다양한 접미사 배열과 lcp 배열 활용 문제를 알아보았다. 이제 아호 코라식을 다룰 차례인데, 다음 블로그에서는 아호 코라식을 다루기에 앞서 필요한 트라이 자료구조에 대해 다루려고 한다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;001&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/001.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/001.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <category>BOJ</category>
      <category>LCP</category>
      <category>lcp배열</category>
      <category>string</category>
      <category>suffix array</category>
      <category>문자열</category>
      <category>백준</category>
      <category>솔브닥</category>
      <category>알고리즘</category>
      <category>접미사배열</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/10</guid>
      <comments>https://kdy40929.tistory.com/10#entry10comment</comments>
      <pubDate>Tue, 12 Aug 2025 23:49:11 +0900</pubDate>
    </item>
    <item>
      <title>PS 문자열 알고리즘 - 6. 접미사 배열과 lcp 배열</title>
      <link>https://kdy40929.tistory.com/9</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 접미사 배열과 lcp 배열에 대해 살펴보고자 한다. 바로 본론으로 들어가자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 접미사 배열은 어떤 문자열 $S$가 주어졌을 떄, 이 문자열의 모든 접미사를 사전순으로 정렬할 때, 각 접미사가 시작한 인덱스를 저장한 배열이다. 예를 들어서 문자열 ABRACADABRA를 살펴보자. 이 문자열의 모든 접미사는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754980231530&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ABRACADABRA 0
 BRACADABRA 1
  RACADABRA 2
   ACADABRA 3
    CADABRA 4
     ADABRA 5
      DABRA 6
       ABRA 7
        BRA 8
         RA 9
          A 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 사전순으로 정렬하면 아래와 같은데, 이때 정렬된 상태에서 각 문자열이 원래 몇 번째 접미사였는지 각각 정수를 저장한 것을 접미사 배열(suffix array)라고 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1754980403627&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;          A 10
       ABRA 7
ABRACADABRA 0
   ACADABRA 3
     ADABRA 5
        BRA 8
 BRACADABRA 1
    CADABRA 4
      DABRA 6
         RA 9
  RACADABRA 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, ABRACADABRA의 접미사 배열은 $[10, 7, 0, 3, 5, 8, 1, 4, 6, 9, 2]$이다. 하지만, 이를 그대로 구현하면 모든 문자열의 접미사를 구해서 배열에 저장하는 것만 해도 $O(N^2)$이 소요된다. 하지만 이 $O(N^2)$은 너무 오래 걸리기 때문에 이를 줄이는 현명한 방법을 찾아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어차피 실제로 문자열 전체를 가지고 정렬하지 않더라도, 가장 앞 자리만 봐도 이미 순서가 정해지는 것들이 있다. 예를 들면, BRA라는 접미사와 CADABRA라는 접미사는 가장 앞 글자만 비교해도 B와 C의 순서에 의해서 뒤쪽 접미사 전체를 확인하지 않고도 비교가 가능하다. 따라서 이런 방식을 활용하고자 하는 것이 접미사 배열을 빠르게 구하는 원리이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 각 접미사의 첫 글자를 바탕으로 접미사 배열을 사용한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;268&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ohpD4/btsPPKz5fIw/hyoBLyubPan5hzZC7VyiW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ohpD4/btsPPKz5fIw/hyoBLyubPan5hzZC7VyiW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ohpD4/btsPPKz5fIw/hyoBLyubPan5hzZC7VyiW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FohpD4%2FbtsPPKz5fIw%2FhyoBLyubPan5hzZC7VyiW0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;170&quot; height=&quot;282&quot; data-origin-width=&quot;268&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 글자만을 이용해서 정렬하면 A, B, C, D, R만 남는다. 그리고 여기에 0, 1, 2, 3, 4라는 인덱스를 붙이자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이제 다시 모든 suffix들을 순회하면서 두 번째 글자를 확인하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MV5SD/btsPO6ckgKH/VOvDOhqnPvduzWWeNUtyw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MV5SD/btsPO6ckgKH/VOvDOhqnPvduzWWeNUtyw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MV5SD/btsPO6ckgKH/VOvDOhqnPvduzWWeNUtyw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMV5SD%2FbtsPO6ckgKH%2FVOvDOhqnPvduzWWeNUtyw0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;350&quot; height=&quot;343&quot; data-origin-width=&quot;574&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 순회에서 매긴 인덱스를 이용해서 1st idx와 2nd idx의 값을 구할 수 있고, 그러면 길이 2의 튜플 형태로 쉽게 비교가 가능하고 새롭게 index를 매길 수 있다. (비어 있는 경우 가장 앞쪽으로 와야 하므로 -1을 매긴다.) 그리고 이 과정을 반복하는데, 이제 길이가 3인 suffix가 아닌 기존의 2배를 한 길이가 4인 suffix로 넘어가는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R3qu6/btsPNOccF7T/63CYkfCrKVj6EQ7Yls4ry0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R3qu6/btsPNOccF7T/63CYkfCrKVj6EQ7Yls4ry0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R3qu6/btsPNOccF7T/63CYkfCrKVj6EQ7Yls4ry0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR3qu6%2FbtsPNOccF7T%2F63CYkfCrKVj6EQ7Yls4ry0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;350&quot; height=&quot;421&quot; data-origin-width=&quot;570&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 지난 순회에서 길이 2의 문자열에 대해 각각 지난 순회에서 길이 2의 문자열에 대해 인덱스를 매겼으므로 길이 4의 문자열을 길이 2의 문자열 2개로 쪼개서 각각에 인덱스를 매길 수 있고, 그러면 정렬을 통해서 크기 2의 튜플의 비교로 정렬을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 반복하면 한 번에 문자열의 길이가 2배씩 증가하므로 문자열 길이가 $N$일 떄 대략 $log N$번 반복하고, 각 반복 과정에서 길이 $2$의 튜플을 $N$개 만들어 정렬하므로 $O(N log^2 N)$의 시간복잡도로 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고, 이 알고리즘의 시간복잡도를 log N을 제거할 수 있는데, 카운팅 정렬과 기수 정렬을 동시에 이용하는 것이다. key를 바탕으로 카운팅 정렬을 하는 함수를 먼저 구현하면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754996477234&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def count_sort(arr, key, k):
    # count sorting by key[idx], range: 0 - k
    n = len(arr)
    cnt = [0] * (k+1)
    for val in arr:
        cnt[key[val]] += 1
    pfs = [0] * (k+1)
    for i in range(k):
        pfs[i+1] = pfs[i] + cnt[i]
    out = [0] * n
    for val in arr:
        out[pfs[key[val]]] = val
        pfs[key[val]] += 1
    return out&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;arr라는 배열의 각각의 값 val을 key[val]의 내림차순으로 정렬하는 코드이다. 이때 key[val]의 값은 $0$ 이상 $k$ 이하여야 한다. 이 코드의 시간복잡도는 $O(n+k)$이지만, 우리가 suffix array를 만들 때는 $k$의 값이 항상 $n$을 넘지 않기 때문에 실제로는 $O(n)$이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길이 2인 튜플에 radix sort를 활용하면 count sort를 두 번 사용해서 정렬을 마무리지을 수 있고, 그러면 정렬의 시간복잡도를 $O(n)$으로 줄일 수 있다. 따라서 이를 활용해서 suffix array를 최종적으로 구하는 코드를 만들면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754996790178&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;from bisect import bisect_left

def count_sort(arr, key, k):
    # count sorting by key[idx], range: 0 - k
    n = len(arr)
    cnt = [0] * (k+1)
    for val in arr:
        cnt[key[val]] += 1
    pfs = [0] * (k+1)
    for i in range(k):
        pfs[i+1] = pfs[i] + cnt[i]
    out = [0] * n
    for val in arr:
        out[pfs[key[val]]] = val
        pfs[key[val]] += 1
    return out

def make_suffix(s):
    n = len(s)
    if n == 0: return []
    sa = [*range(n)]
    uniq = []
    for ch in sorted(s):
        if not uniq or ch != uniq[-1]:
            uniq.append(ch)
    rank = [bisect_left(uniq, ch) for ch in s]
    k = 1
    while k &amp;lt; n:
        key1 = [r+1 for r in rank]
        key2 = [rank[i+k]+1 if i+k &amp;lt; n else 0 for i in range(n)]
        sa = count_sort(sa, key2, max(key2))
        sa = count_sort(sa, key1, max(key1))
        cnt = 0
        new_rank = [0] * n
        new_rank[sa[0]] = 0
        for i in range(1, n):
            a, b = sa[i], sa[i-1]
            ra, rb = rank[a], rank[b]
            rka = rank[a+k] if a+k &amp;lt; n else -1
            rkb = rank[b+k] if b+k &amp;lt; n else -1
            if ra != rb or rka != rkb:
                cnt += 1
            new_rank[a] = cnt
        rank = new_rank[:]
        if cnt == n-1: break
        k *= 2
    return sa&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 suffix array를 가지고 풀 수 있는 문제는 많지 않다. &quot;기껏해야 suffix array를 구하라&quot;라는 문제인 &amp;lt;BOJ 13264 접미사 배열 2&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/13264&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/13264&lt;/a&gt; 정도가 전부이고, 위 함수를 이용하면 되는 문제이니 위 함수를 직접 구현해보는 연습으로 삼아 풀어보면 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 접미사 배열을 만든 이유는 lcp 배열을 만들기 위해서인데, lcp 배열은 Largest Common Prefix의 약자이고, suffix array에서 인접한 두 접미사끼리의 최대 공통 접두사의 길이가 저장된 배열이다. 즉, 위에서 abracadabra의 예시를 바탕으로 lcp 배열을 계산하면 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1754997471939&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;                lcp
         A  10   X
       ABRA 7    1  (A, ABRA)
ABRACADABRA 0    4  (ABRA, ABRACADABRA)
   ACADABRA 3    1  (ABRACADABRA, ACADABRA)
     ADABRA 5    1  (ACADABRA, ADABRA)
        BRA 8    0  (ADABRA, BRA)
 BRACADABRA 1    3  (BRA, BRACADABRA)
    CADABRA 4    0  (BRACADABRA, CADABRA)
      DABRA 6    0  (CADABRA, DABRA)
         RA 9    0  (DABRA, RA)
  RACADABRA 2    2  (RA, RACADABRA)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 맨 처음 인덱스는 이전 값이 없으므로 lcp 배열의 값이 존재하지 않는다. 따라서 lcp 배열의 경우 크기가 $n-1$이 되도록 만들 수도 있고, $lcp[0] = -1$이 되도록 구현하는 경우도 있다. 필자의 경우 lcp 배열의 크기가 $n-1$이 되게끔 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 구현하는 것도 그냥 구하면 시간복잡도가 최악의 경우 $O(N^2)$이 된다. 하지만 이를 $O(N)$으로 줄이는 방법이 있는데, 바로 lcp 배열을 suffix array 순이 아니라, 기존 문자열에서 앞쪽에서부터 시작한 접두사부터, 즉 길이가 긴 접미사부터 구하는 것이다. 따라서 원본 문자열의 각 접미사가 suffix array의 몇 번째 위치에 있었는지를 나타내는 배열 $rev$를 먼저 만들고, rev[0], rev[1], ... 과 같은 순으로 lcp를 계산하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ABRACADABRA에서 lcp 배열의 값을 계산하면 ABRA와 ABRACADABRA의 lcp 배열은 ABRA가 최장 공통 접두사이다. 그 다음으로 구하는 lcp 배열은 BRACADABRA의 lcp 값인데, ABRA라는 문자가 있었으므로 BRA라는 문자가 있음을 확신할 수 있고, 그러면 BRA와 BRACADABRA가 있음에 보장되므로 lcp 배열의 값은 최소 3이다. BRA와 BRACADABRA 사이에 어떤 접미사가 추가로 있을 수 있으므로 lcp 배열의 값이 3보다 클 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 lcp 배열의 값의 최솟값이 보장되고, 직전의 lcp 값에서 최대 1만큼 감소하므로, 총 감소 횟수는 $N$이고, lcp 배열을 직접 증가시킬 때에는 그 인덱스가 전체 문자열에서 처음 인덱스 $0$에서 마지막 인덱스 $N-1$까지 이동하므로 증가 횟수는 감소 횟수 $N$에서 인덱스의 총 증가량 $N-1$을 더하면 $O(N)$에 작동함을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 구현하면 아래와 같다. 이떄 $s$는 원본 문자열이고, $sa$는 위에서 구한 suffix array이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754998732929&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def makelcp(s, sa):
    n = len(s)
    if n &amp;lt;= 1: return []
    rev = [0] * n
    for i, p in enumerate(sa):
        rev[p] = i
    lcp = [0] * (n-1)
    prvl = 0
    for i in range(n):
        r = rev[i]
        if r == 0: prvl = 0; continue
        j = sa[r-1]
        while i+prvl &amp;lt; n and j+prvl &amp;lt; n and s[i+prvl] == s[j+prvl]:
            prvl += 1
        lcp[r-1] = prvl
        if prvl: prvl -= 1
    return lcp&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구한 lcp 배열을 이용해서 다양한 문제를 해결할 수 있다. 이는 다음 블로그에서 설명하려고 한다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;002&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/002.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/002.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/9</guid>
      <comments>https://kdy40929.tistory.com/9#entry9comment</comments>
      <pubDate>Tue, 12 Aug 2025 20:41:56 +0900</pubDate>
    </item>
    <item>
      <title>PS 문자열 알고리즘 - 5. 라빈 카프 알고리즘 (해싱)</title>
      <link>https://kdy40929.tistory.com/8</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 다뤄볼 알고리즘은 해싱, 그 중에서도 라빈 카프 방식에 대해 살펴볼 것이다. 서론은 짧게 하고 빠르게 본론으로 넘어가보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해싱 (Hashing)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해싱은 문자열을 해시 함수에 넣어서 특정한 값으로 추출하는 것을 의미한다. 이때 가장 중요한 것은 같은 문자열을 같은 해시 함수에 여러 번 넣어 추출한 결과는 일정해야 한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해싱을 하면 어떤 부분이 좋을까? 바로 비교 시간복잡도를 줄일 수 있다. &quot;abcba&quot;라는 문자열과 &quot;abcbd&quot;라는 문자열이 서로 같은지 다른지 비교하려고 한다. 일반적으로는 문자열의 가장 앞부터 비교하면서 5번째 문자까지 비교했을 때 비로소 두 문자열이 다르다는 것을 판단할 수 있다. 하지만 해싱을 이용해서 미리 두 문자열에 대해 정수 형태로 값이 나오는 해시 함수 결괏값을 구해놓았다면 f(&quot;abcba&quot;)와 f(&quot;abcbd&quot;) 값만 비교하면 충분하다. 즉, 문자열을 정수 혹은 짧은 문자열의 형태로 바꾸어서 비교의 속도를 높이는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 속도를 높이는 이면에는 단점도 존재한다. 해시 충돌이라는 것인데, 서로 다른 두 문자열이 같은 해시함수 값을 갖는 경우이다. 그러면 서로 다른 문자열에 대해서 같은 문자열이라는 결과를 얻을 수도 있기 때문에 대부분의 프로그래밍 언어는 해시를 이용한 자료 구조를 제공하는 경우 충돌 처리를 하고 있다. 하지만 PS에서는 일반적으로 충돌 처리를 하기보다는 단일 해싱을 이중, 삼중 해싱으로 바꾸어 여러 개의 해시 함수에 대해서 모두 같은 결과를 내는지 비교하는 방식을 사용한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라빈 카프 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라빈 카프 알고리즘은 해싱을 하는 방법의 일종인데, 먼저 적당한 정수 $p$와 모듈로로 사용될 소수 $mod$를 정한다. 일반적으로 $mod$는 $10^9$ 근처의 소수를 사용하고, $p$는 문자열의 한 인덱스에 들어갈 수 있는 문자의 수보다 살짝 큰 값으로 정한다. 이 이유는 라빈 카프 알고리즘의 해싱 방식을 살펴보면 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어 소문자 26개로만 이루어진 문자열에 대한 해싱 함수를 만들고자 한다. $p = 31$로 잡았다고 하자. 그러면 각 자리 수에 31의 거듭제곱을 하나씩 차례로 곱해서 더한 값을 $mod$로 나눈 값을 해싱 함수로 사용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시를 들자면, 문자열 &quot;abc&quot;가 있다고 하자. 그러면 우선 $a = 1$, $b = 2$, $c = 3$, ... $z = 26$ 와 같이 각각을 정수로 변환한 후, 앞자리부터 $31^2$, $31^1$, $31^0$을 곱해서 모두 더하는 것이다. 그러면 이 문자의 해싱 함수 값은 $1 \times 31^2 + 2 \times 31^1 + 3 \times 31^0 = 1026$이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해싱하는 방식을 이용하면 어떤 점이 좋을까? 바로 부분 문자열의 해싱이 간편하다는 것이다. 아이디어는 누적 합 배열을 만드는 것과 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 문자열이 있으면 그 문자열의 $i$번째 인덱스까지의 접두사 문자열을 해싱한 값을 배열에 저장해놓자. 그러면 누적 합 배열을 계산하듯이 일반적인 부분 문자열의 해싱 값을 $O(1)$에 계산할 수 있다. 방법을 아래 예시로 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754790453871&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;S1 :  A B A C C D B A
S2 :  A B A C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A B A C C D B A ... 로 시작하는 문자열에서 C D B A 부분의 해싱 값을 구하고자 한다. 이미 우리는 가장 앞의 A로 시작하는 접두사 문자열의 해싱 값은 구한 상태이다. 그러면 S1에서 S2의 해싱 값을 빼서 C D B A의 해싱값을 구하는 것을 생각해볼 수 있다. 그런데, A B A C 각각이 위치한 자릿수가 S1과 S2에서 다르기 때문에 그대로 빼면 안 된다는 것이다. 이를 구체적으로 각 자리에 곱해지는 $p$의 지수를 아래에 써보면 아래 그림과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1754790584996&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;S1 :  A B A C C D B A
      7 6 5 4 3 2 1 0
S2 :          A B A C
              3 2 1 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 S2의 A B A C에 곱해진 지수는 3, 2, 1, 0으로 S1의 A B A C에 곱해진 7, 6, 5, 4와는 4의 차이가 있다. 그 이유는 우리가 해싱 값을 구하려고 하는 문자열 C D B A의 길이가 4이기 때문이다. 따라서 S2에 $p^4$을 곱해주고 빼면 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1754790680824&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;S1 :  A B A C C D B A
      7 6 5 4 3 2 1 0
S2 :  A B A C
      7 6 5 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 C D B A의 해싱값을 얻을 수 있게 된다. 나머지 연산의 경우 합동식의 성질에 의해서 문제 없이 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 접두사의 해싱 값은 어떻게 구할까? 이것도 단순하다. 기본적으로 수를 계산하는 방식이 $p$진법과 유사하기 때문에 우리가 십진법 수 314에서 뒤에 2를 붙인 3142의 값을 어떻게 계산하는지 생각해보면 된다. 바로 10을 곱하고 2를 더하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 접두사의 해싱 값 또한 $p$를 곱하고 새로 추가된 문자에 대응되는 값을 더하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 이를 이용해서 문제를 해결해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 1605 반복 부분문자열&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/1605&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1605&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 문자열에서 2번 이상 등장하는 부분문자열이 있는지 판단하는 문제이다. 두 부분문자열이 같은지 비교는 해싱을 통해서 할 텐데, 가장 긴 길이부터 탐색하면 $O(L^2)$이 소요된다. 하지만 아래와 같은 관찰을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관찰. 길이 $S$의 같은 부분문자열이 2개 이상 존재하면 길이가 $S$보다 작은 임의의 길이의 같은 부분문자열도 2개 이상 존재한다. 길이 $S$의 부분문자열 2개의 뒷부분 문자만 하나씩 제거해도 찾을 수 있으므로 자명하다. 그리고 이러한 관찰은 이분 탐색의 형태로 부분문자열이 2개 이상 존재하는 최대 길이를 찾을 수 있음을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 해싱과 매개변수 탐색을 결합하여 문제를 해결할 수 있고, 시간 복잡도는 $O(L log L)$이 된다. 코드는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754791422305&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def hashing(left, right):
    return (hashlst[right] - powlst[right-left+1] * hashlst[left-1]) % mod

mod = 10**18 + 9; p = 31
n = int(input())
s = input().rstrip()
hashlst = [0 for _ in range(n+1)]
powlst = [1 for _ in range(n+1)]
for i in range(1, n+1):
    powlst[i] = powlst[i-1] * p % mod
hashlst[0] = ord(s[0]) - 97
for i in range(1, n):
    hashlst[i] = (hashlst[i-1]*p + ord(s[i])-97)%mod
left, right = 0, n
while left + 1 &amp;lt; right:
    mid = (left + right)//2
    ansset = set()
    for i in range(n-mid+1):
        ansset.add(hashing(i, i+mid-1))
    if len(ansset) == n-mid+1:
        right = mid
    else: left = mid
print(left)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 코드는 충돌을 줄이기 위해서 통크게 mod를 $10^18 + 9$로 잡아버렸지만 여러분은 가급적 $10^9$ 근처를 잡고, 해시 충돌이 발생한다면 이중 해싱을 이용하는 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 17228 아름다운 민영로&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/17228&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/17228&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 트리에서 해싱을 사용하는 문제로 조금 더 난이도가 높다. 하지만 부모에서 자식 방향으로 내려가는 것만 가능한 형태이기 때문에 가능한 산책로는 산책의 도착 지점만 정해주면 출발점이 자동으로 결정되어 $N$개를 넘지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 역시 일반적인 해싱 방법과 유사하게 루트에서 $v$번 노드까지 가는 산책로의 해싱 값과 루트에서 $u$번 노드까지 가는 산책로의 해싱 값을 알면 $u$번 노드에서 $v$번 노드까지 가는 산책로의 해싱 값을 구할 수 있다는 점을 이용해서 문제를 해결핧 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나는 산책로의 도착 지점을 알 때 산책로의 출발 지점을 구하는 방식으로 sparse table을 구성하였다. 하지만 sparse table 없이 더 간단하게 $O(N)$에 모든 산책로의 출발 지점을 분명히 dfs 등을 통해서 구할 수 있으므로 참고하면 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 문자열 $S$를 해싱해놓고, 최대 $N$개인 산책로 각각에 대해서 해싱 값이 같은지 비교하는 방식으로 구현하면 된다. sparse table을 사용할 경우 시간 복잡도는 $O(N log N)$이고, $O(N)$에도 구현이 가능하다. 아래는 나의 풀이 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754791784366&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline
def dfs(graph, p, mod):
    hashlst = [-1] * len(graph)
    parent = [0] * len(graph)
    st = [0]; hashlst[0] = 0
    while st:
        cur = st.pop()
        for nxt, val in graph[cur]:
            if hashlst[nxt] != -1: continue
            st.append(nxt)
            parent[nxt] = cur
            hashlst[nxt] = (p*hashlst[cur] + val) % mod
    return hashlst, parent

def hashing(left, right, k):
    return (hashlst[right] - powlst[k] * hashlst[left]) % mod

p = 31; mod = 10**9 + 7
n = int(input())
powlst = [1 for _ in range(n+1)]
for i in range(1, n+1):
    powlst[i] = powlst[i-1] * p % mod
graph = [[] for _ in range(n)]
for _ in range(n-1):
    u, v, c = input().split()
    u, v = int(u)-1, int(v)-1
    graph[u].append((v, ord(c)-97))
s = input().rstrip()
hash_s = 0
for c in s:
    hash_s = (p * hash_s + ord(c)-97) % mod
hashlst, parent = dfs(graph, p, mod)
sparse = [[0]*n.bit_length() for _ in range(n)]
for i in range(n):
    sparse[i][0] = parent[i]
for j in range(1, n.bit_length()):
    for i in range(n):
        sparse[i][j] = sparse[sparse[i][j-1]][j-1]
depart = [0]*n
for i in range(n):
    pr = i
    for j in range(n.bit_length()):
        if len(s)&amp;amp;(1&amp;lt;&amp;lt;j):
            pr = sparse[pr][j]
    depart[i] = pr
ans = 0
for i in range(n):
    if hashing(depart[i], i, len(s)) == hash_s:
        ans += 1
print(ans)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이상으로 라빈 카프 알고리즘으로 해싱하여 문제를 해결하는 테크닉에 대해서 알아보았다. 다음에는 접미사 배열과 lcp 배열에 대해서 다뤄보고자 한다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;017&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/017.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/017.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <category>BOJ</category>
      <category>rabin karp</category>
      <category>string</category>
      <category>라빈카프</category>
      <category>문자열</category>
      <category>백준</category>
      <category>솔브닥</category>
      <category>알고리즘</category>
      <category>해싱</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/8</guid>
      <comments>https://kdy40929.tistory.com/8#entry8comment</comments>
      <pubDate>Sun, 10 Aug 2025 11:11:46 +0900</pubDate>
    </item>
    <item>
      <title>PS 문자열 알고리즘 - 4. Manacher 알고리즘</title>
      <link>https://kdy40929.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 글을 적어볼 알고리즘은 매내처 알고리즘이다. 이 역시 바로 본론으로 들어가자면, 매내처 알고리즘은 어떤 문자열 $S$가 주어졌을 때, $S$의 부분 문자열 (앞뒤에서 0개 이상의 문자를 떼어내 만들 수 있는 문자열) 중 팰린드롬인 것을 빠르게 탐색하는 알고리즘이다. 그리고 실제 구현은 Z 알고리즘과 비슷하게 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Manacher 알고리즘은 부분 문자열 중 팰린드롬을 팰린드롬의 중심을 기준으로 탐색한다. 예시를 들어서 설명하자면, 팰린드롬 abcba의 중심은 c이고, abcba가 팰린드롬임을 알면 중심을 기준으로 대칭인 c, bcb가 팰린드롬이라는 사실을 이용할 수 있는 것이다. 이때 위 팰린드롬의 반경은 $2$이다. 왜냐하면 c를 중심으로 오른쪽으로 두 칸, 왼쪽으로 두 칸이 서로 대칭이라서 팰린드롬을 이루기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매내처 알고리즘은 결국 $i$번째 인덱스의 문자를 중심으로 할 때 최대 팰린드롬 반경 $A[i]$를 빠르게 구하는 알고리즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754704512488&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;A  A  B  A  C  D  C  A  B  A  C  B
   |-----------|--------|--|
              ctr       |  r
                        i&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 문자열을 생각하고, D를 중심으로 하는 팰린드롬의 반경이 4인 상황을 고려하자. 우리는 $i = 8$ 인덱스를 살피고 있다. 여기서 팰린드롬 반경을 어떻게 빠르게 구할 수 있을까? 그 원리는 Z 알고리즘과 비슷하다. 우리는 ctr을 중심으로 서로 문자열이 대칭을 이루고 있음을 알고 있으므로 2*ctr - i에 해당하는, 즉 2번 인덱스의 B를 이용해서 팰린드롬 반경을 빠르게 구할 수 있는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이 상황에서도 조심해야 될 부분이 있다. 바로 팰린드롬 반경이 $r$ 범위를 벗어나는 경우이다. 위 상황에서도 $A[2] = 1$인 반면, $A[8]$은 $A[2]$를 이용해서 빠르게 계산하려고 함에도 반경 $2$만 되려고 해도 $r$ 범위를 넘어서게 된다. 이럴 때에는 Z 알고리즘처럼 직접 문자를 비교해서 $A[i]$를 채워야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 $r$의 범위는 최대한 뒤쪽까지 포함하고 있어야 최적화가 가능하므로 $r$의 값을 갱신해주어야 한다. 위의 상황에서 일어난 갱신을 그림으로 표현하면 아래와 같다. 원래의 $i$ 위치에 $ctr$이 배정되었고, $r$은 $i+A[i]$ 위치가 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1754704822994&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;A  A  B  A  C  D  C  A  B  A  C  B
                  |-----|-----|
                        |     r
                       ctr&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. $r$의 값을 이용해서 $A[i]$ 구하기&lt;/li&gt;
&lt;li&gt;2. $A[i]$의 범위가 $r$을 초과할 경우 직접 비교하기&lt;/li&gt;
&lt;li&gt;3. $r$이 더 뒤로 이동할 수 있다면 $ctr, r$ 갱신하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 3가지를 이용하면 매내처 알고리즘을 구현할 수 있다. 실제로 구현도 원리도 Z 알고리즘과 크게 다르지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이를 Python으로 구현하면 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1754705078822&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def manacher(s):
    n = len(s)
    r, ctr = 0, 0
    arr = [0 for _ in range(n)]
    for i in range(n):
        if i &amp;lt;= r:
            arr[i] = min(arr[2*ctr-i], r-i)
        while i-arr[i] &amp;gt;= 1 and i+arr[i] &amp;lt; n-1 and s[i-arr[i]-1] == s[i+arr[i]+1]:
            arr[i] += 1
        if r &amp;lt; i+arr[i]:
            r, ctr = i+arr[i], i
    return arr&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데...! 여기서 아주 중요한 이슈가 하나 있다. 바로 매내처 알고리즘은 &lt;b&gt;홀수 길이의 팰린드롬&lt;/b&gt;만 찾을 수 있다는 것이다. 이유는 간단하다. 팰린드롬의 중심을 우리는 &quot;문자&quot;로 잡았기 때문이다. 짝수 길이의 팰린드롬, 이를테면 abba는 중심이 문자가 아니다. 두 b 사이 지점이 중심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리가 짝수 길이의 팰린드롬을 찾는 알고리즘을 다시 고안하는 것은 너무 귀찮기 때문에 이웃한 두 문자 사이 지점 자체를 다시 문자로 만드는 일종의 trick을 사용할 것이다. 바로 *와 같은 문자를 끼워넣는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 문자열 abbac가 주어졌다면 이를 *a*b*b*a*c* 로 바꿀 것이고, 그러면 abba라는 팰린드롬은 *a*b*b*a*의 형태가 되어 b와 b 사이의 *을 중심으로 하는 홀수 길이의 팰린드롬으로 바뀌게 되는 것이다. 이때 유의해야 할 점은 끼워넣는 문자는 원래 문자열에서 사용되지 않는 문자여야 한다는 점이다. 따라서 보통은 입력받은 문자열에 포함되어 있지 않은 특수문자를 사용하는 것이 일반적이지만, 어떤 문자를 사용할지는 개인의 선택이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 실제로 매내처 알고리즘을 사용할 때에는 아래 코드와 같이 사용하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1754705445670&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def manacher(s):
    n = len(s)
    r, ctr = 0, 0
    arr = [0 for _ in range(n)]
    for i in range(n):
        if i &amp;lt;= r:
            arr[i] = min(arr[2*ctr-i], r-i)
        while i-arr[i] &amp;gt;= 1 and i+arr[i] &amp;lt; n-1 and s[i-arr[i]-1] == s[i+arr[i]+1]:
            arr[i] += 1
        if r &amp;lt; i+arr[i]:
            r, ctr = i+arr[i], i
    return arr

inp = input().rstrip()
s = '*' + '*'.join(inp) + '*'
manacher(s)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 짝수 길이의 팰린드롬을 처리하다 보면 또 새로운 문제점이 생긴다. 바로 array에 저장되는 팰린드롬 반경의 값이 원래 정의에서의 값과 달라지는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이게 오히려 이점을 주기도 한다. 왜냐하면 이때 array에 저장되는 값은 바로 &lt;b&gt;그 지점을 중심으로 하는 팰린드롬의 최대 길이&lt;/b&gt;가 되기 때문이다. 예시를 들어 문자열 abaa를 살펴본다면, *a*b*a*a*을 이용해 매내처 알고리즘을 돌리면 얻는 배열은 $[0, 1, 0, 3, 0, 1, 2, 1, 0]$이다. b를 중심으로 하는 팰린드롬 반경은 *을 추가한 문자열에서 3이고, 그리고 이 값은 원본 문자열에서 b를 중심으로 하는 팰린드롬의 최대 길이와 정확히 일치한다. 또한, 뒤쪽의 a 2개 사이의 *을 중심으로 하는 팰린드롬 반경은 2이고, 이 역시 원본 문자열에서 두 a 사이를 중심으로 하는 팰린드롬의 최대 길이와 정확히 일치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 살펴볼 것은 매내처 알고리즘의 시간복잡도이다. 앞서 Z 알고리즘과 구현 형태가 비슷하기 때문에 Z 알고리즘과 비슷한 방식으로 시간복잡도 분석을 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;while문 내에서 arr[i] += 1을 1회 진행할 때마다 그 이후 $r$은 최소 1만큼 증가한다는 사실을 쉽게 알 수 있는데, $r$의 범위가 $S$의 길이 이하로 제한되기 때문에 시간복잡도가 $O(N)$이 된다. 잘 이해가 안 된다면 이전 블로그에서 다룬 Z 알고리즘의 시간복잡도 분석을 읽어보면 도움이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용하면 매내처 알고리즘으로 팰린드롬 관련 문자열 문제를 쉽게 해결할 수 있는 경우가 많다. 아래에서는 여러 백준 문제 예시를 통해 살펴보자.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;001&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/001.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/001.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 13275 가장 긴 팰린드롬 부분 문자열&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/13275&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/13275&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고민할 것도 없다. 위에서 설명한 그대로이므로 max(arr)를 출력하면 끝나는 문제가 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 아래와 같다&lt;/p&gt;
&lt;pre id=&quot;code_1754705828970&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def manacher(s):
    n = len(s)
    r, ctr = 0, 0
    arr = [0 for _ in range(n)]
    for i in range(n):
        if i &amp;lt;= r:
            arr[i] = min(arr[2*ctr-i], r-i)
        while i-arr[i] &amp;gt;= 1 and i+arr[i] &amp;lt; n-1 and s[i-arr[i]-1] == s[i+arr[i]+1]:
            arr[i] += 1
        if r &amp;lt; i+arr[i]:
            r, ctr = i+arr[i], i
    return arr

inp = input().rstrip()
s = '*' + '*'.join(inp) + '*'
print(max(manacher(s)))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 16163 #15164번_제보&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/16163&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/16163&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 부분 문자열 중 팰린드롬의 개수를 세는 것이다. 어떤 문자를 중심으로 하는 팰린드롬의 최대 길이가 $x$이면 이 문자를 중심으로 하는 팰린드롬의 개수는 $\left \lfloor{\frac{x+1}{2}}\right \rfloor$ 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이를 이용하여 정답을 구하는 코드를 짜면 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1754721494905&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def manacher(s):
    n = len(s)
    r, ctr = 0, 0
    arr = [0 for _ in range(n)]
    for i in range(n):
        if i &amp;lt;= r:
            arr[i] = min(arr[2*ctr-i], r-i)
        while i-arr[i] &amp;gt;= 1 and i+arr[i] &amp;lt; n-1 and s[i-arr[i]-1] == s[i+arr[i]+1]:
            arr[i] += 1
        if r &amp;lt; i+arr[i]:
            r, ctr = i+arr[i], i
    return arr

inp = input().rstrip()
s = '*' + '*'.join(inp) + '*'
ans = 0
arr = manacher(s)
for i in range(len(arr)):
    ans += (arr[i]+1)//2
print(ans)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 30400 팰린드롬 제거&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/30400&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/30400&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 조금 더 난이도 있는 문제를 풀어보자. 이제는 매내처 알고리즘을 활용한 문제들이다. 부분 문자열 중 길이 $M$ 이상인 팰린드롬이 남아 있지 않도록 최소한의 문자를 파괴해야 하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 매내처를 이용해서 각 문자를 중심으로 하는 팰린드롬의 최대 길이를 찾을 수 있고, 이 중 길이가 $M$ 이상인 것만 남기는 것까지는 어렵지 않게 할 수 있다. 그 다음은 어떤 전략을 사용해야 할까? 여기서 아래와 같은 2가지 관찰을 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관찰 1. 중심이 같은 길이 $M$ 이상인 팰린드롬에 대해서는 가장 짧은 것만 보면 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 예시를 들어서 설명하면 아래와 같다. $M = 3$이라고 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1754721723673&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.. A B C D C B A ..
         |
       |---|
     |-------|
   |-----------|
        ...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;D를 중심으로 하는 팰린드롬이 가장 짧은 것부터 굉장히 많은데, 이 중에서 길이가 3보다 작은 D는 제거하지 않아도 된다. 그 다음인 CDC는 길이가 3이므로 반드시 제거해야 하는데, CDC를 제거하면 자동으로 이보다 더 긴 팰린드롬인 BCDCB, ABCDCBA ...은 무조건 지워진다. 따라서 각 팰린드롬의 중심마다 제거해야 하는 팰린드롬의 수를 최대 1개로 제한할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관찰 2. (왼쪽부터 팰린드롬을 제거할 때) 제거해야 하는 팰린드롬이 생긴다면 팰린드롬의 가장 오른쪽 문자를 제거하는 전략이 유효하다. 이 그리디 전략은 꽤나 자명하다. 가장 오른쪽 문자가 아닌 문자를 제거할 때보다 같거나 더 많은 팰린드롬을 한꺼번에 제거할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 두 가지 관찰을 바탕으로 최소한의 제거 횟수로 길이 $M$ 이상의 팰린드롬을 제거하는 전략을 세워 보면, 관찰 1을 바탕으로 각 위치를 중심으로 하는 제거해야 하는 팰린드롬을 최대 1개 선정한다. 그리고 이 팰린드롬들을 왼쪽에 위치한 것부터 보면서 아직 제거되지 않은 것이 발견될 때마다 가장 오른쪽 문자를 제거하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 구현한 코드가 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1754722017101&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline
def manacher(s):
    n = len(s)
    r, ctr = 0, 0
    arr = [0 for _ in range(n)]
    for i in range(n):
        if i &amp;lt;= r:
            arr[i] = min(arr[2*ctr-i], r-i)
        while i-arr[i] &amp;gt;= 1 and i+arr[i] &amp;lt; n-1 and s[i-arr[i]-1] == s[i+arr[i]+1]:
            arr[i] += 1
        if r &amp;lt; i+arr[i]:
            r, ctr = i+arr[i], i
    return arr

n, m = map(int, input().split())
inp = input().rstrip()
s = '*' + '*'.join(inp) + '*'
arr = manacher(s)
lst = []
for i in range(2*n+1):
    if arr[i] &amp;gt;= m:
        lst.append(((i-m)//2, (m+i-1)//2))
ans, last = 0, -1
for x, y in lst:
    if last &amp;lt; x:
        last = y
        ans += 1
print(ans)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 문자열에서의 인덱스와 *을 추가한 문자열에서 인덱스가 약간 상이하므로 //2 처리를 할 때 꼼꼼한 계산이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 매내처 알고리즘을 공부하였고, 이에 관련된 문제도 3문항 풀어보았다. 다음에는 해싱 (라빈 카프) 알고리즘을 소개할 예정이다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;002&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/002.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/002.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <category>BOJ</category>
      <category>manacher</category>
      <category>매내처</category>
      <category>문자열</category>
      <category>백준</category>
      <category>솔브닥</category>
      <category>알고리즘</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/7</guid>
      <comments>https://kdy40929.tistory.com/7#entry7comment</comments>
      <pubDate>Sat, 9 Aug 2025 15:48:30 +0900</pubDate>
    </item>
    <item>
      <title>PS 문자열 알고리즘 - 3. Z 알고리즘</title>
      <link>https://kdy40929.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지난번에 다루었던 KMP 알고리즘에 이어서, 이번에는 Z 알고리즘에 대해 다뤄보고자 한다. 바로 본론으로 들어가보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Z 알고리즘은 문자열 $S$가 주어졌을 때, 문자열 $S$의 접미사와 문자열 $S$의 최대 공통 접두사의 길이를 빠르게 구하는 알고리즘이다. 예시를 들면 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PAaNO/btsPM0bizb5/iO8hxgwi3EdI6MHrgkHXn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PAaNO/btsPM0bizb5/iO8hxgwi3EdI6MHrgkHXn0/img.png&quot; data-alt=&quot;Z array에 담기는 값&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PAaNO/btsPM0bizb5/iO8hxgwi3EdI6MHrgkHXn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPAaNO%2FbtsPM0bizb5%2FiO8hxgwi3EdI6MHrgkHXn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;429&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Z array에 담기는 값&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 표와 같이 $Z[i]$에 $i$번째 문자로 시작하는 접미사와 원본 문자열의 공통 접두사의 최대 길이를 저장하는 알고리즘이 Z 알고리즘이고, Z 알고리즘으로 구한 이 array를 보통 Z array라고 부른다. 이 역시 Naive하게 구현하면 문자열의 길이가 $N$일 때 $O(N^2)$이 소요된다. Z 알고리즘은 이때 불필요한 탐색을 최적화하여 시간복잡도를 $O(N)$으로 줄인다. 그 원리는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754642351328&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;idx               l         r
                  |         |
A C A B A C B ... A C A B A C A B C...
|---------|       |---------|
                      |
                      i&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원본 문자열을 탐색하면서 Z array를 만들다 보니 위와 같이 $Z[l] = 6$이 되었다고 하자. $Z[l] = 6$이 우리에게 주는 정보는 원본 문자열의 index $0$ ~ $5$에 위치한 문자와 $l$ ~ $l+5$에 위치한 문자가 같다는 것이다. $Z[i](=Z[l+2])$를 채우려고 할 때 우리는 이 정보를 활용하면 $Z[l+2]$를 채울 때 $Z[2]$를 이용할 수 있음을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위의 경우에는 $Z[l+2] = Z[2] = 1$로 쉽게 값을 구할 수 있다. 한편, $i$가 계속 증가해 l+4 위치에 도달했다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754642815362&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;idx               l         r
                  |         |
A C A B A C B ... A C A B A C A B C...
|---------|       |---------|
                          |
                          i&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때도 $Z[l]$에서 얻은 정보를 이용하면 $Z[l+4]$를 채울 때 도움을 받을 수 있는 것처럼 보인다. 다만 이때에는 $Z[l+4] = Z[4]$라고 속단하면 안 된다. $Z[l+4]$의 값은 A C A B까지 공통되므로 4이지만, $Z[4]$는 2이다. 어떻게 된 일일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 일이 벌어진 이유는 $Z[l+4]$의 범위가 $r$을 초과했기 때문이다. 즉, 우리가 얻은 정보는 $S[l+4] = S[4], S[l+5] = S[5]$가 전부이고 $S[l+6] != S[6]$이므로 $Z[l+4]$를 채울 때에는 $Z[4]$를 참고하더라도 2 이상이라는 정보만 알 수 있고, 그 이상은 직접 비교해보아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 효율적인 연산을 위해서는 $r$의 값이 최대한 뒤에 있는 것이 기존의 정보를 최대한으로 활용하는 방법이기 때문에, $r$의 값이 원래보다 커지는 $l$이 존재하는 경우 $l, r$을 갱신해야 한다. 따라서 Z[l+4]에서 정보를 처리하고 나면 아래 그림과 같은 갱신이 이루어져야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1754643113051&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;idx                       l     r
                          |     |
A C A B A C B ... A C A B A C A B C...
|-----|                   |-----|
                          |
                          i&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1. $r$의 값을 이용해서 $Z[i]$ 구하기&amp;nbsp;&lt;/li&gt;
&lt;li&gt;2. $Z[i]$의 범위가 $r$을 초과할 경우 직접 비교하기&lt;/li&gt;
&lt;li&gt;3. $r$의 범위가 더 뒤로 옮겨질 수 있다면 $r$ 갱신하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 3가지를 이용하면 $Z$ 알고리즘을 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 문자열 $S$를 입력받아서 Z array를 구하는 파이썬 함수이다. 이때 $Z[0]$는 접미사와 원본 문자열이 동일하므로 정의에 따라 문자열 $S$의 길이가 되는 것이 자연스럽다. 하지만 큰 의미는 없어 $Z[0] = n$을 바로 할당하고 $i$는 $1$부터 $N$까지 반복하게 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1754643448793&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def makez(s):
    n = len(s)
    z = [0]*n; z[0] = n # 정의에 의해
    l, r = 0, 0
    for i in range(1, n):
        if i &amp;lt;= r: z[i] = min(r-i+1, z[i-l]) # z[i-l]에서 정보 가져오기
        while i+z[i] &amp;lt; n and s[i+z[i]] == s[z[i]]: # IndexError 방지
            z[i] += 1
        if r &amp;lt; i+z[i]-1:
            l, r = i, i+z[i]-1 # l, r 갱신
    return z&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로는 시간복잡도를 분석해보고자 한다. 이 역시 while문이 있어 시간복잡도 분석이 직관적이지 않다. 하지만 z[i] += 1의 시행 횟수와 $r$의 변화량 사이 관계에 주목하면 $O(N)$이 됨을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, z[i] = min(r-i+1, z[i-l])에서 z[i-l]의 값을 가져온 경우 $z[i-l]$ 이후의 영역도 이미 비교가 완료되어 있고 서로 다름이 보장되어 있는 상황이므로 z[i] += 1이 행해지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, z[i] += 1이 행해지는 경우에는 $i &amp;gt; r$이거나 $z[i]$에 바로 $r-i+1$이 할당된 상황임을 알 수 있고 두 경우 모두 z[i] += 1을 시행하기 전 $i+z[i]-1 \ge r$이 성립한다. 따라서 z[i] += 1이 k번 이루어졌다면 $r$이 $i+z[i]-1$로 갱신되므로 기존의 $r$ 값보다 $k$ 이상 커진다는 것을 의미한다. 이때 $r$의 최댓값은 $N$을 넘지 못하여 z[i] += 1의 시행 횟수도 같이 제한되고, z[i] += 1의 시행 횟수는 $i$가 $1$에서 $N$까지 도달할 때 통틀어 최대 $N$번이 된다. 따라서 위 알고리즘의 시간복잡도는 $O(N)$이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이를 이용하는 문제를 풀어보자.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 13713 문자열과 쿼리&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/13713&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/13713&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Z 알고리즘에서 구하라는 것과 아주 살짝 다르게, 접두사와 원본 문자열의 최대 공통 접미사의 길이를 구하는 문제이다. 이 문제의 경우 주어진 문자열을 뒤집으면 Z 알고리즘의 상황과 동일하기 때문에 인덱스 처리에만 주의하면 쉽게 문제를 해결할 수 있다. 아래는 문제를 해결한 코드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754644516056&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline
def makez(s):
    n = len(s)
    z = [0]*n; z[0] = n
    l, r = 0, 0
    for i in range(1, n):
        if i &amp;lt;= r:
            z[i] = min(r-i+1, z[i-l])
        while i+z[i] &amp;lt; n and s[i+z[i]] == s[z[i]]:
            z[i] += 1
        if r &amp;lt; i+z[i]-1:
            l, r = i, i+z[i]-1
    return z

s = input().rstrip()
z = makez(s[::-1])
m = int(input())
for _ in range(m):
    print(z[len(s)-int(input())])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 16229 반복 패턴&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/16229&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/16229&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 앞선 문제보다는 조금 더 생각을 해야 하는 문제이지만, 역시나 어렵지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 문자열의 반복으로 기존 문자열 길이인 $n$만큼은 최소한 채워야 하므로 길이 $i$짜리 문자열의 반복 패턴으로 만들고자 하면 $z[i]$ 자체가 문자열의 끝까지 도달해야 한다. 즉, $i+z[i] == n$을 만족해야 하고, $n$ 이상의 최소의 $i$의 배수 길이만큼으로 채울 때 길이가 $n+k$ 이하여야 한다는 조건까지 확인하면 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, $n$보다 $k$가 큰 경우에는 전체 문자열을 2번 이어붙인 문자열을 만들면 되므로 $n$을 바로 출력하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 문제를 해결한 내 코드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754644782654&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline

def makez(s):
    n = len(s)
    z = [0]*n; z[0] = n
    l, r = 0, 0
    for i in range(1, n):
        if i &amp;lt;= r: z[i] = min(r-i+1, z[i-l])
        while i+z[i] &amp;lt; n and s[i+z[i]] == s[z[i]]:
            z[i] += 1
        if r &amp;lt; i+z[i]-1:
            l, r = i, i+z[i]-1
    return z

n, k = map(int, input().split())
s = input().rstrip()
z = makez(s)
if n &amp;lt;= k:
    print(n); exit(0)
ans = 0
for i in range(1, n):
    if i+z[i] == n and i*((n-1)//i+1) &amp;lt;= n+k:
        ans = i
print(ans)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Z 알고리즘에 해당하는 문제가 많지 않아 Z 알고리즘에 대한 글은 이 정도로 마친다. 다음에는 매내처 알고리즘에 대해 글을 써볼 예정이다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;005&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/005.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/005.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <category>BOJ</category>
      <category>Z</category>
      <category>문자열</category>
      <category>백준</category>
      <category>알고리즘</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/6</guid>
      <comments>https://kdy40929.tistory.com/6#entry6comment</comments>
      <pubDate>Fri, 8 Aug 2025 18:22:55 +0900</pubDate>
    </item>
    <item>
      <title>PS 문자열 알고리즘 - 2. KMP 응용</title>
      <link>https://kdy40929.tistory.com/5</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 글에서 KMP 알고리즘의 원리와 시간복잡도를 알아보았고, 이를 바탕으로 문자열을 검색하는 기본 문제를 풀어보았다. 이번 글에서는 KMP 알고리즘을 활용하는 문제들을 몇 가지 살펴볼 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 1305 광고&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/1305&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1305&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 단순하다. 광고 문구가 AABA이고 광고판이 9글자까지 표현 가능하다면 광고판에는 광고 문구를 반복해서 이어붙인 문자열이 보이고, 1칸씩 이동하는 형태이다. 즉, 아래와 같이 광고판에 문구가 뜨는 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1754277424381&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[A  A  B  A][A  A  B  A][A
 A  B  A][A  A  B  A][A  A
 B  A][A  A  B  A][A  A  B
 A][A  A  B  A][A  A  B  A]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;광고판의 어느 순간의 모습이 주어지면 광고 문구로 가능한 것의 최소 길이를 찾아야 한다. 어차피 광고판의 문자열은 변하고 있으므로 AABA가 광고 문구이든 이걸 한 칸 옮긴 ABAA가 광고 문구이든 이를 구분할 수는 없고, 길이만 찾으면 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 결국 주기성이 있는 수열의 일부분이 주어졌을 때 주기의 최솟값을 찾는 문제이다. AABAAABAA가 주어진 경우를 예시로 해서 주기성이 있을 때 어떤 특징을 가지는지 살펴보자.&lt;/p&gt;
&lt;pre id=&quot;code_1754277585266&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[A  A  B  A][A  A  B  A][A
            [A  A  B  A][A  A  B  A][A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1754277662703&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[A  A  B  A][A  A  B  A][A
                        [A  A  B  A][A  A  B  A][A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 주기의 배수만큼 옮겼을 때 겹쳐지는 부분이 동일함을 알 수 있다. 그런데 이 모습, 어디서 많이 보았던 모습 아닌가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다. &lt;b&gt;접두사와 접미사에 공통 부분이 생기는 상황. 실패 함수에서 보았던 것과 동일하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바꾸어 말하면 실패 함수를 이용해서 주기를 찾을 수 있다는 말도 된다. 전체 문자열의 길이가 $N$, 주기가 $P$이면 $N-P$, $N-2P$, $N-3P$ ... 만큼 겹쳐졌을 때 정확히 일치하고, 이때 실패 함수의 값은 공통 접두&amp;amp;접미사의 최대 길이이므로 $N-P$가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 우리가 구하는 주기의 최솟값은 전체 길이에서 전체 문자열의 실패 함수 값을 빼준 값이 된다. 이를 구현한 코드는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1754277959130&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;l = int(input())
ad = input().rstrip()
failf = [0 for _ in range(l)]
j = 0
for i in range(1, l):
    while j &amp;gt; 0 and ad[i] != ad[j]:
        j = failf[j-1]
    if ad[i] == ad[j]:
        j += 1
        failf[i] = j
print(l - failf[-1])&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 주기는 전체 문자열(수열) 길이에서 전체의 실패 함수 값을 빼서 구할 수 있다는 것을 알게 되었다. 이를 응용해서 풀 수 있는 문제가 매우 많고, 이는 문자열뿐 아니라 수열에서도 사용할 수 있기 때문에 알아두는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 10266 시계 사진들&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/10266&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/10266&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 시곗바늘들이 이루고 있는 모양이 주어졌을 때 이 둘이 서로 일치할 수 있는지 찾는 문제이다. 이게 왜 문자열 검색 알고리즘인 KMP와 관련이 있지?라고 생각할 수 있지만, 조금만 생각해보면 쉽게 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관찰 1. 시계를 회전시켜도 두 시곗바늘이 이루는 각은 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 따라서 한 시계에서 시곗바늘을 정렬한 후 시곗바늘 사이에서 이루는 각을 계산하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관찰 2. 시계는 원형이므로 선형에서 탐색하기 위해서 한쪽을 두 배로 이어붙이면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 이는 원형에서 푸는 문제를 선형으로 해결할 때 많이 사용하는 테크닉 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 예제 1을 아래와 같이 해석할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1754278990200&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;6
1 2 3 4 5 6
7 6 5 4 3 1

step 1) 정렬
1 2 3 4 5 6
1 3 4 5 6 7

step 2) 바늘 사이 각도 (이웃한 항 간의 차이) 계산
1 1 1 1 1 359995 (6 -&amp;gt; 360001로 이동하는 것과 마찬가지)
2 1 1 1 1 359994 (7 -&amp;gt; 360001로 이동하는 것과 마찬가지)

step 3) 하나를 2배로 이어붙이기
1 1 1 1 1 359995 1 1 1 1 1 359995
2 1 1 1 359994

step 4)
이제 두 번째 수열이 첫 번째 수열의 부분배열로 포함되어 있는지
kmp 알고리즘으로 탐색하여 possible / impossible을 정할 수 있음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이를 구현하면 아래와 같다. (코드가 조금 더러워 보이지만 양해 바란다.)&lt;/p&gt;
&lt;pre id=&quot;code_1754279028084&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import sys
input = sys.stdin.readline
n = int(input())
arr1 = sorted([*map(int, input().split())])
arr2 = sorted([*map(int, input().split())])
c1 = [arr1[i+1] - arr1[i] for i in range(n-1)]
c2 = [arr2[i+1] - arr2[i] for i in range(n-1)]
c1.append(360000 + arr1[0] - arr1[-1])
c2.append(360000 + arr2[0] - arr2[-1])
c2.extend(c2[:-1])

failf = [0 for _ in range(n)]
j = 0
for i in range(1, n):
    while j &amp;gt; 0 and c1[i] != c1[j]:
        j = failf[j-1]
    if c1[i] == c1[j]:
        j += 1
        failf[i] = j
ans, j = 0, 0
for i in range(2*n-1):
    while j &amp;gt; 0 and c1[j] != c2[i]:
        j = failf[j-1]
    if c1[j] == c2[i]:
        j += 1
        if j == n:
            ans = 1
            break
print(&quot;possible&quot; if ans else &quot;impossible&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;lt;BOJ 10747 Censoring&amp;gt; &lt;a href=&quot;https://www.acmicpc.net/problem/10747&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/10747&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 문자열 S 내부에 등장하는 첫 번째 문자열 T를 제거하는 것을 반복하면서 문자열 T가 문자열 S에 나타나지 않도록 모두 삭제하여 검열된 문자열을 출력하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 이해는 예제를 보면 아주 쉽다.&lt;/p&gt;
&lt;pre id=&quot;code_1754279347025&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;input:
whatthemomooofun
moo

process:
w h a t t h e m o&quot;m o o&quot;o f u n
| delete
w h a t t h e&quot;m o       o&quot;f u n
| delete
w h a t t h e             f u n

output:
whatthefun&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문자열 제거하는 문제를 몇 번 보았다면 아마 스택을 이용해서 문자열을 제거할 때마다 스택에서 문자를 빼내는 방식을 많이 접해보았을 것이다. 하지만 이 문제는 제거해야 하는 문자도 입력으로 주어지고 길이가 꽤 길기 때문에 일반적인 스택만을 이용해서 풀기에는 역부족이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래와 같은 예시를 생각해 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1754279549866&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;input:
aabcaabcabcabcac
abcabca&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KMP 알고리즘을 사용해서 abcabca를 찾았다고 가정해보자. 그러면 아래와 같이 문자열을 제거한 이후 abcabca를 다시 매칭시키기 시작하는 위치는 아래와 같아야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1754279668676&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aabcaabcabcabcac
     abcabca

aabca       bcac
 abca       bca&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 KMP 알고리즘에서 사용하는 포인터 j의 위치가 abcabca의 5번째 문자인 b를 가리켜야 한다는 것이다. 그런데 이 역시 위 예시를 곰곰이 살펴 보면 해결 방법을 알 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1754279891818&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;aabcaabcabcabcac
 abcabca
     |
     j&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 앞쪽의 abca가 매칭되어 있어야 함에 주목하면 포인터 j의 위치는 앞선 탐색 과정에서 j의 위치였고, 어떤 문자열을 제거한 이후 j의 위치는 제거되지 않은 가장 뒷부분을 i가 지나칠 때 j의 값이 됨을 알 수 있다. 따라서 매 i에 대한 반복이 종료될 때마다 그때 j의 위치를 저장해두면 여기로 j가 이동하여 빠른 탐색이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이때 주의할 점이 있는데, 문자열이 스택에서 pop될 때 pos도 같이 지워줘야 하기 때문에 pos도 스택의 형태로 구현하고 검열된 문자열을 저장하는 스택과 인덱스를 일치시켜주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위 문제를 해결하는 코드는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1754280092154&quot; class=&quot;python&quot; data-ke-language=&quot;python&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;s = input().rstrip()
t = input().rstrip()
failf = [0]*len(t)
j = 0
for i in range(1, len(t)):
    while j &amp;gt; 0 and t[i] != t[j]:
        j = failf[j-1]
    if t[i] == t[j]:
        j += 1
        failf[i] = j
j = 0
st = []
pos = [0]
for i in range(len(s)):
    st.append(s[i])
    while j &amp;gt; 0 and s[i] != t[j]:
        j = failf[j-1]
    if s[i] == t[j]:
        j += 1
        if j == len(t):
            for _ in range(len(t)):
                st.pop()
                j = pos.pop()
    pos.append(j)
print(''.join(st))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 KMP 알고리즘을 응용하는 문제 3가지를 살펴보았다. 이외에도 플래티넘 상위 수준의 더 어려운 KMP 알고리즘 문제들이 있지만, 이는 나중에 기회가 된다면 다뤄보도록 하겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글은 아마 Z 알고리즘에 대해 작성할 것 같다.&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;002&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/002.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/002.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;</description>
      <category>PS</category>
      <category>KMP</category>
      <category>string</category>
      <category>문자열알고리즘</category>
      <category>백준</category>
      <author>kdy40929</author>
      <guid isPermaLink="true">https://kdy40929.tistory.com/5</guid>
      <comments>https://kdy40929.tistory.com/5#entry5comment</comments>
      <pubDate>Mon, 4 Aug 2025 13:03:23 +0900</pubDate>
    </item>
  </channel>
</rss>