<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>The Story of Joon</title>
    <link>https://topology-blog.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Mon, 12 Oct 2026 15:26:20 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>jo_on</managingEditor>
    <image>
      <title>The Story of Joon</title>
      <url>https://tistory1.daumcdn.net/tistory/2668306/attach/b50336fe127c493f96d6e3e1396210ad</url>
      <link>https://topology-blog.tistory.com</link>
    </image>
    <item>
      <title>Cayley&amp;ndash;Hamilton 정리</title>
      <link>https://topology-blog.tistory.com/22</link>
      <description>&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;참고: 이 글은 필자의 기록용으로 불친절하거나 일부 선행 지식을 필요로 할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;선형대수학에서 케일리 해밀턴 정리(Cayley&amp;ndash;Hamilton theorem)는 임의의 정사각행렬 $A$에 대하여 특성다항식&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$f_A(x)=\det(xI-A)$$&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;에 행렬 $A$를 대입하면 영행렬이 나온다는 정리이다. 수학의 정석에서 케일리 해밀턴 정리를 본 적 있는 독자라면 2x2 행렬에서 아래와 같은 식으로 기억하고 있을 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$A^2-(a+d)A+(ad-bc)I=O$$&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 식은 사실 2x2 행렬의 특성다항식에 $A$를 대입한 꼴로, 임의의 $n\times n$ 정사각행렬에서도 이 결과가 성립한다. 심지어 행렬의 성분이 임의의 체 $F$의 원소일 때도 성립한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;간단해 보이는 정리이지만 증명은 생각보다 간단하지 않다. 혹자는 아래와 같은 오류를 범하기도 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$f_A(A)=\det(AI-A)=\det(O)=0$$&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;...당연하지만 틀린 증명이다. $xI$는 주 대각선에 스칼라인 $x$가 있는 행렬로, 여기에 $A$를 대입한다는 건 있을 수 없다. 이게 가능하다면 $x^n=\det(xI)$이므로 $A^n=\det(AI)=\det(A)$라는 말도 안되는 결론이 얻어진다. 애초에 위 식에서도 우변은 0이 아니라 영행렬이 나와야 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 정확한 증명은 무엇일까? &lt;a href=&quot;https://en.wikipedia.org/wiki/Cayley%E2%80%93Hamilton_theorem#A_direct_algebraic_proof&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위키피디아의 해당 문서&lt;/a&gt;를 보면 행렬의 기본적인 연산만으로 하는 증명이 나와있다. 정확한 증명이지만 솔직히 말해 선형대수학을 공부하는 입장에서 얻어갈 수 있는건 별로 없다고 생각한다. 위키피디아에 나와있는 증명은 아니지만, 필자는 삼각화를 이용한 증명을 좋아한다. 단계가 많고 선형대수학의 기초 지식을 필요로 하지만, 행렬과 선형 변환에 대한 직관을 좀더 얻어갈 수 있을 것이다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;행렬의 닮음&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;두 $n\times n$ 행렬 $A$, $B$에 대해 역행렬이 존재하는 $P$가 $B=PAP^{-1}$를 만족하면 $A$와 $B$를 &lt;b&gt;닮음&lt;/b&gt;이라고 한다. 행렬의 닮음은 벡터공간 $V$의 두 기저 $\mathcal{B}$와 $\mathcal{B}'$에 대해 어떤 선형변환 $T:V\to V$를 각각의 기저에 대해 표현했을 때 나타난다. 반대로, 닮음인 행렬에 대해 이러한 조건을 만족하는 $T$와 두 기저를 찾는 것도 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;닮음인 행렬은 같은 선형변환을 표현하는 것이 가능하기 때문에 많은 성질을 공유한다. 대표적으로 행렬식, 고윳값, 특성다항식 등이 모두 같다. 행렬식이 행렬곱에 대해 분리되는 성질로 어렵지 않게 증명 가능하다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$\det(xI-B)=\det(P(xI-A)P^{-1})=\det(P)\det(xI-A)\det(P^{-1})=\det(xI-A)$$&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;따라서 우리의 목적은 $A$와 닮음인 행렬 중에 만만한 녀석 $B$를 찾아서, $A$ 대신 $B$에 대해 케일리 해밀턴 정리가 성립함을 증명하는 것이다. 그러면 $O=f_B(B)=f_A(B)=Pf_A(A)P^{-1}$이므로, $f_A(A)=O$인 것도 증명된다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;대각화와 삼각화&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;만약 $A$가 대각화가 가능하다면 어떨까? 대각행렬의 케일리 해밀턴 정리는 직접 증명해보면 알겠지만, 매우 쉽다. 그렇지만 안타깝게도 모든 행렬이 대각화가 가능하지 않다. 스칼라 체 $F$를 확장하더라도 안될 행렬은 안된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그렇지만 대각행렬과 비슷한 이유로 케일리 해밀턴 정리가 만만한 행렬이 있는데, 바로 삼각행렬이다. $A$와 닮음인 삼각행렬을 찾는 과정을 &lt;b&gt;삼각화&lt;/b&gt;라고 하는데, 삼각화 역시 모든 행렬에 대해서 가능하지는 않다. 그렇지만 다행인 점은, 모든 행렬은 $F$를 확장하면 삼각화가 가능해진다! 따라서 우리의 목표가 좀더 명확해졌는데, $F$를 잘 확장해서 $A$를 삼각화하는 것이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;삼각화 과정에 들어가기 전에, 삼각화가 선형변환의 입장에서 어떤 의미인지 살펴보자. 선형변환 $T:V\to V$에 대해 어떤 기저 $\mathcal{B}=\{\alpha_1,\cdots,\alpha_n\}$가 존재해서 $T$를 $\mathcal{B}$에 대해 표현한 행렬 $[T]_{\mathcal{B}}$이 (상)삼각행렬이라는 것은 $T(\alpha_i)$가 $\alpha_1,\cdots,\alpha_i$로 span한 부분공간 $W_i$에 들어간다는 의미이다. 이는 다시 말하면 어떤 $\lambda_i$가 존재하여 $(T-\lambda_iI)\alpha_i\in W_{i-1}$가 된다는 의미이다. 그런데 $T(W_{i-1})$ 또한 $W_{i-1}$의 부분공간이기 때문에, $W_{i-1}$의 원소와 $\alpha_i$의 선형 결합으로 표현되는 $W_i$의 임의의 벡터는 $(T-\lambda_iI)$에 의해 $W_{i-1}$의 원소로 보내진다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;결국 삼각화라는 것은 $T(W_i)\subseteq W_i$이고 $\dim(W_i)=i$인 부분공간의 체인 (&lt;a href=&quot;https://en.wikipedia.org/wiki/Flag_(linear_algebra)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;flag&lt;/a&gt;라는 용어가 있다)&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$\{0\}=W_0\subset W_1\subset\cdots\subset W_n=V$$&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;를 찾는 것과 같고, 이런 상황에서 적절한 $\lambda_1,\cdots,\lambda_n$이 존재하여 $(T-\lambda_iI)(W_i)\subseteq W_{i-1}$이 된다 (고윳값에 대한 지식이 있는 눈치 빠른 독자는 $\lambda_i$가 고윳값이 된다는 것을 알 수 있는데, $W_{i-1}$이 $W_i$보다 차원이 낮으므로 필연적으로 $T-\lambda_iI$의 kernel이 $\{0\}$이 아니기 때문이다). 이 성질을 $i=n$에서 시작해 $i=1$까지 반복적으로 적용하면&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$(T-\lambda_1I)\cdots(T-\lambda_nI)(V)=\{0\}$$&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 되는데, 이는 다시 말해&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$(T-\lambda_1I)\cdots(T-\lambda_nI)=O$$&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;임을 의미한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 삼각행렬에서의 케일리 해밀턴 정리가 쉽게 얻어지는데, $[T]_{\mathcal{B}}$는 앞서 얘기했듯이 삼각행렬이면서 주 대각선에는 $\lambda_1,\cdots,\lambda_n$이 놓이게 되므로 특성다항식이 $(x-\lambda_1)\cdots(x-\lambda_n)$이 되기 때문이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이제 우리의 목표가 한층 구체화되었는데, 우리는 다음과 같은 알고리즘으로 $T$를 삼각화할 것이다. 우선 $\lambda_1$과 $\alpha_1$을 찾아서 $(T-\lambda_1I)(\alpha_1)=\mathbf{0}$이 되도록 한다. 그 다음 $\lambda_2$와, $\alpha_1$과 선형 독립인 $\alpha_2$를 찾아서 $(T-\lambda_2I)(\alpha_2)$가 $\alpha_1$이 span한 공간 $W_1$에 들어가도록 한다. 이 과정을 반복하여 $n$번째 단계에서는 $\lambda_n$과, $\alpha_1,\cdots,\alpha_{n-1}$과 선형 독립인 $\alpha_n$을 찾아서 $(T-\lambda_nI)(\alpha_n)$이 $\alpha_1,\cdots,\alpha_{n-1}$이 span한 공간 $W_{n-1}$에 들어가도록 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;요약하면, 우리는 임의의 전체가 아닌 부분공간 $W$에 대해 $\lambda$와 $\alpha\notin W$를 찾아서 $(T-\lambda I)\alpha\in W$가 되기를 원한다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Annihilating polynomial&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;첫 번째로 해야 할 일은, 일단 $q(T)=O$이 되는 아무 다항식 $q$를 찾는 것이다. 이런 다항식을 $T$의 &lt;b&gt;annihilating polynomial&lt;/b&gt;이라고 한다. 이것을 찾는 것은 생각보다 어렵지 않은데, 우선 $T$를 나타내는 행렬 $A$는 $n^2$차 벡터로 볼 수 있다. 따라서 $n^2+1$개의 행렬&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$I,A,A^2,\cdots,A^{n^2}$$&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;은 $n\times n$ 행렬로 이루어진 $n^2$차원 벡터공간에서 선형 독립이 될 수 없고, 이 말은 즉 전부 0은 아닌 스칼라 $c_0,\cdots,c_{n^2}\in F$이 존재하여&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$c_0I+c_1A+c_2A^2+\cdots+c_{n^2}A^{n^2}=O$$&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 된다는 의미이다. 우리가 원하는 다항식 $q(x)=c_0+c_1x+\cdots+c_{n^2}x^{n^2}$ 하나를 찾았다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 다음 단계로 $q$를 일차식들로 분해해야 한다. 그러나 여기서 문제에 봉착한다. 그것은 $q$를 일차식들로 분해하는게 $F$에서는 불가능할 수 있다는 것이다. 간단한 예시로 $F=\mathbb{Q}$일 때 $x^2-2$나, $F=\mathbb{R}$일 때 $x^2+1$같은 다항식은 분해가 되지 않는다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그렇지만 이 문제는 나중에 해결하고, 일단 분해가 되었다고 가정하자. 즉 우리는&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$$(T-a_mI)(T-a_{m-1}I)\cdots(T-a_1I)=O$$&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이 되는 $a_1,\cdots,a_m$을 찾았다. 이 말은 아무 벡터 $v\in V$나 가져와서 $(T-a_1I)$부터 $(T-a_mI)$까지 순서대로 적용하다 보면 언젠가는 영벡터가 된다는 뜻이다. 그러면 여기서 이전 절의 마지막 문단의 목표를 어떻게 달성해야할지 알 수 있다. 전체가 아닌 부분공간 $W$에 대해 아무 벡터 $v\notin W$를 잡는다. 여기에 $(T-a_1I)$부터 $(T-a_mI)$까지 하나씩 적용하다 보면 $(k-1)$번째까지는 $(T-a_{k-1}I)\cdots(T-a_1I)v\notin W$이었는데 $k$번째를 적용한 순간 $(T-a_kI)\cdots(T-a_1I)v\in W$이 되는 순간을 포착할 수 있다. 왜냐하면 시작할 때는 $v\notin W$이었는데 마지막 순간에는 $(T-a_mI)\cdots(T-a_1I)v=\mathbf{0}$이 되어야 하기 때문이다. 이후에는 $\lambda=a_k$, $\alpha=(T-a_{k-1}I)\cdots(T-a_1I)v$로 잡으면 목적 달성이다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;그렇다면 이제 남은 것은 $F$를 확장해서 $q$를 일차식들로 분해하는 것이다.&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Splitting field&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;대수학 지식이 있는 독자라면 '아 이제 $F$의 algebraic closure나 $q$에 대한 splitting field를 잡으면 되겠군'이라고 생각하고 넘어갈 수도 있다. 이게 충분하다고 생각되면 이 절은 읽지 않아도 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;$F$를 포함하면서 체인 것을 &lt;b&gt;확장체(extension field)&lt;/b&gt;라고 하고, 다항식 $q$에 대해 $q$가 일차식으로 분해되는 가장 작은 확장체를 $q$에 대한 $F$의 &lt;b&gt;분해체(splitting field)&lt;/b&gt;라고 한다. 용어가 어려워 보이지만, 알고 보면 별 것 아니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;앞서 언급한 $F=\mathbb{Q}$일 때 $q(x)=x^2-2$의 예시를 생각해 보자. 우리는 $\sqrt{2}$라는 수를 알고 있지만, 이는 유리수의 세계 $\mathbb{Q}$에서는 존재하지 않는 수이다. 따라서 우리는 '$q(x)=x^2-2$에 대입하면 0이 되는' $\sqrt{2}$라는 수를 새로 만들고, 연산을 위해 $a+b\sqrt{2}$꼴의 수를 모두 추가해 $\mathbb{Q}(\sqrt{2})=\{a+b\sqrt{2}:a,b\in\mathbb{Q}\}$라는 새로운 수 체계를 만든다. 그러면 (약간의 추가 확인이 필요하지만) $\mathbb{Q}(\sqrt{2})$는 $\mathbb{Q}$를 포함하는 체가 되고, $\mathbb{Q}(\sqrt{2})$는 $x^2-2$에 대한 splitting field가 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;비슷하게, $d$차 다항식 $q(x)$를 열심히 인수분해했으나 더이상 일차식으로 분해되지 않는 $d_1$차 인수 $q_1(x)$가 있다고 하자. 그러면 우리는 $q_1(\alpha)=0$이 되는 $\alpha$라는 수를 정의하고, $F(\alpha)=\{c_0+c_1\alpha+\cdots+c_{d_1-1}\alpha^{d_1-1}:c_i\in F\}$라는 새로운 수 체계를 만들면, $F(\alpha)$는 체가 되면서 (역시 확인이 필요하다) $q_1(x)$에서 일차식 인수 최소 하나를 분리해낼 수 있다. 이 과정을 최대 $d$번 반복하면, 결국 $q$를 일차식으로 완전히 분해하는 확장체를 만들 수 있게 된다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;증명은 확장체에서 했으나 특성다항식 $f_A$의 계수는 결국 $F$의 원소이기 때문에, $F$에서도 케일리 해밀턴 정리가 성립한다.&lt;/p&gt;</description>
      <category>Mathematics</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/22</guid>
      <comments>https://topology-blog.tistory.com/22#entry22comment</comments>
      <pubDate>Sun, 5 Jan 2025 01:22:12 +0900</pubDate>
    </item>
    <item>
      <title>Linear Algebra in Problem Solving (3)</title>
      <link>https://topology-blog.tistory.com/21</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tistory.joonhyung.xyz/18&quot;&gt;Linear Algebra in Problem Solving (1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tistory.joonhyung.xyz/19&quot;&gt;Linear Algebra in Problem Solving (2)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://topology-blog.tistory.com/21&quot;&gt;Linear Algebra in Problem Solving (3)&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;기존 두 포스트에서는 선형대수학에 등장하는 기본적인 행렬 연산과 행렬에 관련된 중요한 식을 어떻게 효율적으로 계산하는지에 대해 알아보았다. 하지만 PS에서 대놓고 이런 값을 구하라고 요구하는 문제는 드물고, 보통 선형대수학을 응용해야 하는 문제가 나오게 된다. 대표적인 예시가 1편에서 나왔듯이 XOR을 \(\mathbb{F}_2\)에서 벡터의 덧셈으로 생각하는 방식이다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 포스트에서는 좀더 고급 응용인, 조합론에서 선형대수학이 응용되는 예시를 다룬다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이분 그래프의 인접 행렬&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;PS는 물론이고 CS와 이산수학 전반적으로 매우 중요한 대상인 그래프는 수학적으로 &lt;a href=&quot;https://en.wikipedia.org/wiki/Adjacency_matrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;인접 행렬(adjacency matrix)&lt;/a&gt;로 많이 표현된다. 이는 간단히 말해 각 행과 각 열이 그래프의 정점을 나타내고, \(i\)번 정점에서 \(j\)번 정점으로 가는 간선이 있을 경우 \((i, j)\) 위치에 1이 적혀 있고 그렇지 않을 경우 0이 적혀있는 그래프이다.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Bipartite_graph&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이분그래프(bipartite graph)&lt;/a&gt;에서도 비슷한 형태의 행렬(&lt;a href=&quot;https://en.wikipedia.org/wiki/Adjacency_matrix#Of_a_bipartite_graph&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;biadjacency matrix&lt;/a&gt;라고도 불린다)이 정의 가능한데, 각 행이 첫 번째 정점 집합에 속한 정점, 각 열이 두 번째 정점 집합에 속한 정점일 때 마찬가지로 대응되는 두 정점이 연결되어 있는지 여부에 따라 1 또는 0이 적혀있는 행렬이다. 첫 번째 정점 집합과 두 번째 정점 집합의 크기가 같을 경우 이 행렬의 행렬식은 이 이분 그래프의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Perfect_matching&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;완전 매칭(perfect matching)&lt;/a&gt;의 개수와 연관이 있다. \(n\times n\) 정사각행렬 \(A\)의 행렬식의 정의를 다시 살펴보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det(A)=\sum_{\sigma\in S_n}\left(\operatorname{sgn}(\sigma)\prod_{i=1}^n a_{i,\sigma_i}\right)\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(A\)가 이분 그래프의 인접 행렬이라면 \(a_{i,\sigma_i}\)들의 곱은 첫 번째 집합의 정점 \(i\)와 두 번째 집합의 정점 \(\sigma_i\)가 모든 \(i\)에 대해 연결되어 있을 때만 1이 된다. 이러한 \(\sigma\)는 완전 매칭을 나타낸다는 것을 알 수 있다. 따라서 위 행렬식의 정의에서 0이 아닌 항은 정확히 완전 매칭의 개수만큼 남아있게 된다.&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;하지만 공교롭게도 \(\operatorname{sgn}(\sigma)\)의 존재 때문에 행렬식이 완전 매칭의 개수가 되지는 못한다. 완전 매칭의 개수와 같아지는 식은 다음과 같이 &lt;a href=&quot;https://en.wikipedia.org/wiki/Permanent_(mathematics)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;permanent&lt;/a&gt;라는 식이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\operatorname{per}(A)=\sum_{\sigma\in S_n}\prod_{i=1}^n a_{i,\sigma_i}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(A\)가 이분 그래프의 인접 행렬일 때 이 식은 정확히 완전 매칭의 개수와 같아진다. 그러나 아쉽게도 이 식은 행렬식처럼 다항 시간에 구할 수 있는 방법이 알려지지 않았다. 인접 행렬의 경우처럼 행렬의 모든 원소가 0 또는 1인 경우에조차 #P-complete이며, 이는 NP-complete보다 어렵다는 뜻이다. 따라서 다항 시간 알고리즘을 찾으려는 시도는 하지 않는 것이 좋다.&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;아쉬운 대로 행렬식을 이용해 완전 매칭의 개수의 홀짝성 정도는 알 수 있다. \(\mathbb{F}_2\)에서는 1과 -1이 같기 때문이다. 만일 홀수라는 결론(즉 \(\mathbb{F}_2\)에서 1)을 얻었다면, 완전 매칭의 개수가 0은 아니라는 뜻이므로 적어도 하나의 완전 매칭의 존재가 보장된다. 하지만 짝수라는 결론을 얻었다고 해서 완전 매칭이 존재하지 않는 것은 아니므로, 단순히 존재성 판단 용도로 사용하기에는 흔한 이분 매칭 알고리즘보다 나을 것이 전혀 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반적인 그래프의 인접 행렬&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이분 그래프의 인접 행렬의 행렬식은 (완전히 만족스럽지는 않지만) 완전 매칭의 개수와 관련이 있음을 알아보았다. 일반적인 그래프의 경우 인접 행렬의 행렬식에 직관적인 의미를 부여하기는 쉽지 않다. 순열의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Permutation#Cycle_notation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;cycle decomposition&lt;/a&gt;을 이용하는 것이 한 가지 방법인데, 행렬식의 정의를 구성하는 각 항은 어떤 순열 \(\sigma\)에 대하여 행렬의 \((i, \sigma_i)\) 위치의 원소들의 곱이라는 것을 생각해 보자. 이 순열 \(\sigma\)를 cycle decomposition 하고 나면 각 cycle은 그래프에서의 cycle과 대응된다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HUMLF/btrUZXVI7LT/oDKwEuxY1RpQCwV2V8hMdk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HUMLF/btrUZXVI7LT/oDKwEuxY1RpQCwV2V8hMdk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HUMLF/btrUZXVI7LT/oDKwEuxY1RpQCwV2V8hMdk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHUMLF%2FbtrUZXVI7LT%2FoDKwEuxY1RpQCwV2V8hMdk%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;632&quot; height=&quot;602&quot; data-origin-width=&quot;632&quot; data-origin-height=&quot;602&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;이 그래프의 인접 행렬은 아래와 같이 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\begin{bmatrix}0&amp;amp;\color{red}{1}&amp;amp;1&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;\color{red}{1}&amp;amp;0&amp;amp;0\\ \color{red}{1}&amp;amp;0&amp;amp;0&amp;amp;1&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;\color{red}{1}\\1&amp;amp;0&amp;amp;0&amp;amp;\color{red}{1}&amp;amp;0\end{bmatrix}\]&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;이 자체로는 큰 의미를 찾기 어렵지만, 인접 행렬의 행렬식이 그래프의 사이클과 관련이 있다는 점을 이용해 사이클이 개수가 적거나 특수한 구조를 가진 그래프의 인접 행렬을 효율적으로 구하는 PS 문제가 Open Cup과 같은 대회에서 종종 출현한다. 예시는 아래 연습문제에서 찾아볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Lindstr%C3%B6m%E2%80%93Gessel%E2%80%93Viennot_lemma&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Lindstr&amp;ouml;m&amp;ndash;Gessel&amp;ndash;Viennot lemma&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;줄여서 LGV라고 많이 부르는 이 보조정리는 특정 조건을 만족하는 경로 조합의 가짓수를 셀 때 이용된다. 행렬식의 흥미로움을 맛볼 수 있는 재미있는 예시이다. 아래와 같이 출발지 4개와 도착지 4개가 있는 길이 복잡하게 얽혀 있다고 생각해 보자. 각 \(i\)에 대해 \(i\)번 출발지에서 \(i\)번 도착지로 이어지는 네 개의 경로를 만들려고 하는데, 경로는 무조건 오른쪽(+x 방향)으로만 향하고 경로끼리 교차하지 않게 하고 싶다. 모두 몇 가지 방법이 있을까?&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;1166&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DHwNa/btrUXpygsRP/QypDUDMy2jO1yNqFfLUnb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DHwNa/btrUXpygsRP/QypDUDMy2jO1yNqFfLUnb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DHwNa/btrUXpygsRP/QypDUDMy2jO1yNqFfLUnb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDHwNa%2FbtrUXpygsRP%2FQypDUDMy2jO1yNqFfLUnb0%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;1166&quot; height=&quot;654&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;654&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;예를 들어 아래와 같은 경로 조합이 가능하다.&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;1180&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JDHoi/btrU1qwqCiM/IaKxC9B0VNDUQKjqUnx4kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JDHoi/btrU1qwqCiM/IaKxC9B0VNDUQKjqUnx4kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JDHoi/btrU1qwqCiM/IaKxC9B0VNDUQKjqUnx4kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJDHoi%2FbtrU1qwqCiM%2FIaKxC9B0VNDUQKjqUnx4kK%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;1180&quot; height=&quot;686&quot; data-origin-width=&quot;1180&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;반면 아래 경로 조합은 교차했기 때문에 탈락이다.&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;1184&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQPHq1/btrU3a7E4Gb/st56hroY1JkKyVyFJKgI3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQPHq1/btrU3a7E4Gb/st56hroY1JkKyVyFJKgI3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQPHq1/btrU3a7E4Gb/st56hroY1JkKyVyFJKgI3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQPHq1%2FbtrU3a7E4Gb%2Fst56hroY1JkKyVyFJKgI3K%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;1184&quot; height=&quot;676&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;676&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;컴퓨터의 도움이 있다면, 단순히 \(i\)번 출발지에서 \(j\)번 도착지로 가는 경우의 수 \(a_{ij}\)는 다이나믹 프로그래밍으로 어렵지 않게 구할 수 있을 것이다. 만일 겹치지 않아야 한다는 조건이 없다면 \(a_{11}\)부터 \(a_{44}\)까지 모두 곱하면 그만이다. 하지만 교차하는 경로 조합은 세어서는 안되기 때문에 빼줘야 한다. 이걸 어떻게 할 수 있을까?&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;LGV에 의하면, 놀랍게도 아래 행렬의 행렬식이 우리가 원하는 경우의 수와 같아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[A=\begin{bmatrix}a_{11}&amp;amp;a_{12}&amp;amp;a_{13}&amp;amp;a_{14}\\a_{21}&amp;amp;a_{22}&amp;amp;a_{23}&amp;amp;a_{24}\\a_{31}&amp;amp;a_{32}&amp;amp;a_{33}&amp;amp;a_{34}\\a_{41}&amp;amp;a_{42}&amp;amp;a_{43}&amp;amp;a_{44}\end{bmatrix}\]&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\)개의 출발지와 \(n\)개의 도착지가 있는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Directed_acyclic_graph&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;directed acyclic graph (DAG)&lt;/a&gt;가 있다고 하자. &lt;u&gt;교차하지 않는&lt;/u&gt; (하지만 출발지와 도착지의 번호가 다를 수 있는) 모든 경로 조합 \(P\)에 대해, \(i\)번 출발지에서 출발했을 때 어떤 도착지에 도착하냐에 따라 순열 \(\sigma(P)\)를 만들 수 있다. 다시 말해 \(i\)번 출발지에서 출발하면 \(\sigma(P)_i\)번 도착지에 도착한다. 이때 \((i, j)\) 위치에 \(i\)번 출발지에서 \(j\)번 도착지로 가는 경로의 수를 넣은 행렬 \(A\)에 대해 다음이 성립한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det(A)=\sum_{P:\text{non-intersecting path tuple}}\operatorname{sgn}(\sigma(P))\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 다음과 같이 쓸 수도 있다. 어떤 순열 \(\sigma\)에 대해 \(i\)번 출발지에서 출발해 \(\sigma_i\)번 도착지에서 도착하는 &lt;u&gt;교차하지 않는&lt;/u&gt; 경로 조합의 가짓수를 \(c(\sigma)\)라고 하면, 아래 식이 성립한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det(A)=\sum_{\sigma:[n]\to[n]}\operatorname{sgn}(\sigma)c(\sigma)\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시에서는 겹치지 않는 경로 조합이 가능한 경우가 \(i\)번에서 출발해 \(i\)번에 도착하는 경우밖에 없기 때문에 이 식에서 \(\sigma\)가 항등함수인 경우만 남아서 성립하게 된다.&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=2\)인 경우를 먼저 보면 도움이 된다. 이 경우 행렬식이 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det(A)=a_{11}a_{22}-a_{12}a_{21}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(a_{11}a_{22}\)가 나타내는 수는 교차를 고려하지 않고 1, 2번 출발지에서 출발해 순서대로 1, 2번 도착지에 도착하는 경우의 수이고, \(a_{12}a_{21}\)이 나타내는 수는 교차를 고려하지 않고 1, 2번 출발지에서 출발해 순서대로 2, 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;1448&quot; data-origin-height=&quot;408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdn1ks/btrUWaux7cA/nSsSfkx7Zr5nGaMk9Z2vgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdn1ks/btrUWaux7cA/nSsSfkx7Zr5nGaMk9Z2vgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdn1ks/btrUWaux7cA/nSsSfkx7Zr5nGaMk9Z2vgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcdn1ks%2FbtrUWaux7cA%2FnSsSfkx7Zr5nGaMk9Z2vgK%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;1448&quot; height=&quot;408&quot; data-origin-width=&quot;1448&quot; data-origin-height=&quot;408&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_{11}a_{22}\)에서 카운팅하는 교차 경로 조합이고, 오른쪽은 \(a_{12}a_{21}\)에서 카운팅한 교차 경로 조합인데, 이 둘은 행렬식에서 부호가 반대가 되기 때문에 상쇄되어 최종 식에서 나타나지 않게 된다.&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;b&gt;교차점에서 둘의 진행방향을 한번 바꿔주는&lt;/b&gt; 것이다. 위의 \(n=4\)인 경우에서 탈락했던 예시를 다시 한번 보자. 그냥 \(a_{11}\cdots a_{44}\)를 계산하면 이 예시까지 카운팅하게 된다. 이를 상쇄시키기 위해 처음으로 생긴 교차점에서 둘의 진행 방향을 바꿔 보자. (&lt;a href=&quot;https://www.acmicpc.net/problem/2136&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;개미 문제&lt;/a&gt;를 풀어본 경험이 있다면 이 '진행 방향을 바꾸는' 아이디어가 좀더 와닿을 수 있다.)&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;1184&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yUjuL/btrU0uZ93CO/6W5N9Iv0SL24vMcIKQk5ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yUjuL/btrU0uZ93CO/6W5N9Iv0SL24vMcIKQk5ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yUjuL/btrU0uZ93CO/6W5N9Iv0SL24vMcIKQk5ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyUjuL%2FbtrU0uZ93CO%2F6W5N9Iv0SL24vMcIKQk5ck%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;1184&quot; height=&quot;672&quot; data-origin-width=&quot;1184&quot; data-origin-height=&quot;672&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;이 예시 역시 탈락해야 할 경로 조합으로, 위의 예시와 짝지어 보자. 진행 방향을 한번 바꿨기 때문에 이 바뀐 경로 조합을 카운팅하는 순열은 원래 순열과 홀짝성이 달라지게 된다. 이는 다시 말해 \(\operatorname{sgn}\)의 부호가 뒤집힌다는 뜻이므로, 행렬식에서 두 예시가 상쇄되어 우리의 목적을 달성하게 된다.&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;LGV는 Young tableaux라는 것의 개수를 세는 문제에도 활용될 수 있는데, 자세한 내용은 생략한다. 관심 있는 독자는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Lindstr%C3%B6m%E2%80%93Gessel%E2%80%93Viennot_lemma#Schur_polynomials&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위키피디아&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;지금까지는 경우의 수를 세는 문제와 연관짓기 위해 LGV의 특수한 경우를 설명했다. 아주 약간 더 일반적인 형태는, 그래프의 간선에 가중치 \(w:E(G)\to\mathbb{R}\)가 주어져 있는 형태이다. (가중치는 대부분 독자에게 익숙한 실수 \(\mathbb{R}\)을 사용했지만, 아무 &lt;a href=&quot;https://en.wikipedia.org/wiki/Ring_(mathematics)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;ring&lt;/a&gt;을 써도 상관없다.) 이 경우 어떤 경로(혹은 경로 조합)의 가중치는 그 경로(혹은 경로 조합)에 있는 간선의 가중치의 곱으로 정의하고, \(a_{ij}\)는 \(i\)번 출발지에서 \(j\)번 도착지로 가는 경로의 수 대신 \(i\)번 출발지에서 \(j\)번 도착지로 가는 모든 가능한 경로들의 가중치의 합으로 정의한다. 그러면 LGV는 다음과 같은 형태가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det(A)=\sum_{P:\text{non-intersecting path tuple}}\operatorname{sgn}(\sigma(P))w(P)\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 논의한 내용은 모든 가중치가 1인 특수한 경우이다. 증명은 특수한 경우와 완전히 동일하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Cauchy%E2%80%93Binet_formula&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cauchy&amp;ndash;Binet formula&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cauchy&amp;ndash;Binet formula는 두 행렬의 곱으로 나타내는 정사각행렬의 행렬식을 구하는 방법에 관한 식이다. \(A\)가 \(n\times m\) 행렬이고 \(B\)가 \(m\times n\) 행렬일 때, 두 행렬의 곱 \(AB\)의 행렬식을 아래와 같이 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det(AB)=\sum_{I\subseteq{[m]\choose n}}\det(A_{[n],I})\det(B_{I,[n]})\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식이 익숙하지 않은 독자를 위해 간단히 설명하면, \([m]=\{1,\cdots,m\}\)의 모든 크기 \(n\)짜리 부분집합 \(I\)에 대해, \(A\)에서 번호가 \(I\)에 들어있는 열벡터를 모은 정사각행렬의 행렬식과 \(B\)에서 번호가 \(I\)에 들어있는 행벡터를 모은 정사각행렬의 행렬식을 곱한 값을 모든 \(I\)에 대해 더하면 \(AB\)의 행렬식이 된다는 것이다.&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;이 식은 조합론에서 상당히 유명하고 중요한 식으로, 증명도 순수 대수적인 증명부터 조합적인 증명까지 매우 다양하다. 그중에서 대수와 조합을 모두 적절히 맛볼 수 있는 증명이 위의 LGV를 이용한 증명이다. (LGV를 C&amp;ndash;B formala의 일반화로 볼 수도 있다.)&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\)개, 그리고 도착지 \(n\)개를 만든다. 그리고 \(i\)번 출발지에서 \(j\)번 경유지로 가중치 \(A_{ij}\)인 간선을 연결하고, \(j\)번 경유지에서 \(k\)번 도착지로 가중치 \(B_{jk}\)인 간선을 만든다. 그러면 \(i\)번 출발지에서 \(k\)번 도착지로 가는 경로의 가중치 총합은 아래와 같이 나타나고, 이 값은 정확히 두 행렬의 곱 \(AB\)의 \(i,k\) 위치 원소이다. 그래프의 인접 행렬로 경로의 경우의 수 구하는 방식을 떠올리면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\sum_{j=1}^mA_{ij}B_{jk}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 LGV에 의하면 \(AB\)의 행렬식에는 출발지에서 도착지까지 가는 &lt;u&gt;교차하지 않는&amp;nbsp;않는 경로 조합&lt;/u&gt;으로만 이루어진 식이 남는다. 출발지의 집합을 \(S\) 도착지의 집합을 \(E\)라고 하면, 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det(AB)=\sum_{P:\text{non-intersecting path tuple } S\Rightarrow E}\operatorname{sgn}(\sigma(P))w(P)\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;교차하지 않는다는 것은, 경로 조합이 경유지를 정확히 \(n\)개 지난다는 의미이다. 이 경유지의 집합을 \(I\)라고 쓰면, 출발지에서 도착지까지의 경로 조합 \(P\)를 \(S\)에서 \(I\)까지의 경로 조합 \(P_1\)과 \(I\)에서 \(E\)까지의 경로 조합 \(P_2\)로 분해할 수 있다. 이때 중요한 것은 \(\operatorname{sgn}(\sigma(P))=\operatorname{sgn}(\sigma(P_1))\operatorname{sgn}(\sigma(P_2))\)가 성립한다는 것이다. (\(\operatorname{sgn}\) 함수가 순열의 곱셈을 보존하기 때문이다.) 따라서 위 식을 아래와 같이 다시 쓸 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;\[\sum_{I\subseteq{[m]\choose n}}\left(\sum_{P_1}\operatorname{sgn}(\sigma(P_1))w(P_1)\sum_{P_2}\operatorname{sgn}(\sigma(P_2))w(P_2)\right)\]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;내부의 두 summation의 값은 (LGV에 의해, 혹은 자명하게) 각각 \(\det(A_{[n],I})\)와 \(\det(B_{I,[n]})\)가 되므로, 증명이 완료된다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Kirchhoff%27s_theorem&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Matrix-tree theorem&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;위 Cauchy&amp;ndash;Binet formula의 흥미로운 응용 중 하나가 바로 matrix-tree theorem이다. Matrix-tree theorem은 (무향) 그래프의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Spanning_tree&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;스패닝 트리&lt;/a&gt;의 개수를 쉽게 구할 수 있는 방법을 알려준다. 먼저 Laplacian matrix라는 것을 정의하는데, 정점의 개수가 \(n\)일 때 행렬의 \((i, j)\) 위치에 \(i=j\)이면 \(i\)번 정점의 차수, \(i\neq j\)이고 두 정점이 연결되어 있으면 \(-1\), 아니면 \(0\)이 들어있는 행렬이다. 아래와 같은 예시 그래프를 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;360&quot;&gt;&lt;a href=&quot;https://commons.wikimedia.org/wiki/File:Graph_with_all_its_spanning_trees.svg&quot; target=&quot;_blank&quot; title=&quot;Andreschulz&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfkTRO/btrUYqDK5sK/oukU2GwVHK3qWuvtRkjzf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfkTRO%2FbtrUYqDK5sK%2FoukU2GwVHK3qWuvtRkjzf0%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;550&quot; height=&quot;360&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/a&gt;&lt;figcaption&gt;Graph with all its spanning trees by Andreschulz, CC BY-SA 3.0&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 그래프의 Laplacian matrix는 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;\[L=\begin{bmatrix}2&amp;amp;-1&amp;amp;-1&amp;amp;0\\-1&amp;amp;3&amp;amp;-1&amp;amp;-1\\-1&amp;amp;-1&amp;amp;3&amp;amp;-1\\0&amp;amp;-1&amp;amp;-1&amp;amp;2\end{bmatrix}\]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이제 matrix-tree theorem이 무엇인지 알아보자. 이 \(L\)에서 아무 행이나 하나 고르고 아무 열이나 하나 고른 후 그 행과 열을 삭제해서 \(3\times 3\) 행렬을 만든다. 아무거나 골라도 되지만, 간단하게 첫 번째 행과 첫 번째 열을 삭제하자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;\[L^*=\begin{bmatrix}3&amp;amp;-1&amp;amp;-1\\-1&amp;amp;3&amp;amp;-1\\-1&amp;amp;-1&amp;amp;2\end{bmatrix}\]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;놀랍게도 이 행렬의 행렬식이 그래프의 스패닝 트리 개수와 같아진다.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 결과 역시 잘 알려져 있고 다양한 증명이 존재하지만, 그중에서 위의 Cauchy&amp;ndash;Binet formula를 이용한 증명을 소개하고자 한다. 이를 위해서는 \(L\)이 incidence matrix라는 것으로 분해될 수 있다는 것을 이해해야 한다. Incidence matrix는 adjacency matrix와 비슷하지만, 정점과 정점 사이의 관계가 아닌 정점과 간선 사이의 관계로 정의된다. Incidence matrix는 무향 그래프와 유향 그래프 모두에서 정의되지만, 여기에서 필요한 것은 &lt;u&gt;유향&lt;/u&gt; 그래프의 incidence matrix이다.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;유향 그래프의 incidence matrix는 정점이 \(n\)개이고 간선이 \(m\)개일 때, \(j\)번 간선이 \(i\)번 정점으로 들어오면 \((i, j)\) 원소가 \(1\)이고 \(j\)번 간선이 \(i\)번 정점에서 나가면 \(-1\), 둘다 아니면 \(0\)으로 정의되는 \(n\times m\) 행렬이다. 지금 그래프에는 방향이 없기 때문에, 우선 각 간선에 방향을 아무렇게나 주고 incidence matrix \(E\)를 구하면, Laplacian matrix가 아래와 같은 형태가 되는 것을 어렵지 않게 알 수 있다. (C&amp;ndash;B formula 증명할 때처럼 인접 행렬로 경로의 수 구하는 방식 생각하면 된다.)&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;\[L=EE^T\]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이제 \(L\)에서 아무 행과 아무 열을 삭제한 \(L^*\)를 만들어야 하는데, 우선은 첫 번째 행과 첫 번째 열을 삭제했다고 가정하자. (증명을 따라가다 보면 왜 아무 행과 아무 열을 골라도 상관 없는지 이해될 것이다.) 그러면 \(E\)에서도 첫 번째 행을 날린 \(\bar{E}\)를 만들면 \(L^*=\bar{E}\bar{E}^T\)와 같이 쓸 수 있다. Cauchy&amp;ndash;Binet formula에 의해 아래 식을 얻을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;\[\det(L^*)=\sum_{I\subseteq{[m]\choose n-1}}\det(\bar{E}_{[n-1], I})\det(\bar{E}_{I,[n-1]}^T)=\sum_{I\subseteq{[m]\choose n-1}}\det(\bar{E}_{[n-1],I})^2\]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;\(\bar{E}_{[n-1],I}\)에서 \(I\)의 의미를 생각해보면, 그래프의 간선 중 \((n-1)\)개를 고른 것이다. 스패닝 트리가 사이클이 없도록 \((n-1)\)개의 간선을 골라 만들어진 부분그래프임을 생각해보자.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;\(\det(\bar{E}_{[n-1],I})\)를 전개했을 때 나오는 각 항의 순열 \(\sigma:[n-1]\to I\)는 \((n-1)\)개의 정점과 선택받은 \((n-1)\)의 간선의 '매칭'을 의미한다. 단 짝지을 간선이 정점에 연결되어 있어야 한다. 그렇지 않으면 incidence matrix 정의상 연결되어있지 않은 짝이 0이 되어 항이 사라지기 때문이다. \(I\)에 속한 각 간선에 대해 그 간선에 짝지어진 정점을 도착점으로 하도록 방향(주의: incidence matrix를 만들기 위해 임의로 부여했었던 방향과는 다르다)을 부여하면, 행렬식의 전개에서 0이 아닌 각 항은 \(E\)에서 삭제한 1번 정점(indegree 0)을 제외한 나머지 정점의 indegree가 1이 되도록 \(I\)의 간선에 방향을 부여한 것으로 해석할 수 있다. 즉, 이렇게 만든 방향 그래프에서는 정점 \(i\)로 들어가는 유일한 간선이 \(\sigma_i\)가 된다. 이 방향과 incidence matrix를 만들기 위해 임의로 부여했었던 방향과의 일치 여부에 따라 대응되는 항의 부호가 결정되게 된다. (이것과 별개로 \(\operatorname{sgn}\)의 존재도 잊어서는 안된다.)&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 만일 \(I\)가 스패닝 트리라면, \(E\)에서 삭제한 1번 정점을 트리의 루트로 간주할 때 나머지 \((n-1)\)개의 정점과 트리를 구성하는 \((n-1)\)개의 간선을 짝지을 방법이 유일하다. 정점과 그 부모를 잇는 간선을 짝지으면 된다. 이것이 의미하는 바는 \(\det(\bar{E}_{[n-1],I})=\pm 1\)이라는 것이다.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;만일 \(I\)가 스패닝 트리가 아니라면 어떻게 될까? 이 경우 \(I\)에 포함된 (무향) 사이클이 반드시 존재한다. 이때 행렬식 전개의 각 순열 \(\sigma:[n-1]\to I\)에 대응되는 방향 그래프는 indegree가 모두 1 이하라는 특징 때문에 &lt;b&gt;유향 사이클&lt;/b&gt;의 존재가 보장된다! (증명은 독자에게 맡긴다) 여기서 LGV를 증명할 때처럼 &lt;u&gt;짝지어 상쇄시키는&lt;/u&gt; 아이디어를 사용한다. LGV에서는 교차점에서 두 경로의 진행 방향을 뒤바꿨다면, 여기서는 이 유향 사이클을 구성하는 간선의 방향을 뒤집어서 만든 방향 그래프와 짝짓는다.&lt;/span&gt;&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;838&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fp2t9/btrU0uFTSS5/yKBnlP2K1bkpLcZXKUxom1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fp2t9/btrU0uFTSS5/yKBnlP2K1bkpLcZXKUxom1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fp2t9/btrU0uFTSS5/yKBnlP2K1bkpLcZXKUxom1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFp2t9%2FbtrU0uFTSS5%2FyKBnlP2K1bkpLcZXKUxom1%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;838&quot; height=&quot;318&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;318&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;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이렇게 뒤집을 경우 incidence matrix를 만들 때 고정했던 방향과의 일치 여부가 전부 바뀌기 때문에 사이클의 길이가 홀수일 경우 부호가 뒤집힌다. 반면 \(\operatorname{sgn}\)의 경우 사이클의 길이가 짝수일 때 부호가 뒤집힌다! (이 역시 증명은 독자에게 맡긴다.) 결과적으로 행렬식의 전개에서 이렇게 짝지은 순열끼리 소멸하여 결과적으로 행렬식이 0이 된다.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;최종적으로 \(\det(L^*)\)의 C&amp;ndash;B formula에서 스패닝 트리에 대응되는 \(I\)만 1의 값을 남기게 되어 결과적으로 스패닝 트리의 개수와 같아지게 된다. 이렇게 matrix-tree 정리를 증명했다.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;Laplacian matrix의 특징 덕분에 근본적으로는 같지만 약간은 다른 방법으로 스패닝 트리의 개수를 구할 수도 있다. 이 행렬은 Hermitian이기 때문에 대각화가 가능하고 중근을 포함해 \(n\)개의 실수 고윳값을 가진다. 자명한 고윳값으로 0을 가지는데, 0을 하나 제외한 나머지 고윳값을 모두 곱하고 \(n\)으로 나눠도 스패닝 트리의 개수를 구할 수 있다. 증명은 순수 선형대수학이고 어렵지 않으므로 수학에 자신 있다면 생각해 보는 것을 권한다.&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;마치며&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;3편에 걸쳐 PS를 위한 선형대수학 시리즈를 포스팅했다. 기본적인 내용부터 상당히 심도 있고 어려운 응용까지 다루었다. 이 정도면 PS에서 자주 등장하는 선형대수학 문제에 필요한 배경 지식은 대부분 정리했다고 생각한다. 초기 계획과 달리 matroid를 다루지 않았는데, linear matroid에 들어가는 선형대수학은 선형대수학 1편 수준의 지식으로 기본적이지만 그에 비해 matroid의 개념 자체가 쉬운 것이 아니기에 matroid의 핵심을 빼놓고 선형대수학의 응용으로만 작성하는 것은 의미가 없다고 생각했다. 추후 matroid 관련 주제를 작성하게 되면 그때 다룰 예정이다.&lt;/span&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;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;지금은 이 정도에서 마무리하지만, 재미있는 선형대수학의 응용에 대해 알게 되면 독립적으로 포스팅할 생각도 있다. 부족한 글을 읽어주신 독자 여러분께 감사의 말씀을 드리며...&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;연습문제&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/7875&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;BOJ 7875: (False) faces&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/17460&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;BOJ 17460: Cactus Determinant&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/18471&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;BOJ 18471: Determinant&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/18763&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;BOJ 18763: Knowledge-Oriented Problem&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/18955&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;BOJ 18955: Determinant of a Graph&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/21117&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;BOJ 21117: Number of Colorful Matchings&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/25313&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 25313: Race for the Galaxy&lt;/a&gt;&lt;/p&gt;</description>
      <category>Computer Science/알고리즘</category>
      <category>선형 대수학</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/21</guid>
      <comments>https://topology-blog.tistory.com/21#entry21comment</comments>
      <pubDate>Sat, 31 Dec 2022 16:31:15 +0900</pubDate>
    </item>
    <item>
      <title>Linear-time construction of the sieve of Eratosthenes</title>
      <link>https://topology-blog.tistory.com/20</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;에라토스테네스의 체&lt;/a&gt;는 소수를 구할 때 흔히 쓰는 방식이다. 구현도 쉽기 때문에 정수론과 관련된 PS 문제에서 자주 만날 수 있다.&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 class=&quot;cpp&quot;&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; sieve_of_eratosthenes(int n) {
    vector&amp;lt;bool&amp;gt; sieve(n + 1);
    vector&amp;lt;int&amp;gt; prime;
    for (int k = 2; k &amp;lt;= n; k++) {
        if (sieve[k]) {
            continue;
        }
        prime.push_back(k);
        for (int i = 2; i * k &amp;lt;= n; i++) {
            sieve[i * k] = true;
        }
    }
    return prime;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 알고리즘은 얼마나 효율적일까? 간단하게 각 정수를 평균적으로 몇 번 방문하는지 보면 된다. 이 방식은 \(n\)까지의 모든 양의 정수의 방문 횟수가 각 정수의 소인수 개수이다. 어떤 정수의 소인수 개수를 알려주는 함수로 &lt;a href=&quot;https://en.wikipedia.org/wiki/Prime_omega_function&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;prime omega function&lt;/a&gt;이라는 것이 있는데, \(n\)의 소인수 개수는 평균적으로 \(O(\log\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;여기서 방법을 살짝 바꿔서 각 정수를 정확히 한 번만 방문하게 할 수 있다. 각 정수의 소인수 하나당 방문을 하는 대신 &lt;b&gt;최소 소인수&lt;/b&gt;에 대해서만 방문을 하는 것이다. 아이디어는 안쪽 루프에서 현재 보고 있는 메인 루프의 정수 \(k\)의 배수를 방문하되 각 소수 \(p\)에 대해 \(pk\)꼴의 수만 방문하고, 작은 \(p\)부터 방문하면서 \(k\)가 \(p\)의 배수가 되면 멈추는 것이다. 이렇게 하면 \(p\)가 \(pk\)의 최소 소인수일 때만 방문을 하게 되므로 우리의 목적을 달성할 수 있다.&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\)가 합성수일 때도 방문을 해주어야 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; sieve_of_eratosthenes(int n) {
    vector&amp;lt;bool&amp;gt; sieve(n + 1);
    vector&amp;lt;int&amp;gt; prime;
    for (int k = 2; k &amp;lt;= n; k++) {
        if (!sieve[k]) {
            prime.push_back(k);
        }
        for (int i = 0; prime[i] * k &amp;lt;= n; i++) {
            int p = prime[i];
            sieve[p * k] = true;
            if (k % p == 0) {
                break;
            }
        }
    }
    return prime;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;냉정히 말해서 이렇게 한다 해도 동작 시간을 크게 줄여주지는 못한다. 기존 체에서 평균 방문 횟수인 \(\log\log n\)이 너무나도 작은 값이기 때문이다. 하지만 이 방식이 기존 방식과 차별화되는 가장 큰 특징은 각 정수 \(n\)을 정확히 한 번 방문하고, 방문할 때 \(n\)의 최소 소인수를 즉시 알 수 있으며 \(n\)을 방문하기 전 \(n\)의 약수를 모두 방문한다는 것이다. (마지막 사실은 증명하지는 않았으나 귀납법으로 쉽게 확인 가능하므로 생략한다.) 이 특징 때문에 특정 정수 이하의 모든 양의 정수에 대해 &lt;a href=&quot;https://en.wikipedia.org/wiki/Multiplicative_function&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;곱셈 함수&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;어떤 &lt;a href=&quot;https://en.wikipedia.org/wiki/Arithmetic_function&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;산술 함수&lt;/a&gt; \(f:\mathbb{Z}^+\to\mathbb{C}\)가 \(f(1)=1\)이고 서로소인 두 양의 정수 \(a\)와 \(b\)에 대해 \(f(ab)=f(a)f(b)\)를 만족하면 곱셈 함수라고 한다. 곱셈 함수의 특징은 모든 소수 \(p\)와 양의 정수 \(\ell\)에 대해 \(f(p^\ell)\)의 값이 정해지면 함수가 유일하게 결정된다는 것이다.&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;이를 이용하면 dynamic programming으로 곱셈 함수를 계산할 수 있다. 어떤 양의 정수 \(n\)의 최소 소인수 \(p\)와 \(p\)로 나누어 떨어지지 않는 \(m\)에 대해 \(n=p^\ell m\)으로 나타날 때, \(f(n)=f(p^\ell)f(m)\)이기 때문이다. 우리는 \(p\)의 값만 알고 \(p^\ell\)의 값은 모르지 않느냐고 생각할 수 있지만, 이 값 역시 (곱셈 함수는 아니지만) dynamic programming으로 계산할 수 있다.&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;a href=&quot;https://en.wikipedia.org/wiki/Euler%27s_totient_function&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Euler's totient function&lt;/a&gt;의 값을 구하는 코드를 마지막으로 글을 마친다. 위키피디아 링크에서 더 다양한 곱셈 함수에 대해 알아볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;vector&amp;lt;int&amp;gt; euler_totient(int n) {
    vector&amp;lt;bool&amp;gt; sieve(n + 1);
    vector&amp;lt;int&amp;gt; prime;
    vector&amp;lt;int&amp;gt; phi(n + 1);
    phi[1] = 1;
    for (int k = 2; k &amp;lt;= n; k++) {
        if (!sieve[k]) {
            prime.push_back(k);
            phi[k] = k - 1;
        }
        for (int i = 0; prime[i] * k &amp;lt;= n; i++) {
            int p = prime[i];
            sieve[p * k] = true;
            if (k % p == 0) {
                phi[p * k] = phi[k] * p;
                break;
            } else {
                phi[p * k] = phi[k] * (p - 1);
            }
        }
    }
    return phi;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Computer Science/알고리즘</category>
      <category>수학</category>
      <category>알고리즘</category>
      <category>정수론</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/20</guid>
      <comments>https://topology-blog.tistory.com/20#entry20comment</comments>
      <pubDate>Sun, 11 Sep 2022 15:45:08 +0900</pubDate>
    </item>
    <item>
      <title>Linear Algebra in Problem Solving (2)</title>
      <link>https://topology-blog.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tistory.joonhyung.xyz/18&quot;&gt;Linear Algebra in Problem Solving (1)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tistory.joonhyung.xyz/19&quot;&gt;Linear Algebra in Problem Solving (2)&lt;/a&gt;&amp;nbsp;(현 포스트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://topology-blog.tistory.com/21&quot;&gt;Linear Algebra in Problem Solving (3)&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;이 포스트에서는 선형대수학 시리즈의 &lt;a href=&quot;https://topology-blog.tistory.com/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;전편&lt;/a&gt;에 이어 좀더 고급 알고리즘에 대해 다룬다. 전편에서 만든 코드베이스를 그대로 사용할 예정이므로 전편을 먼저 읽는 것을 권장한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특성다항식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(n\times n\) 행렬 \(A\)의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Characteristic_polynomial&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;특성다항식(characteristic polynomial)&lt;/a&gt; \(f_A\)는 아래와 같이 정의된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[f_A(x):=\det(xI-A)\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다항식은 최고차항의 계수가 1인 \(n\)차 다항식이며, 아래와 같이 여러 성질을 가지고 있는 중요한 다항식이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;\(f_A(x)=0\)의 근은 \(A\)의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;고윳값&lt;/a&gt;이다.&lt;/li&gt;
