<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>미니멀코드</title>
    <link>https://minimalcode.tistory.com/</link>
    <description>공부한 내용을 나의 언어로 정리하자</description>
    <language>ko</language>
    <pubDate>Sun, 14 Jun 2026 12:18:57 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>_su_min</managingEditor>
    <image>
      <title>미니멀코드</title>
      <url>https://tistory1.daumcdn.net/tistory/6697630/attach/d135721d3c2d4f9f901f02631648656e</url>
      <link>https://minimalcode.tistory.com</link>
    </image>
    <item>
      <title>[03 알고리즘 설계 패러다임] 7. 분할 정복</title>
      <link>https://minimalcode.tistory.com/154</link>
      <description>&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 도입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분할 정복(Divide &amp;amp; Conquer)은 가장 유명한 알고리즘 디자인 패러다임으로, 각개 격파라는 말로 간단히 설명할 수 있습니다. 분할 정복 패러다임을 차용한 알고리즘들은 주어진 문제를 둘 이상의 부분 문제로 나눈 뒤 각 문제에 대한 답을 재귀 호출을 이용해 계산하고, 각 부분 문제의 답으로부터 전체 문제의 답을 계산해 냅니다. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;분할 정복이 일반적인 재귀 호출과 다른 점은 문제를 한 조각과 나머지 전체로 나누는 대신 거의 같은 크기의 부분 문제로 나누는 것입니다.&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;916&quot; data-origin-height=&quot;581&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZum0o/btsMsMIpW5w/hAKwoBv6Ga8hdmAi1M4c1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZum0o/btsMsMIpW5w/hAKwoBv6Ga8hdmAi1M4c1k/img.png&quot; data-alt=&quot;출처: https://gamedevlog.tistory.com/58&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZum0o/btsMsMIpW5w/hAKwoBv6Ga8hdmAi1M4c1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZum0o%2FbtsMsMIpW5w%2FhAKwoBv6Ga8hdmAi1M4c1k%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;916&quot; height=&quot;581&quot; data-origin-width=&quot;916&quot; data-origin-height=&quot;581&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://gamedevlog.tistory.com/58&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;분할 정복을 사용하는 알고리즘들은 대개 세 가지의 구성 요소를 갖고 있습니다.&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;문제를 더 작은 문제로 분할하는 과정(divide)&lt;/li&gt;
&lt;li&gt;각 문제에 대해 구한 답을 원래 문제에 대한 답으로 병합하는 과정(merge)&lt;/li&gt;
&lt;li&gt;더이상 답을 분할하지 않고 곧장 풀 수 있는 매우 작은 문제(base case)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분할 정복을 적용해 문제를 해결하기 위해서는 문제에 몇 가지 특성이 성립해야 합니다. 문제를 둘 이상의 부분 문제로 나누는 자연스러운 방법이 있어야 하며, 부분 문제의 답을 조합해 원래 문제의 답을 계산하는 효율적인 방법이 있어야 합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제: 수열의 빠른 합과 행렬의 빠른 제곱&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서는 1 + 2 + ... + n 의 합을 재귀 호출을 이용해 계산하는 recursiveSum() 함수를 작성했습니다. 여기서는 분할 정복을 이용해 똑같은 일을 하는 fastSum() 함수를 만들어 봅시다. 1부터 n까지의 합을 n개의 조각으로 나눈 뒤, 이들을 반으로 뚝 잘라 n/2 개의 조각들로 만들어진 부분 문제 두 개를 만듭니다.(편의상 n은 짝수라 가정하겠습니다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;faseSum() = 1 + 2 + ... + n&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;= (1 + 2 + ... + n/2) + ((n/2 + 1) + ... + n)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 부분 문제는 fastSum(n/2)로 나타낼 수 있지만, 두 번째 부분 문제는 그렇지 않습니다. 문제를 재귀적으로 풀기 위해서는 각 부분 문제를 1부터 n까지의 합의 꼴로 표현할 수 있어야 하는데, 위의 분할에서 두 번째 조각은 'a부터 b까지의 합' 형태를 가지고 있기 때문이지요. 따라서 다음과 같이 두 번째 부분 문제를 fastSum(x)를 포함하는 형태로 바꿔 써야 합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(n/2 + 1) + ... + n = (n/2 + 1) + (n/2 + 2) + ... + (n/2 + n/2)&lt;br /&gt;&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; = n/2 * n/2 + (1 + 2 + 3 + ... + n/2)&lt;br /&gt;&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; = n/2 * n/2 + fastSum(n/2)&lt;/blockquote&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을 따로 떼내면 놀랍게도 fastSum(n/2)이 나타납니다! 따라서 다음과 같이 쓸 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;fastSum(n) = 2 * fastSum(n/2) + n^2 / 4 (n이 짝수일때)&lt;/blockquote&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이 짝수인 경우에 대해서밖에 동작하지 않기 때문에, 홀수인 입력이 주어질 때는 짝수인 n - 1까지의 합을 재귀 호출로 계산하고 n을 더해 답을 구하게 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740301316580&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 필수 조건: n은 자연수
// 1 + 2 + ... + n 을 반환한다.
int fastSum(int n) {
    // 기저 사례
    if(n == 1) return 1;
    if(n % 2 == 1) return fastSum(n-1) + n;
    return 2 * fastSum(n/2) + (n/2)*(n/2);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시간 복잡도 분석&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fastSum() 이 수행하는 데 걸리는 시간은 recursiveSum() 과 어떻게 다를까요? 두 함수 모두 내부에 반복문이 없기 때문에, fastSum()과 recursiveSum()이 종료하는 데 걸리는 시간은 순전히 함수가 호출되는 횟수에 비례하게 됩니다. recursiveSum()의 경우 n번의 함수 호출이 필요하다는 것을 쉽게 알 수 있습니다. 반면 fastSum()은 호출될 때마다 최소한 두 번에 한 번 꼴로 n이 절반으로 줄어들죠. 그러니 fastSum()의 호출 횟수가 훨씬 적으리란 것을 쉽게 예상할 수 있습니다.&lt;/p&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씩 나누면서 faseSum() 함수를 호출하기 때문에 faseSum() 함수의 총 호출 횟수는 lgN(밑이 2인 log N) 이라는 것을 알 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;n이 4인 경우 : lg4 = 2&lt;br /&gt;n이 8인 경우 : lg8 = 3&lt;br /&gt;n이 16인 경우 : lg16 = 4&lt;br /&gt;...&lt;/blockquote&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(lgn)입니다. 뭐 어차피 한 줄이면 짤 수 있는 코드를 왜 이렇게 최적화하나 생각이 들 수도 있습니다. 하지만 1 + ... + n과 달리 간단히 수식으로 표현되지 않는 문제를 풀 때에도 이 원리는 유용하게 사용됩니다. 그 좋은 예가 행렬의 거듭제곱입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;행렬의 거듭제곱&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N x N 크기의 행렬 A가 주어질 때, A의 거듭제곱(power) A^m은 A를 연속해서 m번 곱한 것입니다. 이것을 계산하는 알고리즘 자체는 어려울 것이 없지만, m이 매우 클 때 (약 백만) A^m을 구하는 것은 꽤나 시간이 오래 걸리는 작업입니다. 행렬의 곱셈에는 O(n^3)의 시간이 들기 때문에 곧이 곧대로 m - 1번 곱셈을 통해 A^m을 구하려면 모두 O(n^3m)번의 연산이 필요합니다. n=100, m=1,000,000 이라고 한다면 필요한 연산의 수는 대략 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;이전에 사용한 아이디어를 이용해 A^m 을 구하는데 필요한 m개의 조각을 절반으로 나눠 봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A^m = A^(m/2) x A^(m/2)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반으로 자르기만 하면 절반 크기의 부분 문제가 갑자기 툭 튀어나오니 fastSum() 보다 간단하면 간단했지 그다지 다를 것이 없습니다. 다음 코드는 이 알고리즘의 구현을 보여줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740841715137&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 정방행렬을 표현하는 SquareMatrix 클래스가 있다고 가정하자.
class SquareMatrix;
// n*n 크기의 항등 행렬(identity matrix)을 반환하는 함수
SquareMatrix identity(int n);
// A^m을 반환한다.
SquareMatrix pow(const SquareMatrix&amp;amp; A, int m) {
    // 기저사례 : A^0 = 1
    if(m == 0)  return identity(A.size());
    if(m % 2 &amp;gt; 0) return pow(A, m-1) * A;
    SquareMatrix half = pow(A, m / 2);
    // A^m = (A^(m/2)) * (A^(m/2))
    return half * half;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;나누어 떨어지지 않을 때의 분할과 시간 복잡도&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;m이 홀수일 때, A^m = A x A^(m-1) 로 나누지 않고, 좀 더 절반에 가깝게 나누는게 좋지 않을까라는 생각을 할 수도 있습니다. 예를 들어 A^7 을 A x A^6 로 나누느 것이 아니라 A^3 x A^4 로 나누는 것이 더 좋지 않은가 하는 것이지요.실제로 문제의 크기가 매번 절반에 가깝게 줄어들면 기저 사례에 도달하기까지 걸리는 분할의 횟수가 줄어들기 때문에 대부분의 분할 정복 알고리즘은 가능한 한 절반에 가깝게 문제를 나누고자 합니다. 퀵 정렬에서 좀더 좋은 분할을 찾기 위해 여러 노력을 하는 것도 좋은 예이지요. 하지만 이 문제에서 이 방식의 분할은 오히려 알고리즘을 더 느리게 만듭니다. 이런 식으로 문제를 나누면 A^m 을 찾기 위해 계산해야 할 부분 문제의 수가 늘어나기 때문이지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 그림은 두 가지 분할 방식을 이용해 pow(A, 31)을 계산할 때 필요한 부분 문제 간의 의존 관계를 보여줍니다. pow(A, x)를 계산하는 과정에서 pow(A, y)를 호출해야 한다면 두 값은 화살표로 연결되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250302_001659367.jpg&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;495&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3ZsEb/btsMAXO4Cm9/M1hYNhhe4j7kI6Xe6KAotk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3ZsEb/btsMAXO4Cm9/M1hYNhhe4j7kI6Xe6KAotk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3ZsEb/btsMAXO4Cm9/M1hYNhhe4j7kI6Xe6KAotk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3ZsEb%2FbtsMAXO4Cm9%2FM1hYNhhe4j7kI6Xe6KAotk%2Fimg.jpg&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;1046&quot; height=&quot;495&quot; data-filename=&quot;KakaoTalk_20250302_001659367.jpg&quot; data-origin-width=&quot;1046&quot; data-origin-height=&quot;495&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 (a)와 (b)는 얼핏 보면 크게 다를 것이 없어 보이지만, 각 부분 문제가 계산되는 횟수가 다르다는 데 큰 차이가 있습니다. (a) 에서 pow(A, 8)은 pow(A, 15)를 계산할 때도 호출되고 pow(A, 16)을 계산할 때도 호출되므로, 모두 두 번 호출된다는 것을 알 수 있지요. 따라서 pow(A, 8)과 pow(A, 7)을 계살 할 때 사용하는&amp;nbsp; pow(A, 4)는 모두 세 번 호출됩니다. 이와 같이 같은 값을 중복으로 계산하는 일이 많기 때문에, m이 증가함에 따라 pow(A, m)을 계산하는 데 필요한 pow()의 호출 횟수는 m에 대해 선형적으로 증가합니다. pow()가 한 번 호출될 때마다 행렬 곱셈을 한 번 하기 때문에, (a) 가 보여주는 분할 방식은 대문자 O 표기법으로 보면 결국 m-1 번 곱셈하는 것과 다를 바가 없습니다. 반면 (b)에서는 pow() 가 O(lgm) 개의 거듭제곱에 대해 한 번씩만 호출된다는 것을 쉽게 알 수 있지요.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;예제: 병합 정렬과 퀵 정렬&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 수열을 크기 순서대로 정렬하는 문제는 전산학에서 가장 유명한 문제 중 하나입니다. 이 문제를 해결하는 수많은 알고리즘 중 가장 널리 쓰이는 것들이 바로 병합 정렬(Merge sort)과 퀵 정렬(Quick sort)인데, 이 두 알고리즘은 모두 분할 정복 패러다임을 기반으로 해서 만들어진 것들입니다. 다음 그림은 두 알고리즘의 동작 과정을 보여줍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250302_142844449.jpg&quot; data-origin-width=&quot;1025&quot; data-origin-height=&quot;703&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dGrWVA/btsMy4hFPw4/ykpuBjn5IDUycwYzQX2kWK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dGrWVA/btsMy4hFPw4/ykpuBjn5IDUycwYzQX2kWK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dGrWVA/btsMy4hFPw4/ykpuBjn5IDUycwYzQX2kWK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdGrWVA%2FbtsMy4hFPw4%2FykpuBjn5IDUycwYzQX2kWK%2Fimg.jpg&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;1025&quot; height=&quot;703&quot; data-filename=&quot;KakaoTalk_20250302_142844449.jpg&quot; data-origin-width=&quot;1025&quot; data-origin-height=&quot;703&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)는 병합 정렬의 동작 과정을 보여줍니다. 각 수열의 크기가 1이 될 때까지 절반씩 쪼개 나간 뒤, 정렬된 부분 배열들을 합쳐 나가는 것을 볼 수 있지요. 병합 정렬의 분할 방식은 아주 단순하고 효율적입니다. 주어진 배열을 가운데서 절반으로 그냥 나누는 것이죠. 따라서 이 과정을 상수 시간인 O(1) 만에 수행할 수 있습니다. 그러나 각각 나눠서 정렬한 배열들을 하나의 배열로 합치기 위해 별도의 병합 과정을 실행해야 합니다. 여기에 O(n)의 시간이 걸립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(b)에서 보여진 퀵 정렬은 각 부분 수열의 맨 처음에 있는 수를 기준으로 삼고, 이들보다 작은 수를 왼쪽으로, 큰 것을 오른쪽으로 가게끔 문제를 분해합니다. 그림에서 동그라미로 표시된 수들은 이전 수를 분할하는 데 사용된 기준을 표현합니다. 이 분할은 O(n)의 시간이 걸리는 복잡한 작업인데다, 우리가 어떤 기준을 선택하느냐에 따라서 비효율적인 불할을 불러올 여지가 있지만 그 덕분에 각 부분 배열이 이미 정렬한 상태가 되어 별도의 병합 작업이 필요없다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시간 복잡도 분석&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병합 정렬과 퀵 정렬의 시간 복잡도는 비슷한 방법으로 분석할 수 있습니다. O(n)의 시간이 걸리는 과정을 재귀 호출 전에 하느냐, 후에 하느냐가 다를 뿐 본질적으로 비슷한 형태의 알고리즘이기 때문입니다. 병합 정렬은 각 단계마다 반으로 나눈 부분 문제를 재귀 호출을 이용해 해결한 뒤, 이들의 결과 수열을 합쳐 전체 문제의 답을 계산합니다. 정렬된 두 부분 수열을 합치는 데는 두 수열의 길이 합 만큼 반복문을 수행해야 하기 때문에 병합 정렬의 수행시간은 이 병합 과정에 의해 지배됩니다. 그런데 아래 단계로 내려갈수록 부분 문제의 수는 두 배로 늘고 각 부분 문제의 크기는 반으로 줄어들기 때문에 한 단계 내에서 모든 병합에 필요한 총 시간은 O(n) 으로 항상 일정합니다. 아래 그림은 병합 정렬이 처리하는 부분 문제의 크기를 단계별로 모은 것인데, 각 단계를 나타내느 각 가로줄에 있는 원소의 수는 항상 일정하다는 것을 쉽게 알 수 있지요. 따라서 단계의 수에 n을 곱하면 병합 정렬에 필요한 전체 시간을 얻을 수 있습니다. 문제의 크기는 항상 거의 절반으로 나누어 지기 때문에 필요한 단계의 수는 O(lgn)이 됩니다. 따라서 병합 정렬의 시간 복잡도가 O(nlgn)이라는 사실을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20250302_144028406.jpg&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oKCbv/btsMy2D43SF/pKw7djUcMuWMExxGiuMHc1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oKCbv/btsMy2D43SF/pKw7djUcMuWMExxGiuMHc1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oKCbv/btsMy2D43SF/pKw7djUcMuWMExxGiuMHc1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoKCbv%2FbtsMy2D43SF%2FpKw7djUcMuWMExxGiuMHc1%2Fimg.jpg&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;1254&quot; height=&quot;442&quot; data-filename=&quot;KakaoTalk_20250302_144028406.jpg&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;442&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;퀵 정렬에서 병합 정렬과 달리 시간 복잡도를 분석하기 까다로운 것은 결과적으로 분할된 두 부분 문제가 비슷한 크기로 나눠진다는 보장을 할 수 없기 때문입니다. 기준으로 택한 원소가 최소 원소나 최대 원소인 경우 부분 문제의 크기가 하나씩만 줄어들 수도 있으니까요. 이런 최악의 경우 퀵 정렬의 시간 복잡도는 O(n^2) 이 됩니다. 다행히 평균적으로 부분 문제가 절반에 가깝게 나눠질 때 퀵 정렬의 시간 복잡도는 병합 정렬과 같은 O(nlgn)이 된다는 것이 알려져 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예제: 카라츠바의 빠른 곱셈 알고리즘&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 재미있는 병합 과정을 가진 분할 정복 알고리즘의 한 예가 카라츠바의 빠른 곱셈 알고리즘으로, 두 개의 정수를 곱하는 알고리즘입니다. 물론 32비트 정수 둘을 곱할 때 쓰는 것은 아니고, 수백자리, 나아가 수만 자리는 되는 큰 숫자들을 다룰 때 주로 사용합니다. 이렇게 큰 숫자들은 배열을 이용해 저장해야 하지요.&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;다음 코드는 두 정수를 곱하는 알고리즘을 보여줍니다. multiply() 가 일반적인 정수형 변수가 아닌 정수형 배열을 입력받는 점을 눈여겨 보시기 바랍니다. 이 배열들은 곱할 수의 각 자릿수를 맨 아래 자리부터 저장하고 있습니다. 이렇게 순서를 뒤집으면 입출력할 때는 불편하지만, A[i]에 주어진 자릿수의 크기를 10^i 로 쉽게 구할 수 있다는 장점이 있습니다. 따라서 A[i]와 B[j]를 곱한 결과를 C[i+j]에 저장하는 등, 훨씬 직관적인 코드를 작성할 수 있습니다. 또 하나 눈여겨볼 점은 자릿수 올림을 처리하는 nomalize()에서 자릿수가 음수인 경우도 처리하고 있는 것입니다. muliply() 에서는 덧셈밖에 하지 않기 때문에, 자릿수가 음수가 될 일이 없지요. 카라츠바 알고리즘의 구현을 보고 나면 왜 이 구현이 필요한지 이해할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740907625156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// num[]의 자릿수 올림을 처리한다.
void normalize(vector&amp;lt;int&amp;gt;&amp;amp; num) {
    num.push_back(0);
    // 자릿수 올림을 처리한다.
    for(int i = 0; i &amp;lt; num.size(); i++) {
        if(num[i] &amp;lt; 0) {
            int borrow = (abs(num[i]) + 9) / 10;
            num[i+1] -= borrow;
            num[i] += borrow * 10;
        }
        else {
            num[i+1] += num[i] / 10;
            num[i] %= 10;
        }
    }
    while(num.size() &amp;gt; 1 &amp;amp;&amp;amp; num.back() == 0) num.pop_back();
}

// 두 긴 자연수의 곱을 반환한다.
// 각 배열에는 각 수의 자릿수가 1의 자리에서부터 시작해 저장되어 있다.
// 예: multiply({3,2,1}, {6,5,4}) = 123 * 456 = 56088 = {8, 8, 0, 6, 5}
vector&amp;lt;int&amp;gt; muliply(const vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b) {
    vector&amp;lt;int&amp;gt; c(a.size() + b.size() + 1, 0);
    for(int i = 0; i &amp;lt; a.size(); i++) {
        for(int j = 0; j &amp;lt; b.size(); j++) {
            c[i+j] += a[i] * b[j];
        }
    }
    normalize(c);
    return c;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 알고리즘의 시간 복잡도는 두 정수의 길이가 모두 n이라고 할 때 O(n^2) 입니다. n번 실행되는 for문이 두 번 겹쳐 있기 때문에 이점은 아주 자명하죠. 이 알고리즘은 굉장히 단순합니다만, 이것보다 빠른 알고리즘을 고안하기란 쉽지 않습니다. 이보다 빠른 첫 번째 알고리즘이 카라츠바 알고리즘이었는데, 이 알고리즘도 1960년에야 출현했을 정도죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카라츠바의 빠른 곱셈 알고리즘은 두 수를 각각 절반으로 쪼갭니다. a와 b가 각각 256자리 수라면 a1과 b1은 첫 128자리, a0와 b0는 그 다음 128자리를 저장하도록 하는 것이죠. 그러면 a와 b를 다음과 같이 쓸 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;a = a1 * 10^128 + a0&lt;br /&gt;b = b1 * 10^128 + b0&lt;/blockquote&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 b를 네 개의 조각을 이용해 표현하는 방법을 살펴보았습니다. 예를 들면 다음과 같이 표현할 수 있지요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;a x b = (a1 * 10^128 + a0) x (b1 * 10^128 + b0)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;= a1 * b1 * 10^256 + (a1 * b0 + a0 * b1) * 10^128 + a0 * b0&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법에서는 큰 정수 두 개를 한 번 곱하는 대신, 절반 크기로 나눈 작은 조각을 네 번 곱합니다. (10의 거듭제곱과 곱하는 것은 그냥 뒤에 0을 붙이는 시프트 연산으로 구현하면 되니 곱셈으로 치지 않습니다.) 이대로도 각각을 재귀 호출해서 해결하면 분할 정복 알고리즘이라고 할 수 있겠지요. 이 방법의 시간 복잡도는 얼마일까요? 이 방법에서 길이 n인 두 정수를 곱하는 데 드는 시간은 덧셈과 시프트 연산에 걸리는 시간 O(n)과, n/2 길이 조각들의 곱셈 네 번으로 나눌 수 있습니다. 그런데 사실 이 방법의 전체 수행 시간이 O(n^2)이 된다는 사실을 증명할 수 있습니다. 이래서는 분할 정복을 애써 구현한 의미가 없지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카라츠바가 발견한 것은 다음과 같이 a x b를 표현했을 때 네 번 대신 세 번의 곱셈만으로 값을 계산할 수 있다는 것입니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;a x b = a1 x b1 x 10^256 + (a1 x b0 + a0 x b1) x 10^128 + (a0 x b0)&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; z2&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;nbsp;z1&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;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;z0&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조각들의 곱을 각각 위와 같이 z2, z1, z0 이라고 씁시다. 우선 z0와 z2를 각각 한 번의 곱셈으로 구합니다. 그리고 다음 식을 이용하지요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;(a0 + a1) x (b0 + b1) = (a0 x b0) + (a1 x b0 + a0 x b1) + (a1 x b1)&lt;br /&gt;&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;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; z0&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;z1&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;nbsp; z2&lt;br /&gt;&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;nbsp; &amp;nbsp; &amp;nbsp; = z0 + z1 + z2&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위 식의 결과에서 z0과 z2를 빼서 z1을 구할 수 있습니다. 다음과 같은 코드 조각을 이용하면 되지요.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;z2 = a1 * b1;&lt;br /&gt;z0 = a0 * b0;&lt;br /&gt;z1 = (a0 + a1) * (b0 + b1) - z0 - z2;&lt;/blockquote&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;background-color: #ffff00;&quot;&gt;이 과정은 곱셈을 세 번밖에 쓰지 않지요!&lt;/span&gt; 그러고 나면 이 세 결과를 적절히 조합해 원래 두 수의 답을 구해낼 수 있습니다. 다음 코드는 이와 같이 구현한 카라츠바 알고리즘의 구현을 보여줍니다.&lt;/p&gt;
&lt;pre id=&quot;code_1740910132871&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// a += b * (10^k); 를 구현한다.
void addTo(vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b, int k);
// a -= b; 를 구현한다. a &amp;gt;= b 를 가정한다.
void subFrom(vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; b);
// 두 긴 정수의 곱을 반환한다.
vector&amp;lt;int&amp;gt; karatsuba(const vector&amp;lt;int&amp;gt;&amp;amp; a, const vector&amp;lt;int&amp;gt;&amp;amp; a) {
    int an = a.size(), bn = b.size();
    // a가 b보다 짧을 경우 둘을 바꾼다.
    if(an &amp;lt; bn) return karatsuba(b, a);
    // 기저 사례: a나 b가 비어 있는 경우
    if(an == 0 || bn == 0) return vector&amp;lt;int&amp;gt;();
    // 기저 사례 : a가 비교적 짧은 경우 O(n^2) 곱셈으로 변경한다.
    if(an &amp;lt;= 50) return multiply(a, b);
    
    int half = an / 2;
    // a와 b를 밑에서 half 자리와 나머지로 분리한다.
    vector&amp;lt;int&amp;gt; a0(a.begin(), a.begin() + half);
    vector&amp;lt;int&amp;gt; a1(a.begin() + half, a.end());
    vector&amp;lt;int&amp;gt; b0(b.begin(), b.begin() + min&amp;lt;int&amp;gt;(b.size(), half));
    vector&amp;lt;int&amp;gt; b1(b.begin() + min&amp;lt;int&amp;gt;(b.size(), half), b.end());
    
    // z2 = a1 * b1
    vector&amp;lt;int&amp;gt; z2 = karatsuba(a1, b1);
    
    // z0 = a0 * b0
    vector&amp;lt;int&amp;gt; z0 = karatsuba(a0, b0);
    
    // a0 = a0 + a1; b0 = b0 + b1
    addTo(a0, a1, 0); addTo(b0, b1, 0);
    
    // z1 = (a0 * b0) - z0 -z2;
    vector&amp;lt;int&amp;gt; z1 = karatsuba(a0, b0);
    
    subFrom(z1, z0);
    subFrom(z1, z2);
    
    // ret = z0 + z1 * 10^half + z2 * 10^(half * 2)
    vector&amp;lt;int&amp;gt; ret;
    addTo(ret, z0, 0);
    addTo(ret, z1, half);
    addTo(ret, z2, half + half);
    return ret;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카라츠바 알고리즘은 분할한 부분 문제의 답에서 원래 문제의 답을 병합해내는 부분을 개선함으로써 알고리즘의 성능을 향상시킨 좋은 예입니다. 스트라센(Strassen)의 행렬 곱셈 알고리즘 또한 이와 비슷한 기법을 사용하지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시간 복잡도 분석&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카라츠바 알고리즘은 두 개의 입력을 절반씩으로 쪼갠 뒤, 세 번 재귀 호출을 하기 때문에 재귀 호출을 한 번이나 두 번만 하던 지금까지의 예제와는 다르게 시간복잡도를 분석해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 카라츠바 알고리즘의 수행 시간을 병합 단계와 기저 사례의 두 부분으로 나눕시다. 위 구현 코드에서 병합 단계의 수행 시간은 addTo()와 subFrom()의 수행 시간에 지배되고, 기저 사례의 처리 시간은 multiply()의 수행 시간에 지배되는 것을 볼 수 있지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 기저 사례를 처리하는데 드는 총 시간을 알아봅시다. 위 코드에서는 더 긴 숫자의 길이가 50자리보다 짧아지면 multiply()를 이용해 답을 계산하기로 했지만, 여기서는 편의를 위해 한 자리 숫자에 도달해야만 multiply() 를 사용한다고 가정하지요. 자릿수 n이 2의 거듭제곱 2^k라고 했을 때 재귀 호출의 깊이는 k가 됩니다. 한 번 쪼갤 때마다 해야 할 곱셈의 수가 세 배씩 늘어나기 때문에 마지막 단계에는 3^k개의 부분 문제가 있는데, 마지막 단계에서는 두 수 모두 한 자리니까 곱셈 한 번이면 충분합니다. 따라서 곱셈의 수는 O(3^k)가 됩니다. n = 2^k 라고 가정했으니 k=lgn 이고, 이때 곱셈의 수를 n에 대해 표현하면 다음과 같은 식이 됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;O(3^k) = (3^lgn) = O(n^lg3)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lg3 은 약 1.585이기 때문에 카라츠바 알고리즘이 O(n^2) 보다 훨씬 적은 곱셈을 필요로 한다는 것을 알 수 있지요. 만약 n이 10만이라고 하면 곱셈의 수는 대략 100배 정도 차이가 납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로 병합 단계에 드는 시간의 총 합을 구해봅시다. addTo()와 subFrom()은 숫자의 길이에 비례하는 시간만이 걸리도록 구현할 수 있습니다. 따라서 각 단계에 해당하는 숫자의 길이를 모두 더하면 병합 단계에 드는 시간을 계산할 수 있습니다. 단계가 내려갈 때마다 숫자의 길이는 절반으로 줄고 부분 문제의 개수는 세 배 늘기 때문에, i번째 단계에서 필요한 연산 수는 (3/2)^i * 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;n * ((3/2) ^ i [i = 0 부터 lgn 까지의 합])&lt;/p&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^lg3 과 같은 속도로 증가합니다. 따라서 카라츠바 알고리즘의 시간 복잡도는 곱셈이 지배하며, 최종 시간 복잡도는 O(n^lg3)이 됩니다. 단, 카라츠바 알고리즘의 구현은 단순한 O(n^2) 알고리즘보다 훨씬 복잡하기 때문에 입력의 크기가 작을 경우 O(n^2) 알고리즘보다 느린 경우가 많다는데 유의합시다. 위의 코드에서 입력된 숫자가 짧을 경우 O(n^2) 알고리즘을 사용하는 것도 이런 이유에서지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 문제: 쿼드 트리 뒤집기 (문제 ID: QUADTREE, 난이도: 하)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 풀이: 쿼드 트리 뒤집기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 문제: 울타리 잘라내기 (문제 ID: FENCE, 난이도: 중)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 풀이: 울타리 잘라내기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 문제: 팬미팅 (문제 ID: FANMEETING, 난이도: 상)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7. 풀이: 팬미팅&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AlgorithmBook/알고리즘 문제해결 전략 1</category>
      <category>분할 정복</category>
      <category>알고리즘</category>
      <category>알고리즘 문제해결전략</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/154</guid>
      <comments>https://minimalcode.tistory.com/154#entry154comment</comments>
      <pubDate>Sun, 23 Feb 2025 17:42:43 +0900</pubDate>
    </item>
    <item>
      <title>시계맞추기 (ID : CLOCKSYNC)</title>
      <link>https://minimalcode.tistory.com/153</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.algospot.com/judge/problem/read/CLOCKSYNC&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740236619030&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;algospot.com ::  
	CLOCKSYNC&quot; data-og-description=&quot;Synchronizing Clocks 문제 정보 문제 그림과 같이 4 x 4 개의 격자 형태로 배치된 16개의 시계가 있다. 이 시계들은 모두 12시, 3시, 6시, 혹은 9시를 가리키고 있다. 이 시계들이 모두 12시를 가리키도록 &quot; data-og-host=&quot;www.algospot.com&quot; data-og-source-url=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot; data-og-url=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dL8vwi/hyYf27c7PB/mxuhVO59p5x9HuxieRZdo0/img.png?width=218&amp;amp;height=221&amp;amp;face=0_0_218_221&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dL8vwi/hyYf27c7PB/mxuhVO59p5x9HuxieRZdo0/img.png?width=218&amp;amp;height=221&amp;amp;face=0_0_218_221');&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;algospot.com :: CLOCKSYNC&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Synchronizing Clocks 문제 정보 문제 그림과 같이 4 x 4 개의 격자 형태로 배치된 16개의 시계가 있다. 이 시계들은 모두 12시, 3시, 6시, 혹은 9시를 가리키고 있다. 이 시계들이 모두 12시를 가리키도록&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.algospot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 문제를 읽고나서 풀기가 굉장히 어렵다고 생각되었던 부분이 현재 '12시' 로 맞춰져있는 시계를 다시 변경해서 답을 찾아가야하는지에 대한 부분이었다. 이렇게 이미 '12시'로 맞춰져 있는 시계를 건드리게 되면 답을 얻기 위해 따져봐야하는 경우의 수가 기하급수적으로 증가해버리기 때문이다. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;그래서 일단은 예제에 나와있는대로 종이와 펜을 들고 그려가면서 시뮬레이션을 해보기로 했다.&lt;/span&gt; 오 그런데 이게 왠걸! 특정 스위치로만 맞출 수 있는 시계가 있는 경우가 있었고, 우선적으로 해당 시계를 먼저 '12시' 로 맞춰준 후 다시는 건들지 않으면 되는 방법이 있었다. 또, 특정 스위치로 맞출 수 있는 시계가 두 개인 경우도 있었는데 이 경우도 한 개의 시계인 경우와 마찬가지로 두 시계를 '12시'로 맞춰주고 다시는 건들이 않으면 되었다. 만약 이 두 시계의 시침이 서로 다른 방향을 가리킨다면 절대 두 시계가 '12시'를 가리킬 수 없기 때문에 문제를 풀기 불가능한 상황이 되어 -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;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;스위치를 다시 살펴보니 모든 시계를 12시를 바라보도록 하기 위한 필연적인 순서가 있었고 이는 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;4번 스위치 (6, 7, 8, 10, 12) : 8, 12 고정&lt;br /&gt;2번 스위치 (4, 10, 14, 15) : 10 고정&lt;br /&gt;9번 스위치 (3, 4, 5, 9, 13) : 13 고정&lt;br /&gt;1번 스위치 (3, 7, 9, 11) : 9, 11 고정&lt;br /&gt;3번 스위치 (0, 4, 5, 6, 7) : 6 고정&lt;br /&gt;7번 스위치 (4, 5, 7, 14, 15) : 7고정&lt;br /&gt;8번 스위치 (1, 2, 3, 4, 5) : 4, 5 고정&lt;br /&gt;6번 스위치 (3, 14, 15) : 3 고정&lt;br /&gt;0번 스위치 (0, 1, 2) : 1 고정&lt;br /&gt;5번 스위치 (0, 2, 14, 15) : 네 방향 확인&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 순서를 따라가면서 코드를 작성하여 제출했더니, 알고스팟의 문제를 풀기 시작한 이후 처음으로 '정답'을 받아봤다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답안 제출한 코드는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1740236599207&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

public class Main {
    public static List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; clockChain = Arrays.asList(
            Arrays.asList(0, 1, 2),
            Arrays.asList(3, 7, 9, 11),
            Arrays.asList(4, 10, 14, 15),
            Arrays.asList(0, 4, 5, 6, 7),
            Arrays.asList(6, 7, 8, 10, 12),
            Arrays.asList(0, 2, 14, 15),
            Arrays.asList(3, 14, 15),
            Arrays.asList(4, 5, 7, 14, 15),
            Arrays.asList(1, 2, 3, 4, 5),
            Arrays.asList(3, 4, 5, 9, 13)
    );

    public static void main(String[] args) {
//        String filePath = &quot;clocksync/input.txt&quot;;
//        InputStream inputStream = Main.class.getClassLoader().getResourceAsStream(filePath);
//        Scanner scanner = new Scanner(inputStream);
        Scanner scanner = new Scanner(System.in);

        int caseCount = Integer.parseInt(scanner.nextLine());
        for (int cc = 0; cc &amp;lt; caseCount; cc++) {
            int[][] clockBoard = new int[4][4];

            String[] hours = scanner.nextLine().trim().split(&quot; &quot;);
            for (int i = 0; i &amp;lt; 16; i++) {
                clockBoard[i / 4][i % 4] = Integer.parseInt(hours[i]);
            }

            System.out.println(program(clockBoard));
        }
    }

    public static int program(int[][] clockBoard) {
        // 모든 시계가 12시 방향으로 정렬된 경우
        if(isFinish(clockBoard)) {
            return 0;
        }

        int click = 0;

        // 4번 스위치 (6, 7, 8, 10, 12) &amp;gt; 8, 12 고정
        if(clockBoard[2][0] != clockBoard[3][0]) {
            return -1;
        } else {
            if(clockBoard[2][0] != 12) {
                int result = getClickCountByTwoClock(clockBoard, 4, 2, 0, 3, 0);
                if(result == -1) {
                    return -1;
                } else {
                    click += result;
                }
            }
        }

        // 2번 스위치 (4, 10, 14, 15) &amp;gt; 10 고정
        if(clockBoard[2][2] != 12) {
            int result = getClickCountByOneClock(clockBoard, 2, 2, 2);
            if(result == -1) {
                return -1;
            } else {
                click += result;
            }
        }

        // 9번 스위치 (3, 4, 5, 9, 13) &amp;gt; 13 고정
        if(clockBoard[3][1] != 12) {
            int result = getClickCountByOneClock(clockBoard, 9, 3, 1);
            if(result == -1) {
                return -1;
            } else {
                click += result;
            }
        }

        // 1번 스위치 (3, 7, 9, 11) &amp;gt; 9, 11 고정
        if(clockBoard[2][1] != clockBoard[2][3]) {
            return -1;
        } else {
            if(clockBoard[2][1] != 12) {
                int result = getClickCountByTwoClock(clockBoard, 1, 2, 1, 2, 3);
                if(result == -1) {
                    return -1;
                } else {
                    click += result;
                }
            }
        }

        // 3번 스위치 (0, 4, 5, 6, 7) &amp;gt; 6 고정
        if(clockBoard[1][2] != 12) {
            int result = getClickCountByOneClock(clockBoard, 3, 1, 2);
            if(result == -1) {
                return -1;
            } else {
                click += result;
            }
        }

        // 7번 스위치 (4, 5, 7, 14, 15) &amp;gt; 7고정
        if(clockBoard[1][3] != 12) {
            int result = getClickCountByOneClock(clockBoard, 7, 1, 3);
            if(result == -1) {
                return -1;
            } else {
                click += result;
            }
        }

        // 8번 스위치 (1, 2, 3, 4, 5) &amp;gt; 4, 5 고정
        if(clockBoard[1][0] != clockBoard[1][1]) {
            return -1;
        } else {
            if(clockBoard[1][0] != 12) {
                int result = getClickCountByTwoClock(clockBoard, 8, 1, 0, 1, 1);
                if(result == -1) {
                    return -1;
                } else {
                    click += result;
                }
            }
        }

        // 6번 스위치 (3, 14, 15) &amp;gt; 3 고정
        if(clockBoard[0][3] != 12) {
            int result = getClickCountByOneClock(clockBoard, 6, 0, 3);
            if(result == -1) {
                return -1;
            } else {
                click += result;
            }
        }

        // 0번 스위치 (0, 1, 2) &amp;gt; 1 고정
        if(clockBoard[0][1] != 12) {
            int result = getClickCountByOneClock(clockBoard, 0, 0, 1);
            if(result == -1) {
                return -1;
            } else {
                click += result;
            }
        }

        // 5번 스위치 (0, 2, 14, 15) &amp;gt; 네 방향 확인
        if(isFinish(clockBoard)) {
            return click;
        }

        boolean is5SwitchClear = false;

        for(int i = 0; i &amp;lt; 4; i++) {
            if(is5SwitchClear) {
                break;
            }

            List&amp;lt;Integer&amp;gt; clockIndexes = clockChain.get(5);

            click++;
            for (int clockIndex : clockIndexes) {
                clockBoard[clockIndex / 4][clockIndex % 4] += 3;
                if (clockBoard[clockIndex / 4][clockIndex % 4] == 15) {
                    clockBoard[clockIndex / 4][clockIndex % 4] = 3;
                }

                if(clockBoard[0][0] == 12 &amp;amp;&amp;amp; clockBoard[0][2] == 12 &amp;amp;&amp;amp; clockBoard[3][2] == 12 &amp;amp;&amp;amp; clockBoard[3][3] == 12) {
                    is5SwitchClear = true;
                    break;
                }
            }

            if(isFinish(clockBoard)) {
                return click;
            }
        }

        if(!is5SwitchClear) {
            return -1;
        }

        return click;
    }

    public static boolean isFinish(int[][] clockBoard) {
        for (int i = 0; i &amp;lt; 16; i++) {
            if (clockBoard[i / 4][i % 4] != 12) {
                return false;
            }
        }
        return true;
    }

    public static int getClickCountByOneClock(int[][] clockBoard, int switchIndex, int trow, int tcol) {
        int click = 0;
        boolean isClear = false;

        for(int i = 0; i &amp;lt; 4; i++) {
            if(isClear) {
                break;
            }

            List&amp;lt;Integer&amp;gt; clockIndexes = clockChain.get(switchIndex);

            click++;
            for (int clockIndex : clockIndexes) {
                clockBoard[clockIndex / 4][clockIndex % 4] += 3;
                if (clockBoard[clockIndex / 4][clockIndex % 4] == 15) {
                    clockBoard[clockIndex / 4][clockIndex % 4] = 3;
                }

                if(clockBoard[trow][tcol] == 12) {
                    isClear = true;
                }
            }

            if(isFinish(clockBoard)) {
                return click;
            }
        }

        if(!isClear) {
            return -1;
        }

        return click;
    }

    public static int getClickCountByTwoClock(int[][] clockBoard, int switchIndex, int trow1, int tcol1, int trow2, int tcol2) {
        int click = 0;
        boolean isClear = false;

        for(int i = 0; i &amp;lt; 4; i++) {
            if(isClear) {
                break;
            }

            List&amp;lt;Integer&amp;gt; clockIndexes = clockChain.get(switchIndex);

            click++;
            for (int clockIndex : clockIndexes) {
                clockBoard[clockIndex / 4][clockIndex % 4] += 3;
                if (clockBoard[clockIndex / 4][clockIndex % 4] == 15) {
                    clockBoard[clockIndex / 4][clockIndex % 4] = 3;
                }

                if(clockBoard[trow1][tcol1] == 12 &amp;amp;&amp;amp; clockBoard[trow2][tcol2] == 12) {
                    isClear = true;
                }
            }

            if(isFinish(clockBoard)) {
                return click;
            }
        }

        if(!isClear) {
            return -1;
        }

        return click;
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;재귀호출을 사용한 완전탐색 풀이&lt;/h4&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;pre id=&quot;code_1740287941186&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

public class Main {
    public static final int INF = 9999;
    public static final int SWITCHES = 10;
    public static final int CLOCKS = 16;

    // linked[i][j] = 'x': i번 스위치와 j번 시계가 연결되어 있다.
    // linked[i][j] = '.': i번 스위치와 j번 시계가 연결되어 있지 않다.
    public static List&amp;lt;String&amp;gt; clockChain = Arrays.asList(
            // 0123456789012345
            &quot;xxx.............&quot;,
            &quot;...x...x.x.x....&quot;,
            &quot;....x.....x...xx&quot;,
            &quot;x...xxxx........&quot;,
            &quot;......xxx.x.x...&quot;,
            &quot;x.x...........xx&quot;,
            &quot;...x..........xx&quot;,
            &quot;....xx.x......xx&quot;,
            &quot;.xxxxx..........&quot;,
            &quot;...xxx...x...x..&quot;
    );
    public static void main(String[] args) {
//        String filePath = &quot;clocksync/input.txt&quot;;
//        InputStream inputStream = Main.class.getClassLoader().getResourceAsStream(filePath);
//        Scanner scanner = new Scanner(inputStream);
        Scanner scanner = new Scanner(System.in);

        int caseCount = Integer.parseInt(scanner.nextLine());
        for (int cc = 0; cc &amp;lt; caseCount; cc++) {
            int[][] clockBoard = new int[4][4];

            String[] hours = scanner.nextLine().trim().split(&quot; &quot;);
            for (int i = 0; i &amp;lt; 16; i++) {
                clockBoard[i / 4][i % 4] = Integer.parseInt(hours[i]);
            }

            int result = solve(clockBoard, 0);
            System.out.println(result == INF ? -1 : result);
        }
    }

    // 모든 시계가 12시를 가리키고 있는지 확인한다.
    public static boolean areAligned(int[][] clockBoard) {
        for (int i = 0; i &amp;lt; 16; i++) {
            if (clockBoard[i / 4][i % 4] != 12) {
                return false;
            }
        }
        return true;
    }

    // 스위치를 누른다.
    public static void push(int[][] clockBoard, int switchIndex) {
        for (int clock = 0; clock &amp;lt; CLOCKS; clock++) {
            if (clockChain.get(switchIndex).charAt(clock) == 'x') {
                clockBoard[clock / 4][clock % 4] += 3;
                if (clockBoard[clock / 4][clock % 4] == 15) {
                    clockBoard[clock / 4][clock % 4] = 3;
                }
            }
        }
    }

    // clocks: 현재 시계들의 상태
    // switchIndex: 이번에 누를 스위치 번호
    // 가 주어질 때 남은 스위치들을 눌러서 clocks 를 12시로 맞출 수 있는 최소 횟수를 반환한다.
    // 만약 불가능하다면 INF 이상의 큰 수를 반환한다.
    public static int solve(int[][] clockBoard, int switchIndex) {
        if (switchIndex == SWITCHES) {
            return areAligned(clockBoard) ? 0 : INF;
        }

        // 이 스위치를 0번 누르는 경우부터 세 번 누르는 경우까지를 모두 시도한다.
        int ret = INF;
        for (int cnt = 0; cnt &amp;lt; 4; cnt++) {
            ret = Math.min(ret, cnt + solve(clockBoard, switchIndex + 1));
            push(clockBoard, switchIndex);
        }

        // push(clocks, switch)가 네 번 호출되었으니 clocks는 원래와 같은 상태가 된다.
        return ret;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/알고리즘 문제해결전략</category>
      <category>clocksync</category>
      <category>무식하게풀기</category>
      <category>브루트포스</category>
      <category>시계맞추기</category>
      <category>알고리즘</category>
      <category>알고리즘문제해결전략</category>
      <category>완전탐색</category>
      <category>재귀함수</category>
      <category>재귀호출</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/153</guid>
      <comments>https://minimalcode.tistory.com/153#entry153comment</comments>
      <pubDate>Sun, 23 Feb 2025 00:03:42 +0900</pubDate>
    </item>
    <item>
      <title>게임판 덮기 (ID : BOARDCOVER)</title>
      <link>https://minimalcode.tistory.com/152</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.algospot.com/judge/problem/read/BOARDCOVER&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740236725747&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;algospot.com ::  
	BOARDCOVER&quot; data-og-description=&quot;게임판 덮기 문제 정보 문제 H*W 크기의 게임판이 있습니다. 게임판은 검은 칸과 흰 칸으로 구성된 격자 모양을 하고 있는데 이 중 모든 흰 칸을 3칸짜리 L자 모양의 블록으로 덮고 싶습니다. 이 &quot; data-og-host=&quot;www.algospot.com&quot; data-og-source-url=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot; data-og-url=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;algospot.com :: BOARDCOVER&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;게임판 덮기 문제 정보 문제 H*W 크기의 게임판이 있습니다. 게임판은 검은 칸과 흰 칸으로 구성된 격자 모양을 하고 있는데 이 중 모든 흰 칸을 3칸짜리 L자 모양의 블록으로 덮고 싶습니다. 이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.algospot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 풀려고 고민을 꽤 오래했지만 결국 풀지못하고 답안 코드를 봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고 스팟에 답안 제출한 코드는 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1740219093719&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

public class Main {
    public static int[][][] coverType = {
            { {0, 0}, {1, 0}, {0, 1} },
            { {0, 0}, {0, 1}, {1, 1} },
            { {0, 0}, {1, 0}, {1, 1} },
            { {0, 0}, {1, 0}, {1, -1} }
    };

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        int caseCount = Integer.parseInt(scanner.nextLine());
        for (int cc = 0; cc &amp;lt; caseCount; cc++) {
            int h, w;
            List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; board = new ArrayList&amp;lt;&amp;gt;();

            String[] hw = scanner.nextLine().trim().split(&quot; &quot;);
            h = Integer.parseInt(hw[0]);
            w = Integer.parseInt(hw[1]);

            for (int r = 0; r &amp;lt; h; r++) {
                String row = scanner.nextLine();
                List&amp;lt;Integer&amp;gt; rowList = new ArrayList&amp;lt;&amp;gt;();
                for (int c = 0; c &amp;lt; w; c++) {
                    if(row.charAt(c) == '#') {
                        rowList.add(1);
                    } else {
                        rowList.add(0);
                    }
                }
                board.add(rowList);
            }

            System.out.println(cover(board));
        }
    }

    public static boolean set(List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; board, int row, int col, int type, int delta) {
        boolean ok = true;
        for (int i = 0; i &amp;lt; 3; i++) {
            int nrow = row + coverType[type][i][0];
            int ncol = col + coverType[type][i][1];
            if (nrow &amp;lt; 0 || nrow &amp;gt;= board.size() || ncol &amp;lt; 0 || ncol &amp;gt;= board.get(0).size()) {
                ok = false;
            } else {
                board.get(nrow).set(ncol, board.get(nrow).get(ncol) + delta);
                if (board.get(nrow).get(ncol) &amp;gt; 1) {
                    ok = false;
                }
            }
        }
        return ok;
    }

    public static int cover(List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; board) {
        // 아직 채우지 못한 칸 중 가장 윗줄 왼쪽에 있는 칸을 찾는다.
        int row = -1, col = -1;
        for (int r = 0; r &amp;lt; board.size(); r++) {
            for (int c = 0; c &amp;lt; board.get(0).size(); c++) {
                if (board.get(r).get(c) == 0) {
                    row = r;
                    col = c;
                    break;
                }
            }
            if (row != -1) {
                break;
            }
        }

        // 기저 사례: 모든 칸을 채웠으면 1을 반환한다.
        if (row == -1) {
            return 1;
        }

        int result = 0;
        for (int type = 0; type &amp;lt; 4; type++) {
            if (set(board, row, col, type, 1)) {
                result += cover(board);
            }
            set(board, row, col, type, -1);
        }

        return result;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/알고리즘 문제해결전략</category>
      <category>BOARDCOVER</category>
      <category>게임판덮기</category>
      <category>무식하게풀기</category>
      <category>브루트포스</category>
      <category>알고리즘</category>
      <category>알고리즘 문제해결전략</category>
      <category>알고스팟</category>
      <category>완전탐색</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/152</guid>
      <comments>https://minimalcode.tistory.com/152#entry152comment</comments>
      <pubDate>Sat, 22 Feb 2025 19:11:42 +0900</pubDate>
    </item>
    <item>
      <title>소풍 (ID : PICNIC)</title>
      <link>https://minimalcode.tistory.com/151</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.algospot.com/judge/problem/read/PICNIC&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740236698577&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;algospot.com ::  
	PICNIC&quot; data-og-description=&quot;소풍 문제 정보 문제 안드로메다 유치원 익스프레스반에서는 다음 주에 율동공원으로 소풍을 갑니다. 원석 선생님은 소풍 때 학생들을 두 명씩 짝을 지어 행동하게 하려고 합니다. 그런데 서로 &quot; data-og-host=&quot;www.algospot.com&quot; data-og-source-url=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot; data-og-url=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;algospot.com :: PICNIC&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소풍 문제 정보 문제 안드로메다 유치원 익스프레스반에서는 다음 주에 율동공원으로 소풍을 갑니다. 원석 선생님은 소풍 때 학생들을 두 명씩 짝을 지어 행동하게 하려고 합니다. 그런데 서로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.algospot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 첫 번째 시도&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 시도에서는 &lt;b&gt;런타임 에러[RTE (nonzero return code)]&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;아래 소스의 43 라인에서 myFriends.get(studnet1) 의 값이 null 인지 체크해주지 않아서 발생한 문제였다. 문제의 원인을 찾았으니 null 체크 코드를 추가하고 다시 답안을 제출해보았다.&lt;/p&gt;
&lt;pre id=&quot;code_1739692232422&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

public class Main {
    public static int answer = 0;

    public static void main(String[] args) {
//        String filePath = &quot;picnic/input.txt&quot;;
//        InputStream inputStream = Main.class.getClassLoader().getResourceAsStream(filePath);
//        Scanner scanner = new Scanner(inputStream);
        Scanner scanner = new Scanner(System.in);

        int caseCount = Integer.parseInt(scanner.nextLine());
        for (int c = 0; c &amp;lt; caseCount; c++) {
            String[] studentAndFriendCount = scanner.nextLine().trim().split(&quot; &quot;);
            int studentCount = Integer.parseInt(studentAndFriendCount[0]);
            int friendCount = Integer.parseInt(studentAndFriendCount[1]);

            Map&amp;lt;Integer, List&amp;lt;Integer&amp;gt;&amp;gt; myFriends = new HashMap&amp;lt;&amp;gt;();
            String[] friendPair = scanner.nextLine().trim().split(&quot; &quot;);

            for (int i = 0; i &amp;lt; friendCount * 2; i = i + 2) {
                int student1 = Integer.parseInt(friendPair[i]);
                int student2 = Integer.parseInt(friendPair[i + 1]);

                myFriends.computeIfAbsent(student1, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;()).add(student2);
                myFriends.computeIfAbsent(student2, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;()).add(student1);
            }

            // 친구 쌍 후보를 구한다.
            List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; friendCandidatePairs = new ArrayList&amp;lt;&amp;gt;();
            for (int i = 0; i &amp;lt; studentCount; i++) {
                for (int j = i + 1; j &amp;lt; studentCount; j++) {
                    friendCandidatePairs.add(Arrays.asList(i, j));
                }
            }

            // 친구 쌍 후보 중에서 실제로 친구인 경우만 추린다.
            List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; friendPairs = new ArrayList&amp;lt;&amp;gt;();
            for(List&amp;lt;Integer&amp;gt; friendCandidatePair : friendCandidatePairs) {
                int student1 = friendCandidatePair.get(0);
                int student2 = friendCandidatePair.get(1);

                if(myFriends.get(student1).contains(student2)) {
                    friendPairs.add(Arrays.asList(student1, student2));
                }
            }

            // 친구 쌍 후보 중에서 정답인 경우를 찾는다.
            answer = 0;
            Stack&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; stack = new Stack&amp;lt;&amp;gt;();
            findCombination(friendPairs, stack, 0, studentCount, friendPairs.size());

            // 정답을 출력한다.
            System.out.println(answer);
        }
    }

    public static void findCombination(List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; friendPairs, Stack&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; stack, int index, int studentCount, int friendPairsCount) {
        if (friendPairsCount &amp;lt;= index) {
            if(stack.size() == studentCount / 2) {
                if(isCombinationOk(stack, studentCount)) {
                    answer = answer + 1;
                }
            }
            return;
        }

        // 친구를 포함하지 않는 경우
        findCombination(friendPairs, stack, index + 1, studentCount, friendPairsCount);

        // 친구를 포함하는 경우
        stack.push(friendPairs.get(index));
        findCombination(friendPairs, stack, index + 1, studentCount, friendPairsCount);
        stack.pop();
    }

    public static boolean isCombinationOk(Stack&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; stack, int studentCount) {
        boolean[] isSelected = new boolean[studentCount];

        for (List&amp;lt;Integer&amp;gt; friendPair : stack) {
            isSelected[friendPair.get(0)] = true;
            isSelected[friendPair.get(1)] = true;
        }

        for (int i = 0; i &amp;lt; studentCount; i++) {
            if (!isSelected[i]) {
                return false;
            }
        }

        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 두 번째 시도&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &lt;b&gt;런타임 에러&lt;/b&gt;는 안났지만 &lt;b&gt;시간초과&lt;/b&gt;가 나왔다. 수행 기준 시간을 초과한 것인데 어떤 부분을 개선해야 속도가 빨라질 수 있을지 고민이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739692348978&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

public class Main {
    public static int answer = 0;

    public static void main(String[] args) {
//        String filePath = &quot;picnic/input.txt&quot;;
//        InputStream inputStream = Main.class.getClassLoader().getResourceAsStream(filePath);
//        Scanner scanner = new Scanner(inputStream);
        Scanner scanner = new Scanner(System.in);

        int caseCount = Integer.parseInt(scanner.nextLine());
        for (int c = 0; c &amp;lt; caseCount; c++) {
            String[] studentAndFriendCount = scanner.nextLine().trim().split(&quot; &quot;);
            int studentCount = Integer.parseInt(studentAndFriendCount[0]);
            int friendCount = Integer.parseInt(studentAndFriendCount[1]);

            Map&amp;lt;Integer, List&amp;lt;Integer&amp;gt;&amp;gt; myFriends = new HashMap&amp;lt;&amp;gt;();
            String[] friendPair = scanner.nextLine().trim().split(&quot; &quot;);

            for (int i = 0; i &amp;lt; friendCount * 2; i = i + 2) {
                int student1 = Integer.parseInt(friendPair[i]);
                int student2 = Integer.parseInt(friendPair[i + 1]);

                myFriends.computeIfAbsent(student1, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;()).add(student2);
                myFriends.computeIfAbsent(student2, k -&amp;gt; new ArrayList&amp;lt;&amp;gt;()).add(student1);
            }

            // 친구 쌍 후보를 구한다.
            List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; friendCandidatePairs = new ArrayList&amp;lt;&amp;gt;();
            for (int i = 0; i &amp;lt; studentCount; i++) {
                for (int j = i + 1; j &amp;lt; studentCount; j++) {
                    friendCandidatePairs.add(Arrays.asList(i, j));
                }
            }

            // 친구 쌍 후보 중에서 실제로 친구인 경우만 추린다.
            List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; friendPairs = new ArrayList&amp;lt;&amp;gt;();
            for(List&amp;lt;Integer&amp;gt; friendCandidatePair : friendCandidatePairs) {
                int student1 = friendCandidatePair.get(0);
                int student2 = friendCandidatePair.get(1);

                if(myFriends.get(student1) != null &amp;amp;&amp;amp; myFriends.get(student1).contains(student2)) {
                    friendPairs.add(Arrays.asList(student1, student2));
                }
            }

            // 친구 쌍 후보 중에서 정답인 경우를 찾는다.
            answer = 0;
            Stack&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; stack = new Stack&amp;lt;&amp;gt;();
            findCombination(friendPairs, stack, 0, studentCount, friendPairs.size());

            // 정답을 출력한다.
            System.out.println(answer);
        }
    }

    public static void findCombination(List&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; friendPairs, Stack&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; stack, int index, int studentCount, int friendPairsCount) {
        if (friendPairsCount &amp;lt;= index) {
            if(stack.size() == studentCount / 2) {
                if(isCombinationOk(stack, studentCount)) {
                    answer = answer + 1;
                }
            }
            return;
        }

        // 친구를 포함하지 않는 경우
        findCombination(friendPairs, stack, index + 1, studentCount, friendPairsCount);

        // 친구를 포함하는 경우
        stack.push(friendPairs.get(index));
        findCombination(friendPairs, stack, index + 1, studentCount, friendPairsCount);
        stack.pop();
    }

    public static boolean isCombinationOk(Stack&amp;lt;List&amp;lt;Integer&amp;gt;&amp;gt; stack, int studentCount) {
        boolean[] isSelected = new boolean[studentCount];

        for (List&amp;lt;Integer&amp;gt; friendPair : stack) {
            isSelected[friendPair.get(0)] = true;
            isSelected[friendPair.get(1)] = true;
        }

        for (int i = 0; i &amp;lt; studentCount; i++) {
            if (!isSelected[i]) {
                return false;
            }
        }

        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 세 번째 시도&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선점을 찾아내지 못해서 알고리즘 문제해결전략 책의 정답 코드를 봤다. 알고스팟에 제출한 정답 코드는 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1739709190136&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

public class Main {
    public static int n = 0;
    public static int m = 0;
    public static boolean[][] areFriends = new boolean[10][10];
    public static boolean[] taken = new boolean[10];
    public static int answer = 0;

    public static void main(String[] args) {
//        String filePath = &quot;picnic/input.txt&quot;;
//        InputStream inputStream = Main.class.getClassLoader().getResourceAsStream(filePath);
//        Scanner scanner = new Scanner(inputStream);
        Scanner scanner = new Scanner(System.in);

        int caseCount = Integer.parseInt(scanner.nextLine());
        for (int c = 0; c &amp;lt; caseCount; c++) {
            init();

            String[] nm = scanner.nextLine().trim().split(&quot; &quot;);
            n = Integer.parseInt(nm[0]);
            m = Integer.parseInt(nm[1]);

            String[] friendPair = scanner.nextLine().trim().split(&quot; &quot;);
            for (int i = 0; i &amp;lt; m * 2; i = i + 2) {
                int student1 = Integer.parseInt(friendPair[i]);
                int student2 = Integer.parseInt(friendPair[i + 1]);
                areFriends[student1][student2] = true;
                areFriends[student2][student1] = true;
            }

            // 정답을 출력한다.
            System.out.println(countPairings(taken));
        }
    }

    public static void init() {
        n = 0;
        m = 0;
        answer = 0;

        for (int i = 0; i &amp;lt; 10; i++) {
            Arrays.fill(areFriends[i], false);
        }
        Arrays.fill(taken, false);
    }

    public static int countPairings(boolean[] taken) {
        // 남은 학생들 중 가장 번호가 빠른 학생을 찾는다.
        int firstFree = -1;
        for (int i = 0; i &amp;lt; n; ++i) {
            if (!taken[i]) {
                firstFree = i;
                break;
            }
        }

        // 기저 사례: 모든 학생이 짝을 찾았으면 한 가지 방법을 찾았으니 종료한다.
        if (firstFree == -1) return 1;
        int ret = 0;

        // 이 학생과 짝지을 학생을 결정한다.
        for (int pairWith = firstFree + 1; pairWith &amp;lt; n; pairWith++) {
            if (!taken[pairWith] &amp;amp;&amp;amp; areFriends[firstFree][pairWith]) {
                taken[firstFree] = taken[pairWith] = true;
                ret += countPairings(taken);
                taken[firstFree] = taken[pairWith] = false;
            }
        }

        return ret;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;참고&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://minimalcode.tistory.com/149#4.%20%ED%92%80%EC%9D%B4%3A%20%EC%86%8C%ED%92%8D-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://minimalcode.tistory.com/149#4.%20%ED%92%80%EC%9D%B4%3A%20%EC%86%8C%ED%92%8D-1&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739709207601&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[03 알고리즘 설계 패러다임] 6. 무식하게 풀기&quot; data-og-description=&quot;1. 도입&amp;nbsp;프로그래밍 대회에서 대부분의 사람들이 가장 많이 하는 실수는 쉬운 문제를 어렵게 푸는 것입니다. 공부를 열심히 할수록 복잡하지만 우아한 답안을 만들고 싶은 마음이 커지기 마련&quot; data-og-host=&quot;minimalcode.tistory.com&quot; data-og-source-url=&quot;https://minimalcode.tistory.com/149#4.%20%ED%92%80%EC%9D%B4%3A%20%EC%86%8C%ED%92%8D-1&quot; data-og-url=&quot;https://minimalcode.tistory.com/149&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/wU4co/hyYfKqPc5U/skXQ1H2TTlGlnUe44J9gI1/img.jpg?width=458&amp;amp;height=471&amp;amp;face=0_0_458_471,https://scrap.kakaocdn.net/dn/4y2X5/hyYf2ZmJdd/LksCBQRWliEJUgVSQUWG1K/img.jpg?width=458&amp;amp;height=471&amp;amp;face=0_0_458_471,https://scrap.kakaocdn.net/dn/GAoLg/hyYfRDyvS6/6Bon6kmSd5eThlTKEJt9uk/img.jpg?width=458&amp;amp;height=471&amp;amp;face=0_0_458_471&quot;&gt;&lt;a href=&quot;https://minimalcode.tistory.com/149#4.%20%ED%92%80%EC%9D%B4%3A%20%EC%86%8C%ED%92%8D-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://minimalcode.tistory.com/149#4.%20%ED%92%80%EC%9D%B4%3A%20%EC%86%8C%ED%92%8D-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/wU4co/hyYfKqPc5U/skXQ1H2TTlGlnUe44J9gI1/img.jpg?width=458&amp;amp;height=471&amp;amp;face=0_0_458_471,https://scrap.kakaocdn.net/dn/4y2X5/hyYf2ZmJdd/LksCBQRWliEJUgVSQUWG1K/img.jpg?width=458&amp;amp;height=471&amp;amp;face=0_0_458_471,https://scrap.kakaocdn.net/dn/GAoLg/hyYfRDyvS6/6Bon6kmSd5eThlTKEJt9uk/img.jpg?width=458&amp;amp;height=471&amp;amp;face=0_0_458_471');&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;[03 알고리즘 설계 패러다임] 6. 무식하게 풀기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. 도입&amp;nbsp;프로그래밍 대회에서 대부분의 사람들이 가장 많이 하는 실수는 쉬운 문제를 어렵게 푸는 것입니다. 공부를 열심히 할수록 복잡하지만 우아한 답안을 만들고 싶은 마음이 커지기 마련&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;minimalcode.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Algorithm/알고리즘 문제해결전략</category>
      <category>무식하게 풀기</category>
      <category>문제해결전략</category>
      <category>브루트 포스</category>
      <category>알고리즘</category>
      <category>알고리즘 문제해결전략</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/151</guid>
      <comments>https://minimalcode.tistory.com/151#entry151comment</comments>
      <pubDate>Sun, 16 Feb 2025 14:10:28 +0900</pubDate>
    </item>
    <item>
      <title>보글 게임 (ID : BOGGLE)</title>
      <link>https://minimalcode.tistory.com/150</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/BOGGLE&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.algospot.com/judge/problem/read/BOGGLE&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740236661594&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;algospot.com ::  
	BOGGLE&quot; data-og-description=&quot;보글 게임 문제 정보 문제 보글(Boggle) 게임은 그림 (a)와 같은 5x5 크기의 알파벳 격자인 게임판의 한 글자에서 시작해서 펜을 움직이면서 만나는 글자를 그 순서대로 나열하여 만들어지는 영어 &quot; data-og-host=&quot;www.algospot.com&quot; data-og-source-url=&quot;https://www.algospot.com/judge/problem/read/BOGGLE&quot; data-og-url=&quot;https://www.algospot.com/judge/problem/read/BOGGLE&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/BOGGLE&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.algospot.com/judge/problem/read/BOGGLE&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;algospot.com :: BOGGLE&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;보글 게임 문제 정보 문제 보글(Boggle) 게임은 그림 (a)와 같은 5x5 크기의 알파벳 격자인 게임판의 한 글자에서 시작해서 펜을 움직이면서 만나는 글자를 그 순서대로 나열하여 만들어지는 영어&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.algospot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 첫 번째 시도&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재귀를 이용하여 완전탐색방법으로 문제를 풀려고 시도했지만 '&lt;b&gt;시간초과&lt;/b&gt;' 가 발생하여 통과하지 못했다. 문제 조건중에 지나간 글자를 다시 지나갈 수 있다는 조건 때문에 visited[][] 배열로 방문 체크를 하지못한 점 때문에 수행시간이 오래걸리지 않았나 싶다. 해당 조건을 해결하면서 수행시간도 줄일 수 있는 방법을 찾아봐야겠다. 고민을 해보다가 너무 오래걸린다 싶으면 정답을 봐야겠다.&lt;/p&gt;
&lt;pre id=&quot;code_1739027172145&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

public class Main {

    private static Scanner scanner;
    private static char[][] pan;
    private static String[] targetWords;
    private static int targetWordCount;
    private static int targetWordsOrder;
    private static int[] drow = {-1, -1, 0, 1, 1, 1, 0, -1};
    private static int[] dcol = {0, 1, 1, 1, 0, -1, -1, -1};

    public static void main(String[] args) {
        scanner = new Scanner(System.in);

        int caseCount = Integer.parseInt(scanner.nextLine());
        for (int c = 0; c &amp;lt; caseCount; c++) {
            // 초기화
            pan = new char[5][5];
            targetWords = new String[10];
            List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; answers = new ArrayList&amp;lt;&amp;gt;();

            // 테스트 데이터 입력
            for (int row = 0; row &amp;lt; 5; row++) {
                String panRow = scanner.nextLine();
                for(int col = 0; col &amp;lt; 5; col++) {
                    pan[row][col] = panRow.charAt(col);
                }
            }

            targetWordCount = Integer.parseInt(scanner.nextLine());

            for (int i = 0; i &amp;lt; targetWordCount; i++) {
                String searchWord = scanner.nextLine();
                targetWords[i] = searchWord;
            }

            // 단어 찾기
            for(int i = 0; i &amp;lt; targetWordCount; i++) {
                targetWordsOrder = i;
                boolean isSearched = searchWord();

                Map&amp;lt;String, String&amp;gt; answer = new HashMap&amp;lt;&amp;gt;();
                answer.put(&quot;work&quot;, targetWords[targetWordsOrder]);
                answer.put(&quot;result&quot;, isSearched ? &quot;YES&quot; : &quot;NO&quot;);
                answers.add(answer);
            }

            // 결과 출력
            for (Map&amp;lt;String, String&amp;gt; answer : answers) {
                System.out.println(answer.get(&quot;work&quot;) + &quot; &quot; + answer.get(&quot;result&quot;));
            }
        }
    }

    private static boolean searchWord() {
        boolean isSearched = false;
        for(int row = 0; row &amp;lt; 5; row++) {
            for(int col = 0; col &amp;lt; 5; col++) {
                isSearched = recursiveSearchWord(row, col, &quot;&quot;, 0);
                if(isSearched) {
                    return true;
                }
            }
        }
        return isSearched;
    }

    private static boolean recursiveSearchWord(int row, int col, String combinedWord, int combinedWordIdx) {
        // 기저 사례 1
        if (targetWords[targetWordsOrder].length() &amp;lt; combinedWordIdx)
            return false;

        if (pan[row][col] == targetWords[targetWordsOrder].charAt(combinedWordIdx)) {
            combinedWord += pan[row][col];

            // 기저 사례 2
            if (targetWords[targetWordsOrder].equals(combinedWord))
                return true;

            for (int i = 0; i &amp;lt; 8; i++) {
                int nextRow = row + drow[i];
                int nextCol = col + dcol[i];

                if (nextRow &amp;gt;= 0 &amp;amp;&amp;amp; nextRow &amp;lt; 5 &amp;amp;&amp;amp; nextCol &amp;gt;= 0 &amp;amp;&amp;amp; nextCol &amp;lt; 5) {
                    boolean isSearched = recursiveSearchWord(nextRow, nextCol, combinedWord, combinedWordIdx + 1);
                    if (isSearched)
                        return true;
                }
            }
        }

        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 두 번째 시도&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수행시간을 줄이기 위해 판 위의 문자 사용여부를 체크하는 isOccupied[][] 배열을 추가했습니다. 그리고 재귀 진행 조건문에서 다음 재귀 함수에서 방문하는 지점의 문자가 타겟 문자열의 다음에 올 문자와 동일한 경우에는 탐색을 진행하도록 소스를 수정해봤지만 이번에도 동일하게 '시간초과'로 통과하지 못했습니다. 시간을 획기적으로 줄일 수 있는 확실한 방법이 필요할 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1739028493504&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import java.util.*;

public class Main {
    private static Scanner scanner;
    private static char[][] pan;
    private static boolean[][] isOccupied;
    private static String[] targetWords;
    private static int targetWordCount;
    private static int targetWordsOrder;
    private static int[] drow = {-1, -1, 0, 1, 1, 1, 0, -1};
    private static int[] dcol = {0, 1, 1, 1, 0, -1, -1, -1};

    public static void main(String[] args) {
        scanner = new Scanner(System.in);

        int caseCount = Integer.parseInt(scanner.nextLine());
        for (int c = 0; c &amp;lt; caseCount; c++) {
            pan = new char[5][5];
            isOccupied = new boolean[5][5];
            targetWords = new String[10];
            List&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; answers = new ArrayList&amp;lt;&amp;gt;();

            for (int row = 0; row &amp;lt; 5; row++) {
                String panRow = scanner.nextLine();
                for(int col = 0; col &amp;lt; 5; col++) {
                    pan[row][col] = panRow.charAt(col);
                }
            }

            targetWordCount = Integer.parseInt(scanner.nextLine());

            for (int i = 0; i &amp;lt; targetWordCount; i++) {
                String searchWord = scanner.nextLine();
                targetWords[i] = searchWord;
            }

            for(int i = 0; i &amp;lt; targetWordCount; i++) {
                initIsOccupied();
                targetWordsOrder = i;
                boolean isSearched = searchWord();

                Map&amp;lt;String, String&amp;gt; answer = new HashMap&amp;lt;&amp;gt;();
                answer.put(&quot;work&quot;, targetWords[targetWordsOrder]);
                answer.put(&quot;result&quot;, isSearched ? &quot;YES&quot; : &quot;NO&quot;);
                answers.add(answer);
            }

            for (Map&amp;lt;String, String&amp;gt; answer : answers) {
                System.out.println(answer.get(&quot;work&quot;) + &quot; &quot; + answer.get(&quot;result&quot;));
            }
        }
    }

    private static void initIsOccupied() {
        for(int row = 0; row &amp;lt; 5; row++) {
            for(int col = 0; col &amp;lt; 5; col++) {
                isOccupied[row][col] = false;
            }
        }
    }

    private static boolean searchWord() {
        boolean isSearched = false;
        for(int row = 0; row &amp;lt; 5; row++) {
            for(int col = 0; col &amp;lt; 5; col++) {
                isSearched = recursiveSearchWord(row, col, &quot;&quot;, 0);
                if(isSearched) {
                    return true;
                }
            }
        }
        return isSearched;
    }

    private static boolean recursiveSearchWord(int row, int col, String combinedWord, int combinedWordIdx) {
        if (targetWords[targetWordsOrder].length() &amp;lt; combinedWordIdx)
            return false;

        if (pan[row][col] == targetWords[targetWordsOrder].charAt(combinedWordIdx)) {
            combinedWord += pan[row][col];
            isOccupied[row][col] = true;

            if (targetWords[targetWordsOrder].equals(combinedWord))
                return true;

            for (int i = 0; i &amp;lt; 8; i++) {
                int nextRow = row + drow[i];
                int nextCol = col + dcol[i];

                if (nextRow &amp;gt;= 0 &amp;amp;&amp;amp; nextRow &amp;lt; 5 &amp;amp;&amp;amp; nextCol &amp;gt;= 0 &amp;amp;&amp;amp; nextCol &amp;lt; 5) {
                    if (pan[nextRow][nextCol] == targetWords[targetWordsOrder].charAt(combinedWordIdx + 1)
                            || !isOccupied[nextRow][nextCol]) {
                        boolean isSearched = recursiveSearchWord(nextRow, nextCol, combinedWord, combinedWordIdx + 1);
                        if (isSearched)
                            return true;
                    }
                }
            }
        }

        return false;
    }
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Algorithm/알고리즘 문제해결전략</category>
      <category>Boggle</category>
      <category>문제해결전략</category>
      <category>보글게임</category>
      <category>알고리즘</category>
      <category>알고스팟</category>
      <category>완전탐색</category>
      <category>자바</category>
      <category>재귀</category>
      <category>재귀함수</category>
      <category>탐색</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/150</guid>
      <comments>https://minimalcode.tistory.com/150#entry150comment</comments>
      <pubDate>Sun, 9 Feb 2025 00:09:40 +0900</pubDate>
    </item>
    <item>
      <title>[03 알고리즘 설계 패러다임] 6. 무식하게 풀기</title>
      <link>https://minimalcode.tistory.com/149</link>
      <description>&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 도입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍 대회에서 대부분의 사람들이 가장 많이 하는 실수는 쉬운 문제를 어렵게 푸는 것입니다. 공부를 열심히 할수록 복잡하지만 우아한 답안을 만들고 싶은 마음이 커지기 마련이고, 그래서 바로 앞에 보이는 쉽고 간단하며 틀릴 가능성이 낮은 답안을 간과하기 쉽습니다. &lt;span style=&quot;background-color: #ffff00; color: #333333;&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;흔히 전산학에서 '무식하게 푼다(brute-force)'는 말은 컴퓨터의 빠른 계산 능력을 이용해 가능한 경우의 수를 일일이 나열하면서 답을 찾는 방법을 의미합니다. 가능한 방법을 전부 만들어 보는 알고리즘들을 가리켜 흔히 완전 탐색(exhaustive search)이라고 부릅니다. 얼핏 보면 이런 것을 언급할 가치가 있나 싶을 정도로 간단한 방법이지만, 완전 탐색은 사실 컴퓨터의 장점을 가장 잘 이용하는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 프로그래밍 대회에서도 프로그램을 빠르고 정확하게 구현하는 능력을 검증하기 위해 입력의 크기를 작게 제한한 문제들이 흔히 출제되며, 완전 탐색은 더 빠른 알고리즘의 기반이 되기도 하기 때문에 완전 탐색에 대해 잘 익혀둘 필요가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 재귀 호출과 완전 탐색&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;재귀 호출&lt;/h4&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이 주어졌을 때 1부터 n까지의 합을 반환하는 함수 sum()의 구현을 보여줍니다. 이 함수를 어떻게 하면 재귀 호출을 이용하도록 바꿀 수 있을까요?&lt;/p&gt;
&lt;pre id=&quot;code_1738857315568&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 필수 조건: n &amp;gt;= 1
// 결과: 1부터 n까지의 합을 반환한다.
int sum(int n) {
    int ret = 0;
    for(int i = 1; i &amp;lt;= n; ++i) {
        ret += i;
    }
    return ret;
}

// 필수 조건: n &amp;gt;= 1
// 결과: 1부터 n까지의 합을 반환한다.
int recursiveSum(int n) {
    if(n == 1) return 1; // 더이상 쪼개지지 않을 때
    return n + recursiveSum(n-1);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n개의 숫자의 합을 구하는 작업을 n개의 조각으로 쪼개, 더할 각 숫자가 하나의 조각이 되도록 합시다. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;재귀 호출을 이용하기 위해서는 이 조각 중 하나를 떼내어 자신이 해결하고, 나머지 조각들은 자기 자신을 호출해 해결해야 합니다.&lt;/span&gt; 이 조각 중에서 n만 따로 빼내기로 합시다. 그러면 1부터 n-1 까지의 조각들이 남는데, 이들을 모두 처리한 결과는 다름아닌 1부터 n-1 까지의 합입니다. 따라서 자기 자신을 호출해 n-1까지의 합을 구한 뒤, 여기에 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;한 가지 유의할 것은 n개의 조각 중 n이 아니라 1을 빼냈을 경우 이런 방법으로 문제를 해결할 수 없다는 것입니다. (역방향) 1을 빼고 나면 2부터 n까지의 합이 남는데, 이것은 1부터 n까지의 합을 구한다는 원래 문제와는 다른 형태이고 따라서 sum()을 호출해 계산할 수 없기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;recursiveSum()은 재귀 호출을 이용해 sum()을 구현한 함수입니다. recursiveSum() 함수의 첫 줄에 있는 if문에 주목해 봅시다. 이 조건문이 없으면 이 함수가 제대로 동작하지 않을 것임은 자명합니다. n = 1이면 조각이 하나뿐이니, 한 개를 빼고 나면 더이상 처리할 작업이 없으니까요. 모든 재귀 함수는 이와 같이 '더이상 쪼개지지 않는' 최소한의 작업에 도달했을 때 답을 곧장 반환하는 조건문을 포함해야 합니다. 이때 쪼개지지 않는 가장 작은 작업들을 가리켜 재귀 호출의 기저 사례(base case)라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기저 사례를 선택할 때는 존재하는 모든 입력이 항상 기저 사례의 답을 이용해 계산될 수 있도록 신경써야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재귀 호출을 이용하면 특정 조건을 만족하는 조합을 모두 생성하는 코드를 쉽게 작성할 수 있습니다. 때문에 재귀 호출은 완전 탐색을 구현할 때 아주 유용한 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 문제: 소풍 (문제 ID: PICNIC, 난이도: 하)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.algospot.com/judge/problem/read/PICNIC&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739695861095&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;algospot.com ::  
	PICNIC&quot; data-og-description=&quot;소풍 문제 정보 문제 안드로메다 유치원 익스프레스반에서는 다음 주에 율동공원으로 소풍을 갑니다. 원석 선생님은 소풍 때 학생들을 두 명씩 짝을 지어 행동하게 하려고 합니다. 그런데 서로 &quot; data-og-host=&quot;www.algospot.com&quot; data-og-source-url=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot; data-og-url=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.algospot.com/judge/problem/read/PICNIC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;algospot.com :: PICNIC&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;소풍 문제 정보 문제 안드로메다 유치원 익스프레스반에서는 다음 주에 율동공원으로 소풍을 갑니다. 원석 선생님은 소풍 때 학생들을 두 명씩 짝을 지어 행동하게 하려고 합니다. 그런데 서로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.algospot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 풀이: 소풍&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&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;재귀 호출을 이용해 문제를 해결하려면 우선 각 답을 만드는 과정을 여러 개의 조각으로 나누어야 합니다. 여기서는 전체 문제를 n/2 개의 조각으로 나눠서 한 조각마다 두 학생을 짝지어 주는 것으로 하지요. 이때 문제의 형태는 '아직 짝을 찾지 못한 학생들의 명단이 주어질 때 친구끼리 둘씩 짝짓는 경우의 수를 계산하라'가 됩니다. 명단에서 서로 친구인 두 학생을 찾아 이들을 짝지어 주고나면 남은 학생들을 짝지어 주는 문제도 원래 문제와 같은 형태가 되니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;잘못된 재귀 호출 코드&amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739696241027&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int n;
bool areFriends[10][10];
// taken[i] = i번째 학생이 짝을 이미 찾았으면 true, 아니면 false
int countPairings(bool taken[10]) {
  // 기저 사례: 모든 학생이 짝을 찾았으면 한 가지 방법을 찾았으니 종료한다.
  bool finished = true;
  for(int i = 0; i &amp;lt; n; ++i) if(!taken[i]) finished = false;
  if(finished) return 1;
  int ret = 0;
  // 서로 친구인 두 학생을 찾아 짝을 지어 준다.
  for(int i = 0; i &amp;lt; n; ++i) {
     for(int j = 0; j &amp;lt; n; ++j) {
       if(!taken[i] &amp;amp;&amp;amp; !taken[j] &amp;amp;&amp;amp; areFriends[i][j]) {
         taken[i] = taken[j] = true;
         ret += countPairings(taken);
         taken[i] = taken[j] = false;
       }
     }
  }
  return ret;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;결과 출력&amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1739708914306&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2
24
192&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&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;&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;같은 학생 쌍을 두 번 짝지어 줍니다. 예를 들어 (0, 1)과 (1, 0)을 따로 세고 있습니다.&lt;/li&gt;
&lt;li&gt;다른 순서로 학생들을 짝지어 주는 것을 서로 다른 경우로 세고 있습니다. 예를 들어 (0, 1) 후에 (2, 3)을 짝지어 주는 것과 (2, 3) 후에 (0, 1)을 짝지어주는 것은 완전히 같은 방법인데 다른 경우로 세고 있지요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실질적으로 같은 답을 중복으로 세는 이런 상황은 경우의 수를 다룰 때 굉장히 흔하게 마주칩니다. 이 상황을 해결하기 위해 선택할 수 있는 좋은 방법은 항상 특정 형태를 갖는 답만을 세는 것입니다. 흔히 사용하는 방법으로는 같은 답 중에서 사전순으로 가장 먼저 오는 답 하나만을 세는 것이 있습니다. 예를 들어 (2, 3), (0, 1) 이나 (1, 0), (2, 3) 은 세지 않지만 (0, 1), (2, 3)은 세는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 속성을 강제하기 위해서는 각 단계에서 남아 있는 학생들 중 가장 번호가 빠른 학생의 짝을 찾아 주도록 하면 됩니다. 이렇게 하면 앞의 두 가지 문제를 모두 해결할 수 있음을 쉽게 알 수 있습니다. 가장 번호가 빠른 학생의 짝은 그보다 번호가 뒤일 수 밖에 없기 때문에 (1,0)과 같은 짝은 나올 수 없습니다. 또한 항상 번호가 가장 빠른 학생부터 짝을 짓기 때문에 (2, 3), (0, 1)의 순서로 짝을 지어줄 일도 없지요. 다음 코드는 이러한 알고리즘의 구현을 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1739696766648&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int n;
bool areFriends[10][10];
// taken[i] = i번째 학생이 짝을 이미 찾았으면 true, 아니면 false
int countPairings(bool taken[10]) {
  // 남은 학생들 중 가장 번호가 빠른 학생을 찾는다.
  int firstFree = -1;
  for(int i=0;i&amp;lt;n;++i) {
    if(!taken[i]) {
      firstFree = i;
      break;
    }
  }
  
  // 기저 사례: 모든 학생이 짝을 찾았으면 한 가지 방법을 찾았으니 종료한다.
  if(firstFree == -1) return 1;
  int ret = 0;
  // 이 학생과 짝지을 학생을 결정한다.
  for(int pairWith = firstFree + 1; pairWith &amp;lt; n; ++pairWith) {
    if(!taken[pairWith] &amp;amp;&amp;amp; areFriends[firstFree][pairWith]) {
      taken[firstFree] = taken[pairWith] = true;
      ret += countPairings(taken);
      taken[firstFree] = taken[pairWith] = false;
    }
  }
  return ret;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;답의 수의 상한&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 답을 생성해 가며 답의 수를 세는 재귀 호출 알고리즘은 답의 수에 정비례하는 시간이 걸립니다. 따라서 실제로 프로그램을 짜기 전에 답의 수가 얼마나 될지 예측해 보고 모든 답을 만드는 데 시간이 얼마나 오래 걸릴지를 확인해야겠지요. 이 문제에서 가장 많은 답을 가질 수 있는 입력은 열 명의 학생이 모두 친구인 경우입니다. 이때 가장 번호가 빠른 학생이 선택할 수 있는 짝은 아홉명이고, 그 다음 학생이 선택할 수 있는 짝은 일곱 명입니다. 결국 최종 답의 개수는 9 * 7 * 5 * 3 * 1 = 945 가 된다는 것을 알 수 있지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 문제: 게임판 덮기 (문제 ID: BOARDCOVER, 난이도: 하)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.algospot.com/judge/problem/read/BOARDCOVER&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739891361788&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;algospot.com ::  
	BOARDCOVER&quot; data-og-description=&quot;게임판 덮기 문제 정보 문제 H*W 크기의 게임판이 있습니다. 게임판은 검은 칸과 흰 칸으로 구성된 격자 모양을 하고 있는데 이 중 모든 흰 칸을 3칸짜리 L자 모양의 블록으로 덮고 싶습니다. 이 &quot; data-og-host=&quot;www.algospot.com&quot; data-og-source-url=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot; data-og-url=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.algospot.com/judge/problem/read/BOARDCOVER&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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;algospot.com :: BOARDCOVER&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;게임판 덮기 문제 정보 문제 H*W 크기의 게임판이 있습니다. 게임판은 검은 칸과 흰 칸으로 구성된 격자 모양을 하고 있는데 이 중 모든 흰 칸을 3칸짜리 L자 모양의 블록으로 덮고 싶습니다. 이&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.algospot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 풀이: 게임판 덮기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제도 경우의 수를 세는 것이니만큼, 게임판을 덮을 수 있는 모든 경우를 생성하는 완전 탐색을 이용해 해결할 수 있습니다. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;우선 입력으로 주어진 게임판에서 흰 칸의 수가 3의 배수가 아닐 경우에는 무조건 답이 없으니 이 부분을 따로 처리하도록 합시다.&lt;/span&gt; 이 외의 경우에는 흰 칸의 수를 3으로 나눠서 내려놓을 블록의 수 N을 얻은 뒤, 문제의 답을 생성하는 과정을 N조각으로 나눠 한 조각에서 한 블록을 내려놓도록 합시다. 재귀 함수는 주어진 게임판에 블록을 한개 내려놓고 남은 흰 칸들을 재귀 호출을 이용해 덮도록 하면 되겠지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;중복으로 세는 문제&lt;/h4&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;78&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmas0e/btsMmpT84Ys/WfRGPoxMtTL5GePWtmddO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmas0e/btsMmpT84Ys/WfRGPoxMtTL5GePWtmddO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmas0e/btsMmpT84Ys/WfRGPoxMtTL5GePWtmddO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbmas0e%2FbtsMmpT84Ys%2FWfRGPoxMtTL5GePWtmddO0%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;242&quot; height=&quot;78&quot; data-origin-width=&quot;242&quot; data-origin-height=&quot;78&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;h4 data-ke-size=&quot;size20&quot;&gt;답의 수의 상한&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 답은 최대 얼마일까요? 우리의 알고리즘에서는 한 블록을 놓을 때마다 모두 네 가지의 선택지가 있는데, 우리는 최대 [50 / 3] = 16 개의 블록을 놓기 때문에 가능한 답의 상한은 4^16 = 2^32 개가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것만 봐서는 도저히 시간 내에 모두 생성할 수 없을 것 같지만, 실제 입력을 손으로 풀어 보면 각 단계에서 우리가 선택할 수 있는 블록 배치가 크게 제한됨을 알 수 있습니다. 예를 들어 흰 칸이 6칸 있는 입력이 주어진다면, &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;이 이론상으로는 4^2 = 16가지의 방법이 있어야 하는데, 실제로는 잘 해봐야 두 가지 방법으로밖에 배치할 수 없지요. 흰 칸이 48개 있는 세 번째 예제 입력의 답이 1514 밖에 되지 않음을 보더라도 실제 답의 수는 이 상한보다 훨씬 작으리라고 예측할 수 있습니다.&lt;/span&gt; 프로그램을 실제 작성하고 손으로 작성한 여러 입력을 넣어 보면 시간 안에 답을 구할 수 있다는 확신을 얻을 수 있지요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&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;한 칸을 덮는 네 가지 방법을 각각의 코드로 구현하는 것이 아니라 coverType[] 배열에 저장했습니다. 이 배열은 네 가지 방법에서 새로 채워질 칸들의 상대 좌표의 목록을 저장합니다.&lt;/li&gt;
&lt;li&gt;set() 은 delta에 따라 블록을 놓는 역할과 치우는 역할을 같이 할 수 있습니다. 블록을 놓는 것과 치우는 것을 별도로 짤 필요가 없어지죠.&lt;/li&gt;
&lt;li&gt;set()은 해당 위치에 블록을 놓을 수 있는지 여부도 같이 판단합니다. 단 이때 곧장 함수를 종료하는 것이 아니라 마지막까지 함수를 실행한다는 데 유의할 필요가 있습니다. 만약 블록을 구성하는 세 칸 중에 한 칸에 표시를 한 뒤 두 번째 칸에 이미 블록이 놓여 있다는 것을 발견했다고 합니다. 이때 함수를 곧장 종료하면 나중에 덮었던 블록을 치울 때, 두 번째 칸에 이미 있던 블록마저 치워버리게 됩니다. 따라서 set() 은 그 자리에 그냥 1을 더함으로써 이칸에는 두 개의 블록이 겹쳐서 놓여 있다고 표시합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1739892929160&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 주어진 칸을 덮을 수 있는 네 가지 방법
// 블록을 구성하는 세 칸의 상대적 위치 (drow, dcol)의 목록
const int coverType[4][3][2] = {
    { { 0, 0 }, { 1, 0 }, { 0, 1 } },  // (1)
    { { 0, 0 }, { 0, 1 }, { 1, 1 } },  // (2)
    { { 0, 0 }, { 1, 0 }, { 1, 1 } },  // (3)
    { { 0, 0 }, { 1, 0 }, { 1, -1 } }  // (4)
};

// board의 (row, col)를 type번 방법으로 덮거나, 덮었던 블록을 없앤다.
// delta = 1 이면 덮고, -1이면 덮었던 블록을 없앤다.
// 만약 블록이 제대로 덮이지 않은 경우(게임판 밖으로 나가거나, 
// 겹치거나, 검은 칸을 덮을 때) false를 반환한다.
bool set(vector&amp;lt;vector&amp;lt;int&amp;gt; &amp;gt;&amp;amp; board, int row, int col, int type, int delta) {
    bool ok = true;
    for(int i = 0; i &amp;lt; 3; ++i) {
        const int nrow = row + coverType[type][i][0];
        const int ncol = col + coverType[type][i][1];
        if(nrow &amp;lt; 0 || nrow &amp;gt;= board.size() || ncol &amp;lt; 0 || ncol &amp;gt;= board[0].size())
            ok = false;
        else if(board[nrow][ncol] += delta) &amp;gt; 1)
            ok = false;
    }
    return ok;
}

// board의 모든 빈 칸을 덮을 수 있는 방법의 수를 반환한다.
// board[i][j] = 1 이미 덮인 칸 혹은 검은 칸
// board[i][j] = 0 아직 덮이지 않은 칸
int cover(vertor&amp;lt;vector&amp;lt;int&amp;gt; &amp;gt;&amp;amp; board) {
    // 아직 채우지 못한 칸 중 가장 윗줄 왼쪽에 있는 칸을 찾는다.
    int row = -1, col = -1;
    for(int i = 0; i &amp;lt; board.size(); i++) {
        for(int j = 0; j &amp;lt; board[i].size(); j++) {
            if(boardr[i][j] == 0) {
                row = i;
                col = j;
                break;
            }
        }
        if (row != -1) breakl;
    }
    // 기저 사례: 모든 칸을 채웠으면 1을 반환한다.
    if(row == -1) return 1;
    int ret = 0;
    for(int type = 0; type &amp;lt; 4; type++) {
        // 만약 board[row][col]를 type 형태로 덮을 수 있으면 재귀 호출한다.
        if(set(board, row, col, type, 1))
            ret += cover(board);
        // 덮었던 블록을 치운다.
        set(board row, col, type, -1);
    }
    return ret;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7. 최적화 문제&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 다뤘던 문제와는 달리 문제의 답이 하나가 아니라 여러 개이고, 그 중에서 어떤 기준에 따라 가장 '좋은 답'을 찾아 내는 문제들을 통칭해 최적화 문제(Optimization problem)라고 부릅니다. 예를 들어 n개의 원소 중에서 r개를 순서 없이 골라내는 방법의 수를 계산하는 것은 최적화 문제가 아닙니다. 우리가 원하는 답은 딱 하나밖에 없고, 더 좋은 답이나 덜 좋은 답이 없기 때문이죠. 반면 n개의 사과 중에서 r개를 골라서 무게의 합을 최대화하는 문제, 아니면 가장 무거운 사과와 가장 가벼운 사과의 무게 차이를 최소화하는 문제 등은 최적화 문제가 됩니다. 사과를 골라내는 방법은 여러 가지인데, 이 중 특정 기준에의해 가장 좋은 답을 고르는 문제이기 때문입니다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;예제: 여행하는 외판원 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 유명한 최적화 문제 중 하나로 여행하는 외판원 문제(Traveling Salesman Problem, TSP)가 있습니다. 어떤 나라에 n(2&amp;lt;=n&amp;lt;=10) 개의 큰 도시가 있다고 합시다. 한 영업 사원이 한 도시에서 출발해 다른 도시들을 전부 한 번씩 방문한 뒤 시작 도시로 돌아오려고 합니다. 문제를 간단히 하기 위해, 각 도시들은 모두 직선 도로로 연결되어 있다고 합시다. 다음 그림은 한 도로망의 예를 보여줍니다. 이때 영업 사원이 여행해야 할 거리는 어느 순서로 각 도시들을 방문하느냐에 따라 달라집니다. 이때 가능한 모든 경로 중 가장 짧은 경로를 어떻게 찾아낼 수 있을까요?&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;825&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k7AdE/btsMt9bbFNX/jCdKIGBdk4GXfGXKzg8O6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k7AdE/btsMt9bbFNX/jCdKIGBdk4GXfGXKzg8O6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k7AdE/btsMt9bbFNX/jCdKIGBdk4GXfGXKzg8O6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk7AdE%2FbtsMt9bbFNX%2FjCdKIGBdk4GXfGXKzg8O6K%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;825&quot; height=&quot;636&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;636&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;h4 data-ke-size=&quot;size20&quot;&gt;무식하게 풀 수 있을까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완전 탐색으로 문제를 풀기 위한 첫 번째 단계는 시간 안에 답을 구할 수 있을지 확인하는 것입니다. 우리는 시작한 도시로 돌아오는 경로를 찾기 때문에, 경로의 시작점은 신경 쓰지 않고 무조건 0번 도시에서 출발한다고 가정해도 경로의 길이는 다르지 않습니다. 그러면 남은 도시들을 어떤 순서로 방문할지를 정하기만 하면 되지요. n-1 개의 도시를 나열하는 방법은 모두 (n-1)! 가지가 있습니다. 도시가 열 개라면 경로의 수는 9! = 362,880 개가 됩니다. 물론 사람이 찾아보기에는 너무 큰 수입니다. 여전히 컴퓨터는 1초 안에 가볍게 처리할 수 있는 숫자입니다. 따라서 안전하게 완전 탐색을 사용해 문제를 해결할 수 있음을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;재귀 호출을 통한 답안 생성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 모든 답은 재귀 호출을 이용해 간단하게 만들 수 있습니다. n개의 도시로 구성된 경로를 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;shortestPath(path) = 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;다음 코드는 이 아이디어를 직접적으로 구현합니다. 단, 원래 우리가 생각했던 함수 현태와는 달리 각 정점을 방문했는지를 나타내는 불린 값 배열 visited와 현재 경로의 길이 currentLength를 path와 함께 인자로 받고 있는 점을 유의해서 보세요.&lt;/p&gt;
&lt;pre id=&quot;code_1740222714652&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;int n; // 도시의 수
double dist[MAX][MAX]; // 두 도시 간의 거리를 저장하는 배열

// path: 지금까지 만든 경로
// visited: 각 도시의 방문 여부
// currentLength: 지금까지 만든 경로의 길이
// 나머지 도시들을 모두 방문하는 경로들 중 가장 짧은 것의 길이를 반환한다.
double shortestPath(vector&amp;lt;int&amp;gt;&amp;amp; path, vector&amp;lt;bool&amp;gt;&amp;amp; visited, double currentLength) {
    // 기저 사례 : 모든 도시를 다 방문했을 때는 시작 도시로 돌아가고 종료한다.
    if(path.size() == n)
        return currentLength + dist[path[0]][path.back()];
    double ret = INF;  // 매우 큰 값으로 초기화
    // 다음 방문할 도시를 전부 시도해본다.
    for(int next = 0; next &amp;lt; n; next++) {
        if(visited[next]) continue;
        int here = path.back();
        path.push_back(next);
        visited[next] = true;
        // 나머지 경로를 재귀 호출을 통해 완성하고 가장 짧은 경로의 길이를 얻는다.
        double cnad = shortestPath(path, visited, currentLength + dist[here][next]);
        ret = min(ret, cand);
        visited[next] = false;
        path.pop_back();
    }
    return ret;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;8. 문제: 시계 맞추기 (문제 ID: CLOCKSYNC, 난이도: 중)&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.algospot.com/judge/problem/read/CLOCKSYNC&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1740223225468&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;algospot.com ::  
	CLOCKSYNC&quot; data-og-description=&quot;Synchronizing Clocks 문제 정보 문제 그림과 같이 4 x 4 개의 격자 형태로 배치된 16개의 시계가 있다. 이 시계들은 모두 12시, 3시, 6시, 혹은 9시를 가리키고 있다. 이 시계들이 모두 12시를 가리키도록 &quot; data-og-host=&quot;www.algospot.com&quot; data-og-source-url=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot; data-og-url=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dL8vwi/hyYf27c7PB/mxuhVO59p5x9HuxieRZdo0/img.png?width=218&amp;amp;height=221&amp;amp;face=0_0_218_221&quot;&gt;&lt;a href=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.algospot.com/judge/problem/read/CLOCKSYNC&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dL8vwi/hyYf27c7PB/mxuhVO59p5x9HuxieRZdo0/img.png?width=218&amp;amp;height=221&amp;amp;face=0_0_218_221');&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;algospot.com :: CLOCKSYNC&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Synchronizing Clocks 문제 정보 문제 그림과 같이 4 x 4 개의 격자 형태로 배치된 16개의 시계가 있다. 이 시계들은 모두 12시, 3시, 6시, 혹은 9시를 가리키고 있다. 이 시계들이 모두 12시를 가리키도록&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.algospot.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;9. 풀이: 시계 맞추기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 있는 그대로 풀려고 하면 꽤나 복잡해집니다. 그러나 문제의 특성을 이용해 적절히 단순화하면 완전 탐색으로 해결할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;이 문제에서 처음 깨달아야 할 것은 예제 입출력 설명이 유도하는 방향과는 달리 스위치를 누르는 순서는 전혀 중요하지 않다는 것입니다.&lt;/span&gt; 두 스위치를 누르는 순서를 바꾼다고 해서 그 결과가 바뀌지 않기 때문입니다. 따라서 우리가 계산해야 할 것은 각 스위치를 몇 번이나 누를 것이냐 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 문제를 바꾼다고 하더라도 완전 탐색을 곧장 적용할 수는 없습니다. 완전 탐색 알고리즘을 사용하려면 스위치를 누르는 횟수의 모든 조합을 하나하나 열거할 수 있어야 하는데, 각 스위치를 몇 번 누르는지는 상관없고 따라서 그 조합의 수는 무한하기 때문입니다. 시계는 12시간이 지나면 다시 제 자리로 돌아온다는 점을 이용하면 무한한 조합의 수를 유한하게 바꿀 수 있습니다. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;어떤 스위치를 네 번 누르면 연결된 시계는 모두 12시간씩 앞으로 이동하니 하나도 누르지 않은 것과 다름이 없습니다.&lt;/span&gt; 따라서 각 스위치를 누르는 횟수는 0에서 3 사이의 정수입니다. 열 개의 스위치가 있으니 전체 경우의 수는 4^10 = 1,048,576 개가 되지요. 구현에 따라 다르겠지만 일반적으로는 시간 안에 모든 경우를 세어 보기에 무리 없는 크기죠. 따라서 완전 탐색으로 무난하게 풀 수 있습니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;완전 탐색 구현하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 모든 경우를 세어 보는 재귀 호출 프로그램을 보여줍니다. 문제를 모두 열 조각으로 나눈 후 각 조각에서 한 스위치를 누를 횟수를 정하는 식으로 구현되었지요. 재귀 호출의 깊이가 정해져 있기 때문에 사실 이 코드는 10중 for문과 다르지 않지만 이 쪽이 훨씬 구현하기도 편하고 디버깅하기도 쉽습니다. 다음 부분을 유의해서 보세요.&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;만약 답을 구할 수 없을 경우 재귀 함수의 반환 값은 문제의 출력 값인 -1이 아니라 매우 큰 값이 됩니다. solve()는 재귀 호출하면서 가장 작은 출력 값을 찾게 되는데, -1 대신에 매우 큰 값을 반환하기로 약속하면 답이 없는 경우를 따로 확인하지 않아도 되니까요. 마지막에 답을 출력할 때 답이 매우 크다면 -1을 대신 출력해 주면 됩니다.&lt;/li&gt;
&lt;li&gt;어떤 스위치가 어떤 시계에 연결되어 있는지를 2차원 배열을 통해 저장했습니다. 이때 i번째 스위치가 j번째 시계에 연결되어 있는지를 보려면 linked[i][j] 를 보면 됩니다. 이런 형태의 표현은 스위치마다 연결되어 있는 시계의 개수가 다르다는 점에 신경 쓸 필요가 없다는 장점이 있지만, 문제에 적혀있는 표현과 다르기 때문에 눈으로 확인하기가 약간 까다롭다는 단점도 있어요.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1740281307596&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const int INF = 9999, SWITCHES = 10, CLOCKS = 16;
// linked[i][j] = 'x': i번 스위치와 j번 시계가 연결되어 있다.
// linked[i][j] = '.': i번 스위치와 j번 시계가 연결되어 있지 않다.
const char linked[SWITCHES][CLOCKS+1] = {
    // 0123456789012345
    &quot;xxx.............&quot;,
    &quot;...x...x.x.x....&quot;,
    &quot;....x.....x...xx&quot;,
    &quot;x...xxxx........&quot;,
    &quot;......xxx.x.x...&quot;,
    &quot;x.x...........xx&quot;,
    &quot;...x..........xx&quot;,
    &quot;....xx.x......xx&quot;,
    &quot;.xxxxx..........&quot;,
    &quot;...xxx...x...x..&quot;,
}

// 모든 시계가 12시를 가리키고 있는지 확인한다.
bool areAligned(const vector&amp;lt;int&amp;gt;&amp;amp; clocks);
// switch번 스위치를 누른다.
void push(vector&amp;lt;int&amp;gt;&amp;amp; clocks, int switch) {
    for(int clock = 0; clock &amp;lt; CLOCKS; ++clock) {
        if(linked[switch][clock] == 'x') {
            clocks[clock] += 3;
            if(clocks[clock] == 15) clocks[clock] = 3;
        }
    }
}
// clocks: 현재 시계들의 상태
// switch: 이번에 누를 스위치 번호
// 가 주어질 때 남은 스위치들을 눌러서 clocks를 12시로 맞출 수 있는 최소 횟수를 반환한다.
// 만약 불가능하다면 INF 이상의 큰 수를 반환한다.
int solve(vector&amp;lt;int&amp;gt;&amp;amp; clocks, int switch) {
    if(switch == SWITCHES) return areAligned(clocks) ? 0 : INF;
    // 이 스위치를 0번 누르는 경우부터 세 번 누르는 경우까지를 모두 시도한다.
    int ret = INF;
    for(int cnt = 0; cnt &amp;lt; 4; cnt++) {
        ret = min(ret, cnt + solve(clocks, switch + 1));
        push(clocks, switch);
    }
    // push(clocks, switch)가 네 번 호출되었으니 clocks는 원래와 같은 상태가 된다.
    return ret;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;10. 많이 등장하는 완전 탐색 유형&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;모든 순열 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 N개의 원소를 일렬로 줄 세운 것을 순열(permutation)이라고 부릅니다. 주어진 원소의 모든 순열을 생성해서 풀 수 있는 문제는 꽤 자주 만날 수 있습니다. 다른 문제의 부분 문제로 나타나기도 하는 만큼 모든 순열을 생성하는 코드를 한 번 신경써서 작성해 보면 좋습니다. 단, 가능한 순열의 수는 N!이 되는데, N이 10을 넘어간다면 시간 안에 모든 순열을 생성하기 어려우므로 완전 탐색 말고 다른 방법을 생각해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;모든 조합 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 N개의 원소 중에서 R개를 순서 없이 골라낸 것을 조합(combination)이라고 부릅니다. 피자에 올릴 수 있는 다섯 종류의 토핑인 소시지, 쇠고기, 올리브, 피망, 양파 중 세 가지를 고르는 것이 조합의 좋은 예입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2^n가지 경우의 수 만들기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n 개의 질문에 대한 답이 예/아니오 중의 하나라고 할 때 존재할 수 있는 답의 모든 조합의 수는 2^n가지 입니다. 이 모든 조합들을 생성하는 것은 굉장히 흔한 문제인데, 각 조합을 하나의 n비트 정수로 표현한다고 생각하면 재귀 호출을 사용할 것 없이 1차원 for문 하나로 이 조합들을 간단하게 모두 시도할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AlgorithmBook/알고리즘 문제해결 전략 1</category>
      <category>무식하게 풀기</category>
      <category>문제해결전략</category>
      <category>브루트포스</category>
      <category>알고리즘</category>
      <category>알고리즘 문제해결전략</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/149</guid>
      <comments>https://minimalcode.tistory.com/149#entry149comment</comments>
      <pubDate>Fri, 7 Feb 2025 00:35:10 +0900</pubDate>
    </item>
    <item>
      <title>[02 알고리즘 분석] 5. 알고리즘의 정당성 증명</title>
      <link>https://minimalcode.tistory.com/148</link>
      <description>&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 도입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘의 정확한 증명을 위해서는 각종 수학적인 기법이 동원되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전 세계 99%의 프로그래머는 알고리즘을 새로 만들기보다 배워서 써먹는 쪽에 지대한 관심을 가지고 있습니다. 그래서 대개 알고리즘의 증명에 관해서는 일찌감치 잊어버리는 게 어떨까 하는 유혹을 받기가 쉽고요. 하지만, 알고리즘의 증명을 이해하지 않고서는 알고리즘을 제대로 공부했다고 할 수 없습니다.&lt;/p&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;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 수학적 귀납법과 반복문 불변식&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100 개의 도미노가 순서대로 놓여 있는 광경을 상상해 봅시다. 그리고 우리가 다음 두 가지 사실을 안다고 가정합시다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 번째 도미노는 직접 손으로 밀어서 쓰러뜨린다.&lt;/li&gt;
&lt;li&gt;한 도미노가 쓰러지면 다음 도미노는 역시 반드시 쓰러진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 마지막 도미노 또한 당연히 쓰러진다는 것을 직관적으로 알 수 있지요. 수학적 귀납법(mathematical indunction)은 이와 같이 반복적인 구조를 갖는 명제들을 증명하는 데 유용하게 사용되는 증명 기법입니다. 귀납법 증명은 크게 세 단계로 나누어집니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단계 나누기 : 증명하고 싶은 사실을 여러 단계로 나눕니다. 앞의 예에서는 100개의 도미노를 도미노 하나씩으로 나누었죠. 이 과정은 너무 당연해서 잊어버리기 쉽지만 가끔은 중요할 때가 있습니다.&lt;/li&gt;
&lt;li&gt;첫 단계 증명 : 그중 첫 단계에서 증명하고 싶은 내용이 성립함을 보입니다. 앞의 예에서는 첫 번째 도미노가 넘어짐을 증명하는 것이 이 과정이죠&lt;/li&gt;
&lt;li&gt;귀납 증명 : 한 단계에서 증명하고 싶은 내용이 성립한다면, 다음 단계에서도 성립함을 보입니다. 앞의 예에서는 한 도미노가 쓰러지면 다음 도미노는 반드시 쓰러짐을 보이는 것이 이 과정이지요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;실제 귀납법을 이용한 증명의 예로 사다리 게임을 생각해 봅시다. 사다리 게임을 하다 보면 맨 위 선택지와 맨 아래 선택지가 언제나 1:1 대응이 되는 것이 신기할 때가 있습니다. 귀납법을 이용하면 이 사실을 쉽게 증명할 수 있습니다.&lt;/span&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;단계 나누기 : 텅 빈 N개의 세로줄에서부터 시작해서 원하는 사다리가 될 때까지 하나씩 가로 줄을 그어 간다고 합시다. 이때 가로 줄을 하나 긋는 것을 한 단계라고 하지요.&lt;/li&gt;
&lt;li&gt;첫 단계 증명 : 텅 빈 N개의 세로줄에서는 항상 맨 위 선택지와 맨 아래 선택지가 1:1 대응이 됩니다.&lt;/li&gt;
&lt;li&gt;귀납 증명 : 가로줄을 그어서 두 개의 세로줄을 연결했다고 하죠. 이때 두 세로줄의 결과는 서로 뒤바뀝니다. 두 세로줄의 결과가 뒤바뀌어도 1:1 대응은 변하지 않으므로 다음 단계에서도 1:1 속성이 유지되지요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 귀납법에 의해 가로줄만을 사용하는 사다리들은 항상 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;size18&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;귀납법을 이용해 알고리즘의 정당성을 증명할 때는 반복문 불변식(loop in-variant)이라는 개념이 유용하게 쓰입니다. 반복문 불변식이란 반복문의 내용이 한 번 실행될 때마다 중간 결과가 우리가 원하는 답으로 가는 길 위에 잘 있는지를 명시하는 조건입니다. 반복문이 마지막에 정답을 계산하기 위해서는 항상 이 식이 변하지 않고 (그래서 불변식입니다) 성립해야 하는 것이죠.&lt;/p&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;반복문 진입시에 불변식이 성립함을 보인다.&lt;/li&gt;
&lt;li&gt;반복문 내용이&amp;nbsp; 불변식을 깨뜨리지 않음을 보인다. 다르게 말하면, 반복문 내용이 시작할 때 불변식이 성립했다면 내용이 끝날 때도 불변식이 항상 성립함을 보인다.&lt;/li&gt;
&lt;li&gt;반복문 종료시에 불변식이 성립하면 항상 우리가 정답을 구했음을 보인다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 적용 예를 위해 이진 탐색 알고리즘을 보도록 합시다.&lt;/p&gt;
&lt;pre id=&quot;code_1738501558906&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 필수 조건: A는 오름차순으로 정렬되어 있다.
// 결과: A[i - 1] &amp;lt; x &amp;lt;= A[i] 인 i를 반환한다.
// 이때: A[-1] = 음의 무한대, A[n] = 양의 무한대라고 가정한다.
int binsearch(const vector&amp;lt;int&amp;gt;&amp;amp; A, int x) {
    int n = A.size();
    int lo = -1, hi = n;
    // 반복문 불변식 1: lo &amp;lt; hi
    // 반복문 불변식 2: A[lo] &amp;lt; x &amp;lt;= A[hi]
    // (*) 불변식은 여기서 성립해야 한다.
    while(lo + 1 &amp;lt; hi) {
        int mid = (lo + hi) / 2;
        if(A[mid] &amp;lt; x)
            lo = mid;
        else
            hi = mid;
        // (**) 불변식은 여기서도 성립해야 한다.
    }
    return hi;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 이진 탐색의 한 구현을 보여줍니다. 이진 탐색 내부의 while 문은 두 개의 불변식을 유지합니다. 첫 번째는 lo &amp;lt; hi 이고, 두 번째는 A[lo] &amp;lt; x &amp;lt;= A[hi] 이지요.이 불변식이 while 문이 완전히 종료하고 함수의 마지막 줄에 올 때까지 계속 성립했다고 가정해 봅시다. 그러면 다음 두 가지 사실을 알 수 있지요.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;lo + 1 = hi : while 문이 종료했으니 lo + 1 &amp;gt;= hi 인데, 불변식에 의하면 lo &amp;lt; hi 이니 lo + 1 = hi 일 수 밖에 없습니다.&lt;/li&gt;
&lt;li&gt;A[lo] &amp;lt; x &amp;lt;= A[hi] : 애초에 불변식이 성립한다고 가정했으니 이것은 당연히 성립합니다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 원하는 결과값 i는 A[i-1] &amp;lt; x &amp;lt;= A[i] 인 i이므로 이때 우리가 원하는 답은 hi라는 사실을 쉽게 알 수 있지요. 따라서 불변식이 while문 종료시에 항상 성립한다는 것을 보인다면 이 알고리즘의 정당성은 증명한 셈입니다.&lt;/p&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;background-color: #ffff00; color: #333333;&quot;&gt;반복문이 처음 시작될 때 해당 불변식이 만족함을 보이고, 반복문 내용이 한 번 지나가도 이 조건이 다시 유지됨을 보여 주면 됩니다.&lt;/span&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;초기 조건 : while 문이 시작할 때 lo와 hi는 초기값 -1과 n으로 초기화된 상태입니다. 만약 n=0이라면 while문을 아예 건너뛰기 때문에 불변식 1은 항상 성립하지요. 우리는 A[-1] = -&amp;infin;, A[n] = &amp;infin; 이라고 가정하므로 불변식 2 또한 성립합니다.&lt;/li&gt;
&lt;li&gt;유지 조건 : while 문 내부가 불변식을 깨뜨리지 않음을 보이면 됩니다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;불변식 1 : while 문 내부로 들어왔다는 말은 hi와 lo의 차이가 2 이상이라는 의미이므로 mid는 항상 두 값의 사이에 위치하게 됩니다. 따라서 mid를 lo에 대입하건 hi에 대입하건 항상 불변식 1은 계속 유지됩니다.&lt;/li&gt;
&lt;li&gt;불변식 2
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A[mid] &amp;lt; x 인 경우 : 반복문을 시작할 떄 x &amp;lt;= A[hi] 는 이미 알고 있었죠. 따라서 A[mid] &amp;lt; x &amp;lt;= [hi] 이므로, lo에 mid를 대입해도 불변식은 성립합니다.&lt;/li&gt;
&lt;li&gt;x &amp;lt;= A[mid] 인 경우 : 반복문을 시작할 때 알고 있었던 A[lo] &amp;lt; x 과 합쳐 보면 A[lo] &amp;lt; x &amp;lt;= A[mid] 를 얻을 수 있죠. 따라서 hi에 mid를 대입해도 불변식 2는 성립합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 과정을 거쳐 while 문이 종료될 때 우리가 원하는 값이 A[hi]에 저장되어 있음을 알 수 있습니다. 반복문 불변식은 이와 같이 알고리즘의 정당성을 증명하기 위한 좋은 도구입니다. 까다로운 코드를 짤 때 해당 코드가 가져야 할 불변식을 파악하고 작성하면 좀 더 오류가 적은 코드를 작성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 귀류법&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 막 출발한 완행 열차의 차장이 철도청 웹사이트의 오류 때문에 규정 인원보다 많은 승객이 표를 예매했다는 것을 깨달았다고 합시다. 다행히 처음에는 자리가 남아 있었지만 앞으로 지나칠 역에서 승객들이 더 탑승하면 열차가 미어터질 것이 뻔한 상황이지요. 차장은 안전 규정을 어기느니 원성을 듣더라도 일부 승객에게 내려서 다음 열차를 기다리라고 말하고 싶습니다. 각 승객이 어느 역에서 승차해 어느 역에서 하차할지를 모두 알고 있을 때, 열차에서 쫓겨나는 불쌍한 승객의 수를 최소화하기 위해서는 누구를 쫓아내는 것이 좋을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현명한 차장의 선택은 바로 가장 멀리 가는 사람들입니다. 역에 정차할 때마다 승객들 중 멀리 가는 순서대로 안전 규정을 초과하는 인원수만큼을 내쫓는 것이죠. 왜 이 방법이 최선일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 답하기 위한 좋은 방법은 가장 멀리 가는 사람을 내쫓는 대신 다른 사람을 내쫓았다고 가정하고 논리를 전개해 보는 것입니다. 차장이 차내를 둘러보니 다음 역까지 가는 비실비실하게 생긴 청년과 종착역까지 가는 우락부락한 할아버지가 있었습니다. 할아버지에게 겁을 먹은 차장이 청년을 대신 쫓아냈는데, 다행히 결과적으로 할아버지를 쫓아냈을 때보다 쫓겨나는 사람의 수가 더 적었다고 합니다. 이런 일이 있을 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 이런 일은 불가능합니다. 청년을 태웠을 경우와 할아버지를 태웠을 경우를 비교해 봅시다. 청년을 태웠다면 청년이 내린 뒤 그 자리에 사람을 더 태울 수 있을 겁니다. 반면 할아버지를 태웠다면 그 자리는 쭉 할아버지가 앉아 있었겠지요. 경우에 따라서는 청년을 쫓아냈을 때에도 기차에 탈 수 있는 사람의 수가 변하지 않을 수는 있겠지만, 청년을 쫓아내는 것이 이득인 상황은 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이와 같이 우리가 원하는 바와 반대되는 상황을 가정하고 논리를 전개해서 결론이 잘못됐음을 찾아내는 증명 기법을 귀류법이라고 합니다. 귀류법은 대게 어떤 선택이 항상 최선임을 증명하고자 할 때 많이 이용됩니다. 우리가 선택한 답보다 좋은 답이 있다고 가정한 후에, 사실은 그런일이 있을 수 없음을 보이면 우리가 최선의 답을 선택했음을 보일 수 있으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;책장 쌓기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상자 형태로 된 책장을 여러 개 쌓아올리려고 합니다. 무거운 것부터 가벼운 것, 책만 꽂아도 부서질 것 같은 비실비실한 것부터 코끼리가 올라가도 될 것 같은 튼튼한 것까지 다양한 책장들이 있습니다. 각 책장마다 버틸 수 있는 최대 무게 M(i)와 자신의 무게 W(i)가 주어진다고 합시다. 이때 책장을 가장 높이 쌓는다면 몇 개나 쌓을 수 있을까요? 단 above(i)가 i번 책장 위에 쌓인 모든 책장의 집합이라고 할 때, 다음이 성립해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;j &amp;isin; above(i) &amp;sum; W(j) &amp;lt;= M(i)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해 책장 위에 올라간 다른 책장들의 무게의 합이 견딜 수 있는 최대 무게를 초과하면 안 된다는 말이지요.&lt;/p&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;W(i)로 정렬해 가장 무거운 책장일수록 아래에 놓아야 할까요? 아니면 M(i)로 정렬해 가장 튼튼한 책장을 아래에 놓아야 할까요? 정답은 견딜 수 있는 최대 무게와 자신의 무게의 합 다시 말해 M(i) + W(i) 가 큰 것부터 아래에 놓아야 한다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 증명하기 위해 귀류법을 써 봅시다. 귀류법을 쓰기 위해서는 우리가 증명하려는 사실의 반대를 가정해야 하죠. 어떤 입력의 최적해를 구했는데 M(i) + W(i) 가 더 큰 책장 A가 더 작은 책장 B 위에 올라간 형태라고 가정해 봅시다. 합이 큰 것부터 아래에 놓아야 하는데 A가 B 위에 올라가 있으니 이것은 우리가 원하는 바와 반대지요. 이때 A와 B의 위치를 항상 바꿀 수 있음을 증명해봅시다. 그러면 M(i) + W(i) 가 큰 것이 밑에 가도록 책장을 쌓아도 최선의 답을 얻을 수 있다는 증거가 되지요. B는 윗칸으로 올라가니 견뎌야 할 무게가 더 줄어들고, 반드시 위에 올릴 수 있습니다. 문제는 기존에 위에 있던 상자들에 더해 B의 무게까지를 A가 견딜 수 있느냐는 것입니다. 그러니 부등식 M(A) + W(A) &amp;gt; M(B) + W(B) 에서 M(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;M(A) &amp;gt; M(B) + W(B) - W(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;A 위에 올라가 있는 상자들의 무게의 합을 X 라고 합시다. 가장 좋은 답에서 A가 B 위에 올라갔으니 M(B) &amp;gt;= W(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;M(A) &amp;gt; M(B) + W(B) - W(A) &amp;gt;= (W(A) + X) + W(B) - W(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;맨 오른쪽의 W(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;M(A) &amp;gt; X + W(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;이 말은 A도 B와 나머지 모든 상자들을 지탱할 수 있다는 말이지요. 따라서 우리가 원하는 순서대로 쌓았을 때 가장 높은 탑을 얻지 못하는 경우는 존재하지 않는다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;최적해는 책장 A가 책장 B 위에 올라가 있는 상황이었는데, 도출한 식을 통해 책장 A와 책장 B의 위치를 바꾸는 것도 성립한다는 것을 보임. 그러면 책장 B가 책장 A 위에 올라가 있는 상황도 최적해가 된다는 말인데 이것은 초기 전제와 모순됨. 즉, 어떤 입력의 최적해를 구했을 때 M(i) + W(i) 가 더 큰 책장 A가 더 작은 책장 B 위에 올라간 형태라는 전제는 거짓이 되어 M(i) + W(i) 가 더 큰 책장 A 가 더 작은 책장 B 밑에 내려간 형태가 최적해라는 가정이 참이 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 다른 기술들&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&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;이처럼 얼핏 보면 대답하기 힘든 질문을 대답하는 데 유용하게 쓰이는 것이 바로 비둘기집의 원리입니다. 비둘기집의 원리를 한마디로 설명하면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10마리의 비둘기가 9개의 비둘기집에 모두 들어갔다면, 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;너무 당연하게 느껴지시나요? 똑같은 논리를 아까 머리털 문제에도 적용해 봅시다. 서울의 인구는 천만 명이 약간 넘는다고 합니다. 사람의 머리에는 평균적으로 10만 가닥의 머리털이 있다고 하는데, 넉넉하게 잡아서 머리털이 제일 많은 사람이 100만 가닥 있다고 하겠습니다. 천만 마리의 비둘기가 백만 개의 비둘기집에 들어갔다면, 반드시 같은 비둘기집에 들어간 비둘기가 두 마리 이상 있기 마련이죠. 따라서 머리털 개수가 정확히 같은 두 사람은 반드시 존재합니다. 이와 같이 비둘기집의 원리는 별것 아닌 것 같지만 이곳저곳에서 유용하게 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&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;예를 들어 하늘을 나는 교통 수단을 만들 수 있다는 주장을 증명하려 한다고 합시다. 비구성적 증명은 양력의 법칙에서부터 시작해 지구의 공기 밀도, 사용할 수 있는 재료들의 강도와 탄성들을 하나하나 열거해 가며 이러한 가정 하에서 교통 수단이 하늘을 날 수 있음을 보이려 할 것입니다. 반대로 구성적 증명이 하는 일은 비행기를 만들어서 보여 주거나, 비행기 만드는 법이 적힌 설명서를 건네 주는 것입니다. '답이 존재하는가'에 대한 대답으로 '이렇게 만들면 된다'라고 하는 것이 구성적 증명이기 때문에, 구성적 증명의 내용은 사실상 알고리즘인 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;안정적 결혼 문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n명의 남성과 여성이 단체 미팅에서 만났습니다. 여러 게임을 진행하는 동안 모든 사람은 자신이 원하는 상대방의 우선순위를 맘 속에 정했고, 이제 시간이 되어 남자 1호와 여자 1호가, 남자 2호와 여자 2호가 각각 짝이 되었습니다. 그런데 남자 1호와 여자 2호는 자신들의 짝(여자 1호, 남자2호) 보다, 서로를 더 선호한다는 사실을 알게 되었습니다. 이런 일이 일어나지 않도록 짝을 지어줄 수 있는 방법이 항상 있을까요? 아니면 불가능한 경우가 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히들 안정적 결혼 문제라고 부르는 이 문제는 구성적 증명으로 해결되는 대표적 문제입니다. 이 문제를 푼 사람들은 답의 존재성을 보이는 대신 답을 만드는 알고리즘을 제시함으로써 답이 존재함을 보였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 알고리즘은 다음과 같이 진행됩니다.&lt;/p&gt;
&lt;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;decimal&quot;&gt;
&lt;li&gt;처음에는 여성들이 모두 자신이 가장 선호하는 남성의 앞에 가서 프로포즈를 합니다. 남성이 그중 제일 마음에 드는 여성을 고르면 나머지는 퇴짜를 맞고 제 자리로 돌아갑니다.&lt;/li&gt;
&lt;li&gt;퇴짜를 맞은 여성들은 (상대에게 짝이 있는지 없는지는 관계없이) 다음으로 마음에 드는 남성에게 다가가 프로포즈합니다. 남성들은 현재 짝이 없다면 찾아온 여성을 선택하고, 짝이 있는데 더 맘에 드는 여성이 다가왔다면 지금의 파트너에게 퇴짜를 놓고 새 여성에게 넘어갑니다.&lt;/li&gt;
&lt;li&gt;더 프로포즈할 여성이 없을 때까지 2번 항목을 반복합니다.&lt;/li&gt;
&lt;/ol&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 여성은 퇴짜 맞을 때마다 지금까지 프로포즈했던 남성들보다 우선순위가 낮은 남성에게 프로포즈합니다. 따라서 각 여성이 최대 n명의 남성들에게 순서대로 프로포즈한 이후엔 더이상 프로포즈할 남성이 없으므로, 이 과정은 언젠가 반드시 종료합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 사람들이 짝을 찾는지 증명
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로포즈를 받은 남성은 그중 한 사람을 반드시 선택하고, 더 우선순위가 높은 여성이 프로포즈해야만 짝을 바꾸므로 한 번이라도 프로포즈를 받은 남성은 항상 짝이 있기 마련이죠. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;귀류법을 적용하여 남녀 한 사람씩 짝을 찾지 못하고 남았다고 가정합시다.&lt;/span&gt; 그런데 여성은 우선순위가 높은 순서대로 모두에게 한 번씩 프로포즈하기 때문에 이 남성에게도 한 번은 프로포즈했겠죠. 이 남성은 프로포즈를 받아들였어야 하고, 따라서 짝을 찾지 못한 사람은 있을 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;짝들의 안정성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;역시 귀류법으로 증명합니다. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;이 과정의 결과로 짝을 지었는데 짝이 아닌 두 남녀가 서로 자신의 짝보다 상대방을 더 선호한다고 가정합시다.&lt;/span&gt; 여성은 지금 자신의 짝 이전에 그 남성에게 반드시 프로포즈했어야 합니다. 그런데도 이 남성이 이 여성과 짝지어지지 않았다는 말은 더 맘에 드는 여성에게 프로포즈를 받아서 수락했다는 뜻이죠. 남성은 더 맘에 드는 여성이 나타났을 때만 짝을 바꾸므로, 프로포즈 받았던 여성보다 맘에 들지 않는 여성과 최종적으로 짝이 되는 일은 없습니다. 따라서 우리가 가정했던 상황은 존재할 수 없지요.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AlgorithmBook/알고리즘 문제해결 전략 1</category>
      <category>문제해결전략</category>
      <category>알고리즘</category>
      <category>알고리즘 문제해결전략</category>
      <category>정당성</category>
      <category>증명</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/148</guid>
      <comments>https://minimalcode.tistory.com/148#entry148comment</comments>
      <pubDate>Sun, 2 Feb 2025 22:17:00 +0900</pubDate>
    </item>
    <item>
      <title>[02 알고리즘 분석] 4. 알고리즘의 시간 복잡도 분석</title>
      <link>https://minimalcode.tistory.com/147</link>
      <description>&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 도입&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;반복문이 지배한다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기 자동차 두 개가 있습니다. 서울에서 부산까지 서둘러 가야 하는데, 어느 쪽을 타고 가야 더 빨리 도착할 수 있을지를 알고 싶습니다. 다음은 두 자동차에 대한 정보입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;자동차 A&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;자동차 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;시동 거는 데 걸리는 시간&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;3분&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;5초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;문 열었다 닫는 데 걸리는 시간&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;1분&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;0초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;운전석 등받이 조절에 걸리는 시간&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;5분&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;0초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;앞 유리 닦는 데 걸리는 시간&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;4분&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;0초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;최대 시속&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;200km/h&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;40km/h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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는 자전거입니다. 문이 없으니 문을 열었다 닫을 필요도 없고, 등받이가 없으니 등받이를 조절할 필요도 없지요. 하지만 그렇다고 해서 서울에서 부산까지 자전거를 타는 사람은 아마 별로 없을 겁니다. 자동차에서 더 오래 걸리는 항목들은 출발할 떄 한 번만 적용되는 반면, 최대 시속은 서울에서 부산까지 달리는 내내 적용되기 때문입니다. 서울에서 부산까지 달리는 데 30분쯤 걸린다면 시동 거는 데 걸리는 시간이 중요할 수 있지만, 그렇지 않기 때문에 앞 유리 닦는데 4분이 걸리건 5분이 걸리건 상관 없는 것입니다. 이렇게 한 가지 항목이 전체의 대소를 좌지우지하는 것을 지배한다(dominate)고 표현합니다.&lt;/p&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;주어진 배열에서 가장 많이 등장하는 수를 찾는 코드가 있다고 해봅시다. 이 알고리즘의 수행 시간은 배열의 크기 N에 따라 변합니다. N번 수행되는 반복문이 두 개 겹쳐져 있으므로 반복문의 가장 안쪽은 항상 N&lt;sup&gt;2&lt;/sup&gt; 번 실행되지요. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;따라서 이 알고리즘의 수행시간은 N&lt;sup&gt;2&lt;/sup&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;그런데 입력으로 주어지는 숫자들이 사실 100점 만점으로 주어지는 중간 고사 점수였다고 합시다. 이처럼 숫자의 법위가 작다면 배열을 이용해 각 숫자가 등장하는 횟수를 쉽게 셀 수 있습니다. 그리고 마지막에 빈도수 배열을 순회하면서 최대치의 위치를 찾으면 되지요. 이 구현에는 반복문이 두 개 있습니다. 하나는 N번 수행되고, 다른 하나는 100번 수행되므로 전체 반복문의 수행 횟수는 N+100이 됩니다. 그런데 N이 커지면 커질수록 사실 후자의 반복문은 수행 시간에서 차지하는 비중이 줄어들게 됩니다. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;따라서 궁극적으로는 이 알고리즘의 수행 시간은 N이라고 씁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 선형 시간 알고리즘&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다이어트 현황 파악: 이동 평균 계산하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이동 평균(moving average)은 주식의 가격, 연간 국내 총생산(GDP), 여자친구의 몸무게 등 시간에 따라 변화하는 값들을 관찰할 때 유용하게 사용할 수 있는 통계적 기준입니다. 시간에 따라 관찰된 숫자들이 주어질 때 M-이동 평균은 마지막 M개의 관찰 값의 평균으로 정의됩니다. 따라서 새 관찰 값이 나오면 M-이동 평균은 새 관찰 값을 포함하도록 바뀝니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;N개의 측정치가 주어질 때 매달 M달 건의 이동 평균을 계산하는 프로그램을 짜면 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738222290033&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vector&amp;lt;double&amp;gt; movingAverage1(const vector&amp;lt;double&amp;gt;&amp;amp; A, int M) {
    vector&amp;lt;double&amp;gt; ret;
    int N = A.size();
    for(int i = M-1; i &amp;lt; N; ++i) {
        double partialSum = 0
        for(int j = 0; j &amp;lt; M; ++j) {
            partialSum += A[i-j];
        }
        ret.push_back(partialSum / M);
    }
    return ret;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드의 수행시간은 for문에 지배됩니다. j를 사용하는 반복문은 항상 M번 실행되고 i를 사용하는 반복문은 N - M + 1번 실행되니, 전체 반복문은 M x (N - M + 1) = N*M - M^2 + M번 반복됩니다. N = 12, M = 3 이면 반복 횟수는 30번이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 위 프로그램을 좀 더 빠르게 만들 수는 없을까요? 여기에서 중요한 아이디어는 중복된 계산을 없애는 것입니다. 측정치가 M개는 되어야 이동평균을 계산할수 있으니 태어난 이후 M-1일부터 이동 평균을 계산할 수 있습니다. 이때 M-1일의 이동 평균과 M일의 이동 평균에 포함되는 숫자들을 보면 0일과 M일의 몸무게를 제외하면 전부 겹친다는 것을 알 수 있습니다. 그러면 측정한 몸무게의 합을 일일이 구할 필요 없이 M-1일에 구했던 몸무게의 합에서 0일째에 측정한 몸무게를 빼고 M일째에 측정한 몸무게를 더하면 어떨까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 아이디어를 구현하면 다음과 같은 코드가 됩니다. 하나로 묶여있던 두 개의 반복문이 분리되었습니다. 따라서 이 프로그램의 반복문 수행 횟수는 M - 1 + (N - M + 1) = N 이 됩니다. 이제 N값이 커져도 두렵지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738222845069&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;vector&amp;lt;double&amp;gt; movingAverage2(const vector&amp;lt;double&amp;gt;&amp;amp; A, int M) {
  vector&amp;lt;double&amp;gt; ret;
  int N = A.size();
  double partialSum = 0;
  for(int i=0;i&amp;lt;M-1;++i) {
      partialSum += A[i];
  }
  for(int i=M-1;i&amp;lt;N;++i) {
      partialSum += A[i];
      ret.push_back(partialSum / M);
      partialSum -= A[i-M+1];
  }
  return ret;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 프로그램의 수행시간은 N에 정비례합니다. N이 두 배 커지면 실행도 두 배 오래걸리고, 반으로 줄어들면 수행 시간도 반으로 줄어듭니다. 입력의 크기에 대비해 걸리는 시간을 그래프로 그려 보면 정확히 직선이 되지요. 때문에 이런 알고리즘을 선형 시간(linear time) 알고리즘이라고 부릅니다. 선형 시간에 실행되는 알고리즘은 대개 우리가 찾을 수 있는 알고리즘 중 가장 좋은 알고리즘인 경우가 많습니다. 주어진 입력을 최소한 한 번씩 쳐다보기라도 하려면 선형시간이 걸릴 수 밖에 없으니까요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;3. 선형 이하 시간 알고리즘&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 문제건 입력된 자료를 모두 한 번 훑어보는 데에는 입력의 크기에 비례하는 시간, 즉 선형 시간이 걸립니다. 그럼 선형 시간보다 빠르게 동작하는 알고리즘들은 입력된 자료를 다 보지도 않는단 말인데, 과연 이런 알고리즘이 어떻게 존재할 수 있을까요? &lt;span style=&quot;background-color: #ffff00; color: #333333;&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;구체적인 예를 들어보겠습니다. 발라드 그룹 멤버 A군의 코 성형 전 고등학교 사진이 공개되었다고 합시다. 남들 몰래 A군의 팬인 저는 A군이 대체 언제 성형을 했는지 알고 싶어서 검색엔진으로 A군의 사진 십만장을 찾아서 이들을 촬영 날짜 순으로 정렬했습니다. 그럼 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;가장 좋은 방법은 남은 사진들을 항상 절반으로 나눠서 가운데 있는 사진을 보는 것입니다. 우선 가운데 있는 5만번째 사진을 확인해봅니다. 만약 이 사진에서는 코를 성형하지 않은 상태라면 이 전의 사진은 볼 필요가 없습니다. 따라서 우리가 확인해야 할 사진은 뒤의 5만장으로 줄어들었습니다. 그중 가운데 있는 7만 5천번째 사진을 확인하면 이제 확인해야 할 장수는 2만 5천장으로 줄어듭니다. 확인할 때마다 남은 장수가 대략 절반으로 줄어든다고 하면 전부 몇장의 사진을 확인해야 할까요?&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style4&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;확인한 사진의 수&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;...&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;12&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;13&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;14&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;15&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;남은 사진&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;100,000&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;50,000&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;25,000&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;...&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;24&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;12&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 10%;&quot;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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을 계속 절반으로 나눠서 1 이하가 될 때까지 몇 번이나 나눠야 하는지로 알 수 있는데 이것을 나타내는 함수가 바로 로그입니다. 매번 절반씩 나누니 밑이 2인 로그가 됩니다. 이를 줄여서 lg로 쓰겠습니다. 따라서 확인해야 하는 사진의 장수는 대략 lgN이라고 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lg는 굉장히 느리게 증가하는 함수입니다. 만약 지구에서 달까지 닿도록 A군의 사진을 1.5조 장을 쌓아 놓더라도 이 중에서 원하는 성형전 사진을 마흔장만 보고 찾아낼 수 있지요. 이와 같이 입력의 크기가 커지는 것보다 수행시간이 느리게 증가하는 알고리즘들을 선형 이하 시간(sublinear time) 알고리즘이라고 부릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;4. 지수 시간 알고리즘&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;다항 시간 알고리즘&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수 N과 N^2, 그 외 N의 거듭제곱들의 선형 결합으로 이루어진 식들을 다항식이라고 부릅니다. 반복문의 수행 횟수를 입력 크기의 다향식으로 표현할 수 있는 알고리즘들을 다항 시간 알고리즘이라고 부릅니다. N이나 N^2도 다항시간이지만, N^6도, N^42도 심지어는 N^100 도 다항시간입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;지수 시간 알고리즘&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2^N 과 같은 지수 함수는 알고리즘 전체 수행 시간에 엄청난 영향을 미칩니다. N개의 선택에 대해서 예, 아니오 두 선택지가 있을 때 가능한 모든 경우의 수는 2^N 이 됩니다. 이와 같이 N이 하나 증가할 때마다 걸리는 시간이 배로 증가하는 알고리즘들은 지수 시간(exponential time)에 동작한다고 말합니다. 지수 시간은 가장 큰 수행 시간 중 하나로, 입력의 크기게 따라 다항 시간과는 비교도 안되게 빠르게 증가합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 알고리즘이 이렇게 오래 걸리는 이유는 어디까지나 이 알고리즘이 무식하기 때문이겠죠? 이것보다 훨씬 빠른 알고리즘이 있겠죠? 아닙니다. 지수시간보다 빠른 알고리즘을 찾지 못한 문제들이 전산학에는 쌓이고 쌓였습니다. 이처럼 아직 지수 시간보다 빠른 알고리즘을 찾아내지 못한 문제들이 아주 많기 때문에 다항 시간과 지수 시간 사이의 경계는 현재의 전산학에서 효율적으로 해결할 수 있는 문제와 효율적으로 해결하는 방법을 아직 찾아내지 못한 문제의 경계 역할을 하고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;5. 시간 복잡도&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 복잡도는 가장 널리 사용되는 알고리즘의 수행시간 기준으로, 알고리즘이 실행되는 동안 수행하는 기본적인 연산의 수를 입력의 크기에 대한 함수로 표현한 것입니다. 기본적인 연산이란 더 작게 쪼갤 수 없는 최소 크기의 연산이라고 생각하면 됩니다.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;점근적 시간 표기: O 표기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 가장 깊이 중첩된 반복문만을 고려했던 것처럼 전체 수행 시간에 큰 영향을 미치지 않는 상수 부분은 무시하고 반복문의 반복 수만 고려하게 됩니다. 여기에서 한 반짝 더 나아가서 많은 사람들은 이것을 더욱 간단하게 표현한 대문자 O 표기법(Big-O Notation)이라는 것을 사용해 알고리즘의 수행 시간을 표현합니다. O 표기법은 간단하게 말해 주어진 함수에서 가장 빨리 증가하는 항만을 남긴 채 나머지를 다 버리는 표기법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 어떤 알고리즘 수행 시간이 5/3N^2 - NlgN/2 + 16N - 7 이라고 합시다. 여기에는 네 개의 항이 있는데 N이 증가할 때 가장 빨리 증가하는 항은 5/3N^2 이고, 여기에서 상수를 떼어내면 N^2이 되지요. 그러면 우리는 이 알고리즘의 수행 시간을 O(N^2)이라고 씁니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘의 입력의 크기가 두 개 이상의 변수로 표현될 때는 그중 가장 빨리 증가하는 항들만을 떼 놓고 나머지를 버립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 N^2M +NlgM + NM^2 의 경우는 N^2M과 NM^2 중 어느 한쪽이 빠르게 증가한다고 할 수 없기 때문에 둘 다 O 표기에 포함됩니다. O(N^2M + NM^2) 가 됩니다.&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;O 표기법이 수행 시간의 상한을 나타낸다는 사실을 통해 알고리즘의 최악의 수행 시간을 알아냈다고 착각하는 일이 흔히 있습니다. 하지만 O 표기법은 각 경우의 수행 시간을 간단히 나타내는 표기법일 뿐, 특별히 최악의 수행 시간과 관련이 있는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;6. 수행 시간 어림짐작하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그래밍 대회 참가자들이 사용하는 주먹구구 법칙은 다음과 같습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;입력의 크기를 시간 복잡도에 대입해서 얻은 반복문 수행 횟수에 대해, 1초당 반복문 수행 횟수가 1억을 넘어가면 시간 제한을 초과할 가능성이 있다.&lt;/blockquote&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이 10000인 경우 시간 안에 실행할 수 있을지 판단해 봅시다. O(N^3) 알고리즘과 O(NlgN) 알고리즘은 쉽게 판단할 수 있습니다. N^3에 10000을 대입하면 1억을 훨씬 초과하고, NlgN에 10000을 대입하면 1억에 훨씬 미치지 못하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 주먹구구 법칙은 주먹구구일 뿐입니다. 절대로 맹신해서는 안됩니다. 이 기준보다 느리지만 시간 안에 수행되는 프로그램이 얼마든지 있을 수 있고, 가끔은 이 기준보다 빠르지만 시간 안에 수행되지 않는 프로그램도 있기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;7. 계산 복잡도 클래스: P, NP, NP-완비&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 복잡도는 알고리즘의 특성이지 문제의 특성이 아닙니다. 한 문제를 푸는 두 가지 이상의 알고리즘이 있을 수 있고, 이들의 시간 복잡도는 각각 다를 수 있기 때문입니다. 이때 각 문제에 대해 이를 해결하는 얼마나 빠른 알고리즘이 존재하는지를 기준으로 문제들을 분류하고 각 분류의 특성을 연구하는 학문이 있습니다. 이론 컴퓨터 과학의 중요한 분야인 계산 복잡도 이론이 그것이지요. 몇몇 중요한 개념을 짚고 넘어가 봅시다.&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제의 특성 공부하기&lt;/h4&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;정렬 문제 : 주어진 N개의 정수를 정렬한 결과는 무엇인가?&lt;/li&gt;
&lt;li&gt;부분 집합의 합 문제 : N개의 수가 있을 때 이 중 몇 개를 골라내서 그들의 합이 S가 되도록 할 수 있는가?&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;계산 복잡도 이론에서는 이렇게 다항 시간 알고리즘이 존재하는 문제들의 집합을 P 문제라고 합니다. 예를 들어 정렬 문제에는 다항 시간 알고리즘이 수없이 많이 존재하므로, 정렬 문제는 P문제입니다. P 문제처럼 같은 성질을 갖는 문제들을 모아놓은 집합을 계산 복잡도 클래스(complexity class)라고 부릅니다. 엄청나게 다양하고 많은 복잡도 클래스가 있지만, 그중 두 가지의 클래스가 가장 중요합니다. 바로 P 문제와 NP 문제입니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;난이도의 함정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P가 다항 시간에 풀 수 있는 문제면, 그 짝인 NP는 당연히 풀 수 없는 문제여야 하는데 왜 이렇게 비직관적으로 만들었는가 항의하고 싶을 겁니다. 이때 주목할 부분은 어떤 문제를 다항 시간에 풀 수 있음을 증명하기란 쉽지만, 풀 수 없음을 보이기란 어렵다는 점입니다. 흔히 말하는 UFO의 존재 논쟁과 비슷합니다. UFO가 존재함을 증명하려면 UFO 하나만 가져오면 되지만, 존재하지 않음을 증명하려면 전 우주를 다 뒤져야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 다항 시간 알고리즘이 존재하는 문제와 존재하지 않는 문제로 문제들을 구분하기란 어렵습니다. 계산 복잡도에서 흔히 말하는 '어려운 문제'들은 다음과 같이 정의됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정말 어려운 문제를 잘 골라서 이것을 어려운 문제의 기준으로 삼습니다.&lt;/li&gt;
&lt;li&gt;그리고 앞으로는 기준 문제만큼 어렵거나 그보다 어려운 문제들, 다시 말해 '기준 이상으로 어려운 문제들'만을 어렵다고 부릅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산 복잡도 이론에서는 두 문제 사이의 계산 난이도 비교를 위해 환산(reduction)이라는 기법을 이용합니다. 환산이란 한 문제를 다른 문제로 바꿔서 푸는 기법입니다. B 문제의 입력을 적절히 변형해 A 문제의 입력으로 바꾸는 환산 알고리즘이 존재한다고 합시다. &lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt;이때 A를 푸는 가장 빠른 알고리즘을 가져오면, 이것을 환산 알고리즘과 결합해 B를 푸는 알고리즘을 만들 수 있습니다.&lt;/span&gt; 환산 알고리즘이 무시할 수 있을 정도로 빠르다고 가정하면 결합된 알고리즘은 A를 푸는 가장 빠른 알고리즘과 같은 시간이 걸릴 겂니다. B를 푸는 가장 빠른 알고리즘은 앞에서 결합된 알고리즘과 같거나 더 빠를 테니, (B 문제를 푸는 가장 빠른 알고리즘은 최소한 'A + 환산 알고리즘' 과 같은 시간이 걸리고, 아직 밝혀지지는 않았지만 더 빠르게 B문제를 푸는 알고리즘이 존재할 수 있음) 결국 B를 푸는 가장 빠른 알고리즘은 A를 푸는 가장 빠른 알고리즘과 같거나 더 빠를 수 밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A ≒ (A + 환산 알고리즘) =&amp;nbsp;B&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;앞에서 정의한 대로, 이 경우 A가 B 이상으로 어렵다는 것을 알게 되지요. 간단한 예로 주어진 배열을 비교 정렬하는 문제와 최소치를 찾는 문제의 난이도를 비교해 봅시다.&lt;span style=&quot;background-color: #ffff00; color: #333333;&quot;&gt; 주어진 배열을 다항 시간에 정렬하고 첫 번째 값을 취하면 최소치를 쉽게 얻을 수 있습니다. 따라서 최소치를 구하는 데 걸리는 시간이 정렬보다 오래 걸릴 수는 없지요.&lt;/span&gt; 그래서 정렬 문제는 최소치 문제 이상으로 어렵다고 말할 수 있습니다.&lt;/p&gt;
&lt;h4 style=&quot;text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;NP 문제, NP 난해 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 문제들의 난이도를 비교할 수 있으니 어려운 문제의 기준을 정해봅니다. 이 기준을 정하기 또한 쉽지 않은데요. 이 때 어려운 문제의 기준이 되는 것이 바로 SAT 문제(satisfiability problem) 입니다. SAT 문제란 N개의 불린 값 변수로 구성된 논리식을 참으로 만드는 변수값들의 조합을 찾는 문제입니다. 예를 들어 불린 값 변수 a, b, c로 구성된 다음 논리식은 세 변수의 값이 특정 조합을 이루어야 만족됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738233031747&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;((a||b||!c)&amp;amp;&amp;amp;(!c||!a)&amp;amp;&amp;amp;((!a&amp;amp;&amp;amp;b)||(b&amp;amp;&amp;amp;!c)))&amp;amp;&amp;amp;(!b||(!a&amp;amp;&amp;amp;!c))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제가 대체 뭐길래 어려운 문제의 기준으로 삼는 걸까요? SAT 문제에는 아주 중요한 의미가 있습니다. 바로 SAT 문제는 모든 NP 문제 이상으로 어렵다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NP 문제란 답이 주어졌을 때 이것이 정답인지를 다항 시간 내에 확인할 수 있는 문제를 말합니다. 예를 들어 부분 집합 합 문제는 NP 문제입니다. 부분 집합 합 문제의 답이 주어졌을 때 이것이 원래 집합의 부분 집합인지, 그리고 원소들의 합이 S인지 다항 시간에 쉽게 확인할 수 있기 때문입니다. 또한 모든 P 문제들은 모두 NP 문제에도 포함됩니다. NP 문제들은 중요하고, 유명하고, 우리가 관심있는 수많은 문제들을 엄청나게 많이 포함합니다. SAT가 모든 NP 문제 이상으로 어렵다는 말은 SAT를 다항 시간에 풀 수 있으면 NP 문제들을 전부 다항 시간에 풀 수 있다는 얘기입니다. 이런 속성을 갖는 문제들의 집합을 NP-난해(NP-Hard) 문제라고 부릅니다. NP-난해 문제들은 그 이름처럼 아주 어려워서, 아직 아무도 NP-난해 문제를 다항 시간에 푸는 방법을 발견하지 못했습니다. 계산 복잡도 이론에서 어렵다고 부르려면 이 정도쯤은 되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NP-난해 문제이면서 NP인 문제들을 NP-완비(NP-Complete) 문제라고 합니다. NP-완비 문제는 생각보다 자주 만날 수 있습니다. 부분 집합 합 문제도 NP-완비 문제 중 하나죠.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;P=NP?&lt;/h4&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;밀레니엄 7대 수학 난제로 유명해진 P=NP 문제는 P와 NP가 같은지를 확인하는 문제입니다. NP-난해 문제 중 하나를 다항 시간에 풀 수 있다면, 이 알고리즘을 이용해 NP에 속한 모든 문제를 다항 시간에 풀 수 있습니다. 이 경우 NP에 속한 모든 문제를 다항 시간에 해결할 수 있으므로 P=NP 임을 알 수 있겠지요. 그 반대로 NP 문제 중 하나를 골라 P에 포함되어 있지 않음을, 다시 말해 다항 시간에 푸는 방법이 없음을 증명하면 P != NP 임을 보일 수 있습니다. 이 문제가 아직까지 미해결이라는 말은 둘 중 하나에 성공한 사람이 아직 아무도 없다는 이야기입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;P와 NP 문제에 대해서는 아래 영상을 통해서 자세하게 확인할 수 있다.&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/nxbufH4JnpA?si=-eelaNoSp9pu3MP3&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/nxbufH4JnpA?si=-eelaNoSp9pu3MP3&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=nxbufH4JnpA&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/BeWAg/hyX7SI8sp2/csQIoJkJCn9Mnpn8I0j3m0/img.jpg?width=480&amp;amp;height=360&amp;amp;face=226_111_271_160,https://scrap.kakaocdn.net/dn/heoI4/hyX7SPOJ2g/KRsvs3Ks6jkrYDpQiezkR1/img.jpg?width=480&amp;amp;height=360&amp;amp;face=226_111_271_160&quot; data-video-width=&quot;480&quot; data-video-height=&quot;360&quot; data-video-origin-width=&quot;480&quot; data-video-origin-height=&quot;360&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;[난제] PNP 문제&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/nxbufH4JnpA&quot; width=&quot;480&quot; height=&quot;360&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;P 문제란?&lt;br /&gt;결정론적 튜링 기계를 사용해 다항시간 내에 답을 구할 수 있는 문제&lt;br /&gt;대표적인 예로는 거품정렬이 있는데, n개의 정수 입력값이 있고, 두 수를 비교하는 데 1초가 걸린다고 할 때 계산에 드는 총 시간은 ((n-1) * n ) / 2 만큼 걸린다. 이렇게 n에 대한 2차식으로 즉, 다항시간으로 표현이 가능하므로 거품정렬은 P문제라고 할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;NP 문제란?&lt;br /&gt;비결정론적 튜링 기계(여러 개의 선택지)를 사용해 다항시간 내에 답을 구할 수 있는 문제&lt;br /&gt;대표적인 예로는 부분집합의 합이 있다. {-2, 10, 6, -5, -3} 이라는 집합이 있을 때 부분집합의 합이 0이 되는 부분집합이 있는지 물어보는 문제가 있다고해보자. 정답은 Yes이다. {-2, 10, -5, -3} 으로 뽑으면 부분집합의 합이 0이 되기때문이다. 이때 운이 좋아서 -2, 10, -5, -3 을 차례대로 뽑았다고 하면 이때 걸리는 시간은 (n - 1) 에 해당하고 이는 다항시간이다. 그런데 문제는 이 숫자조합을 찾아내는 것이다.&amp;nbsp;&lt;br /&gt;만약, n 개의 원소를 갖는 집합에 대해서 결정론적 튜링기계를 사용한다면 부분집합의 개수인 2^n 만큼 연산을 해야할 것이다. 즉 다항시간이 걸리는 것이 아니라 지수시간이 소요된다. 입력의 크기가 커지면 커질수록 소요되는 시간이 기하급수적으로 늘어나게 된다.&lt;/blockquote&gt;</description>
      <category>AlgorithmBook/알고리즘 문제해결 전략 1</category>
      <category>문제해결전략</category>
      <category>시간복잡도</category>
      <category>알고리즘</category>
      <category>알고리즘 문제해결전략</category>
      <category>프로그래밍</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/147</guid>
      <comments>https://minimalcode.tistory.com/147#entry147comment</comments>
      <pubDate>Tue, 28 Jan 2025 22:34:35 +0900</pubDate>
    </item>
    <item>
      <title>[01 문제 해결 시작하기] 3. 코딩과 디버깅에 관하여</title>
      <link>https://minimalcode.tistory.com/146</link>
      <description>&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 실수 자료형의 이해&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실수 연산의 어려움&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 [1, n] 범위의 모든 자연수 중 1/x * x = 1 인 x의 수를 세는 countObvious()의 구현을 보여줍니다. 누가 보더라도 이 식은 항상 성립해야 하니, countObvious(n)을 호출하면 당연히 n이 반환되어야 할 것입니다. 그런데 이 함수를 직접 실행해보면 결과는 그렇지 않다는 것을 알게 됩니다. 대표적으로 제 컴퓨터에서는 countObvious(50) = 48 라고 나옵니다. 설마 우리 프로그램에 버그가 있는 것일까요? 아니면 CPU에 문제가 있는 것일까요? 이 의문을 해결하려면 컴퓨터가 사용하는 실수 표현 방식에 대해 알아야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738058487911&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 잘못된 실수 연산의 예제
int coundObvious(int n) {
    int same = 0;
    for(int x = 1; x &amp;lt;= n; ++x) {
        double y = 1.0 / x;
        if (y * x == 1.0)
            ++same;
    }
    return same;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실수와 근사값&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 일상적으로 다루는 정수들은 컴퓨터가 모두 정확하게 표현할 수 있습니다. 반면 실수에서는 이야기가 다릅니다. 모든 것이 셀 수 있고 단위가 정해져 있는 정수들과는 달리 실수를 다루게 되면 일상 생활에서도 무한의 세계로 날아가 버리게 됩니다. 컴퓨터의 메모리는 항상 유한하고, 이 모든 값들을 모두 정확하게 담을 수는 없으니 어쩔 수 없이 적절히 비슷한 값을 사용하는 것으로 만족해야 합니다. 이 뒤에서 자세히 설명하겟지만, 컴퓨터의 모든 실수 변수는 정확도가 제한된 근사 값을 저장합니다. 근사 값으로 연산한 결과는 수학적으로 정확하지 않을 수 있기 때문에 실수는 훨씬 다루기 까다롭습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;IEEE 754 표준&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 많은 컴퓨터/컴파일러들에서 사용하는 실수 표기 방식은 IEEE 754 표준이라고 불립니다. IEEE754의 가장 큰 특징 몇 가지는 다음과 같습니다.&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;/li&gt;
&lt;li&gt;부동 소수점(floating-point) 표기법&lt;/li&gt;
&lt;li&gt;무한대, 비정규 수, Nan 등의 특수한 값이 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;부동 소수점 표기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;32비트의 공간을 이용해 실수를 이진법으로 표기한다고 합시다. 이때 고민되는 것은 사용 가능한 비트들을 정수부와 소수부에 어떻게 배분할 것인지 입니다. 32비트 변수 중 첫 16비트는 정수부, 뒤 16비트는 소수부로 쓰는 식으로 실수를 표기할 수 있습니다. 이 방식은 이해하기 쉽다는 장점이 있지만, 모두를 만족시킬 수 있는 분할이 존재하지 않는다는 문제가 있습니다. 정수부에 너무 많은 비트를 사용해 버리면 소수부의 정확도가 떨어집니다. 그렇다고 소수부에 너무 많은 비트를 사용해 버리면 소수부의 정확도가 떨어집니다. 그렇다고 소수부에 너무 많은 비트를 배정하면 큰 수를 표현할 수 없겠지요. 이 문제를 해결하기 위해 IEEE754 를 포함한 대부분의 실수 표준에서는 소수점을 옮길 수 있도록 했습니다. 어떤 형태의 숫자건 소수점을 적절히 옮겨서 소수점 위에 한 자리만 남도록 한 뒤, 최상위 비트에서부터 표현할 수 있는 만큼 표시하고 나머지는 반올림 처리하는 것이죠. 소수점을 몇 칸이나 옮겼는지를 기록해두면 여기서부터 원래 값을 쉽게 재구성해 낼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 11.625는 이진법으로 쓰면 1011.101이 됩니다. 이때 소수점을 왼쪽으로 세칸 옮기면 1.011101이 되고, 이 수를 맨 앞에서부터 저장 공간이 허락하는 만큼 저장하는 것입니다. 그런데 이진법에서 소수점 위에 있을 수 있는 유일한 숫자는 1입니다. 따라서 IEEE754에서는 1을 제외한 나머지를 저장하는 방식으로 1비트를 절약합니다. 예를 들어 5비트만을 저장할 수 있다면 우리는 최상위 비트를 제외한 다음 5비튼인 01110을 저장하게 되지요. 이때 실수 변수는 다음과 같은 3가지의 정보를 저장하게 됩니다.&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;부호 비트(sign bit) : 양수인지 음수인지 여부&lt;/li&gt;
&lt;li&gt;지수(exponent) : 소수점을 몇 칸 옮겼나?&lt;/li&gt;
&lt;li&gt;가수(mantissa) : 소수점을 옮긴 실수의 최상위 X 비트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IEEE754를 만든 사람들은 실수형에서 지수보다 가수에 훨씬 많은 비트 수를 부여하기로 결정했습니다. 지수는 소수점을 움직이는 횟수이기 때문에, 지수가 상대적으로 작더라도 실생활에서 사용하는 거의 모든 숫자들을 표현할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;countObvious() 가 1/10 * 3 과 3/10 이 같은지 비교한다고 합시다. 1/10과 3/10은 둘 다 실수 변수에 정확하게 담을 수 없으며 표현할 수 있는 가장 가까운 실수로 근사해 표현하게 됩니다. 그런데 IEEE754에서 3/10 을 담은 실수 변수는 참 값보다 작은 값, 1/10을 담은 실수 변수는 참 값보다 약간 큰 값으로 근사됩니다. 참 값보다 크게 표현된 1/10에 3을 곱한 결과는 3/10 보다 약간 커집니다. 결과적으로 두 근사 값은 모두 참 값인 3/10과 굉장히 가깝지만, 하나는 그보다 크고 하나는 작으며 따라서 비교가 실패하게 됩니다. 두 실수 값이 같은지를 비교할 때는 항상 어느 정도의 오차를 염두에 두어야 합니다. 구체적으로는 두 값의 차이가 매우 작은 경우 두 값이 같다고 판단해야 합니다. 두 수가 같은지 비교하는 함수를 다음과 같이 구현할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1738070094293&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bool absoluteEqual(double a, double b) {
    return fabs(a - b) &amp;lt; 1e-10;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 값의 오차가 1/(10의 10승) 보다 작은지를 확인합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AlgorithmBook/알고리즘 문제해결 전략 1</category>
      <category>디버깅</category>
      <category>문제해결전략</category>
      <category>알고리즘</category>
      <category>알고리즘 문제해결전략</category>
      <category>코딩</category>
      <category>프로그래밍</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/146</guid>
      <comments>https://minimalcode.tistory.com/146#entry146comment</comments>
      <pubDate>Tue, 28 Jan 2025 19:01:31 +0900</pubDate>
    </item>
    <item>
      <title>[01 문제 해결 시작하기] 2. 문제 해결 개관</title>
      <link>https://minimalcode.tistory.com/145</link>
      <description>&lt;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;1. 도입&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 언급했듯이 프로그래밍 대회는 문제 해결 능력을 수련하기에 무척 좋은 환경입니다. 그러나 무작정 알고리즘을 외우고 문제를 푼다고 해서 문제 해결 실력이 쌓이는 것은 아닙니다. 문제 해결 능력은 프로그래밍 언어나 알고리즘처럼 명확히 정의된 실체가 없는 추상적인 개념이기 때문에 단순한 반복만으로 연마하기 어렵습니다. 실제로 우리는 초등학교 산수 시간부터 문제를 푸는 방법을 배우지만, 많은 경우 당장 주어진 문제를 풀기 위한 요령을 익히는 데 급급합니다. 결국 대다수 사람들의 문제 해결 기술이 기계적으로 문제를 풀면서 익힌 감과 막연한 시도에 머무르는 것이 현실입니다.&lt;/p&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;h3 style=&quot;box-sizing: border-box; border-right-width: 0px; border-bottom: #FF5A4A 2px solid; margin: 5px 0px; border-left: #FF5A4A 10px solid; letter-spacing: 1px; line-height: 1.5; border-top-width: 0px; padding: 3px 5px 3px 5px;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2. 문제 해결 과정&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 해결 과정은 단계별로 나누어 접근합니다. 문제를 해결하기 위해 거쳐 가야 하는 과정들을 세분화함으로써, 어디가 부족하고 어디를 개선해야 하는지 판단할 수 있게 됩니다. 그러면 프로그래밍 대회를 위한 여섯 단계 알고리즘을 만들어봅시다.&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;decimal&quot;&gt;
&lt;li&gt;문제를 읽고 이해한다.&lt;/li&gt;
&lt;li&gt;문제를 익숙한 용어로 재정의한다.&lt;/li&gt;
&lt;li&gt;어떻게 해결할지 계획을 세운다.&lt;/li&gt;
&lt;li&gt;계획을 검증한다.&lt;/li&gt;
&lt;li&gt;프로그램으로 구현한다.&lt;/li&gt;
&lt;li&gt;어떻게 풀었는지 돌아보고, 개선할 방법이 있는지 찾아본다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size18&quot;&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AlgorithmBook/알고리즘 문제해결 전략 1</category>
      <category>대회</category>
      <category>문제 해결 전략</category>
      <category>알고리즘</category>
      <category>알고리즘 문제 해결 전략</category>
      <category>프로그래밍</category>
      <author>_su_min</author>
      <guid isPermaLink="true">https://minimalcode.tistory.com/145</guid>
      <comments>https://minimalcode.tistory.com/145#entry145comment</comments>
      <pubDate>Tue, 28 Jan 2025 18:29:49 +0900</pubDate>
    </item>
  </channel>
</rss>