&lt;li&gt;\(f_A\)의 \(x^{n-1}\)의 계수는 \(A\)의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Trace_(linear_algebra)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;trace&lt;/a&gt;의 부호를 뒤집은 값이다.&lt;/li&gt;
&lt;li&gt;\(f_A\)의 상수항은 \((-1)^n\det(A)\)이다.&lt;/li&gt;
&lt;li&gt;\(B=P^{-1}AP\)라면, \(f_A=f_B\)이다.&lt;/li&gt;
&lt;li&gt;\(f_A(A)=O\)이다. (&lt;a href=&quot;https://en.wikipedia.org/wiki/Cayley%E2%80%93Hamilton_theorem&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cayley&amp;ndash;Hamilton 정리&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 위 특성다항식의 성질 중 네 번째, 즉 \(B=P^{-1}AP\)이면 \(f_A=f_B\)라는 사실을 이용하는 것이다. 저런 \(P\)가 존재하면 \(A\)와 \(B\)를 &lt;a href=&quot;https://en.wikipedia.org/wiki/Matrix_similarity&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;닮은(similar)&lt;/a&gt; 행렬이라고 하는데, \(A\)와 닮음이면서 형태가 간단해서 특성다항식을 계산하기 쉬운 \(B\)를 찾을 수 있다면, 우리는 \(A\) 대신 \(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;a href=&quot;https://en.wikipedia.org/wiki/Diagonal_matrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;대각행렬&lt;/a&gt;과 &lt;a href=&quot;https://en.wikipedia.org/wiki/Triangular_matrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;삼각행렬&lt;/a&gt;은 특성다항식을 계산하기 매우 쉽다. 하지만 모든 행렬이 대각화나 삼각화가 가능하지는 않고, 설사 가능하다고 해도 계산하는 것이 쉽지 않다. 따라서 대안으로 사용하는 것이 &lt;a href=&quot;https://en.wikipedia.org/wiki/Hessenberg_matrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hessenberg matrix&lt;/a&gt;이다. 정확히는 우리는 upper Hessenberg matrix를 사용할 것인데, upper triangular matrix가 주대각선 아래로 모두 0인 행렬이라면, upper Hessenberg matrix는 마찬가지로 주대각선 아래로 대부분 0인 행렬이지만, 예외적으로 주대각선 &lt;u&gt;바로 아래&lt;/u&gt;까지는 0이 아니어도 되는 행렬이다. 예를 들어 다음과 같은 행렬이 upper Hessenberg matrix이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\begin{bmatrix}1&amp;amp;5&amp;amp;5&amp;amp;0&amp;amp;-2\\ -1&amp;amp;2&amp;amp;3&amp;amp;7&amp;amp;1\\ 0&amp;amp;4&amp;amp;4&amp;amp;-2&amp;amp;0\\ 0&amp;amp;0&amp;amp;3&amp;amp;2&amp;amp;2\\ 0&amp;amp;0&amp;amp;0&amp;amp;7&amp;amp;-1\end{bmatrix}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수학적으로 표현하면, \(B\)의 \(i\)번째 행 \(j\)번째 열에 오는 원소를 \(b_{ij}\)라고 할 때, \(i\geq j+2\)일 때 \(b_{ij}=0\)이면 \(B\)를 upper Hessenberg matrix라고 한다.&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;주어진 행렬과 닮음인 upper Hessenberg matrix를 찾는 방법을 논하기 전에, upper Hessenberg matrix의 특성다항식을 구하는 방법을 먼저 알아보자. 어떤 행렬 \(M\)의 첫 \(k\)개의 행과 열로 이루어진 부분행렬을 &lt;b&gt;leading principal minor&lt;/b&gt;라고 하는데, 이 행렬을 \([M]_k\)라고 쓰자. \(M\)이 upper Hessenberg matrix라면 모든 leading principal minor도 upper Hessenberg이기 때문에, \(\det[M]_k\)를 \(k\)번째 열에 &lt;a href=&quot;https://en.wikipedia.org/wiki/Laplace_expansion&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;cofactor expansion&lt;/a&gt;을 적용하여 재귀적으로 구할 수 있다. 식으로 적으면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det[M]_k=\sum_{i=0}^{k-1}(-1)^{k-1-i}\det[M]_i\cdot m_{i+1,k}\prod_{j=i+2}^{k}m_{j,j-1}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(m_{ij}\)는 \(M\)의 \(i\)번째 행 \(j\)번째 열에 있는 원소이고, \(\det[M]_0\)은 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;이렇게 계산하면 동적 계획법(dynamic programming)으로 \(\det(M)=\det[M]_n\)을 계산하는 데 \(O(n^2)\)의 시간이면 충분하기 때문에, 일반적인 행렬의 행렬식을 계산하는 것보다 훨씬 빠르다. 이제 \(B\)가 upper Hessenberg matrix이면 \(xI-B\)도 upper Hessenberg이기 때문에, 위 점화식에서 \(M=xI-B\)를 대입해 보자. 이 행렬은 원소 중에 일차식이 있기는 하지만 주대각선에만 일차식이 있고 나머지는 모두 상수항이다. 따라서 점화식에서 \(\det[M]_i\) 뒤에 오는 부분은, \(i=k-1\)일 때 \(m_{i+1,k}\)만 일차식이고 나머지는 모조리 상수이다! 따라서 \(\det[M]_k\)을 구할 때 \(k\)차 이하의 다항식 \(k\)개를 더하는 것이 시간을 가장 오래 차지하는 부분이고, 총 \(O(n^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;정리하자면, (upper) Hessenberg matrix는 \(O(n^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\)와 닮음인 upper Hessenberg matrix \(B\)를 찾는 것이다. 이를 위해서는 elementary row operation을 응용하면 되는데, 아쉽게도 elementary row operation이 적용된 행렬은 원래 행렬과 닮음이 아닐 수 있다. 하지만 elementary row operation을 적용한 후에 비슷한 elementary &lt;b&gt;column&lt;/b&gt; operation을 적용하여 원래 행렬과 닮음이 되게 할 수 있다! 각 연산별로 어떻게 하는지 알아보자.&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;먼저 가장 간단한 row switching 연산의 경우, \(i\)번째 행과 \(j\)번째 행을 바꾸는 것은 아래와 같이 &lt;a href=&quot;https://en.wikipedia.org/wiki/Permutation_matrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;permutation matrix&lt;/a&gt;를 왼쪽에 곱한 것과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\begin{bmatrix}1&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;1&amp;amp;0\\0&amp;amp;0&amp;amp;1&amp;amp;0&amp;amp;0\\0&amp;amp;1&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;1\end{bmatrix}A\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 2번째 행과 4번째 행을 바꾸는 경우이다. 이후 \(A\)와 닮음으로 만들어주기 위해서는 해당 permutation matrix의 역행렬을 오른쪽에 곱해줘야 한다. 두 위치만 바꾸는 연산은 역연산이 자기 자신이므로, 같은 행렬을 오른쪽에 곱해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\begin{bmatrix}1&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;1&amp;amp;0\\0&amp;amp;0&amp;amp;1&amp;amp;0&amp;amp;0\\0&amp;amp;1&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;1\end{bmatrix}A\begin{bmatrix}1&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;1&amp;amp;0\\0&amp;amp;0&amp;amp;1&amp;amp;0&amp;amp;0\\0&amp;amp;1&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;1\end{bmatrix}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽에 이 행렬을 곱해주는 것은, \(i\)번째 열과 \(j\)번째 열을 바꾸는 것과 효과가 같다. 따라서 row switching 연산은, &lt;b&gt;같은 인덱스를 바꾸는 column switching 연산&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;다음으로 row addition 연산의 경우, \(i\)번째 행에 \(j\)번째 행의 \(c\)배를 더하는 것은 아래와 같이 곱하는 것과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\begin{bmatrix}1&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;1&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;1&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;1&amp;amp;0\\0&amp;amp;-2&amp;amp;0&amp;amp;0&amp;amp;1\end{bmatrix}A\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시는 5번째 행에 2번째 행의 -2배를 더하는 연산이다. 이 행렬의 역행렬은 \(c\)의 부호를 뒤집는 것으로, 닮음으로 만들기 위해 아래와 같이 오른쪽에 곱해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\begin{bmatrix}1&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;1&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;1&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;1&amp;amp;0\\0&amp;amp;-2&amp;amp;0&amp;amp;0&amp;amp;1\end{bmatrix}A\begin{bmatrix}1&amp;amp;0&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;1&amp;amp;0&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;1&amp;amp;0&amp;amp;0\\0&amp;amp;0&amp;amp;0&amp;amp;1&amp;amp;0\\0&amp;amp;2&amp;amp;0&amp;amp;0&amp;amp;1\end{bmatrix}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 \(j\)번째  열에 \(i\)번째 열의 \(-c\)배를 더하는 것과 같은 효과이다. 따라서 row addition 연산은, &lt;b&gt;부호를 바꾸고 두 인덱스의 역할을 바꾼 column addition 연산&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;row multiplication이 빠졌는데, 쉬울 뿐더러 실제로 활용하지 않을 예정이므로 생략한다.&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\)와 닮음인 upper Hessenberg matrix를 만들 수 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;1열부터 \(n-2\)번째 열까지 순서대로 처리한다.&lt;/li&gt;
&lt;li&gt;현재 \(j\)번째 열에 있다면, \((j+1)\)번째 행부터 내려가면서 0이 아닌 원소의 위치를 찾는다.&lt;/li&gt;
&lt;li&gt;그러한 원소가 없다면 다음 열로 넘어가서 2번 과정으로 돌아간다.&lt;/li&gt;
&lt;li&gt;그러한 원소가 발견되면 row switching으로 \((j+1)\)번째 행으로 가져오고, 대응되는 column switching을 적용한다.&lt;/li&gt;
&lt;li&gt;\((j+1)\)번째 행에 적절한 상수를 곱한 후 더해서 \((j+2)\)번째 행부터 모두 0이 되도록 row addition 연산을 적용하고, 각 연산 이후에 대응되는 column addition 연산도 수행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 잘 살펴보면, 닮음을 유지하기 위한 elementary column operation이 현재 작업중인 열에 영향을 주지 않는다는 것을 알 수 있다. 따라서 모든 과정이 끝나면 원래 행렬과 닮음인 upper Hessenberg matrix가 완성된다.&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^3)\)이고, Hessenberg matrix의 특성다항식 알고리즘을 적용하면 전체 \(O(n^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;이제 이 과정을 구현해보자. 먼저 아래와 같이 elementary column operation을 추가한다. 이 코드에서는 column switching 연산을 column index 배열을 이용하는 트릭을 사용하여 상수 시간에 구현하였다. Elementary row operation 부분은 동일하므로 생략하였다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
class matrix {
public:
    int n, m;
    vector&amp;lt;int&amp;gt; cidx;
    vector&amp;lt;vector&amp;lt;T&amp;gt;&amp;gt; entry;

    matrix(int n_, int m_) : n(n_), m(m_), cidx(m_), entry(n_, vector&amp;lt;T&amp;gt;(m_)) {
        iota(cidx.begin(), cidx.end(), 0);
    }
    matrix(int n_) : matrix(n_, n_) {}

    T&amp;amp; operator[](const pair&amp;lt;int, int&amp;gt;&amp;amp; p) {
        return entry[p.first][cidx[p.second]];
    }

    const T&amp;amp; operator[](const pair&amp;lt;int, int&amp;gt;&amp;amp; p) const {
        return entry[p.first][cidx[p.second]];
    }

    /* ... */

    void cswap(int i, int j) {
        swap(cidx[i], cidx[j]);
    }

    void cmul(int j, T x) {
        for (int i = 0; i &amp;lt; n; i++) {
            entry[i][cidx[j]] *= x;
        }
    }

    void cadd(int j1, int j2, T x) {
        for (int i = 0; i &amp;lt; n; i++) {
            entry[i][cidx[j1]] += entry[i][cidx[j2]] * x;
        }
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 이 연산을 이용해서 특성다항식을 구하면 된다. Column multiplication 연산은 사용하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
vector&amp;lt;T&amp;gt; char_poly(matrix&amp;lt;T&amp;gt; &amp;amp;A) {
    int n = A.n;
    assert(n == A.m);

    /* Transform to upper Hessenberg matrix */
    for (int j = 0; j &amp;lt; n - 2; j++) {
        for (int i = j + 1; i &amp;lt; n; i++) {
            if (A[{i, j}] != 0) {
                A.rswap(i, j + 1);
                A.cswap(i, j + 1);
                break;
            }
        }
        if (A[{j + 1, j}] != 0) {
            for (int i = j + 2; i &amp;lt; n; i++) {
                T x = -A[{i, j}] / A[{j + 1, j}];
                A.radd(i, j + 1, x);
                A.cadd(j + 1, i, -x);
            }
        }
    }

    /* Compute the char poly */
    vector&amp;lt;vector&amp;lt;T&amp;gt;&amp;gt; dp(n + 1);
    dp[0] = {1};
    for (int i = 1; i &amp;lt;= n; i++) {
        dp[i].resize(i + 1);
        for (int j = 1; j &amp;lt;= i; j++) {
            dp[i][j] = dp[i - 1][j - 1];
        }
        for (int j = 0; j &amp;lt; i; j++) {
            dp[i][j] -= A[{i - 1, i - 1}] * dp[i - 1][j];
        }
        T prod = 1;
        for (int j = i - 2; j &amp;gt;= 0; j--) {
            prod *= A[{j + 1, j}];
            for (int jj = 0; jj &amp;lt;= j; jj++) {
                dp[i][jj] -= A[{j, i - 1}] * prod * dp[j][jj];
            }
        }
    }
    return dp[n];
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가우스 소거법 구현과 마찬가지로 \(\mathbb{F}_2\)에서 bitset을 사용할 수 있지만, 열 연산의 존재로 인해 행렬을 \(n^2\) 크기의 bitset으로 구현해야 해서 구현은 좀더 까다롭다. 이 경우 시간복잡도는 \(O(n^3/w)\).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최소다항식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 짧게 언급한 Cayley&amp;ndash;Hamilton 정리에 의하면 \(f_A(A)=0\)이 된다. 최소다항식 \(p_A(x)\)는 \(p_A(A)=O\)을 만족하는 다항식 중 가장 차수가 낮으면서 최고차항의 계수가 1인 다항식이다. 수학적으로 이러한 다항식이 유일함을 증명할 수 있으며, \(g(A)=O\)을 만족하는 모든 다항식 \(g(x)\)가 \(p_A(x)\)로 나누어 떨어진다는 사실도 증명할 수 있다. 따름정리로 \(f_A(x)\) 역시 \(p_A(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;PS에서 최소다항식을 사용해야하는 경우는 매우 드물지만, 최소다항식을 구하는 알고리즘 역시 가우스 소거법의 좋은 적용 예이므로 소개하고자 한다. 핵심 아이디어는 행렬을 \(n^2\)차원 벡터로 취급하는 것이다. 그럼 아래 나열된 행렬의 선형 결합이 0이 되는 계수 조합 중 0이 아닌 계수가 가능한 한 앞쪽에 있게 하고 싶은 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[I, A, A^2, \cdots, A^n\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 아래와 같이 열벡터가 위 행렬인 \(n^2\times (n+1)\) 행렬을 만들고 RREF 변환 과정&lt;span&gt;(단순 가우스 소거법이 아니다!)&lt;/span&gt;을 진행하다가, 처음으로 leading one을 찾을 수 없는 열이 나오자마자 멈추면 된다. 행렬과 구분하기 위해 행렬을 \(n^2\)차원 열벡터로 편 것을 굵게 표시하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\left[\begin{array}{c|c|c|c|c}\mathbf{I}&amp;amp;\mathbf{A}&amp;amp;\mathbf{A^2}&amp;amp;\cdots&amp;amp;\mathbf{A^n}\end{array}\right]\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 그러한 열이 나왔을 때 최소다항식의 계수는 어떻게 될까? 그 열벡터에 적혀있다! 정확히는 \(k\)번째 열벡터에서 멈췄다면 해당 열벡터의 첫번째 원소부터 \((k-1)\)번째 원소까지에 최소다항식의 상수항부터 \(x^{k-2}\)의 계수까지가 부호가 뒤집힌 채로 적혀있다. 최고차항의 계수는 당연히 1이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
vector&amp;lt;T&amp;gt; min_poly(const matrix&amp;lt;T&amp;gt;&amp;amp; A) {
    int n = A.n;
    assert(n == A.m);
    vector&amp;lt;matrix&amp;lt;T&amp;gt;&amp;gt; mv(n + 1, matrix&amp;lt;T&amp;gt;(n));
    for (int i = 0; i &amp;lt; n; i++) {
        mv[0][{i, i}] = 1;
    }
    for (int i = 1; i &amp;lt;= n; i++) {
        mv[i] = matmul(mv[i - 1], A);
    }
    matrix&amp;lt;T&amp;gt; M(n * n, n + 1);
    for (int i = 0; i &amp;lt; n; i++) {
        for (int j = 0; j &amp;lt; n; j++) {
            for (int k = 0; k &amp;lt;= n; k++) {
                M[{i * n + j, k}] = mv[k][{i, j}];
            }
        }
    }
    rref(M);
    for (int d = 1; d &amp;lt;= n; d++) {
        if (M[{d, d}] == 0) {
            vector&amp;lt;T&amp;gt; res(d + 1);
            for (int i = 0; i &amp;lt; d; i++) {
                res[i] = -M[{i, d}];
            }
            res[d] = 1;
            return res;
        }
    }
    assert(0);
    return vector&amp;lt;T&amp;gt;();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간복잡도는 \(O(n^4)\)이며, \(\mathbb{F}_2\)에서 bitset을 활용할 경우 \(O(n^4/w)\)로 줄일 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;\(\det(xA+B)\)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(n\times n\) 행렬 \(A\)와 \(B\)가 있을 때, 다항식 \(\det(xA+B)\)를 구하는 문제를 생각해 보자. 이 형태는 특히 조합론 문제를 선형대수학을 이용해 푸는 예시에서 종종 볼 수 있다. (이러한 예시는 다음 포스트에서 좀더 자세히 다룰 예정이다.) 만약 \(A\)가 역행렬이 존재한다면 \(-A^{-1}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;목표는 \(xA+B\)에 elementary row operation과 elementary column operation을 적용하여 아래와 같은 형태로 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\left[\begin{array}{@{}c|c@{}}\begin{matrix}\\xI_k+C\\ \quad \end{matrix} &amp;amp; O \\ \hline&amp;nbsp; D &amp;amp;&amp;nbsp; L\end{array}\right]\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(I_k\)는 \(k\times k\) 단위행렬, \(C\)는 상수로만 이루어진 행렬, \(O\)는 영행렬, 그리고 \(L\)은 주대각선이 모두 1인 \((n-k)\times (n-k)\) lower triangular matrix이다. 이런 행렬의 행렬식은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det(xI+C)\cdot\det(L)=\det(xI+C)\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이므로 \(-C\)의 특성다항식을 구하면 된다. 즉 우리는 기본 연산만으로 \(xA+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;이 변환 과정을 재귀적으로 생각해보면 어렵지 않게 방법을 찾을 수 있다. 첫 \(\alpha\)개의 열과 마지막 \(\beta\)개의 열이 우리의 의도대로 이미 변형되었다고 가정하고, 그 사이의 열에서 작업을 하는 것이 핵심이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;처음에 \(\alpha=\beta=0\)이다.&lt;/li&gt;
&lt;li&gt;\(\alpha+\beta=n\)이 되면 처리가 완료되었으므로 종료한다.&lt;/li&gt;
&lt;li&gt;\((\alpha+1)\)번째 열을 본다. 첫번 째 행부터 \(\alpha\)번째 행까지에 일차항이 있으면 첫번째 열부터 \(\alpha\)번째 열까지를 이용해 column addition 연산으로 일차항을 모두 없애준다.&lt;/li&gt;
&lt;li&gt;\((\alpha+1)\)번째 열에서 \((\alpha+1)\)번째 행부터 \((n-\beta)\)번째 행까지 순서대로 보면서 일차항이 있는지 찾는다.&lt;/li&gt;
&lt;li&gt;일차항을 발견하면 elementary row operation을 적절히 이용해 \((\alpha+1\)번째 행에만 일차항 \(x\)가 남아있게 한다. \(\alpha\)에 1을 더하고 2번으로 돌아간다.&lt;/li&gt;
&lt;li&gt;일차항을 발견하지 못하면 \((n-\beta)\)번째 행까지는 상수만 있다는 뜻이다. 해당 열을 column switching으로 \((n-\beta)\)번째 열로 옮겨준다.&lt;/li&gt;
&lt;li&gt;이제 \((n-\beta)\)번째 열을 본다. \((n-\beta)\)번째 행부터 역순으로 보면서 0이 아닌 원소가 있는지 찾는다.&lt;/li&gt;
&lt;li&gt;없으면 행렬식이 0이라는 뜻이므로 바로 종료해도 된다.&lt;/li&gt;
&lt;li&gt;0이 아닌 원소를 찾았고 행의 번호가 \((\alpha+1)\) 이상일 경우 row switching으로 \((n-\beta)\)번째 행으로 옮긴 후 row multplication으로 해당 원소를 1로 만들고 row addition으로 그 이전 행까지를 모두 0으로 만든다. 이후 \(\beta\)에 1을 더하고 2번으로 돌아간다.&lt;/li&gt;
&lt;li&gt;0이 아닌 원소를 찾았고 행의 번호가 \(\alpha\) 이하인 경우 row switching으로 \(\alpha\)번째 행으로 옮겨오고 &lt;b&gt;같은 인덱스로 column switching까지 진행&lt;/b&gt;한다.&lt;/li&gt;
&lt;li&gt;이제 row switching으로 \((n-\beta)\)번째 행까지 옮기고 9번과 유사하게 처리한다. 이후 \(\beta\)에 1을 더하지만, \(\alpha\)번째 열의 정합성이 깨졌을 수 있으므로 \(\alpha\)에서 1을 뺀 후 2번으로 돌아간다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정이 길어 복잡하게 느껴지지만, 직접 행렬을 그려가면서 따라가면 직관적으로 이해할 수 있다. 10번 과정이 주목할만한데, column switching을 수행하는 이유는 첫 열부터 \(\alpha\)번째 열까지의 정합성을 깨뜨리는 것이 불가피하기 때문에 그것을 마지막인 \(\alpha\)번째 열로 설정하기 위해서이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
vector&amp;lt;T&amp;gt; linear_det(matrix&amp;lt;T&amp;gt;&amp;amp; A, matrix&amp;lt;T&amp;gt;&amp;amp; B) {
    int n = A.n;
    assert(n == A.m &amp;amp;&amp;amp; n == B.n &amp;amp;&amp;amp; n == B.m);
    T det = 1;

    /* ... */

    int a = 0, b = 0;
    while (a + b &amp;lt; n) {
        for (int i = 0; i &amp;lt; a; i++) {
            cadd(a, i, -A[{i, a}]);
        }
        for (int i = a; i &amp;lt; n - b; i++) {
            if (A[{i, a}] != 0) {
                rswap(a, i);
                break;
            }
        }
        if (A[{a, a}] != 0) {
            rmul(a, 1 / A[{a, a}]);
            for (int i = a + 1; i &amp;lt; n; i++) {
                radd(i, a, -A[{i, a}]);
            }
            a++;
        } else {
            int r = n - 1 - b;
            cswap(a, r);
            int pos = -1;
            for (int i = r; i &amp;gt;= 0; i--) {
                if (B[{i, r}] != 0) {
                    pos = i;
                    break;
                }
            }
            if (pos == -1) {
                return vector&amp;lt;T&amp;gt;();
            } else {
                if (pos &amp;lt; a) {
                    rswap(pos, a - 1);
                    cswap(pos, a - 1);
                    rswap(a - 1, r);
                    a--;
                } else {
                    rswap(pos, r);
                }
                rmul(r, 1 / B[{r, r}]);
                for (int i = 0; i &amp;lt; r; i++) {
                    radd(i, r, -B[{i, r}]);
                }
                b++;
            }
        }
    }
    matrix&amp;lt;T&amp;gt; C(a);
    for (int i = 0; i &amp;lt; a; i++) {
        for (int j = 0; j &amp;lt; a; j++) {
            C[{i, j}] = -B[{i, j}];
        }
    }
    auto res = char_poly(C);
    for (int i = 0; i &amp;lt;= a; i++) {
        res[i] *= det;
    }
    return res;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주석 처리된 부분에는 원래 코드에 쓰이는 행 연산과 열 연산 함수를 \(A\)와 \(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;이제 시간복잡도를 확인해보자. 메인 루프 내의 모든 작업은 \(O(n^2)\)에 이루어지고, 루프를 돌 때마다 \(\alpha+2\beta\)의 값이 항상 증가하므로 루프를 도는 횟수는 \(O(n)\)이다. 따라서 전체 시간복잡도는 \(O(n^3)\)으로 특성다항식을 구하는 시간복잡도와 같다. \(\mathbb{F}_2\)에서 bitset을 이용할 경우 마찬가지로 \(O(n^3/w)\).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;종결식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Resultant&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;종결식(resultant)&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;종결식은 두 다항식 \(A(x)=a_0+a_1x+\cdots+a_dx^d\)와 \(B(x)=b_0+b_1x+\cdots+b_ex^e\)에 대해서 정의되며, 아래와 같다(위키피디아와 계수 인덱스가 다르니 주의).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\operatorname{res}(A, B):=\det\begin{bmatrix} a_d&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; \cdots &amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; b_e&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; \cdots &amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;\\ a_{d-1}&amp;nbsp; &amp;nbsp; &amp;amp; a_d&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; \cdots &amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; b_{e-1}&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; b_e&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; \cdots &amp;amp; 0&amp;nbsp;&amp;nbsp;\\ a_{d-2}&amp;nbsp; &amp;nbsp; &amp;amp; a_{d-1}&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; \ddots &amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; b_{e-2}&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; b_{e-1}&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; \ddots &amp;amp; 0 \\ \vdots&amp;nbsp;&amp;nbsp;&amp;amp;\vdots&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; \ddots &amp;amp; a_d&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;amp; \vdots&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp;\vdots&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; \ddots &amp;amp; b_e&amp;nbsp; \\ a_0&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; a_1 &amp;amp; \cdots &amp;amp; \vdots&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; b_0&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; b_1&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; \cdots &amp;amp; \vdots\\ 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; a_0&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; \ddots &amp;amp;&amp;nbsp;&amp;nbsp;\vdots&amp;nbsp;&amp;nbsp;&amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; b_0&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;amp; \ddots &amp;amp;&amp;nbsp;&amp;nbsp;\vdots&amp;nbsp;&amp;nbsp;\\ \vdots&amp;nbsp;&amp;nbsp;&amp;amp; \vdots&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; \ddots &amp;amp; a_1&amp;nbsp;&amp;nbsp;&amp;amp; \vdots&amp;nbsp;&amp;nbsp;&amp;amp; \vdots&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; \ddots &amp;amp; b_1&amp;nbsp;&amp;nbsp;&amp;nbsp;\\ 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; \cdots&amp;nbsp;&amp;nbsp;&amp;amp; a_0&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; 0&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;amp; \cdots &amp;amp; b_0&amp;nbsp; &amp;nbsp;\end{bmatrix}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만일 두 다항식이 일차식의 곱으로 완전히 인수분해되어 \(A(x)=a_d(x-\alpha_1)\cdots(x-\alpha_d)\)와 \(B(x)=b_e(x-\beta_1)\cdots(x-\beta_e)\)와 같이 표현된다면, 종결식은 아래와 같은 식으로도 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\operatorname{res}(A,B)=a_d^eb_e^d\prod_{i,j}(\alpha_i-\beta_j)\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 근과 계수와의 관계에 의해 아래와 같이 쓸 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\operatorname{res}(A,B)=a_d^e\prod_{i=1}^dB(\alpha_i)=(-1)^{de}b_e^d\prod_{j=1}^eA(\beta_j)\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대대수학을 조금 알고 있는 독자라면 행렬식을 이용한 정의는 임의의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Commutative_ring&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;가환환(commutative ring)&lt;/a&gt;에서 정의되지만, 근을 이용한 정의는 그 환을 포함하는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Algebraically_closed_field&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;대수적으로 닫힌 체(algebraically closed field)&lt;/a&gt;가 있어야 하므로 &lt;a href=&quot;https://en.wikipedia.org/wiki/Integral_domain&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;integral domain&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;위 식을 이용해 아래와 같은 여러 성질을 쉽게 얻어낼 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;\(\operatorname{res}(A,B)=(-1)^{de}\operatorname{res}(B,A)\)&lt;/li&gt;
&lt;li&gt;\(\operatorname{res}(AB,C)=\operatorname{res}(A,C)\operatorname{res}(B,C)\)&lt;/li&gt;
&lt;li&gt;\(A(x)=0\)와 \(B(x)=0\)가 공통근을 가질 필요충분조건은 \(\operatorname{res}(A,B)=0\)이다.&lt;/li&gt;
&lt;li&gt;\(\operatorname{res}(aA, bB)=a^eb^d\operatorname{res}(A,B)\)&lt;/li&gt;
&lt;li&gt;차수가 \(r&amp;lt;e\)인 다항식 \(R(x)\)에 대해 \(B=AQ+R\)이라면, \(\operatorname{res}(A,B)=a_d^{e-r}\operatorname{res}(A,R)\)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종결식을 계산할 때 가장 중요한 성질은 마지막 성질로, 유클리드 알고리즘으로 두 다항식의 최대공약수를 구하는 과정에서 두 다항식의 종결식도 쉽게 구할 수 있다는 것이다. 특히 최고차항의 계수가 1이면 \(a_d^{e-r}=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;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트에서는 조금 더 고급 알고리즘인 특성다항식, 최소다항식 등을 구하는 알고리즘에 대해 알아보았다. 이 정도 알고리즘이 필요한 문제는 &lt;a href=&quot;https://www.acmicpc.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ&lt;/a&gt;와 &lt;a href=&quot;https://solved.ac/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;solved.ac&lt;/a&gt;에서 상당히 고난이도 문제로 취급된다. 좀더 다양한 문제를 풀기 위해서는 조합론이나 그래프 이론 등에서 선형대수학이 응용되는 예시를 함께 알면 좋은데, 이는 다음 포스트에서 다룰 예정이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;&lt;a href=&quot;https://www.acmicpc.net/problem/18163&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 18163: Binary Matrix&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/18542&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 18542: Permutant&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/19562&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 19562: Matrix and Queries&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/25509&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 25509: Lord of the Characteristic Polynomials (1)&lt;/a&gt; (주어진 대수 구조가 integral domain이 아닐 수 있으므로 주의가 필요하다.)&lt;/p&gt;</description>
      <category>Computer Science/알고리즘</category>
      <category>선형대수학</category>
      <category>수학</category>
      <category>알고리즘</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/19</guid>
      <comments>https://topology-blog.tistory.com/19#entry19comment</comments>
      <pubDate>Fri, 2 Sep 2022 12:52:32 +0900</pubDate>
    </item>
    <item>
      <title>Linear Algebra in Problem Solving (1)</title>
      <link>https://topology-blog.tistory.com/18</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tistory.joonhyung.xyz/18&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Linear Algebra in Problem Solving (1)&lt;/a&gt; (현 포스트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tistory.joonhyung.xyz/19&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Linear Algebra in Problem Solving (2)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://topology-blog.tistory.com/21&quot;&gt;Linear Algebra in Problem Solving (3)&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;&lt;a href=&quot;https://en.wikipedia.org/wiki/Linear_algebra&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;선형대수학(linear algebra)&lt;/a&gt;은 &lt;a href=&quot;https://en.wikipedia.org/wiki/Vector_space&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;벡터 공간(vector space)&lt;/a&gt;과 &lt;a href=&quot;https://en.wikipedia.org/wiki/Linear_map&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;선형 변환(linear transformation)&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;이 포스트 시리즈에서는 PS에서 등장하는 선형대수학 관련 문제들을 해결하기 위한 여러 가지 선형대수학 알고리즘을 다룰 예정이다. 일반적인 PS에서 선형대수학이 필요한 경우는 드물지만, &lt;a href=&quot;https://www.acmicpc.net/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ&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;아쉽게도 이 시리즈에서는 기초적인 선형대수학 개념을 다루지는 않는다. 선형대수학을 토대부터 쌓는 것은 굉장히 시간이 많이 걸리기 때문이다. 대신 필요할 경우 위키피디아 등 잘 정리된 레퍼런스로 링크할 예정이다. 이 글을 무리없이 읽기 위해서는 대학 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;본 시리즈의 첫 번째인 이 포스트에서는 행렬 곱셈, 가우스 소거법, 역행렬, 행렬식 등 기초적인 값을 구하는 방법에 대해 다룬다.&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;언어는 C++로 진행하며, 편의상 아래와 같은 클래스를 바탕으로 설명한다. 설명의 편의를 위해 간략하게 만든 코드이며, 필자도 문제를 풀 때 이 클래스를 쓰지는 않는다. 실제 구현할 때는 본인이 원하는 형태로 구현하면 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
class matrix {
public:
    int n, m;
    vector&amp;lt;vector&amp;lt;T&amp;gt;&amp;gt; entry;

    matrix(int n_, int m_) : n(n_), m(m_), entry(n_, vector&amp;lt;T&amp;gt;(m_)) {}
    matrix(int n_) : matrix(n_, n_) {}

    T&amp;amp; operator[](const pair&amp;lt;int, int&amp;gt;&amp;amp; p) {
        return entry[p.first][p.second];
    }

    const T&amp;amp; operator[](const pair&amp;lt;int, int&amp;gt;&amp;amp; p) const {
        return entry[p.first][p.second];
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;행렬의 원소&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기초 선형대수학을 공부해본 독자라면 행렬의 원소가 실수(\(\mathbb{R}\))고 벡터공간 또한 \(\mathbb{R}^n\)인 경우가 가장 익숙할 것이다. 하지만 대부분의 선형대수학 이론은 실수보다 더욱 일반적인 &lt;a href=&quot;https://en.wikipedia.org/wiki/Field_(mathematics)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;체(field)&lt;/a&gt;라는 대수적 구조에서도 전개가 가능하다. 심지어 체에서 나눗셈이 없어진 &lt;a href=&quot;https://en.wikipedia.org/wiki/Ring_(mathematics)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;환(ring)&lt;/a&gt;이라는 더욱 일반화된 대수적 구조에서도 전개가 가능한 이론도 많지만, 이쪽은 선형대수학이 아니라 &lt;a href=&quot;https://en.wikipedia.org/wiki/Module_(mathematics)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;모듈(module)&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;체는 간단하게 말해서 덧셈, 곱셈 연산이 있고, 각각에 대한 항등원과 역원(단, 덧셈에 대한 항등원은 곱셈에 대한 역원의 요구사항에서 배제된다)이 있으며 교환법칙, 결합법칙, 그리고 분배법칙이 성립하는 대수적 구조이다. 대표적으로 우리에게 가장 익숙한 실수 집합 \(\mathbb{R}\)과 복소수 집합 \(\mathbb{C}\)가 있으며, 정수를 소수 \(p\)로 나눈 나머지들의 집합인 \(\mathbb{F}_p\)도 있다. 특히 \(p=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;실수 집합과 복소수 집합의 경우 실생활에서 보기 쉽고 이론을 전개하기에 편한 점이 있지만, 컴퓨터에서 구현할 경우 사칙연산이 상당히 많은 선형대수 알고리즘 특성상 실수 오차로 인해 수치적으로 불안정해지고 정확한 결과를 기대하기 어려운 경우가 많다. 따라서 PS에서 보기 가장 흔한 체는 \(\mathbb{F}_p\)이다. 예를 들어 선형대수학으로 풀 수 있는 문제가 결과를 998244353으로 나눈 나머지를 출력하라고 한다면, \(p=998244353\)인 체에서 문제를 풀면 된다.&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;h3 data-ke-size=&quot;size23&quot;&gt;행렬 곱셈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;행렬 곱셈은 가장 기본적인 행렬 연산으로 아래와 같은 코드로 쉽게 해결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
matrix&amp;lt;T&amp;gt; matmul(const matrix&amp;lt;T&amp;gt;&amp;amp; A, const matrix&amp;lt;T&amp;gt;&amp;amp; B) {
    assert(A.m == B.n);
    matrix&amp;lt;T&amp;gt; C(A.n, B.m);
    for (int i = 0; i &amp;lt; C.n; i++) {
        for (int k = 0; k &amp;lt; A.m; k++) {
            for (int j = 0; j &amp;lt; C.m; j++) {
                C[{i, j}] += A[{i, k}] * B[{k, j}];
            }
        }
    }
    return C;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 다중 반복문을 돌리는 순서에 따라 성능이 달라질 수 있는데, 이 코드처럼 i-k-j 순서로 돌리는 것이 i-j-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;이렇게 구현하면 \(n\times m\) 행렬과 \(m\times k\) 행렬을 곱할 때 \(O(nmk)\) 시간이 걸린다. 이것보다 더 빠른 행렬 곱셈 중 그나마 실제로 구현해 볼만한 것은 &lt;a href=&quot;https://en.wikipedia.org/wiki/Strassen_algorithm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Strassen algorithm&lt;/a&gt;인데, PS에서 활용할 범위 내에서는 성능 향상이 그리 크지 않을 뿐더러 작은 행렬에 대해서는 오히려 나이브한 곱셈이 더 빠르기 때문에 그다지 권장하지 않는다. 그래도 관심이 있는 독자는 &lt;a href=&quot;http://www.secmem.org/blog/2021/03/20/strassen-algorithm/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 포스트&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;만약 \(\mathbb{F}_2\) 위의 행렬, 즉 행렬의 원소가 비트라면 &lt;a href=&quot;https://en.cppreference.com/w/cpp/utility/bitset&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;bitset&lt;/a&gt;을 활용해 좀더 효율적으로 구현할 수 있다. 단 &lt;a href=&quot;https://en.wikipedia.org/wiki/Hamming_weight#Processor_support&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;POPCNT&lt;/a&gt; 연산이 하드웨어의 지원을 받는 경우에 한한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;size_t M, size_t K&amp;gt;
vector&amp;lt;bitset&amp;lt;M&amp;gt;&amp;gt; matmul(const vector&amp;lt;bitset&amp;lt;M&amp;gt;&amp;gt;&amp;amp; A, const vector&amp;lt;bitset&amp;lt;K&amp;gt;&amp;gt;&amp;amp; B) {
    int n = (int)A.size(), m = (int)B.size();
    assert(m == M);
    vector&amp;lt;bitset&amp;lt;K&amp;gt;&amp;gt; C(n);
    vector&amp;lt;bitset&amp;lt;M&amp;gt;&amp;gt; Bt(K);
    for (int i = 0; i &amp;lt; K; i++) {
        for (int j = 0; j &amp;lt; m; j++) {
            Bt[i][j] = B[j][i];
        }
    }
    for (int i = 0; i &amp;lt; n; i++) {
        for (int j = 0; j &amp;lt; K; j++) {
            C[i][j] = (A[i] &amp;amp; Bt[j]).count() &amp;amp; 1;
        }
    }
    return C;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 시간복잡도는 bitset의 word size를 \(w\)라고 했을 때 \(O(nmk/w)\)로, 일반적으로 \(w=64\)임을 생각해 본다면 꽤 큰 성능 향상을 기대해볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가우스 소거법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Gaussian_elimination&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;가우스 소거법(Gaussian elimination)&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;구현은 우리에게 익숙한 가우스 소거법 과정을 그대로 시뮬레이션하면 된다. 가우스 소거법 과정을 복습해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;1열(column)에서 시작해서 leading one의 위치를 찾기 시작한다.&lt;/li&gt;
&lt;li&gt;이전 leading one이 위치한 다음 행(row)부터 차례대로 보기 시작해서 leading one으로 선정할 행을 찾는다. 즉 해당 위치에 0이 아닌 원소가 있어야 한다.&lt;/li&gt;
&lt;li&gt;그러한 행이 없다면 그 열은 leading one이 없는 열이다. 다음 열로 넘어가서 2번 과정으로 돌아간다.&lt;/li&gt;
&lt;li&gt;그러한 행을 찾았으면 이전 leading one 바로 다음 행에 위치하도록 위치를 교환한다. (Row switching)&lt;/li&gt;
&lt;li&gt;적절한 상수를 곱해 leading one 위치에 1이 오도록 한다. (Row multiplication)&lt;/li&gt;
&lt;li&gt;해당 행에 적절한 상수를 곱하고 그 다음 행들에 더해 leading one의 아래쪽으로는 모두 0이 되도록 한다. (Row addition)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 row switching, row multiplication, row addition을 한 데 묶어 &lt;b&gt;elementary row operation&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;이제 이를 직접 구현하면 된다. 우선 편의를 위해 행렬 클래스에 elementary row operation을 수행하는 함수를 만든다. 여기서 row switching 연산은 상수 시간에 효율적으로 구현할 수 있는데, 직접 원소들을 바꿔줄 필요 없이 레퍼런스를 바꿔주면 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
class matrix {
public:
    int n, m;
    vector&amp;lt;vector&amp;lt;T&amp;gt;&amp;gt; entry;

    matrix(int n_, int m_) : n(n_), m(m_), entry(n_, vector&amp;lt;T&amp;gt;(m_)) {}
    matrix(int n_) : matrix(n_, n_) {}

    T&amp;amp; operator[](const pair&amp;lt;int, int&amp;gt;&amp;amp; p) {
        return entry[p.first][p.second];
    }

    const T&amp;amp; operator[](const pair&amp;lt;int, int&amp;gt;&amp;amp; p) const {
        return entry[p.first][p.second];
    }

    void rswap(int i, int j) {
        swap(entry[i], entry[j]);
    }

    void rmul(int i, T x) {
        for (int j = 0; j &amp;lt; m; j++) {
            entry[i][j] *= x;
        }
    }

    void radd(int i1, int i2, T x) {
        for (int j = 0; j &amp;lt; m; j++) {
            entry[i1][j] += entry[i2][j] * x;
        }
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 활용해서 가우스 소거법을 구현하면 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
void gaussian_elimination(matrix&amp;lt;T&amp;gt;&amp;amp; A) {
    for (int j = 0, r = 0; j &amp;lt; A.m &amp;amp;&amp;amp; r &amp;lt; A.n; j++) {
        for (int i = r; i &amp;lt; A.n; i++) {
            if (A[{i, j}] != 0) {
                A.rswap(r, i);
                break;
            }
        }
        if (A[{r, j}] != 0) {
            A.rmul(r, 1 / A[{r, j}]);
            for (int i = r + 1; i &amp;lt; A.n; i++) {
                A.radd(i, r, -A[{i, j}]);
            }
            r++;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(n\times m\) 행렬의 가우스 소거법을 계산하는 시간복잡도는 \(O(nm\cdot\min(n, m))\). 행렬 곱셈에서와 마찬가지로, \(\mathbb{F}_2\) 위라면 bitset을 활용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;size_t M&amp;gt;
void gaussian_elimination(vector&amp;lt;bitset&amp;lt;M&amp;gt;&amp;gt;&amp;amp; A) {
    int n = (int)A.size();
    for (int j = 0, r = 0; j &amp;lt; M &amp;amp;&amp;amp; r &amp;lt; n; j++) {
        for (int i = r; i &amp;lt; n; i++) {
            if (A[i][j]) {
                swap(A[r], A[i]);
                break;
            }
        }
        if (A[r][j]) {
            for (int i = r + 1; i &amp;lt; n; i++) {
                if (A[i][j]) {
                    A[i] ^= A[r];
                }
            }
            r++;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 시간복잡도는 \(O(nm\cdot\min(n, m)/w)\).&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;행렬의 rank 구하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가우스 소거법의 부산물로 행렬의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Rank_(linear_algebra)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;rank&lt;/a&gt;도 구할 수 있다. 가우스 소거법이 완료된 후 leading one의 개수가 곧 행렬의 rank이므로, 위 코드에서 변수 r에 행렬의 rank가 저장된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
int rank(matrix&amp;lt;T&amp;gt;&amp;amp; A) {
    int r = 0;
    for (int j = 0; j &amp;lt; A.m &amp;amp;&amp;amp; r &amp;lt; A.n; j++) {
        for (int i = r; i &amp;lt; A.n; i++) {
            if (A[{i, j}] != 0) {
                A.rswap(r, i);
                break;
            }
        }
        if (A[{r, j}] != 0) {
            A.rmul(r, 1 / A[{r, j}]);
            for (int i = r + 1; i &amp;lt; A.n; i++) {
                A.radd(i, r, -A[{i, j}]);
            }
            r++;
        }
    }
    return r;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;행렬식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(n\times n\) 정사각 행렬 \(A\)의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Determinant&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;행렬식&lt;/a&gt;은 아래와 같은 식의 값으로 알려져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\det(A)=\sum_{\sigma\in S_n}\left(\operatorname{sgn}(\sigma)\prod_{i=1}^n a_{i,\sigma_i}\right)\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(S_n\)은 \(\{1,\cdots,n\}\)의 모든 순열의 집합이고, \(\operatorname{sgn}\)은 순열의 홀짝성을 나타내는 함수이다. 순열의 홀짝성이란 간단히 말해 두 원소의 swap만으로 순열을 정렬할 때 필요한 swap 횟수의 홀짝성이다. (어떤 과정을 거치든 홀짝성은 같다.) 이 횟수가 짝수면 \(\operatorname{sgn}\)의 값이 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;a href=&quot;https://en.wikipedia.org/wiki/Laplace_expansion&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;cofactor expansion&lt;/a&gt;이라는 방식도 있지만, 모두 지수 시간이 걸리는 비효율적인 알고리즘이다. 하지만 위에서 구현한 가우스 소거법을 이용하면 \(O(n^3)\)에 행렬식을 구할 수 있다. 가우스 소거법이 완료되면 주대각선 밑으로는 모두 0인 상삼각행렬(upper triangular matrix)이 되는데, 이런 행렬의 행렬식은 주대각선의 원소를 모두 곱하면 되므로 쉽게 구할 수 있기 때문이다.&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;따라서 각각의 elementary row operation이 행렬식을 어떻게 변화시키는지 보면 되는데, 매우 간단하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Row switching: 행렬식의 부호만 뒤집힌다.&lt;/li&gt;
&lt;li&gt;Row multiplication: 행렬식에도 곱한 상수만큼 곱해진다.&lt;/li&gt;
&lt;li&gt;Row addition: 변화가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 아래와 같이 구현하면 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
T det(matrix&amp;lt;T&amp;gt;&amp;amp; A) {
    assert(A.n == A.m);
    T ret = 1;
    for (int j = 0; j &amp;lt; A.n; j++) {
        for (int i = j; i &amp;lt; A.n; i++) {
            if (A[{i, j}] != 0) {
                if (i &amp;gt; j) {
                    A.rswap(j, i);
                    ret = -ret;
                }
                break;
            }
        }
        if (A[{j, j}] == 0) {
            return 0;
        }
        ret *= A[{j, j}];
        A.rmul(j, 1 / A[{j, j}]);
        for (int i = j + 1; i &amp;lt; A.n; i++) {
            A.radd(i, j, -A[{i, j}]);
        }
    }
    return ret;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reduced row echelon form (RREF)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가우스 소거법의 결과로 만들어지는 행렬 꼴을 &lt;a href=&quot;https://en.wikipedia.org/wiki/Row_echelon_form&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;row echelon form&lt;/a&gt;이라고 하는데, &lt;a href=&quot;https://en.wikipedia.org/wiki/Row_echelon_form#rref&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;reduced row echelon form&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;단순 row echelon form이 각각의 leading one 아래로는 모두 0인 행렬이었다면, RREF는 단순히 그 위까지도 0인 행렬을 의미한다. 따라서 구현도 반복문 하나만 수정해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
void rref(matrix&amp;lt;T&amp;gt;&amp;amp; A) {
    for (int j = 0, r = 0; j &amp;lt; A.m &amp;amp;&amp;amp; r &amp;lt; A.n; j++) {
        for (int i = r; i &amp;lt; A.n; i++) {
            if (A[{i, j}] != 0) {
                A.rswap(r, i);
                break;
            }
        }
        if (A[{r, j}] != 0) {
            A.rmul(r, 1 / A[{r, j}]);
            for (int i = 0; i &amp;lt; A.n; i++) {
                if (i != r) {
                    A.radd(i, r, -A[{i, j}]);
                }
            }
            r++;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;선형 연립 방정식 풀기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선형 연립 방정식은 \(n\times m\) 행렬 \(A\)와 크기 \(n\)인 벡터 \(\mathbf{b}\)가 있을 때, 아래 식을 만족하는 크기 \(m\)인 벡터 \(\mathbf{x}\)를 구하는 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[A\mathbf{x}=\mathbf{b}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 형태는 \(n=m\)이고 \(A\)가 역행렬이 존재하는 경우이다. 그래야 아래와 같이 해가 유일하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\mathbf{x}=A^{-1}\mathbf{b}\]&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;권장되는 방법은 \(A\)에 \(\mathbf{b}\)를 붙여서 &lt;a href=&quot;https://en.wikipedia.org/wiki/Augmented_matrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;augmented matrix&lt;/a&gt;를 만든 후 RREF 형태를 만드는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\begin{bmatrix} A &amp;amp;&amp;nbsp; |\mathbf{b}\end{bmatrix}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(A\)의 역행렬이 존재한다는 조건으로 인해 RREF 형태가 되면 원래 \(A\)였던 부분은 반드시 단위행렬이 되고, \(\mathbf{b}\) 부분에는 우리가 구하고 싶었던 \(\mathbf{x}\)가 남게 된다. 이는 우리가 선형 연립방정식을 손으로 푸는 과정과 일치한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
vector&amp;lt;T&amp;gt; solve_linear_equation(const matrix&amp;lt;T&amp;gt;&amp;amp; A, const vector&amp;lt;T&amp;gt;&amp;amp; b) {
    int n = A.n;
    assert(n == A.m);
    assert(n == (int)b.size());
    matrix&amp;lt;T&amp;gt; B(n);
    for (int i = 0; i &amp;lt; n; i++) {
        for (int j = 0; j &amp;lt; n; j++) {
            B[{i, j}] = A[{i, j}];
        }
        B[{i, n}] = b[i];
    }
    rref(B);
    vector&amp;lt;T&amp;gt; x(n);
    for (int i = 0; i &amp;lt; n; i++) {
        assert((B[{i, i}] == 1));
        x[i] = B[{i, n}];
    }
    return x;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(A\)의 역행렬이 존재하지 않거나 \(n\neq m\)인 경우에도 마찬가지 방법을 사용할 수 있다. 이 경우 RREF 변환이 끝난 후 어떤 결과가 나오느냐에 따라 해가 존재하는지, 존재하다면 유일한지 혹은 무한한지 등을 판별할 수 있다. 독자들은 선형대수학을 이미 어느 정도 알고 있으므로 이는 쉬운 연습문제로 남긴다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;응용: XOR이 특정 수가 되는 부분집합 구하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(2^{w}\)보다 작은 음이 아닌 정수 \(n\)개와 \(b\)가 주어질 때, \(n\)개의 수 중 &lt;a href=&quot;https://en.wikipedia.org/wiki/Exclusive_or&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;XOR&lt;/a&gt;을 했을 때 \(b\)가 되는 수의 부분집합을 찾는 문제를 생각해보자. 만약 XOR이 아니라 합이었다면 그 유명한 &lt;a href=&quot;https://en.wikipedia.org/wiki/Subset_sum_problem&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;subset sum&lt;/a&gt; 계열의 문제가 되고, NP-complete이다. 하지만 XOR의 경우 선형대수학과 bitset을 활용해 \(O(wn\cdot\min(w, n)/w)\cong O(wn)\)에 해결할 수 있다.&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;핵심은 각 수를 \(\mathbb{F}_2\)에서의 \(w\)차원 벡터로 취급하는 것이다. 그러면 XOR은 이 벡터공간에서는 합이 되기 때문에, 결국 합이 특정 벡터가 되는 벡터 집합을 찾는 문제가 된다. \(\mathbb{F}_2\)에서는 선형 결합이 곧 어떤 부분집합의 원소들의 합이기 때문에, 주어진 \(n\)개의 벡터의 선형결합이 \(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;이렇듯 음이 아닌 정수를 \(\mathbb{F}_2\) 위에서의 벡터, XOR을 벡터의 합으로 해석해서 선형대수학적으로 접근하는 문제들을 흔히 만날 수 있기 때문에 알아두면 좋다. 선형대수 알고리즘을 직접 사용하지 않더라도 문제에 접근하는데 많은 도움을 준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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\times n\) 행렬 \(A\)의 역행렬을 구하기 위해서는 아래 식을 만족하는 행렬 \(X\)를 구하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[AX=I\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(I\)는 \(n\times n\) 단위행렬로, 주대각선은 모두 1이고 나머지는 0인 행렬이다.&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;a href=&quot;https://en.wikipedia.org/wiki/Cramer%27s_rule&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cramer's rule&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;이 문제를 열 단위로 쪼개서 생각해보자. \(X\)의 \(i\)번째 열을 구하기 위해서는 아래 식을 만족하는 벡터 \(\mathbf{x}_i\)를 구하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[A\mathbf{x}_i=\mathbf{\delta}_i\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(\mathbf{\delta}_i\)는 \(i\)번째 원소만 1이고 나머지는 0인 벡터이다. 이  문제를 선형 연립 방정식을 풀 때와 동일한 환경이다. 따라서 각 \(\mathbf{\delta}_i\)에 대해 augmented matrix를 만든 후 가우스 소거법을 \(n\)번 실행하면 \(X\)의 각 열을 구할 수 있지만, \(A\) 부분이 소거되는 과정은 완전히 동일하므로 굳이 이렇게 할 필요 없다. 아예 \(I\)를 통으로 augment하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\begin{bmatrix}A&amp;amp;|&amp;amp;I\end{bmatrix}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 RREF 형태로 만들면 \(A\) 부분은 단위행렬이 되고 \(I\) 부분은 \(A\)의 역행렬이 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template&amp;lt;class T&amp;gt;
matrix&amp;lt;T&amp;gt; matinv(const matrix&amp;lt;T&amp;gt;&amp;amp; A) {
    int n = A.n;
    assert(n == A.m);
    matrix&amp;lt;T&amp;gt; B(n, n + n);
    for (int i = 0; i &amp;lt; n; i++) {
        for (int j = 0; j &amp;lt; n; j++) {
            B[{i, j}] = A[{i, j}];
            B[{i, n + i}] = 1;
        }
    }
    rref(B);
    matrix&amp;lt;T&amp;gt; X(n);
    for (int i = 0; i &amp;lt; n; i++) {
        assert((B[{i, i}] == 1));
        for (int j = 0; j &amp;lt; n; j++) {
            X[{i, j}] = B[{i, n + j}];
        }
    }
    return X;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트에서는 선형대수학의 가장 기본적인 알고리즘을 알아보았다. 다음 포스트에서는 좀더 심화된 선형대수 알고리즘(특성다항식, 일차 다항식 행렬, 종결식 등)과 여러 응용(그래프 이론, 매트로이드, 조합론 등)에 대해서 다룰 예정이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&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;&lt;a href=&quot;https://www.acmicpc.net/problem/11191&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 11191: Xor Maximization&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/13712&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 13712: 키르히호프의 법칙&lt;/a&gt; (&lt;a href=&quot;https://en.wikipedia.org/wiki/Kirchhoff%27s_circuit_laws&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;회로 이론에서의 키르히호프 법칙&lt;/a&gt;에 대한 사전지식이 필요하다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/15737&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 15757: 일반 그래프 매칭&lt;/a&gt; (&lt;a href=&quot;https://en.wikipedia.org/wiki/Tutte_matrix&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Tutte matrix&lt;/a&gt;에 대한 사전지식이 필요하다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/16904&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 16904: 집합과 쿼리&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/20307&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 20307: RREF&lt;/a&gt;&lt;/p&gt;</description>
      <category>Computer Science/알고리즘</category>
      <category>선형대수학</category>
      <category>수학</category>
      <category>알고리즘</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/18</guid>
      <comments>https://topology-blog.tistory.com/18#entry18comment</comments>
      <pubDate>Wed, 31 Aug 2022 17:50:29 +0900</pubDate>
    </item>
    <item>
      <title>사원수와 3차원 회전</title>
      <link>https://topology-blog.tistory.com/16</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;3차원 컴퓨터 그래픽스에서 회전(rotation)은 매우 중요한 개념이다. 대학에서 또는 독자적으로 컴퓨터 그래픽스의 원리를 공부한 적이 있는 독자라면 3차원 회전을 사원수(quaternion)로 표현하는 방식에 대해 본 적이 있을 것이다. 단위 벡터(unit vector) \((x, y, z)\)를 회전축으로 반시계 방향으로 \(\theta\)만큼 회전시키는 동작을 사원수 방식으로는 아래와 같이 나타낸다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\cos(\theta/2)+x\sin(\theta/2)\mathbf{i}+y\sin(\theta/2)\mathbf{j}+z\sin(\theta/2)\mathbf{k}\,.\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사원수는 우리가 익히 아는 일반적인 실수나 복소수와는 또 다른 시스템이다. 놀랍게도, 이렇게 나타내면 마법과 같이 3차원 회전에 관련된 모든 연산이 편리해지고, 컴퓨터에 구현하기도 쉬워진다. (어떤 점이 편리해지는지에 대해서는 조금 뒤에 다룰 예정이다.) 대체 어떻게 3차원 회전을 사원수라는 이상한 시스템으로 표현할 수 있으며, 그 속에는 어떤 원리가 담겨져 있는 것일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아쉽게도 사원수의 원리를 명쾌하게 설명하는 것은 매우 어렵다. 여기에는 놀랍도록 깊은 수학이 숨어져 있으며, 컴퓨터 그래픽스를 공부하는 사람에게 이러한 지식은 불필요하게 난해하다. 따라서 많은 컴퓨터 그래픽스 교재는 주입식 교육처럼 사원수와 3차원 회전 연산을 공식으로 보여주고 암기하게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필자 역시 대학에서 컴퓨터 그래픽스 강의를 들었고, 이 강의에서 역시 원리를 깊게는 설명하지 않았다. 하지만 필자는 수학 전공자였기에 이러한 수학의 흑마법을 공식으로 외우고 그냥 지나칠 수는 없었다. 필자처럼 3차원 회전 속에 숨어있는 수학적 원리에 대해 호기심을 가졌거나 가지고 있는 독자라면, 올바른 포스트로 찾아오셨다!&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무엇에 관한 글인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 사원수가 3차원 회전을 어떻게 표현할 수 있는지, 그 속에 숨은 원리에 대해 독자들이 &lt;b&gt;기하학적인&amp;nbsp;직관&lt;/b&gt;을 가질 수 있게 도와주는 것이 목표이다. 컴퓨터 그래픽스를 공부하며 '대체 누가 사원수 같은 걸 생각해낸 것이지?'라는 생각을 해본 적이 있다면, 이 글이 도움이 되기를 바란다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무엇에 관한 글이 아닌가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, 이 글은 컴퓨터 그래픽스나 3차원 회전에 대한 입문서가 아니다. 그러한 목적이라면 좋은 책이나 강의가 많으니 그쪽을 찾아보는 것을 권한다. 또한, 이 글은 사원수와 3차원 회전에 대해 엄밀한 수학적 이론을 정리하기 위한 글이 아니다. 엄밀함보다는 직관을 부여하는 데 집중하였으며, 그 과정에서 지루한 계산이나 너무 난해한 수학은 생략하였다. 그 부족함을 채우고 싶은 독자는 하단의 참고 자료를 보기 바란다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사전 지식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글을 작성할 때 수학적 지식이 깊지 않은 독자라도 읽을 수 있도록 최대한 노력했다. 우선, 고등학교나 대학교 1학년 수준의 기하와 벡터(요즘 교육과정을 잘 모르겠다...) 지식은 필수이다. 독자의 편의를 위해 글의 도입부에서 사원수와 3차원 회전에 대해 간략하게는 설명했지만, 3차원 회전에 대한 지식이 전무하다면 다소 불친절하게 느낄 수 있다. 또한 글이라는 방식의 한계로 인해 어느 정도의 공간지각력은 필요할 수 있다. 약간의 선형대수학 지식이 있다면 글을 더 쉽게 읽을 수 있지만, 필수는 아니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3차원 회전의 표현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터 그래픽스에서 어떤 3차원 물체를 표현하는 기본적인 방법은, 그 물체 고유의 3차원 좌표계(frame)를 정하고, 해당 물체의 형태는 그 고유의 좌표계 안에서 표현을 하는 것이다. 그러고 나면 해당 물체의 움직임, 즉 평행이동이나 회전 등을 표현하기 위해서 그 물체의 구성요소를 일일이 움직여줄 필요가 없고, 대신 그 물체의 좌표계만 움직여주면 된다. 예를 들어서 어떤 물체의 회전을 표현하기 위해서는, 좌표계를 회전시키는 방법만 알면 된다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;544&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6SYVS/btrpu26ShAs/EQ3aXgMK8KIAJhxAGokbr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6SYVS/btrpu26ShAs/EQ3aXgMK8KIAJhxAGokbr1/img.png&quot; data-alt=&quot;2차원 좌표계의 회전. 출처: Foundations of 3D Computer Graphics, S. Gortler&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6SYVS/btrpu26ShAs/EQ3aXgMK8KIAJhxAGokbr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6SYVS%2Fbtrpu26ShAs%2FEQ3aXgMK8KIAJhxAGokbr1%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;1728&quot; height=&quot;544&quot; data-origin-width=&quot;1728&quot; data-origin-height=&quot;544&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2차원 좌표계의 회전. 출처: Foundations of 3D Computer Graphics, S. Gortler&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 3차원 좌표계는 서로 수직인 세 개의 단위벡터(unit vector)로 구성되어 있다. 여기서 각 벡터의 방향은 오른손 법칙(right-hand rule)을 따른다. 즉 첫 번째 벡터가 가리키는 방향에서 두 번째 벡터가 가리키는 방향으로 오른손을 휘감았을 때 엄지손가락이 가리키는 방향을 세 번째 벡터가 가리켜야 한다. 아래 그림과 같이 이해할 수도 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;220&quot; data-origin-height=&quot;199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHSaYZ/btrpqyL1kj3/W7g2iI40NgfCKWt1gSVw81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHSaYZ/btrpqyL1kj3/W7g2iI40NgfCKWt1gSVw81/img.png&quot; data-alt=&quot;오른손 법칙. 출처: 위키피디아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHSaYZ/btrpqyL1kj3/W7g2iI40NgfCKWt1gSVw81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHSaYZ%2FbtrpqyL1kj3%2FW7g2iI40NgfCKWt1gSVw81%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;220&quot; height=&quot;199&quot; data-origin-width=&quot;220&quot; data-origin-height=&quot;199&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;h3 data-ke-size=&quot;size23&quot;&gt;행렬로 표현하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선형대수학 지식이 있는 독자라면 회전 변환은 선형 변환임을 알고 있을 것이다. 3차원에서의 선형 변환은 3&amp;times;3 행렬로 나타낼 수 있다. 즉, 회전 변환은 3&amp;times;3 행렬로 나타낼 수 있다는 뜻이다. (하지만 당연히 모든 행렬이 회전 변환은 아니다!) 예를 들어, 아래와 같은 행렬은 회전 변환이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[ \begin{bmatrix} 1/\sqrt{2} &amp;amp; -1/\sqrt{2} &amp;amp; 0 \\ 1/\sqrt{2} &amp;amp; 1/\sqrt{2} &amp;amp; 0 \\ 0 &amp;amp; 0 &amp;amp; 1 \end{bmatrix} \]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀더 구체적으로는, 어떤 행렬이 회전 변환을 나타내기 위해서는 각 열벡터(column vector)가 단위벡터이고, 열벡터가 서로 수직이며, 행렬식이 1이어야 한다. 첫 두 조건을 만족하는 행렬을 &lt;b&gt;직교행렬&lt;/b&gt;(orthogonal matrix)이라고 하며, 선형대수학에서 매우 중요한 개념이다. 직교행렬의 행렬식은 1 또는 -1이고, 그중 행렬식이 1인 직교행렬만 회전 변환이 될 수 있다. 참고로, 이러한 행렬을 &lt;b&gt;특수직교행렬&lt;/b&gt;(special orthogonal matrix)이라고 한다. 이러한 이유로 3차원 회전 변환의 집합을 특수직교군(special orthogonal group)이라고도 하고, \(SO(3)\)라고 쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇듯 3차원 회전을 표현하는 가장 쉽고 간단한 방법은 행렬로 표현하는 것이다. 하지만 이 방식에는 여러 가지 문제점이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수치적으로 불안정(numerically unstable)하다. 앞서 언급했듯이 어떤 행렬이 회전 변환이 되기 위해서는 여러 가지 조건이 필요하다. 즉 컴퓨터로 표현할 때는 이 많은 조건을 매번 확인해야 한다는 뜻이고, 회전 변환을 여러번 거치게 되면 수치적으로 더 빠르게 불안정해질 수 있다는 의미이다.&lt;/li&gt;
&lt;li&gt;회전축과 회전 각도를 알아내기 어렵다. 오직 행렬로부터 이 정보를 뽑아내기 위해서는 매우 복잡한 계산이 필요하고, 이는 수치적으로 불안하다는 문제와도 연관되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회전축과 회전 각도를 알아내는 것이 왜 필요할까? 이는 보간(interpolation)이라고 하는 문제와 연관되어 있다. 예를 들어 애니메이션을 만든다고 하자. 초당 60프레임의 애니메이션을 만들 때, 초당 60개의 장면을 모두 그리는 것은 너무 시간이 오래 걸린다. 한 시간짜리 애니메이션을 만든다 해도 216,000개의 장면을 그려야 한다! 따라서 이보다 훨씬 적은 수의 장면만을 직접 그리고, 그 사이는 컴퓨터의 연산으로 만들어내게 된다. 이때 두 장면 사이가 부드럽게 이어지도록 하는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평행이동의 경우, 0.1초 간격의 두 장면 사이에 어떤 물체가 1m 이동했는데 그 사이에 10개의 프레임을 넣고 싶다면, 0.1m 간격으로 물체를 찍으면 된다. 회전의 경우는 어떨까? 회전축과 회전 각도를 알고 있다면 비슷한 방식으로 해결할 수 있지만, 이 정보를 모르면 상당히 난감해지게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오일러 각도(Euler angles)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;340px-Euler2a.gif&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDmYDQ/btrpkuEk0PY/wi5ArgLMSZskKAEk7rW8F1/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDmYDQ/btrpkuEk0PY/wi5ArgLMSZskKAEk7rW8F1/img.gif&quot; data-alt=&quot;오일러 각도 표현 중 하나. 출처: 위키피디아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDmYDQ/btrpkuEk0PY/wi5ArgLMSZskKAEk7rW8F1/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dDmYDQ/btrpkuEk0PY/wi5ArgLMSZskKAEk7rW8F1/img.gif&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;340&quot; height=&quot;322&quot; data-filename=&quot;340px-Euler2a.gif&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;322&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 data-ke-size=&quot;size16&quot;&gt;오일러 각도를 이용한 표현 방법은 여러 가지가 있다. 그중 하나는 고정된 \(x,y,z\) 좌표계에 대하여 차례대로 \(x\)축을 기준으로 \(\alpha\)만큼 회전, \(y\)축을 기준으로 \(\beta\)만큼 회전, 그리고 \(z\)축을 기준으로 \(\gamma\)만큼 회전하는 것이다. 이를 식으로 나타내면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[ R=R_z(\gamma)R_y(\beta)R_x(\alpha) \]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변환(함수)이기 때문에 적용 순서는 오른쪽부터이다. 이를 왼쪽부터 읽으면 회전하면서 같이 움직이는 회전축의 경우로 해석할 수 있다. 즉 \(z\)축을 기준으로 \(\gamma\)만큼 먼저 회전하고, 회전하면서 바뀐 좌표계의 \(y\)축을 기준으로 \(\beta\)만큼 회전, 그리고 또 회전하면서 바뀐 좌표계의 \(x\)축을 기준으로 \(\alpha\)만큼 회전이다. 이 두번째 해석을 응용한 것이 아래 그림에 있는 짐벌(gimbal)이라는 시스템이다. 이에 대한 자세한 설명은 이 글의 주제를 벗어나므로 생략하겠다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Gyroscope_operation.gif&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk2uxk/btrpu5Js3E6/uBOACkuAmQMDZ29gAOb12K/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk2uxk/btrpu5Js3E6/uBOACkuAmQMDZ29gAOb12K/img.gif&quot; data-alt=&quot;짐벌. 출처: 위키피디아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk2uxk/btrpu5Js3E6/uBOACkuAmQMDZ29gAOb12K/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bk2uxk/btrpu5Js3E6/uBOACkuAmQMDZ29gAOb12K/img.gif&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;300&quot; height=&quot;300&quot; data-filename=&quot;Gyroscope_operation.gif&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;300&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세 개의 오일러 각도가 유일하게 결정되지 않을 수 있다. 이는 사실 3차원 회전을 세 개의 매개변수로 표현하려 했기 때문에 생길 수밖에 없는 어쩔 수 없는 문제이다. 위의 짐벌 응용의 사례에서 짐벌락(gimbal lock)이라는 현상이 발생하는 원인이다.&lt;/li&gt;
&lt;li&gt;보간 문제에서 여전히 회전축과 회전 각도를 알아내기 어렵다. 이를 위해서는 위의 행렬 표현으로 변환을 거쳐야 하기 때문에 행렬 표현보다도 더 어렵다고도 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사원수 표현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사원수는 위의 두 표현 방식에 비하면 상당히 비직관적이지만, 상기한 문제점들을 모두 해결할 수 있는 흑마법 같은 시스템이다. 먼저 사원수는 네 실수 \(a,b,c,d\)에 대해 아래와 같이 표현되는 '수'이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[ a+b\mathbf{i}+c\mathbf{j}+d\mathbf{k}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(a\)를 &lt;b&gt;스칼라 부분&lt;/b&gt;, \(b\mathbf{i}+c\mathbf{j}+d\mathbf{k}\)를 &lt;b&gt;벡터 부분&lt;/b&gt;이라고도 한다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;사원수는 복소수의 확장으로도 이해할 수 있다. 이 수들의 덧셈은 자연스럽게 정의되고, 곱셈은 아래의 곱셈 규칙을 추가하면 자연스럽게 정의된다. 대수학 지식이 있는 독자라면 \(\mathbf{i},\mathbf{j},\mathbf{k}\)가 사원수군(quaternion group) \(Q_8\)을 만들어낸다는 사실을 알고 있을 것이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 15.9303%; height: 227px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot; bgcolor=&quot;#dddddd&quot;&gt;&amp;times;&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot; bgcolor=&quot;#dddddd&quot;&gt;\(\mathbf{i}\)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot; bgcolor=&quot;#dddddd&quot;&gt;\(\mathbf{j}\)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot; bgcolor=&quot;#dddddd&quot;&gt;\(\mathbf{k}\)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot; bgcolor=&quot;#dddddd&quot;&gt;\(\mathbf{i}\)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;-1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;\(\mathbf{k}\)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;\(-\mathbf{j}\)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot; bgcolor=&quot;#dddddd&quot;&gt;\(\mathbf{j}\)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;\(-\mathbf{k}\)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;-1&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;\(\mathbf{i}\)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot; bgcolor=&quot;#dddddd&quot;&gt;\(\mathbf{k}\)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;\(\mathbf{j}\)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;\(-\mathbf{i}\)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;-1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은, 사원수는 실수나 복소수 체계처럼 교환법칙이 성립하지 않는다는 것이다. 당장 \(\mathbf{i}\mathbf{j}\)와 \(\mathbf{j}\mathbf{i}\)가 다르다는 것만 보아도 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서론에서 언급한 공식을 다시 살펴보자. 단위벡터 \(\mathbf{u}=(x,y,z)\)를 기준으로 반시계방향으로 \(\theta\)만큼 회전하는 동작은 사원수로 아래와 같이 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[ \begin{align} \mathbf{q} &amp;amp;= \cos(\theta/2)+x\sin(\theta/2)\mathbf{i}+y\sin(\theta/2)\mathbf{j}+z\sin(\theta/2)\mathbf{k} \\ &amp;amp;= \cos(\theta/2)+\sin(\theta/2)(x\mathbf{i}+y\mathbf{j}+z\mathbf{k}) \end{align} \]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(\mathbf{q}\)와 \(-\mathbf{q}\)는 같은 회전을 의미함에 유의하자. \(\theta\)만큼 회전하는 것과 \(\theta+2\pi\)만큼 회전하는 것이 같다는 것, 그리고 \(\mathbf{u}\)를 기준으로 \(\theta\)만큼 회전하는 것과 \(-\mathbf{u}\)를 기준으로 \(-\theta\)만큼 회전하는 것이 같다는 것을 생각하면 쉽게 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사원수 표현을 이용하면 3차원 회전에 대한 연산들을 매우 쉽게 수행할 수 있다. 먼저 3차원 회전 \(\mathbf{q}_1\)과 \(\mathbf{q}_2\)를 생각하자. 첫 번째 회전 변환과 두 번째 회전 변환의 합성을 나타내는 사원수는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\mathbf{q}=\mathbf{q}_2\mathbf{q}_1\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(\mathbf{q}_1\)과 \(\mathbf{q}_2\)의 순서가 바뀌는 이유는 오일러 회전에서와 마찬가지로 함수의 합성처럼 취급되기 때문이다. 다시 한번 강조하자면, 사원수는 교환법칙이 성립하지 않기 때문에 \(\mathbf{q}_1\mathbf{q}_2\)와 \(\mathbf{q}_2\mathbf{q}_1\)는 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 벡터 \(\mathbf{v}\)에 사원수 \(\mathbf{q}\)가 나타내는 회전을 적용하기 위해서는 다음과 같이 하면 된다. 먼저 벡터 \(\mathbf{v}=(a,b,c)\)를 사원수 \(\mathbf{v}=a\mathbf{i}+b\mathbf{j}+c\mathbf{k}\)로 취급하자. 즉 \(\mathbf{v}\)는 스칼라 부분이 0이고 벡터 부분이 회전시키고자 하는 벡터를 나타내는 사원수이다. 그러면 회전 변환이 적용된 벡터 \(\mathbf{v}'\)은 아래와 같이 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\mathbf{v}'=\mathbf{q}\mathbf{v}\mathbf{q}^{-1}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 \(\mathbf{q}^{-1}\)는 \(\mathbf{q}\)의 역원, 즉 \(\mathbf{q}\)가 나타내는 회전을 반대로 수행한 역회전을 나타낸다. \(\mathbf{q}\)가 \(\theta\)만큼 회전시키면, \(\mathbf{q}^{-1}\)는 동일한 회전축에 대해 \(-\theta\)만큼 회전시킨 것을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 산출한 사원수 \(\mathbf{v}'\)는 \(\mathbf{v}\)와 마찬가지로 스칼라 부분이 0이고 벡터 부분만 남게 되는데, 이것이 우리가 얻고자 한 회전시킨 벡터가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터의 회전을 이렇게 계산하는 이유는 사원수 \(\mathbf{v}\)와 \(\mathbf{v}'\) 또한 일종의 '회전'으로 간주하면 이해할 수 있다. 회전한 좌표계에서 \(\mathbf{v}'\)의 역할을 알기 위해서는 먼저 원래 좌표계로 돌려보내고(\(\mathbf{q}^{-1}\)), 거기서 \(\mathbf{v}\)의 역할을 적용한 뒤, 다시 회전한 좌표계로 가져오면(\(\mathbf{q}\)) 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 3차원 회전을 쉽게 표현할 수 있다는 것 외에, 행렬 표현과 오일러 회전에서 발생하는 문제를 모두 해결할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3차원 회전을 4개의 실수로 표현하기 때문에 수치적으로 안정적이다.&lt;/li&gt;
&lt;li&gt;하나의 3차원 회전을 나타내는 사원수는 부호만 다른 두 사원수로 유이하게 결정된다.&lt;/li&gt;
&lt;li&gt;회전축과 회전 각도의 정보를 직접 담고 있기 때문에 보간 문제가 쉽게 해결된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3차원 회전 공간의 기하학&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적으로 3차원 회전의 사원수 표현을 직관적으로 이해해 보자. 이를 위해 가장 중요한 작업은, 모든 3차원 회전의 집합을 시각화하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 3차원 회전은 회전축 벡터 \((a,b,c)\)를 나타내는 세 개의 매개변수, 그리고 회전 각도를 나타내는 매개변수 \(\theta\) 이렇게 총 4개의 변수로 매개화될 수 있다. 여기서 핵심은, \(\theta\)가 변화함에 따라 3차원 회전의 집합이 어떻게 변하는지 관찰하여 기하학적 구조에 관한 직관을 이끌어내는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이것은 생각보다 쉽지 않다. 결론부터 이야기하면 3차원 회전의 집합을 시각화하려면 &lt;b&gt;4차원 초구(hypersphere)&lt;/b&gt;가 필요하다! 하지만 우리는 4차원의 도형을 쉽게 상상할 수 없기 때문에, 좀더 단순한 경우부터 시작하는 것이 좋다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회전축 벡터가 \(xy\) 평면에 놓이는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선은 회전축 벡터가 \(xy\) 평면에 놓일 때, 즉 위의 회전축 벡터에서 \(c\)가 0인 경우부터 생각하자. 역시 결론부터 말하자면, 이 경우 3차원 구껍질을 통해 회전 변환의 집합을 시각화할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/siz0L/btrputkNcyz/BD2PLxeEGFZK4kYOelktzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/siz0L/btrputkNcyz/BD2PLxeEGFZK4kYOelktzK/img.png&quot; data-alt=&quot;회전 변환의 공간. 출처: 위키피디아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/siz0L/btrputkNcyz/BD2PLxeEGFZK4kYOelktzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsiz0L%2FbtrputkNcyz%2FBD2PLxeEGFZK4kYOelktzK%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;250&quot; height=&quot;280&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;280&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;위 그림에서 구 표면의 각 점은 하나의 회전 변환을 나타낸다. 세로축은 회전 각도를 의미하고, 각 점이 중심 세로축으로부터 떨어진 방향(예를 들어 위 그림에서 노란색과 빨간색 벡터의 방향)은 회전축 벡터의 방향을 나타낸다. 따라서 구를 지표면과 평행한 단면으로 잘라서 얻어진 원은 고정된 회전 각도 \(\theta\)를 가진 회전 변환의 집합이 된다. 위 그림은 두 가지 단면을 보여주는데, 각각 회전 각도가 80˚, 160˚인 회전 변환의 집합이다. 여기서 주의할 점은, 구의 중심을 기준으로 대칭인 두 점(antipodal points)은 같은 회전 변환을 의미한다는 것이다! 이는 두 회전축 벡터의 부호가 반대일 때 회전 각도의 합이 \(2\pi\)이면 같은 회전 변환이라는 것으로부터 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이런 식으로 시각화가 가능한지, 회전 각도 \(\theta\)를 0에서부터 서서히 키워가며 관찰해보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;\(\theta=0\)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 \(\theta=0\)인 경우는, 회전을 하지 않기 때문에 회전축과 관계없이 좌표계에 아무런 변화가 없는 변환이 된다. 수학적으로는 항등(identity)이라고 표현한다. 따라서 이 경우 회전축의 집합은 원소 하나이다. 위 그림에서 북극에 대응되는 점이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;\(0\leq\theta&amp;lt;\pi\)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpnHLB/btrput59pUa/cSszZwh80vwMvT1mPubQXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpnHLB/btrput59pUa/cSszZwh80vwMvT1mPubQXk/img.png&quot; data-alt=&quot;회전 각도가 180˚ 이하인 경우. 출처: 위키피디아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpnHLB/btrput59pUa/cSszZwh80vwMvT1mPubQXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpnHLB%2Fbtrput59pUa%2FcSszZwh80vwMvT1mPubQXk%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;250&quot; height=&quot;239&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;회전 각도가 180˚ 이하인 경우. 출처: 위키피디아&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 \(\theta\)가 점점 커지기 시작한다. 회전 각도가 \(\pi\), 즉 180˚보다 작은 경우는, 고정된 \(\theta\)에 대해 서로 다른 회전축 벡터가 모두 다른 회전 변환을 만들어낸다. 회전축이 \(xy\) 평면에 놓인다고 가정했기 때문에, 가능한 회전축 벡터의 집합은 2차원 단위벡터의 집합이 되고, 기하학적으로는 원이 된다는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 작은 회전 각도를 가지는 회전 변환의 집합이 큰 회전 각도를 가지는 변환의 집합보다 기하학적으로 더 '비슷한' 변환이 모인 집합이라는 점을 이해하는 것이 중요하다. (사실 수학적으로 엄밀하거나 정확한 표현은 아니지만, 느낌으로 이해하자.) 예를 들어 1˚ 회전시키는 변환들이 90˚ 회전시키는 변환들보다 더 서로 비슷한 변환들이라는 것이다. 따라서 기하학적으로, 작은 \(\theta\)의 경우 회전 변환의 집합은 더 작은 원을 형성하고, 큰 \(\theta\)의 경우 더 큰 원을 형성한다고 생각할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 \(0\leq\theta&amp;lt;\pi\)인 경우를 모두 종합하면, 한 점(\(\theta=0\))에서 시작해 원형(\(\theta&amp;gt;0\))으로 점점 커지는 모양을 상상하면 된다. 여기까지 하면 구 표면 시각화에서 상반구(upper hemisphere)까지 만들어냈다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;\(\theta=\pi\)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 180˚ 회전시키는 경우인데, 원형으로 시각화할 수 있다는 점은 비슷하지만, 회전축 벡터의 부호가 반대일 경우 같은 회전 변환을 의미한다는 것에 유의해야 한다. 이 경우는 위의 구 표면 시각화에서 적도(equator)를 이루게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;\(\pi&amp;lt;\theta\leq 2\pi\)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 범위는 \(0\leq\theta&amp;lt;\pi\)인 경우와 완전히 대칭적으로 생각하면 되고, 구 표면 시각화에서 하반구(lower hemisphere)를 형성한다. 하반구의 각 점은 구의 중심을 기준으로 대칭인 점이 상반구에 존재하고, 둘은 같은 회전 변환을 나타냄에 유의하자. (회전축 벡터의 방향이 반대이고, 회전 각도의 합이 \(2\pi\)이기 때문이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수학의 고급 지식이 있는 독자라면 이러한 회전 변환의 공간의 기하학적 구조가 2차원 실수 사영 공간(real projective space, \(\mathbb{R}P^2\))이라는 것을 눈치챌 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;회전 변환의 매개화&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 회전 변환의 집합을 시각화했으니, 3차원 좌표를 이용해서 매개화해보자.&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;240&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccvZMr/btrpzKMlzYL/FefHjlbOXv5HdVrKEWwjsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccvZMr/btrpzKMlzYL/FefHjlbOXv5HdVrKEWwjsK/img.png&quot; data-alt=&quot;구면좌표. 출처: 위키피디아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccvZMr/btrpzKMlzYL/FefHjlbOXv5HdVrKEWwjsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccvZMr%2FbtrpzKMlzYL%2FFefHjlbOXv5HdVrKEWwjsK%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;240&quot; height=&quot;240&quot; data-origin-width=&quot;240&quot; data-origin-height=&quot;240&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;이 그림에서 \(\mathbf{P}\)가 나타내는 회전 변환의 회전축 벡터가 \((a,b,0)\)이고 회전 각도가 \(\theta\)라면, \(\mathbf{P}\)의 좌표를 어떻게 나타낼 수 있을까? 앞서 이야기했던 대로 \(\mathbf{P}\)의 \(x\)좌표와 \(y\)좌표는 회전축 벡터의 방향을 나타낸다. 따라서 \(z\)축과 \(\mathbf{P}\) 사이의 각도를 \(\phi\)라고 하면, \(\mathbf{P}\)의 좌표는 아래와 같이 나타낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[ (a\sin\phi, b\sin\phi, \cos\phi)&amp;nbsp;\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 \(\phi\)는 무엇일까? 위의 회전 변환의 구 표면 시각화와 더불어 생각해 보자. \(\phi\)가 0부터 \(\pi\)까지 커지면, \(\theta\)는 0부터 \(2\pi\)까지 커진다. 또한 적도에서 \(\phi=\pi/2\)이지만 \(\theta=\pi\)이다. 따라서 \(\phi=\theta/2\)임을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종합하면, 회전축 단위벡터가 \((a,b,0)\)이고 회전 각도가 \(\theta\)인 회전 변환은 구 표면에서 아래의 점에 대응된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[ (a\sin(\theta/2), b\sin(\theta/2), \cos(\theta/2)) \]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벌써 사원수 표현과의 유사점이 보이지 않는가?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반적인 3차원 회전의 표현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 회전축 벡터가 모든 3차원 벡터가 될 수 있는 경우를 살펴보자. 이 경우 고정된 회전 각도 \(\theta\)에 대해 회전축 벡터로 가능한 것은 임의의 3차원 단위 벡터, 즉 회전 변환의 집합은 3차원 구로 시각화된다. 따라서 앞서 살펴본 경우와 같은 방식으로 생각하면, 모든 회전 변환의 집합은 4차원 초구로 시각화할 수 있다는 것을 알 수 있다! 이 경우에도 마찬가지로 초구의 중심을 기준으로 마주보는 두 점은 같은 회전 변환을 나타낸다는 것임은 항상 유의하자. 역시 고급 수학의 지식이 있는 독자라면 3차원 실수 사영 공간이 떠오를 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전히 같은 방식으로 초구를 매개화하면, 회전축 단위벡터가 \((a,b,c)\)이고 회전 각도가 \(\theta\)인 회전 변환은 초구 표면에서 아래의 점에 대응된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[ \mathbf{q}=(a\sin(\theta/2), b\sin(\theta/2), c\sin(\theta/2), \cos(\theta/2)) \]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서론에서 이야기했던 3차원 회전 변환의 사원수 표현과 일치한다는 것을 알 수 있다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트에서는 3차원 회전 변환이 사원수로 깔끔하게 표현되는 흑마법 뒤에 숨은 기하학적 구조에 대해 간략하게 알아보았다. 사실 이 글에는 아직 부족한 점이 많다. 예를 들어서, 사원수 표현의 핵심인 &lt;b&gt;회전 변환의 합성과 사원수의 곱셈이 일치&lt;/b&gt;하는 현상의 직관적인 이해에 관해서는 자세히 이야기하지 않았다. 필자는 이 현상에 대해 필자만의 그림을 가지고 있으나, 솔직히 말해서 수학적으로 완전히 뒷받침된다고 보기 어렵고 글로 풀어쓰기에도 너무 난이도가 높았다. (처음에는 이 내용을 작성했으나, 고민한 뒤에 삭제했다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 회전 변환이라는 것은 수학적으로 상당히 깊은 주제이다. 예를 들어서 3차원 회전 변환과 같이 곱셈 구조와 미분다양체(differential manifold) 구조를 모두 가지고 있는 군을 리 군(Lie group)이라고 하고, 수학 안에서도 꽤나 난해하고 아직까지도 활발히 연구되고 있는 주제이다. 또한 잠깐 언급했던 실수 사영 공간 역시 매우 흥미로운 기하학적 성질을 가지고 있고, 이를 분석하는 데 대수적 위상수학(algebraic topology)과 같은 도구가 많이 쓰인다. 관련된 아주 재미있는 현상 중에서 벨트 트릭(belt trick)이라는 것이 있는데, 여유가 된다면 관련하여 포스트를 추가 작성할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수학에 관심이 많은 독자이고 흥미를 느꼈다면 이러한 주제에 대해 한번 공부해보는 것을 추천한다!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/3D_rotation_group&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://en.wikipedia.org/wiki/3D_rotation_group&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Foundations of 3D Computer Graphics, S. Gortler, MIT Press, 2012&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Computer Science</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/16</guid>
      <comments>https://topology-blog.tistory.com/16#entry16comment</comments>
      <pubDate>Mon, 3 Jan 2022 03:02:39 +0900</pubDate>
    </item>
    <item>
      <title>2015 ACM-ICPC 한국 예선 F - 파일 합치기</title>
      <link>https://topology-blog.tistory.com/15</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11066&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 11066 - 파일 합치기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1588422262453&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;11066번: 파일 합치기&quot; data-og-description=&quot;문제 소설가인 김대전은 소설을 여러 장(chapter)으로 나누어 쓰는데, 각 장은 각각 다른 파일에 저장하곤 한다. 소설의 모든 장을 쓰고 나서는 각 장이 쓰여진 파일을 합쳐서 최종적으로 소설의 완성본이 들어있는 한 개의 파일을 만든다. 이 과정에서 두 개의 파일을 합쳐서 하나의 임시파일을 만들고, 이 임시파일이나 원래의 파일을 계속 두 개씩 합쳐서 소설의 여러 장들이 연속이 되도록 파일을 합쳐나가고, 최종적으로는 하나의 파일로 합친다. 두 개의 파일을&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/11066&quot; data-og-url=&quot;https://www.acmicpc.net/problem/11066&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b69ME8/hyFS6rPidy/FxetXVf9EA22oQ1fov6zSK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/v9NFM/hyFS3IDu3m/IiFymi0QT3vWMbfPLs3ZL1/img.png?width=8084&amp;amp;height=751&amp;amp;face=0_0_8084_751,https://scrap.kakaocdn.net/dn/cZOGIO/hyFS148rnt/L7RSX4VBrM36vs4VUfMxlk/img.png?width=6067&amp;amp;height=751&amp;amp;face=0_0_6067_751&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11066&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/11066&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b69ME8/hyFS6rPidy/FxetXVf9EA22oQ1fov6zSK/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/v9NFM/hyFS3IDu3m/IiFymi0QT3vWMbfPLs3ZL1/img.png?width=8084&amp;amp;height=751&amp;amp;face=0_0_8084_751,https://scrap.kakaocdn.net/dn/cZOGIO/hyFS148rnt/L7RSX4VBrM36vs4VUfMxlk/img.png?width=6067&amp;amp;height=751&amp;amp;face=0_0_6067_751');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;11066번: 파일 합치기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;문제 소설가인 김대전은 소설을 여러 장(chapter)으로 나누어 쓰는데, 각 장은 각각 다른 파일에 저장하곤 한다. 소설의 모든 장을 쓰고 나서는 각 장이 쓰여진 파일을 합쳐서 최종적으로 소설의 완성본이 들어있는 한 개의 파일을 만든다. 이 과정에서 두 개의 파일을 합쳐서 하나의 임시파일을 만들고, 이 임시파일이나 원래의 파일을 계속 두 개씩 합쳐서 소설의 여러 장들이 연속이 되도록 파일을 합쳐나가고, 최종적으로는 하나의 파일로 합친다. 두 개의 파일을&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제는 2015년 ACM-ICPC 예선에 나왔던 문제로, DP 테이블을 정의하면 \(O(K^3)\)의 시간복잡도로 어렵지 않게 풀리는 문제다. 난이도는 &lt;a href=&quot;https://solved.ac/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;solved.ac&lt;/a&gt; 기준 골드 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;\(DP[i][j]\) := \(i\)번째 파일부터 \((j - 1)\)번째 파일까지를 합치는 데 드는 최소 비용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(S[i][j]\) := \(i\)번째 파일부터 \((j - 1)\)번째 파일까지 크기의 총합&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\(DP[i][j] = \min_{i&amp;lt;k&amp;lt;j}(DP[i][k] + DP[k][j] + S[i][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;하지만 이 문제는 \(O(K^3)\)보다 빠르게 푸는 방법이 존재한다. 그중 두 가지 방법을 이 포스트에서 소개하고자 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;\(O(K^2)\)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/13974&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 13974 - 파입 합치기 2&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1588422840339&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;website&quot; data-og-title=&quot;13974번: 파일 합치기 2&quot; data-og-description=&quot;소설가인 김대전은 소설을 여러 장(chapter)으로 나누어 쓰는데, 각 장은 각각 다른 파일에 저장하곤 한다. 소설의 모든 장을 쓰고 나서는 각 장이 쓰여진 파일을 합쳐서 최종적으로 소설의 완성본이 들어있는 한 개의 파일을 만든다. 이 과정에서 두 개의 파일을 합쳐서 하나의 임시파일을 만들고, 이 임시파일이나 원래의 파일을 계속 두 개씩 합쳐서 소설의 여러 장들이 연속이 되도록 파일을 합쳐나가고, 최종적으로는 하나의 파일로 합친다. 두 개의 파일을 합칠&quot; data-og-host=&quot;www.acmicpc.net&quot; data-og-source-url=&quot;https://www.acmicpc.net/problem/13974&quot; data-og-url=&quot;https://www.acmicpc.net/problem/13974&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/Lcicb/hyFS3ommcx/VkL0ggpAwDGUcA2ih2a7b1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cUFmxP/hyFS9PC2dc/xLokawwY6H068ZTdfs3C7k/img.png?width=8084&amp;amp;height=751&amp;amp;face=0_0_8084_751,https://scrap.kakaocdn.net/dn/z2vW2/hyFS7jZaqT/sikthIbF5g5dkN8NPjsuNk/img.png?width=6067&amp;amp;height=751&amp;amp;face=0_0_6067_751&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/13974&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.acmicpc.net/problem/13974&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/Lcicb/hyFS3ommcx/VkL0ggpAwDGUcA2ih2a7b1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/cUFmxP/hyFS9PC2dc/xLokawwY6H068ZTdfs3C7k/img.png?width=8084&amp;amp;height=751&amp;amp;face=0_0_8084_751,https://scrap.kakaocdn.net/dn/z2vW2/hyFS7jZaqT/sikthIbF5g5dkN8NPjsuNk/img.png?width=6067&amp;amp;height=751&amp;amp;face=0_0_6067_751');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;13974번: 파일 합치기 2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소설가인 김대전은 소설을 여러 장(chapter)으로 나누어 쓰는데, 각 장은 각각 다른 파일에 저장하곤 한다. 소설의 모든 장을 쓰고 나서는 각 장이 쓰여진 파일을 합쳐서 최종적으로 소설의 완성본이 들어있는 한 개의 파일을 만든다. 이 과정에서 두 개의 파일을 합쳐서 하나의 임시파일을 만들고, 이 임시파일이나 원래의 파일을 계속 두 개씩 합쳐서 소설의 여러 장들이 연속이 되도록 파일을 합쳐나가고, 최종적으로는 하나의 파일로 합친다. 두 개의 파일을 합칠&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.acmicpc.net&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 형태의 DP는, 최적화할 수 있는 방법이 존재한다. 이 문제는 사실 약간 변형하면 &lt;a href=&quot;https://en.wikipedia.org/wiki/Optimal_binary_search_tree&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Optimal Binary Search Tree&lt;/a&gt; 문제의 특수한 경우임을 알 수 있고, optimal binary search tree problem을 해결하는 여러 가지 알고리즘 중 &lt;a href=&quot;https://en.wikipedia.org/wiki/Optimal_binary_search_tree#Knuth's_dynamic_programming_algorithm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Donald Knuth의 알고리즘&lt;/a&gt;이 우리나라에 &lt;a href=&quot;https://blog.myungwoo.kr/98&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Knuth Optimization&lt;/a&gt;이라는 이름으로 잘 알려져 있다. 위 파일 합치기 2는 다른 조건 변화 없이 \(K\)의 제한이 5000까지 늘어난 문제이고, Knuth algorithm을 사용하도록 의도되었으며, 이 알고리즘이 어느 정도 알려져 있음을 고려해 &lt;span style=&quot;color: #333333;&quot;&gt;난이도는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;a href=&quot;https://solved.ac/&quot;&gt;solved.ac&lt;/a&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;기준 플래티넘 2로 평가되고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Knuth algorithm은 아래와 같은 observation을 바탕으로 시간복잡도를 \(O(K^2)\)까지 단축되며, 이 observation의 정당성과 이렇게 시간복잡도가 줄어드는 것의 증명은 추가 예정이다.&lt;/span&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;span style=&quot;color: #333333;&quot;&gt;\(K[i][j]\) := 위 정의에서 \(DP[i][j]\)가 최적이 되는 가장 작은 \(k\)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;\(K[i][j - 1]\leq K[i][j]\leq K[i + 1][j]\)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1588425186738&quot; class=&quot;c++ arduino&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;bits/stdc++.h&amp;gt;
using namespace std;

int v[5003], s[5003], chk[5003][5003];
long long dp[5003][5003];

int main() {
	ios::sync_with_stdio(false);
	cin.tie(NULL);
	int t;
	cin &amp;gt;&amp;gt; t;
	while (t--) {
		int n;
		cin &amp;gt;&amp;gt; n;
		for (int i = 0; i &amp;lt; n; i++) {
			cin &amp;gt;&amp;gt; v[i];
			s[i + 1] = s[i] + v[i];
			dp[i][i + 1] = 0LL;
			chk[i][i + 1] = i;
		}
		for (int k = 2; k &amp;lt;= n; k++) {
			for (int i = 0; i + k &amp;lt;= n; i++) {
				dp[i][i + k] = (long long)2e18;
				for (int j = chk[i][i + k - 1]; j &amp;lt;= chk[i + 1][i + k]; j++) {
					if (j &amp;lt;= i) continue;
					if (j &amp;gt;= i + k) break;
					if (dp[i][i + k] &amp;gt; dp[i][j] + dp[j][i + k] + s[i + k] - s[i]) {
						dp[i][i + k] = dp[i][j] + dp[j][i + k] + s[i + k] - s[i];
						chk[i][i + k] = j;
					}
				}
			}
		}
		cout &amp;lt;&amp;lt; dp[0][n] &amp;lt;&amp;lt; '\n';
	}
	return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;\(O(K\log K)\)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;BOJ에 '파일 합치기 4'로 추가될 예정이다&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UPDATE&lt;/b&gt;: BOJ에 추가되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/19089&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 19089 - 파입 합치기 4&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;앞서 언급했듯이 이 문제는 optimal binary search tree problem의 특수한 경우인데, 이렇게 특수한 경우이기 때문에 사용할 수 있는 더 빠른 알고리즘이 있다. 이는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Garsia%E2%80%93Wachs_algorithm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Garsia-Wachs algorithm&lt;/a&gt;으로, 정확히 이런 문제를 \(O(K\log 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;Garsia-Wachs 알고리즘에 대해 더 이야기하기 전에 한 가지 간단히 언급하고 넘어가려 한다. 알고리즘을 조금 깊게 공부했거나 정보이론 등을 공부해본 독자라면 &lt;a href=&quot;https://en.wikipedia.org/wiki/Huffman_coding&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Huffman coding&lt;/a&gt;에 대해 알고 있을 것으로 생각한다. Huffman coding은 &lt;b&gt;external path length&lt;/b&gt;를 최소화하는 이진 트리를 찾는 알고리즘이자, 정보이론적인 측면에서는 어떤 symbol set에 대해 &lt;b&gt;expected codeword length&lt;/b&gt;를 최소화하는 prefix-free binary code를 찾는 optimal coding 알고리즘이다. 이 둘은 formulation만 약간 다를 뿐 근본적으로 같은 문제이며, 위의 파일 합치기 문제에 대응할 경우, 인접하지 않은 파일의 합치기도 허용한다면 바로 이 Huffman coding으로 문제를 해결할 수 있다. 이 문제가 &lt;a href=&quot;https://www.acmicpc.net/problem/13975&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 13975 - 파일 합치기 3&lt;/a&gt;이며, &lt;a href=&quot;https://solved.ac/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;solved.ac&lt;/a&gt; 기준 난이도 골드 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;하지만 파일 합치기와 파일 합치기 2는 인접한 파일의 합치기만 허용하고 있다. 이 제한은 optimal coding 문제에서 &lt;b&gt;symbol set의 code의 (alphabetical) order가 고정된다&lt;/b&gt;는 조건으로 해석할 수 있으며, 이러한 coding 방식을 &lt;a href=&quot;https://en.wikipedia.org/wiki/Huffman_coding#Optimal_alphabetic_binary_trees_(Hu%E2%80%93Tucker_coding)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;alphabetical Huffman coding 혹은 Hu-Tucker coding&lt;/a&gt;이라고 부른다. &lt;b&gt;따라서 파일 합치기 3이 Huffman coding을 구하는 문제라면, 파일 합치기와 파일 합치기 2는 사실 Hu-Tucker coding을 구하는 문제인 것이다.&lt;/b&gt; Garsia-Wachs algorithm이 바로 이 Hu-Tucker coding을 \(O(K\log K)\)에 구하는 알고리즘이다. (Hu와 Tucker의 알고리즘을 단순화한 버전이라고 한다.)&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\)이라고 할 때, 이 알고리즘은 다음과 같이 동작한다.&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;0. 정답을 출력할 변수 \(X=0\)를 잡는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. \(A_i\leq A_{i+2}\)를 만족하는 가장 작은 \(i\)를 찾는다. 그러한 \(i\)가 없으면 \(i=n-1\)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. \(A_i\)와 \(A_{i+1}\)을 합치고 이 수를 \(B\)라고 하자. \(A_i\)와 \(A_{i+1}\)은 배열에서 제거하고 그 자리에 \(B\)를 놓는다. 배열의 크기가 1 감소했고, 따라서 \(n\)은 1 줄어든다. \(X\)에 \(B\)를 더한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. \(B\)의 왼쪽에 있는 수가 \(B\)보다 크거나 같을 때까지, 또는 \(B\)가 배열의 첫 번째 원소가 될 때까지, \(B\)를 왼쪽 수와 swap한다. (Bubble sort의 느낌)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 배열에 원소가 2개 이상 남아있으면 1번으로 돌아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. 문제의 정답은 \(X\)이다.&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;400&quot; data-origin-height=&quot;172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/toUlR/btqDQ87xwLp/lBe8xYbIuc7kiaqTIZjmNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/toUlR/btqDQ87xwLp/lBe8xYbIuc7kiaqTIZjmNk/img.png&quot; data-alt=&quot;예시 출처: Wikipedia&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/toUlR/btqDQ87xwLp/lBe8xYbIuc7kiaqTIZjmNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtoUlR%2FbtqDQ87xwLp%2FlBe8xYbIuc7kiaqTIZjmNk%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;400&quot; height=&quot;172&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;예시 출처: Wikipedia&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;주의할 점은, 2번 단계에서 합치는 두 파일을, 실제 문제의 규정대로라면 합칠 수 없을 수도 있다. 즉 위의 과정대로 binary tree를 구성하면 문제의 조건에 위배된다. (이에 대해서는 위 예시 그림을 참조하면 이해가 빠르다.) 하지만 위의 조건대로 파일 합치기를 수행했을 때, 각 파일이 합쳐진 횟수는 동일하면서 문제의 조건을 만족하는 binary tree가 존재한다는 것을 증명할 수 있으며, 그렇게 해서 나온 답인 \(X\)이 최적임도 역시 증명할 수 있다. (왼쪽 그림이 조건에 위배되는 binary tree, 오른쪽이 문제의 조건을 만족하도록 변형된 binary tree이다.) 하지만 증명은 난이도가 있는 편이고, 디테일이 궁금하다면 &lt;a href=&quot;https://doi.org/10.1016%2F0196-6774%2888%2990009-0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kingston&lt;/a&gt;이나 &lt;a href=&quot;https://doi.org/10.1016%2FS0304-3975%2896%2900296-4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Karpinski, Larmore &amp;amp; Rytter의 논문&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;사실 위의 과정을 배열이나 리스트 기반으로 naive하게 구현하면 여전히 \(O(K^2)\)의 시간이 소요된다. 그래도 Knuth algorithm보다 상수가 훨씬 작기 때문에 실제 컴퓨터에서 동작할 때 걸리는 시간은 훨씬 짧다. 이 시간을 \(O(K\log 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;우선 처음부터 배열 전체를 보면서 1번 과정에 해당하는 \(i\)를 찾는 대신, 배열에 원소가 \(A_1\)부터 차례차례 추가된다고 상상해 보자. 1번 과정에 해당하는 \(i\)가 발견되기 전까지는, 배열은 \(A_j &amp;gt; A_{j+2}\)를 만족한다. 따라서 배열의 홀수 번째 원소끼리, 짝수 번째 원소끼리 따로 모아서 배열을 두 개로 분리하게 되면, 각각의 배열의 원소들은 역순으로 정렬된 상태를 유지하게 된다. 이렇게 정렬된 상태를 유지하면서, 2번과 3번 과정을 효율적으로 수행하기 위해서는 insert, binary search, delete 연산과 \(k\)번째 원소를 찾는 연산 등을 모두 \(O(\log n)\)에 수행할 수 있는 자료구조를 찾게 되고, Splay tree가 이런 용도로는 가장 적합하다. (여기서 \(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;짝수 번째 원소와 홀수 번째 원소들을 Splay tree로 관리하게 되면, 위 과정을 아래와 같이 \(O(\log 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;1. 앞서 언급했듯이 원소를 하나씩 넣으면서 조건을 만족하는 \(i\)가 생기는지 확인하면 된다. 이것만 해서는 사실 불완전한데, 자세한건 아래 3번 참조.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 배열에서 원소 제거는 delete 연산으로 처리하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 이 단계가 난관이자 핵심이다. \(B\)가 들어갈 위치를 찾기 위해서는, 우선 두 개의 Splay tree에 \(B\)에 대한 binary search 연산을 통해, \(B\)가 들어가야 할 위치를 찾는다. 그런데 문제는, \(B\)를 끼워넣기 위해서는 두 가지 Splay tree (odd 혹은 even) 중 하나에 insert를 해야 하는데, 그냥 찾은 위치에 그대로 insert를 하게 되면 binary search tree의 기본 전제인 원소의 정렬 상태가 깨질 수도 있다. 예를 들어 \(A_{2i}\geq B\)라면, \(B\)는 배열의 \((2i+1)\)번째 원소가 되어야 하므로 Splay tree에서 들어가야할 위치는 홀수 번째 원소들의 Splay tree에서 \(A_{2i-1}\)의 바로 다음이지만, \(A_{2i-1}\leq B\)가 될 수도 있다는 것이다. 그런데 이렇게 되면 \(A_{2i-1}\), \(A_{2i}\), \(B\) &lt;b&gt;이 세 개의 수가 바로 1번의 조건을 만족&lt;/b&gt;하게 된다. 따라서 이 경우 재귀적으로 1번 과정으로 돌아가서 처리하고 나면, \(B\)는 비로소 Splay tree의 정렬 상태를 깨지 않고 삽입될 수 있게 된다.&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;위와 같이 구현이 꽤나 복잡한 편이지만, Splay tree를 능숙하게 다룰 수 있다면 아예 못할 정도는 아니다. Splay tree를 하나만 사용하는 좀더 간단한 구현도 존재한다!&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에 문제를 추가했다. 검수해주신 &lt;a href=&quot;https://koosaga.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;koosaga&lt;/a&gt;님과 &lt;a href=&quot;https://tamref.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;TAMREF&lt;/a&gt;님 감사합니다.&lt;s&gt;&lt;/s&gt;&lt;/p&gt;</description>
      <category>Computer Science/알고리즘</category>
      <category>ACM-ICPC</category>
      <category>DP</category>
      <category>알고리즘</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/15</guid>
      <comments>https://topology-blog.tistory.com/15#entry15comment</comments>
      <pubDate>Sat, 2 May 2020 23:57:06 +0900</pubDate>
    </item>
    <item>
      <title>Link/Cut Tree (2)</title>
      <link>https://topology-blog.tistory.com/7</link>
      <description>&lt;ul style=&quot;list-style-type: square;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;http://topology-blog.tistory.com/5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;Link/Cut Tree (1)&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 포스트에서 LCT에 대한 기본적인 내용을 다루었는데, 이번 포스트에서는 LCT가 어떻게 활용될 수 있는지 간단하게만 알아보려고 한다. 보충 자료 같은 느낌의 포스트이다.&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;b&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;1. Link 연산의 확장&lt;/span&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;원래의 Link 연산은 붙이는 쪽이 represented tree의 루트인 경우에만 가능했다. 그러나 실제로 루트가 아닌 노드를 붙이고 싶을 때도 있을 것이다. 이를 위해서는 represented tree의 루트를 변경하는 작업이 필요하다.&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;어떤 represented tree의 한 노드를 $v$라고 했을 때, access($v$)를 하면 v에서 루트로 연결되는 preferred path가 생긴다는 점은 이전 포스트에서 알아보았다. 그런데 여기서 주목할 점은 이 preferred path의 양끝이 각각 루트와 $v$라는 것이다. 우리는 Splay Tree라는 자료구조로 preferred path(또는 auxiliary tree)를 관리해왔고, access($v$)를 수행하면서 $v$가 이 Splay Tree의 루트에 오게 되므로, 이 Splay Tree를 뒤집어주면 $v$가 가장 왼쪽에 오는 노드가 되면서 전체 represented tree의 루트의 포지션을 갖게 된다.&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;Splay Tree를 직접 뒤집어주면 시간이 너무 오래걸리기 때문에, 우리는 lazy propagation이라는 테크닉을 이용한다. Splay Tree의 lazy propagation에 대해서는 &lt;a class=&quot;tx-link&quot; href=&quot;http://cubelover.tistory.com/10&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 블로그&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;먼저 자신의 서브트리가 뒤집혔는지 알려주는 값을 노드 구조체에 추가해야 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;struct node {
    node *l, *r, *p, *pp;
    bool inv;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 propagate을 시키는 함수를 만든다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void propagate(node *cur) {
    if (!cur-&amp;gt;inv) return;
    swap(cur-&amp;gt;l, cur-&amp;gt;r);
    cur-&amp;gt;inv = false;
    if (cur-&amp;gt;l) cur-&amp;gt;l-&amp;gt;inv = !cur-&amp;gt;l-&amp;gt;inv;
    if (cur-&amp;gt;r) cur-&amp;gt;r-&amp;gt;inv = !cur-&amp;gt;r-&amp;gt;inv;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 swap 함수는 C++ STL의 swap이다. C를 사용하고 싶다면 다음과 같이 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void propagate(node *cur) {
    if (!cur-&amp;gt;inv) return;
    node *tmp = cur-&amp;gt;l;
    cur-&amp;gt;l = cur-&amp;gt;r;
    cur-&amp;gt;r = tmp;
    cur-&amp;gt;inv = false;
    if (cur-&amp;gt;l) cur-&amp;gt;l-&amp;gt;inv = !cur-&amp;gt;l-&amp;gt;inv;
    if (cur-&amp;gt;r) cur-&amp;gt;r-&amp;gt;inv = !cur-&amp;gt;r-&amp;gt;inv;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lazy propagation 테크닉에서 항상 주의해야 하는 것은 propagate을 시키는 타이밍이다. 일반적으로 부모에서 자식을 접근할 때 propagate을 해주는데, splay나 rotate 함수와 같은 경우는 조금 애매하다. 그런데 rotate 함수를 자세히 보면 rotate시키려는 노드와 그 노드의 부모를 제외한 나머지 서브트리는 내부적으로 접근하지 않는다는 것을 알 수 있다. 즉 그 노드와 부모의 lazy만 없애주면 안전하다는 뜻이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void rotate(node *cur) {
    node *p = cur-&amp;gt;p;
    propagate(p);
    propagate(cur);
    if (cur == p-&amp;gt;l) {
        if ((p-&amp;gt;l = cur-&amp;gt;r)) cur-&amp;gt;r-&amp;gt;p = p;
        cur-&amp;gt;r = p;
    } else {
        if ((p-&amp;gt;r = cur-&amp;gt;l)) cur-&amp;gt;l-&amp;gt;p = p;
        cur-&amp;gt;l = p;
    }
    cur-&amp;gt;p = p-&amp;gt;p;
    p-&amp;gt;p = cur;
    if (cur-&amp;gt;p) {
        if (cur-&amp;gt;p-&amp;gt;l == p) cur-&amp;gt;p-&amp;gt;l = cur;
        else cur-&amp;gt;p-&amp;gt;r = cur;
    } else {
        cur-&amp;gt;pp = p-&amp;gt;pp;
        p-&amp;gt;pp = NULL;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Splay 함수는 rotate로 구현되어 있으므로 건드리지 않아도 된다. 다만 일반적으로 rotate함수를 직접 부를 일은 많지 않기 때문에 rotate 함수 대신 splay 함수를 직접 고칠 수도 있다. 이 경우 중간 노드를 한 번만&amp;nbsp;propagate시키면 되기 때문에&amp;nbsp;성능은&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;추가로, access 함수와 find_root 함수에서도 자식 노드를 접근하기 때문에 역시 propagate을 해주어야 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void access(node *cur) {
    splay(cur);
    propagate(cur);
    if (cur-&amp;gt;r) {
        cur-&amp;gt;r-&amp;gt;pp = cur;
        cur-&amp;gt;r-&amp;gt;p = NULL;
        cur-&amp;gt;r = NULL;
    }
    while (cur-&amp;gt;pp) {
        node *pp = cur-&amp;gt;pp;
        splay(pp);
        propagate(pp);
        if (pp-&amp;gt;r) {
            pp-&amp;gt;r-&amp;gt;pp = pp;
            pp-&amp;gt;r-&amp;gt;p = NULL;
        }
        pp-&amp;gt;r = cur;
        cur-&amp;gt;p = pp;
        cur-&amp;gt;pp = NULL;
        splay(cur);
    }
}

node *find_root(node *cur) {
    access(cur);
    while (cur-&amp;gt;l) {
        cur = cur-&amp;gt;l;
        propagate(cur);
    }
    access(cur);
    return cur;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에 link 함수, cut 함수 등에서도 자식 노드를 호출하지만, access 함수를 통해 이미 노드에서 propagate이 진행되기 때문에 추가적으로 해줄 필요는 없다.&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$를 자신이 속한 represented tree의 루트로 만드는 함수를 작성한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void make_root(node *cur) {
    access(cur);
    cur-&amp;gt;inv = !cur-&amp;gt;inv;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;드디어 Link 연산을 확장할 수 있게 되었다. 이제 임의의 노드를 임의의 다른 트리의 노드로 붙이는 것이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void connect(node *cur, node *nxt) {
    make_root(cur);
    link(cur, nxt);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-size: 14pt;&quot;&gt;&lt;b&gt;2. 다양한 경로 쿼리&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;이제 LCT를 이용해서 다양한 경로 쿼리를 처리해 보자. 일반적인 트리에서 경로 합 쿼리, 경로 최솟값 쿼리 등은 HLD(Heavy Light Decomposition)를 이용해 어렵지 않게 해결할 수 있었다. 그러나 트리의 Link 연산이나 Cut 연산이 추가될 경우 HLD로는 어렵게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HLD의 기본 아이디어를 떠올려 보자.&amp;nbsp;어떤 노드에서 루트까지 올라가는 경로를 $O(\log N)$개의 구간으로 쪼갠 뒤, 노드와 조상 노드 사이의 쿼리를 우선적으로 처리하는 것이다. 그러고 나면 임의의 두 노드 사이의 쿼리도 LCA를 구한 뒤 따로따로 처리하는 방법을 사용할 수 있었다.&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;LCT에서도 비슷한 아이디어를 사용할 것이다. 다만 트리가 계속 변하기 때문에 이제 LCA를 $O(\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;같은 represented 트리에 속한 두 노드 $v$와 $w$의 LCA를 구해 보자. 일단 다른 연산들이 그렇듯 일단 access 연산은 하고 봐야 한다. access($v$)와 access($w$)를 차례대로 수행하면 무슨 일이 일어날까? access($v$)를 한 뒤에는 $v$에서 루트로 가는 preferred path가 만들어진다. 이후 access($w$)를 하면, $w$에서 루트로 가는 경로가 만들어지면서 원래 $v$와 루트를 잇던 경로에 변화가 생긴다. 이 변화가 생기는 지점이 $v$와 $w$의 LCA라는 것을 알 수 있다.&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;781&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://t1.daumcdn.net/cfile/tistory/99DD2733599A7B801C?original&quot; data-phocus=&quot;https://t1.daumcdn.net/cfile/tistory/99DD2733599A7B801C?original&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/99DD2733599A7B801C&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Ft1.daumcdn.net%2Fcfile%2Ftistory%2F99DD2733599A7B801C&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;781&quot; height=&quot;329&quot; data-origin-width=&quot;781&quot; data-origin-height=&quot;329&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;좀더 정확하게 이야기하면, access($v$) 직후에는&amp;nbsp;$v$와 LCA는 (만일 다르다면) preferred edge로 이어져 있겠지만, access($w$)가 수행되면 LCA는 $v$가 포함된 서브트리와의 연결을 끊고 path-parent pointer로 잇게 될 것이다. 예외가 있는데, LCA가 $v$ 자신인 경우이다. 이 경우 $v$는 $w$의 조상노드이므로 $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;즉, 위 두 access 연산이 끝난 뒤 $v$가 포함된 auxiliary tree의 path-parent pointer가 존재한다면 그 노드가 LCA이고, 존재하지 않는다면 $v$는 $w$의 조상, 즉 $v$ 자신이 LCA가 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;node *lca(node *cur, node *nxt) {
    access(cur);
    access(nxt);
    splay(cur);
    return cur-&amp;gt;pp ? cur-&amp;gt;pp : cur;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 준비는 모두 끝났다. 먼저 한 가지 짚고 넘어갈 것이 있는데, 경로 쿼리에는 경로에 있는 노드의 값들에 관한 쿼리와 경로에 있는 간선 값들에 관한 쿼리가 있다. 전자의 경우 별 문제가 없다. 후자의 경우 문제가 될 수 있는데, 노드의 값을 자신과 부모를 잇는 간선의 값으로 설정하면 쉽게 해결될 것 같아 보인다. 그러나 Splay Tree를 뒤집어야 하는 경우 부모자식 관계가 뒤바뀌기 때문에 꽤나 골치가 아프다. 이 경우 간선 중앙에 정점을 추가한 후 그 정점에 값을 적어놓으면 해결 가능하다. (알려주신 koosaga님과 cki86201님 감사합니다.)&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;경로 합 쿼리는 같은 방식으로 처리할 수 있으므로 경로 최솟값 쿼리를 처리해 보자. 우리는 LCA를 구할 수 있기 때문에 노드와 조상 노드 사이의 쿼리만 효율적으로 처리하면 된다. 즉, 노드 $w$의 조상 노드를 $v$라 하고 $v$와 $w$를 잇는 경로에 있는 노드 값들의 최솟값을 구해 보자.&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 class=&quot;cpp&quot;&gt;&lt;code&gt;struct node {
    node *l, *r, *p, *pp;
    bool inv;
    int val, min_val;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브트리의 최솟값은 부모자식 관계가 바뀔 때마다 재설정되어야 한다. 이를 위한 reset 함수를 만든다. 이 함수는 rotate, cut, link 함수 마지막 부분에 호출해 주어야 한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void reset(node *cur) {
    cur-&amp;gt;min_val = cur-&amp;gt;val;
    if (cur-&amp;gt;l &amp;amp;&amp;amp; cur-&amp;gt;min_val &amp;gt; cur-&amp;gt;l-&amp;gt;min_val) cur-&amp;gt;min_val = cur-&amp;gt;l-&amp;gt;min_val;
    if (cur-&amp;gt;r &amp;amp;&amp;amp; cur-&amp;gt;min_val &amp;gt; cur-&amp;gt;r-&amp;gt;min_val) cur-&amp;gt;min_val = cur-&amp;gt;r-&amp;gt;min_val;
}

void rotate(node *cur) {
    node *p = cur-&amp;gt;p;
    propagate(p);
    propagate(cur);
    if (cur == p-&amp;gt;l) {
        if ((p-&amp;gt;l = cur-&amp;gt;r)) cur-&amp;gt;r-&amp;gt;p = p;
        cur-&amp;gt;r = p;
    } else {
        if ((p-&amp;gt;r = cur-&amp;gt;l)) cur-&amp;gt;l-&amp;gt;p = p;
        cur-&amp;gt;l = p;
    }
    cur-&amp;gt;p = p-&amp;gt;p;
    p-&amp;gt;p = cur;
    if (cur-&amp;gt;p) {
        if (cur-&amp;gt;p-&amp;gt;l == p) cur-&amp;gt;p-&amp;gt;l = cur;
        else cur-&amp;gt;p-&amp;gt;r = cur;
    } else {
        cur-&amp;gt;pp = p-&amp;gt;pp;
        p-&amp;gt;pp = NULL;
    }
    reset(p);
    reset(cur);
}

void link(node *root, node *cur) {
    access(root);
    access(cur);
    root-&amp;gt;l = cur;
    cur-&amp;gt;p = root;
    reset(root);
}

void cut(node *cur) {
    access(cur);
    if (cur-&amp;gt;l) {
        cur-&amp;gt;l-&amp;gt;p = NULL;
        cur-&amp;gt;l = NULL;
        reset(cur);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드의 값이 중간중간에 바뀔 수도 있다. 이를 위해 update 함수도 추가한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void update(node *cur, int val) {
    access(cur);
    cur-&amp;gt;val = val;
    reset(cur);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 본격적인 path_min 함수를 만들어 보자. 노드 $v$가 $w$의 조상이라는 가정을 했으므로 access($w$)를 수행하면 $v$와 $w$는 하나의 Splay Tree에 모이게 된다. 이제 Splay Tree에서 구간 최솟값 구하기 문제로 바뀐다. 이에 대한 좀더&amp;nbsp;자세한 설명은 역시 &lt;a class=&quot;tx-link&quot; href=&quot;http://cubelover.tistory.com/10&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 블로그&lt;/a&gt;를 참고하면 좋다. path_min($v$, $w$) 함수에서는 $v$는 $w$의 조상이고 서로 다르다고 가정한다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;int path_min(node *cur, node *nxt) {
    access(cur);
    access(nxt);
    splay(cur);
    cur-&amp;gt;r-&amp;gt;p = NULL;
    splay(nxt);
    nxt-&amp;gt;p = cur;
    int res = cur-&amp;gt;val &amp;gt; nxt-&amp;gt;val ? nxt-&amp;gt;val : cur-&amp;gt;val;
    return nxt-&amp;gt;l &amp;amp;&amp;amp; res &amp;gt; nxt-&amp;gt;l-&amp;gt;min_val ? nxt-&amp;gt;l-&amp;gt;min_val : res;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 경로 최솟값 쿼리 문제도 끝나게 된다. 비슷한 방식으로 Splay Tree의 강력한 기능을 이용해 다양한 경로&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;LCT는 BOJ에 연습문제가 많지 않다. 다음 문제들은 HLD로 풀리는 문제들이지만, LCT 구현을 제대로 했는지 확인해볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: square;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;https://www.acmicpc.net/problem/13510&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 13510: 트리와 쿼리 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;https://www.acmicpc.net/problem/2927&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 2927: 남극 탐험&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 추가 - 아래 문제집에서 Splay Tree와 Link/Cut Tree를 연습해볼 수 있다. 문제집을 만들고 알려주신 &lt;a class=&quot;tx-link&quot; href=&quot;http://koosaga.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;koosaga&lt;/a&gt;님 감사합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: square;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;https://www.acmicpc.net/workbook/view/912&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 문제집: Segtrees are too mainstream&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Computer Science/알고리즘</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/7</guid>
      <comments>https://topology-blog.tistory.com/7#entry7comment</comments>
      <pubDate>Mon, 21 Aug 2017 17:13:58 +0900</pubDate>
    </item>
    <item>
      <title>Fast Fourier Transform</title>
      <link>https://topology-blog.tistory.com/6</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;고속 푸리에 변환(Fast Fourier Transform, FFT)은 convolution을 $O(N\log N)$에 구할 때 활용된다. 이 포스트에서는 코드 자체보다도 FFT 알고리즘의 원리를 알아보는 것이 목적이다. 코드만 보고싶다면 맨 아래로 내려가면 된다.&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에서 활용되는 푸리에 변환은 수열을 수열로 보내는 이산 푸리에 변환(Discrete Fourier Transform, DFT)이다.&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$인 수열 $\lbrace a_j\rbrace_{j=0}^{N-1}$의 DFT $\lbrace A_n\rbrace_{n=0}^{N-1}$은&amp;nbsp;다음과 같이 정의된다. $e^{i\theta}=\cos\theta+i\sin\theta$로 정의됨에 유의하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[A_n = \sum_{j=0}^{N-1} e^{-2\pi i j n / N}a_j\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(일반적인) 푸리에변환에는 좋은 성질이 있는데, 역변환을 구하기 쉽다는 것이다. $\lbrace A_n\rbrace_{n=0}^{N-1}$의 이산 푸리에 역변환(Inverse Discrete Fourier Transform)은 다음과 같이 계산한다. 증명은 직접 대입해보면 알 수 있으므로 여기서는 생략한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[a_n = \sum_{j=0}^{N-1} \frac{1}{N}e^{2\pi i j n / N}A_j\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 DFT를 구하는 것과 IDFT를 구하는 것은&amp;nbsp;방법이 거의 같다고도 말할 수 있다. 여기서 $e^{2\pi i}=1$이라는 사실을 이용하면 위 식을 다음과 같이 적을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[a_n = \sum_{j=0}^{N-1}\frac{1}{N}e^{-2\pi i j n / N}A_{N-j}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 항에 나타나는 $A_N$은, 수열의 주기가 $N$이므로 $A_0$와 같다. 즉 어떤 수열의 IDFT를 구하기 위해&amp;nbsp;2번째 항(0-based로 index 1)부터 마지막 항까지를 뒤집어준 뒤 DFT를 취하고, 각 항을&amp;nbsp;$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;또 푸리에변환의 좋은 성질 중 하나는 convolution을 곱으로 바꿔준다는 것이다. 수학적 표현으로는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\mathcal{F}\lbrace \mathbf{a} * \mathbf{b}\rbrace&amp;nbsp;= \mathcal{F}\lbrace \mathbf{a}\rbrace\mathcal{F}\lbrace \mathbf{b}\rbrace\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주기가 $N$인 수열 $\lbrace a_j\rbrace_{j=0}^{N-1}$와 $\lbrace b_j\rbrace_{j=0}^{N-1}$의 convolution $\lbrace c_n\rbrace_{n=0}^{N-1}$은 다음과 같이 정의된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[c_n = \sum_{j=0}^{N-1}a_j b_{n - j}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정의를 보면, $b$의 첨자가 음수가 될 수도 있다는 사실이 이상할 수도 있는데, DFT에서 모든 수열은 주기가 $N$이다. 따라서 $b_{-k}$는 $b_{N-k}$와 같다. convolution에 DFT를 취하면 곱이 된다는 사실 역시 원 식에 대입하면 확인할 수 있다. 증명은 생략한다.&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;여기서 눈여겨볼 점은, $\lbrace c_n\rbrace_{n=0}^{N-1}$을 직접 계산하면 $O(N^2)$의 시간이 걸리지만, DFT를 빠르게 구할 수 있다면 두 수열에 DFT를 취한 뒤 각 원소를 곱하고 IDFT를 취하면 convolution을 얻을 수 있으므로 $O(N^2)$보다 빠른 시간에 원하는 결과를 얻을 수 있다. 고속 푸리에 변환(Fast Fourier Transform)은 DFT를 $O(N\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;FFT에는 여러 종류가 있지만, 가장 잘 알려져 있고 구현하기 편한 것은 &lt;a class=&quot;tx-link&quot; href=&quot;https://en.wikipedia.org/wiki/Cooley%E2%80%93Tukey_FFT_algorithm&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Cooley-Tukey Algorithm&lt;/a&gt;이다. 이 방법은 $N$이 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;먼저 $\lbrace a_j\rbrace_{j=0}^{N-1}$의 푸리에 변환 식을 조금 변형해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[\begin{aligned}A_n &amp;amp;= \sum_{j=0}^{N-1} e^{-2\pi i j n / N}a_j \\ &amp;amp;= \sum_{j=0}^{N/2-1}e^{-2\pi i (2j) n / N}a_{2j} + \sum_{j=0}^{N/2-1}e^{-2\pi i (2j+1)n / N}a_{2j+1} \\ &amp;amp;= \sum_{j=0}^{N/2-1}e^{-2\pi i j&amp;nbsp;n / (N/2)}a_{2j} + e^{-2\pi i n / N}\sum_{j=0}^{N/2-1}e^{-2\pi i j n / (N / 2)}a_{2j+1}\end{aligned}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우측에 익숙한 식이 두 개 보인다. 이들은 각각 짝수번째 항들의 DFT와 홀수번째 항들의 DFT이다. 이것들은 $n$ 대신 $n+N/2$이 들어가도 변하지 않는 값이다. $n$ 대신 $n+N/2$이 들어갔을 때 변하는 값은 $e^{-2\pi i n / N}$밖에 없고, 부호만 바뀐다는 것을 알 수 있다. 즉 $n&amp;lt;N/2$일 때 다음과 같이 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[A_{n+N/2}&amp;nbsp;= \sum_{j=0}^{N/2-1}e^{-2\pi i j&amp;nbsp;n / (N/2)}a_{2j} -&amp;nbsp;e^{-2\pi i n / N}\sum_{j=0}^{N/2-1}e^{-2\pi i j n / (N / 2)}a_{2j+1}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은, 짝수번째 항들의 DFT와 홀수번째 항들의 DFT를 알고 있다면, 전체 DFT를 계산하는 데 $O(N)$의 시간이면 충분하다는 것이다. 따라서 $O(N\log N)$에 DFT를 계산할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;typedef complex&amp;lt;double&amp;gt; base;

void fft(vector&amp;lt;base&amp;gt; &amp;amp;a, vector&amp;lt;base&amp;gt; &amp;amp;A) {
    int n = (int) a.size();
    if (n == 1) {
        A[0] = a[0];
        return;
    }
    vector&amp;lt;base&amp;gt; even(n / 2), odd(n / 2), Even(n / 2), Odd(n / 2);
    for (int i = 0; i &amp;lt; n / 2; i++) {
        even[i] = a[2 * i];
        odd[i] = a[2 * i + 1];
    }
    fft(even, Even);
    fft(odd, Odd);
    double th = -2.0 * M_PI / n;
    base w = base(cos(th), sin(th));
    base z = base(1);
    for (int i = 0; i &amp;lt; n / 2; i++) {
        A[i] = Even[i] + z * Odd[i];
        A[i + n / 2] = Even[i] - z * Odd[i];
        z *= w;
    }
}

void ifft(vector&amp;lt;base&amp;gt; &amp;amp;A, vector&amp;lt;base&amp;gt; &amp;amp;a) {
    reverse(++A.begin(), A.end());
    fft(A, a);
    int n = (int) a.size();
    for (int i = 0; i &amp;lt; n; i++) {
        a[i] /= n;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구현에서 한 가지 문제점이 있다면, 추가적인 벡터의 생성이 너무 많다는 것이다. 물론 위 구현으로도 문제들을 푸는 데 지장은&amp;nbsp;없지만, 이왕이면 좀더 효율적으로 FFT를 수행하는 방법을 찾아보자.&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=8$인 경우부터 살펴보도록 하자. 짝수번째 항들을 앞쪽으로 모으고 홀수번째 항들을 뒤쪽으로 모아보자.&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;386&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://t1.daumcdn.net/cfile/tistory/99C3A6335992778304?original&quot; data-phocus=&quot;https://t1.daumcdn.net/cfile/tistory/99C3A6335992778304?original&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/99C3A6335992778304&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Ft1.daumcdn.net%2Fcfile%2Ftistory%2F99C3A6335992778304&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;386&quot; height=&quot;193&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;193&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;여기서 앞 4개 항과 뒤 4개 항 각각에 FFT를 행해 보자.&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;296&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://t1.daumcdn.net/cfile/tistory/99B6B03359928B1607?original&quot; data-phocus=&quot;https://t1.daumcdn.net/cfile/tistory/99B6B03359928B1607?original&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/99B6B03359928B1607&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Ft1.daumcdn.net%2Fcfile%2Ftistory%2F99B6B03359928B1607&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;296&quot; height=&quot;290&quot; data-origin-width=&quot;296&quot; data-origin-height=&quot;290&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;이제 위 식에서와 같이 $E_i$와 $O_i$를 조합하면 $A_i$와 $A_{i+N}$를 만들 수 있고 위치도 같다. 따라서 $E_i$와 $O_i$를 모두 구했다면, $O(1)$의 추가 메모리로 DFT를 구하는 데 충분하다. 즉, inplace로 연산을 수행할 수 있다.&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개 항의 DFT를 구할 때는 어떻게 할까? 같은 방법으로 각각 안에서 홀수 항과 짝수항을 앞뒤로 민다.&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;290&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://t1.daumcdn.net/cfile/tistory/990B793359928C9412?original&quot; data-phocus=&quot;https://t1.daumcdn.net/cfile/tistory/990B793359928C9412?original&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/990B793359928C9412&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Ft1.daumcdn.net%2Fcfile%2Ftistory%2F990B793359928C9412&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;290&quot; height=&quot;290&quot; data-origin-width=&quot;290&quot; data-origin-height=&quot;290&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;이제 $\lbrace a_0, a_4\rbrace$ 두 항으로 이루어진 수열과 $\lbrace a_2, a_6\rbrace$ 두 항으로 이루어진 수열에 FFT를 수행하고 앞 문단의 방식과 같이&amp;nbsp;inplace로 조합하면, 수열 $\lbrace a_0, a_2, a_4, a_6\rbrace$의 DFT를 구할 수 있다. 그러고 나면 앞 문단의 그림으로 환원되므로, 같은 방식으로 진행해&amp;nbsp;$O(1)$보다 큰 메모리 없이 전체 배열의 FFT를 완료할 수 있다.&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$에 대하여&amp;nbsp;다음과 같은 결론을 얻을 수 있다: 만일 처음 배열을 적당히 잘 재배열해 놓는다면, 그 다음부터는 $O(1)$보다 큰 추가&amp;nbsp;메모리 없이 전체 배열의 FFT를 완료할 수 있다. 그렇다면 재배열을 어떻게 하면 될까? 재배열하는 과정에서 $a_k$의 움직임을 관찰해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;$k$가 짝수라면, $a_k$는 앞 $N/2$ 크기의 배열로 들어가고, 홀수라면 뒤 $N/2$ 크기의 배열로 들어간다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;새로 들어간 배열에서 인덱스가 짝수라면 앞 $N/4$ 크기의 배열, 홀수라면 뒤 $N/4$ 크기의 배열로 들어간다.&lt;/li&gt;
&lt;li&gt;새로 들어간 배열의 크기가 1이 될 때까지 반복.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때 새로 들어간 배열에서의 인덱스는, 이전 배열에서의 인덱스에서 2를 나눈 몫이라는 것을 쉽게 알 수 있다. 더 쉬운 이해를 위해&amp;nbsp;$N=8$일 때 $a_5$의 움직임을 관찰해 보자.&lt;/p&gt;
&lt;p style=&quot;text-align: center; clear: none; float: none;&quot; 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;417&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://t1.daumcdn.net/cfile/tistory/994198335992990108?original&quot; data-phocus=&quot;https://t1.daumcdn.net/cfile/tistory/994198335992990108?original&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/994198335992990108&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Ft1.daumcdn.net%2Fcfile%2Ftistory%2F994198335992990108&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;417&quot; height=&quot;291&quot; data-origin-width=&quot;417&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center; clear: none; float: none;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center; clear: none; float: none;&quot; 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칸씩, 4칸씩 FFT 수행 후 inplace 조합 과정을 반복하면 $O(1)$보다 큰 추가 메모리 없이 전체 배열의 FFT를 완료할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;typedef complex&amp;lt;double&amp;gt; base;

void fft(vector&amp;lt;base&amp;gt; &amp;amp;a, bool inv) {
    int n = (int) a.size();
    vector&amp;lt;base&amp;gt; b = a;
    for (int i = 0; i &amp;lt; n; i++) {
        int sz = n, shift = 0, idx = i;
        while (sz &amp;gt; 1) {
            if (idx &amp;amp; 1) shift += sz &amp;gt;&amp;gt; 1;
            idx &amp;gt;&amp;gt;= 1;
            sz &amp;gt;&amp;gt;= 1;
        }
        a[shift + idx] = b[i];
    }
    for (int i = 1; i &amp;lt; n; i &amp;lt;&amp;lt;= 1) {
        double x = inv ? M_PI / i : -M_PI / i;
        base w = {cos(x), sin(x)};
        for (int j = 0; j &amp;lt; n; j += i &amp;lt;&amp;lt; 1) {
            base th = {1, 0};
            for (int k = 0; k &amp;lt; i; k++) {
                base tmp = a[i + j + k] * th;
                a[i + j + k] = a[j + k] - tmp;
                a[j + k] += tmp;
                th *= w;
            }
        }
    }
    if (inv) {
        for (int i = 0; i &amp;lt; n; i++) {
            a[i] /= n;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 유일한 오점은 복사 배열을 하나 더 만들어야 한다는 것이다. 그런데 $a_k$의 움직임을 잘 관찰하면 중요한 성질을 하나 알 수 있는데, $N=2^n$일 때 재배열 후 $a_k$의 위치는 $k$를 $n$자리 이진법 전개를 했을 때 $k$의 bit reversal이라는 것이다. 예를 들어 $N=8$이고 $k=5$라면, 5의 세 자리 이진법 전개는 $101$이므로 bit reversal인 $101$, 즉 자기 자신인 5가 재배열 후 위치가 된다. 만일 $N=16$이고 $k=11$이면, $1011$의 bit reversal인 $1101$, 즉 13이 재배열 후 위치가 된다. 결국&amp;nbsp;$k$의 재배열 후 위치가 $l$이라면 $l$의 재배열 후 위치는 $k$가 되고, 복사 배열을 생성할 필요 없이 swap만으로 재배열을 완료할 수 있게 된다. 재배열 후 위치가 이러한 성질을 갖게 되는 증명은 어렵지 않으므로 독자 여러분에게 맡긴다.&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(1)$의 추가 메모리로 $O(N\log N)$ 시간에 비재귀로 FFT를 수행할 수 있게 되었다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;typedef complex&amp;lt;double&amp;gt; base;

void fft(vector&amp;lt;base&amp;gt; &amp;amp;a, bool inv) {
    int n = (int) a.size();
    for (int i = 1, j = 0; i &amp;lt; n; i++) {
        int bit = n &amp;gt;&amp;gt; 1;
        while (!((j ^= bit) &amp;amp; bit)) bit &amp;gt;&amp;gt;= 1;
        if (i &amp;lt; j) swap(a[i], a[j]);
    }
    for (int i = 1; i &amp;lt; n; i &amp;lt;&amp;lt;= 1) {
        double x = inv ? M_PI / i : -M_PI / i;
        base w = {cos(x), sin(x)};
        for (int j = 0; j &amp;lt; n; j += i &amp;lt;&amp;lt; 1) {
            base th = {1, 0};
            for (int k = 0; k &amp;lt; i; k++) {
                base tmp = a[i + j + k] * th;
                a[i + j + k] = a[j + k] - tmp;
                a[j + k] += tmp;
                th *= w;
            }
        }
    }
    if (inv) {
        for (int i = 0; i &amp;lt; n; i++) {
            a[i] /= n;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 PS에서 FFT가 어떻게 활용될 수 있는지 알아보자. 2의 거듭제곱이 아닐 수도 있는 임의의 $N$에 대해,&amp;nbsp;$\lbrace a_j\rbrace_{j=0}^{N-1}$와 $\lbrace b_j\rbrace_{j=0}^{N-1}$에 대해 다음을 만족하는 $\lbrace c_n\rbrace_{n=0}^{N-1}$을 구하는 문제를 생각해 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;\[c_n&amp;nbsp;= \sum_{j=0}^{n}a_jb_{n-j}\]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포스트의 앞부분에서 정의한 convolution과 거의 같지만, $n-j$가 음수인 경우의 값은 원하지 않는다는 차이가 있다. 이 때문에 우리는 $\lbrace a_j\rbrace_{j=0}^{N-1}$와 $\lbrace b_j\rbrace_{j=0}^{N-1}$의 크기를 2배 이상으로 늘인 후 새로 생긴 곳에 모두 0을 넣는다. 또한 지금까지 이야기한 FFT 알고리즘인 Cooley-Tukey Algorithm은 배열의 크기가 2의 거듭제곱일 때만 작동하므로, 우리는 배열의 크기를&amp;nbsp;원래 크기의 2배 이상인 2의 거듭제곱까지 늘이게 된다. 이후 이 포스트 앞부분에서와 같이 FFT를 한 후 각 항을 곱하고 IFFT를 수행하면, 수열&amp;nbsp;$\lbrace c_n\rbrace_{n=0}^{N-1}$의 모든 항을 $O(N\log N)$ 시간에 구할 수 있게 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void multiply(vector&amp;lt;base&amp;gt; &amp;amp;a, vector&amp;lt;base&amp;gt; &amp;amp;b) {
    int n = (int) max(a.size(), b.size());
    int i = 0;
    while ((1 &amp;lt;&amp;lt; i) &amp;lt; (n &amp;lt;&amp;lt; 1)) i++;
    n = 1 &amp;lt;&amp;lt; i;
    a.resize(n);
    b.resize(n);
    fft(a, false);
    fft(b, false);
    for (int i = 0; i &amp;lt; n; i++) {
        a[i] *= b[i];
    }
    fft(a, true);
}&lt;/code&gt;&lt;/pre&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;ul style=&quot;list-style-type: square;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;https://www.acmicpc.net/problem/10531&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 10531: Golf Bot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;https://www.acmicpc.net/problem/5051&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 5051: 피타고라스의 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;https://www.acmicpc.net/problem/13277&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;BOJ 13277: 큰 수 곱셈&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Computer Science/알고리즘</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/6</guid>
      <comments>https://topology-blog.tistory.com/6#entry6comment</comments>
      <pubDate>Tue, 15 Aug 2017 16:30:18 +0900</pubDate>
    </item>
    <item>
      <title>Link/Cut Tree (1)</title>
      <link>https://topology-blog.tistory.com/5</link>
      <description>&lt;ul style=&quot;list-style-type: square;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;http://topology-blog.tistory.com/7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;font-size: 12pt;&quot;&gt;Link/Cut Tree (2)&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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;&lt;a class=&quot;tx-link&quot; href=&quot;https://en.wikipedia.org/wiki/Link/cut_tree&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;위키피디아 링크&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;*&amp;nbsp;&lt;a class=&quot;tx-link&quot; href=&quot;https://courses.csail.mit.edu/6.851/spring12/scribe/L19.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MIT Lecture Note&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;Link/Cut Tree를 이해하거나 구현하기&amp;nbsp;위해서는 먼저 &lt;a class=&quot;tx-link&quot; href=&quot;http://topology-blog.tistory.com/4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Splay Tree&lt;/a&gt;에 대한 이해가 필요하다. Splay Tree에 대한 내용은 여러&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;Link/Cut Tree는 여러 개의 트리를 관리하는 자료구조이다. 이 자료구조의 특징은, find_root(노드가 속한 트리의 루트), link(한 트리의 루트를 다른 트리의 노드로 연결), cut(루트가 아닌 노드와 부모 사이의 연결 제거) 연산을 모두 amortized $O(\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;앞서 말했듯이 LCT는 여러 개의 트리로 되어있는데, 이 각각의 트리를 represented tree라고 부른다. 그런데 이 트리에는 preferred child와 preferred edge라는 특수한 구조가 있다. 노드 $w$가&amp;nbsp;부모&amp;nbsp;노드 $v$의&amp;nbsp;preferred child라는 의미는, $v$의 서브트리에 속한 노드 중 가장 최근에 연산이 수행된 노드가 $w$의 서브트리에 속해 있다는 뜻이다. 만일 가장 최근에 연산이 수행된 노드가 $v$ 자신이거나 연산이 수행된 적이 없다면 $v$의 preferred child는 없게 된다. 어떤 노드와 그 노드의 preferred child를 잇는 간선을 preferred edge라고 한다.&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;560&quot; data-origin-height=&quot;349&quot;&gt;&lt;span data-url=&quot;https://t1.daumcdn.net/cfile/tistory/99936633599170170A?original&quot; data-phocus=&quot;https://t1.daumcdn.net/cfile/tistory/99936633599170170A?original&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/cfile/tistory/99936633599170170A&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Ft1.daumcdn.net%2Fcfile%2Ftistory%2F99936633599170170A&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;560&quot; height=&quot;349&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;349&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;위의 정의를 보면 알 수 있듯이, preferred child 관계는 새로운 연산이 들어올 때마다 업데이트되어야 한다. 이 그림의 두 번째 트리는 첫 번째 트리에서 노드 $l$에 연산이 들어왔을 때 업데이트된 모습이다. 노드 $l$에 연산이 들어오자 preferred edge들이 $l$에서 루트 노드인 $a$로 통하는 길을 만들어줬다는 것을 눈치챌 수 있다. 이런 식으로 연속적으로 구성된 preferred edge들을 모아서 preferred path라고 부른다. 즉 $l-j-f-c-a$는 하나의 preferred path다.&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;이제 각각의 preferred path를&amp;nbsp;새로운 트리로 관리할 것이다. 이 트리를 auxiliary tree라고 부르기로 하자. 이 트리는 key가 depth인&amp;nbsp;Splay Tree로 구현하게 된다. (편의상 depth가 작은 노드를 Splay Tree의 왼쪽에 두자)&amp;nbsp;이 auxiliary tree들끼리는 path-parent pointer라는 것으로 이어져 있다. 즉 노드 $v$가 노드 $w$의 부모 노드이고 다른 auxiliary tree에 속해있다면 두 노드를 잇는 간선을 path-parent pointer가 대신하게 된다. 이 때&amp;nbsp;주의할 점은 이 path-parent pointer는 $w$와 $v$를 잇는 것이 아니라 $w$가 속한 auxiliary tree의 루트에서 $v$로 잇는다는 것이다.&amp;nbsp;위에서 세 번째 그림은 첫 번째 트리를 auxiliary tree와 path-parent pointer들로 표현한 것이다.&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;이제 Splay Tree를 바탕으로 해서 연산들을 구현해 보자. 이제부터 auxiliary tree와 Splay Tree는 구분없이 쓸 것이다.&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;먼저 노드마다 path-parent pointer를 추가해 준다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;struct node {
    node *l, *r, *p, *pp;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 포인터는 노드가 Splay Tree의 루트일 때만 의미를 갖는다. Rotate 함수에서 부모 노드가 루트일 경우 이 포인터 값을 넘겨받으면 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void rotate(node *cur) {
    node *p = cur-&amp;gt;p;
    if (cur == p-&amp;gt;l) {
        if ((p-&amp;gt;l = cur-&amp;gt;r)) cur-&amp;gt;r-&amp;gt;p = p;
        cur-&amp;gt;r = p;
    } else {
        if ((p-&amp;gt;r = cur-&amp;gt;l)) cur-&amp;gt;l-&amp;gt;p = p;
        cur-&amp;gt;l = p;
    }
    cur-&amp;gt;p = p-&amp;gt;p;
    p-&amp;gt;p = cur;
    if (cur-&amp;gt;p) {
        if (cur-&amp;gt;p-&amp;gt;l == p) cur-&amp;gt;p-&amp;gt;l = cur;
        else cur-&amp;gt;p-&amp;gt;r = cur;
    } else {
        cur-&amp;gt;pp = p-&amp;gt;pp;
        p-&amp;gt;pp = NULL;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 말했듯이 각 노드로 접근할 때마다 preferred path를 갱신해주어야 한다. 이 역할을 하는 함수 access를 만들자. 노드 $v$에 access($v$)를 호출하면 다음 과정이 일어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Splay 연산을 통해 $v$가 속한 Splay Tree의 루트로 올라간다.&lt;/li&gt;
&lt;li&gt;$v$의 preferred child 관계가 있다면 모두 끊고 path-parent 관계로 바꾼다.&lt;/li&gt;
&lt;li&gt;$v$의 parent-path pointer가 NULL이라면 종료한다.&lt;/li&gt;
&lt;li&gt;$v$의 parent-path $p$에 Splay 연산을 행해 루트로 올린다.&lt;/li&gt;
&lt;li&gt;$p$의 오른쪽에 노드가 있다면 끊고 그 자리에 $v$를 연결한다. 연결이 끊긴 노드는 path-parent pointer로 $p$를 가리킨다.&lt;/li&gt;
&lt;li&gt;$v$에 Splay 연산을 통해 $p$의 자리로 올리고 3번으로 돌아간다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void access(node *cur) {
    splay(cur);
    if (cur-&amp;gt;r) {
        cur-&amp;gt;r-&amp;gt;pp = cur;
        cur-&amp;gt;r-&amp;gt;p = NULL;
        cur-&amp;gt;r = NULL;
    }
    while (cur-&amp;gt;pp) {
        node *pp = cur-&amp;gt;pp;
        splay(pp);
        if (pp-&amp;gt;r) {
            pp-&amp;gt;r-&amp;gt;pp = pp;
            pp-&amp;gt;r-&amp;gt;p = NULL;
        }
        pp-&amp;gt;r = cur;
        cur-&amp;gt;p = pp;
        cur-&amp;gt;pp = NULL;
        splay(cur);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수를 만들었다면 나머지 연산은 쉬워진다. 먼저 find_root($v$) 함수는&amp;nbsp;access($v$)를 하고 나면 preferred path로 루트까지 연결되므로 루트와 같은 Splay Tree 내에 속하게 된다. access($v$) 함수가 끝나면 $v$는 그 Splay Tree의 루트 자리에 있으므로 왼쪽으로 계속 가면 represented tree의 루트가 나온다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;node *find_root(node *cur) {
    access(cur);
    while (cur-&amp;amp;gt;l) cur = cur-&amp;amp;gt;l;
    access(cur);
    return cur;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cut($v$) 함수는 access($v$)를 한 뒤 Splay Tree 내에서 $v$의 왼쪽 노드와 연결을 끊으면 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void cut(node *cur) {
    access(cur);
    if (cur-&amp;gt;l) {
        cur-&amp;gt;l-&amp;gt;p = NULL;
        cur-&amp;gt;l = NULL;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;link($v$, $w$) 함수는 기본적으로 $v$가 represented tree의 루트이고 $w$와 다른 represented tree에 속함을 가정하고 있다. 이 함수 역시 간단한데, access($v$)와 access($w$)를 한 뒤 $v$의 왼쪽에 $w$를 붙이면 된다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void link(node *root, node *cur) {
    access(root);
    access(cur);
    root-&amp;gt;l = cur;
    cur-&amp;gt;p = root;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 함수들이 어떻게 amortized $O(\log N)$에 작동하는 지는 글머리에 첨부한 MIT 렉쳐 노트에 설명되어 있다.&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;Link/Cut Tree의 장점은, 여러 트리 간 연결-잘라내기가 자유롭고, Splay Tree를 기반으로 한 자료구조로&amp;nbsp;다양한 쿼리를 효율적으로 처리할 수가 있다는 점이다. 이에 관해 좀더 자세한 내용은 &lt;a class=&quot;tx-link&quot; href=&quot;http://topology-blog.tistory.com/7&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Link/Cut Tree (2)&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;연습문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;https://www.acmicpc.net/problem/13309&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;트리 (KOI 2016 고등부 3번)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class=&quot;tx-link&quot; href=&quot;https://www.acmicpc.net/problem/13539&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;트리와 쿼리 11&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Computer Science/알고리즘</category>
      <author>jo_on</author>
      <guid isPermaLink="true">https://topology-blog.tistory.com/5</guid>
      <comments>https://topology-blog.tistory.com/5#entry5comment</comments>
      <pubDate>Sun, 13 Aug 2017 15:54:27 +0900</pubDate>
    </item>
  </channel>
</rss>