<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>조준화의 오류정정</title>
    <link>https://jun-n.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 07:18:38 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>조준왕</managingEditor>
    <item>
      <title>[백준] 1800번: 인터넷 설치 C++ - 다익스트라가 메인 로직이 아닐 수 있다. 결정 문제의 비밀을 알려드립니다.  ☠️☠️ </title>
      <link>https://jun-n.tistory.com/245</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1800&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1800&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 분석해보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 다익스트라로 1-&amp;gt;N까지의 경로는 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 하지만 그 경로가 정답인지 확정할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 문제는 &quot;최대 비용을 최소화&quot; 하라는 최적화 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최적화 문제의 변수를 &quot;mid&quot;라 하자. 답을 저장할 변수를 mid라 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래의 문제는 1-&amp;gt;N 경로 중 최대 요금이 가장 작은 mid를 찾는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 결정 문제로 변경하면 1-&amp;gt;N 경로 중 최대 요금을 mid 이하로 만들 수 있을까? 가 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;mid로 경로를 경로를 만들었는데 해당 경로의 요금이 mid 초과인 수를 셌을 때 K 초과라면?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;mid로는 답을 낼 수 없다. 현재 mid 보다 더 큰 요금을 상한선으로 잡아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;반대라면 현재의 mid도 가능하고 더 적어도 된다.&lt;/li&gt;
&lt;li&gt;현재의 mid로 경로를 구할 때 다익스트라를 변경하면 된다. 요금이 mid 이상이면 weight를 1, 아니라면 0으로 두는 것이다. 그리고 다익스트라를 때려서 최종 경로의 weight가 K 초과인지만 판단하면 된다.&lt;/li&gt;
&lt;/ol&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;최대 요금이 100원 일 때 연결이 된다면 101원도 가능하다.&lt;/li&gt;
&lt;li&gt;100원 일 때 불가능하다면 49원은 불가능하다.&lt;/li&gt;
&lt;li&gt;단조성이 증명되었다!&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1760947085543&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**  **/
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;queue&amp;gt;
using namespace std;

int n, p, k, ans = -1;
vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; adj[1002];

int func(int mid)
{
    // mid보다 크면 weight를 1로 생각
    vector&amp;lt;int&amp;gt; dist(n + 1, 0x7f7f7f7f);
    priority_queue&amp;lt;pair&amp;lt;int, int&amp;gt;, vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;, greater&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;gt; pq;

    dist[1] = 0;
    pq.push({0, 1});

    while (!pq.empty())
    {
        auto cur = pq.top();
        pq.pop();

        // dist[cur.second]와 우선순위 큐의 정점이 다른 경우
        if (dist[cur.second] &amp;lt; cur.first)
            continue;
        for (auto nxt : adj[cur.second])
        {
            int nxt_cost = cur.first + (nxt.first &amp;gt; mid);
            if (dist[nxt.second] &amp;gt; nxt_cost)
            {
                // 갱신
                dist[nxt.second] = nxt_cost;
                pq.push({nxt_cost, nxt.second});
            }
        }
    }

    return dist[n];
}

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; p &amp;gt;&amp;gt; k;
    for (int i = 0; i &amp;lt; p; i++)
    {
        int u, v, w;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v &amp;gt;&amp;gt; w;
        adj[u].push_back({w, v});
        adj[v].push_back({w, u});
    }

    int left = 0, right = 1000000;
    while (left &amp;lt; right)
    {
        int mid = (left + right) / 2;
        if (func(mid) &amp;lt;= k)
        {
            ans = mid;
            right = mid;
        }
        else
        {
            left = mid + 1;
        }
    }

    cout &amp;lt;&amp;lt; ans;
}

/*
1~N번 연결
K개의 선은 공짜
나머지 선 중 가장 비싼 것을 구하기.

다익스트라 -&amp;gt; ElogE. E=10000
K개의 선이 공짜라서 단순 다익스트라로는 불가능함.

결정 문제로 바꿔서 남은 것 중 최대 비용을 mid로 한다면 연결이 가능한가?를 보면 된다.
1~n까지를 연결해서 mid 초과인 간선을 셌을 때 K 이상이라면?
-&amp;gt; mid는 현재보다 크다.
아니라면 mid는 현재보다 작다.

경로를 구하는 것은. 케이블 비용이 mid 보다 크다면 가중치를 1로 두고, 아니라면 0으로 둬서 다익스트라로 구할 수 있다.

*/&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘/문제풀이</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/245</guid>
      <comments>https://jun-n.tistory.com/245#entry245comment</comments>
      <pubDate>Mon, 20 Oct 2025 16:58:45 +0900</pubDate>
    </item>
    <item>
      <title>[백준] 15732번: 도토리 숨기기 - 왜 이분탐색인가?</title>
      <link>https://jun-n.tistory.com/244</link>
      <description>&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(1 &amp;le; N &amp;le; 1,000,000)과 규칙의 개수 K(1 &amp;le; K &amp;le; 10,000), 도토리의 개수 D(1 &amp;le; D &amp;le; 1,000,000,000)가 주어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 쉽게 떠올릴 수 있는 방식은?&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브루트포스이다. 하나의 규칙에 대해 N개의 상자를 모두 돌면서 개수를 세야하니 O(NK)가 되어서 시간초과가 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;O(NK)가 문제이고... k개의 규칙에서 상자 순회가 존재해서는 안된다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 하나의 규칙을 보면 [a, b]에서 간격 c로 도토리를 넣을 때 a~b 구간의 총 도토리 개수는 O(1)로 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수식은 대충 (b-a)/c + 1 정도가 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 k개의 규칙에 대해 O(k)로 구간 [1, n]에 대해서 총 도토리 개수를 구할 수 있는 것이다. 이제 가닥이 좀 잡히는데, 매개변수 탐색을 쓰면 될 것 같다. 그 근거는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;총 도토리의 개수는 구간과 비례해서 선형적으로 증가한다. 여기서 구간이란 답이되는 변수를 말한다. 따라서 매개변수 탐색을 쓸 수 있다. 매개변수는 구간 = 답이 되는 변수로 설정하면 된다.&lt;/li&gt;
&lt;li&gt;구간 mid에 대해 1~mid의 도토리 개수는 O(k)로 구할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;이분 탐색으로 구간을 구한다면 전체 시간 복잡도는 O(k log n) 이므로 효율성은 통과!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그대로 구현해주면 된다. 그런데 이분 탐색, 매개변수 탐색 문제는 다음 경우에 따라 구현에 실패하는 경우가 종종 있었다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;mid = (st+en)/2 or mid = (st+en+1)/2&lt;/li&gt;
&lt;li&gt;while(st&amp;lt;en) or while(st&amp;lt;=en)&lt;/li&gt;
&lt;li&gt;st=mid or st=mid-1&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 조건은 upper인지 lower인지에 달려있다. 가령 FFFTTT 배열에서 첫 번째 T의 인덱스를 찾는 lower bound는 내림으로 mid를 구하는 것을 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번째는 문제가 요구하는 것에 따라 mid와 그 오른쪽이 답이 되는지, mid가 포함이 되는지 그런걸 잘 따져보면 되고 2번은 하나로 고정해서 쓰면 될 것 같다. 아래는 간단하게 AI를 돌려 정리한 내용이다.&lt;/p&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_1760605201515&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 파라미터 서치 **/
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;
using namespace std;
int n, k, d;
int rules[10005][3]; // A번부터 B번까지 C의 간격

long long count_(int box)
{
    // 1~box까지 도토리 개수 구하기
    long long rt = 0;
    for (int i = 0; i &amp;lt; k; i++)
    {
        long long a = rules[i][0], b = rules[i][1], c = rules[i][2];
        if (a &amp;gt; box)
            continue;
        // a~b까지 간격 c
        b = min(box, (int)b); // b가 box보다 크면 안됨. box까지만 탐색해야함.
        rt += (b - a) / c + 1;
    }
    return rt;
}

int func()
{
    // 파라미터릭 서치로 1~mid의 범위에 도토리를 모두 넣으며 개수를 구함.
    // 구한 개수가 cnt라 했을 때 cnt가 d보다 작다면 mid+1~en에 대해 다시 mid 갱신
    // cnt가 d보다 같거나 크다면 st~mid에 대해 다시 mid 갱신
    int st = 1, en = n;
    while (st &amp;lt; en)
    {
        int mid = (st + en) / 2;
        if (count_(mid) &amp;gt;= d)
            en = mid;
        else
            st = mid + 1;
    }
    return st;
}

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; k &amp;gt;&amp;gt; d;
    for (int i = 0; i &amp;lt; k; i++)
    {
        int a, b, c;
        cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
        rules[i][0] = a;
        rules[i][1] = b;
        rules[i][2] = c;
    }

    cout &amp;lt;&amp;lt; func();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Lower Bound (최솟값 찾기)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표: 조건을 만족하는 값들 중 가장 작은 값을 찾는 것. (e.g., [F, F, F, T, T, T] 배열에서 첫 번째 T의 인덱스 찾기)&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;while (st &amp;lt; en) {
    int mid = (st+en) / 2; // ⭐️ 중간값 (내림)

    if (decision(mid)) { // mid가 조건을 만족하는가? (T)
        // mid가 답일 수 있으니, mid를 포함하여 왼쪽 구간을 다시 탐색
        en = mid;
    } else { // mid가 조건을 만족하지 못하는가? (F)
        // mid와 그 왼쪽은 절대 답이 될 수 없으므로, 오른쪽 구간을 탐색
        st = mid + 1;
    }
}
// 루프 종료 시 st와 en은 같은 값을 가짐
return st;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로직 분석&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;decision(mid)가 참이면, mid가 정답일 수 있지만 더 작은 정답이 [st, mid] 구간에 있을 수 있으므로 en = mid로 범위를 좁힙니다. mid를 버리지 않는 것이 핵심입니다.&lt;/li&gt;
&lt;li&gt;decision(mid)가 거짓이면, mid는 확실히 답이 아니므로 [mid + 1, en] 구간만 탐색하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Upper Bound (최댓값 찾기)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표: 조건을 만족하는 값들 중 가장 큰 값을 찾는 것. (e.g., [T, T, T, F, F, F] 배열에서 마지막 T의 인덱스 찾기)&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;while (st &amp;lt; en) {
    int mid = (st+en+1) / 2; // ⭐️ 중간값 (올림)

    if (decision(mid)) { // mid가 조건을 만족하는가? (T)
        // mid가 답일 수 있으니, mid를 포함하여 오른쪽 구간을 다시 탐색
        st = mid;
    } else { // mid가 조건을 만족하지 못하는가? (F)
        // mid와 그 오른쪽은 절대 답이 될 수 없으므로, 왼쪽 구간을 탐색
        en = mid - 1;
    }
}
// 루프 종료 시 st와 en은 같은 값을 가짐
return st;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로직 분석&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;decision(mid)가 참이면, mid가 정답일 수 있지만 더 큰 정답이 [mid, en] 구간에 있을 수 있으므로 st = mid로 범위를 좁힙니다.&lt;/li&gt;
&lt;li&gt;decision(mid)가 거짓이면, mid는 확실히 답이 아니므로 [st, mid - 1] 구간만 탐색하면 됩니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가장 중요한 점&lt;/b&gt;: st = mid 로직 때문에 mid를 계산할 때 **반드시 올림(+1)**을 해야 합니다. 그렇지 않으면 무한 루프에 빠질 수 있습니다. (아래에서 자세히 설명)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무한 루프 방지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무한 루프는 &lt;b&gt;탐색 범위가 더 이상 줄어들지 않을 때&lt;/b&gt;, 특히 st와 en이 딱 1 차이 날 때 발생합니다. 코드를 짜면서 바로 검증할 수 있는 방법은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2단계 정신적 시뮬레이션&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;st = k, en = k + 1인 최악의 상황을 가정합니다.&lt;/li&gt;
&lt;li&gt;mid를 계산하고, if와 else 분기 모두에서 st가 증가하거나 en이 감소하는지 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 1: Upper Bound 템플릿에서 mid를 잘못 계산한 경우&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상황: st = 5, en = 6&lt;/li&gt;
&lt;li&gt;잘못된 코드: int mid = (st + en) / 2; // 올림 안 함&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;mid = (5 + 6) / 2 = 5가 됩니다.&lt;/li&gt;
&lt;li&gt;if (decision(5))가 true라면? &amp;rarr; st = mid가 실행되어 st는 여전히 5입니다.&lt;/li&gt;
&lt;li&gt;결과: st=5, en=6 상태가 변하지 않으므로 무한 루프에 빠집니다. ❌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;올바른 코드: int mid = (st + en + 1) / 2;&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;mid = (5 + 6 + 1) / 2 = 6이 됩니다.&lt;/li&gt;
&lt;li&gt;if (decision(6))가 true라면? &amp;rarr; st = mid 실행, st가 6이 됨 &amp;rarr; st와 en이 같아져 루프 종료.&lt;/li&gt;
&lt;li&gt;if (decision(6))가 false라면? &amp;rarr; en = mid - 1 실행, en이 5가 됨 &amp;rarr; st와 en이 같아져 루프 종료.&lt;/li&gt;
&lt;li&gt;결과: 어떤 경우든 범위가 좁혀지므로 안전합니다. ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예시 2: Lower Bound 템플릿 검증&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상황: st = 5, en = 6&lt;/li&gt;
&lt;li&gt;코드: int mid = (st + en) / 2;&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;mid = (5 + 6) / 2 = 5가 됩니다.&lt;/li&gt;
&lt;li&gt;if (decision(5))가 true라면? &amp;rarr; en = mid 실행, en이 5가 됨 &amp;rarr; st와 en이 같아져 루프 종료.&lt;/li&gt;
&lt;li&gt;if (decision(5))가 false라면? &amp;rarr; st = mid + 1 실행, st가 6이 됨 &amp;rarr; st와 en이 같아져 루프 종료.&lt;/li&gt;
&lt;li&gt;결과: 안전합니다. ✅&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>알고리즘/문제풀이</category>
      <category>도토리</category>
      <category>백준</category>
      <category>이분탐색</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/244</guid>
      <comments>https://jun-n.tistory.com/244#entry244comment</comments>
      <pubDate>Thu, 16 Oct 2025 18:00:19 +0900</pubDate>
    </item>
    <item>
      <title>[백준] C++ 풀이 22866번: 탑 보기 - 이게 스택이라고??</title>
      <link>https://jun-n.tistory.com/243</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/22866&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/22866&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상당히 어렵게 풀었다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각설하고 우선 완전탐색 풀이는 굉장히 쉽지만 n이 100,000이기에 불가능하다. n log n이나 n으로도 풀어야 한다. 한 번의 순회가 최대라는 것이다. (log n으로는 순회가 불가능하니 n log n이면 보통 한 번의 순회에서 log n짜리 처리를 하는 것)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런저런 생각을 해보며 한 번의 순회로 어떻게 풀 수 있을까 고민을 해봤다. 일단 몇 가지 핵심 포인트를 찾았는데&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;순회가 두 번은 일어나야 한다. n^2이 아니라 2n인 것. 왜냐하면, 한 번 스캔으로는 절대 모든 케이스를 찾을 수 없다. 왼쪽 -&amp;gt; 오른쪽 / 오른쪽 -&amp;gt; 왼쪽으로 잡아두자.&lt;/li&gt;
&lt;li&gt;i번째 건물에서 딱 오른쪽 방향으로만 보이는 방향을 관찰하면 무언가 규칙이 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H4lqi/btsQnGq2YyD/i3CzVKfWMTVkiOGUbaxGL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H4lqi/btsQnGq2YyD/i3CzVKfWMTVkiOGUbaxGL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H4lqi/btsQnGq2YyD/i3CzVKfWMTVkiOGUbaxGL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH4lqi%2FbtsQnGq2YyD%2Fi3CzVKfWMTVkiOGUbaxGL0%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;605&quot; height=&quot;143&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽에서부터 순회하며 오른쪽 방향으로만 보이는 방향을 관찰해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;8번 건물 : X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7번 건물 : 8&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6번 건물 : 8&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5번 건물 : 6, 8 // 자신보다 6이 건물 높이가 높아서 바로 관찰할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4번 건물 : 8 // 기존 관찰 가능하던 6이 자신보다 높이가 낮아서 관찰할 수 없다. 여기서 한 가지 통찰을 얻었는데, 7번 건물은 한 번 관찰이 불가능하니 계속 불가능하다. 한 방향으로 제한했기에 규칙을 확정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번 건물 : 4, 8&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 건물 : X&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 건물 : 7&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙을 좀 정리해보면&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;한 번 관찰 불가능한 건물은 계속 불가능하다.&lt;/li&gt;
&lt;li&gt;한 번 관찰 가능한 건물은 그 건물보다 높은 건물이 나올때까지 계속 관찰 가능하다&lt;/li&gt;
&lt;li&gt;예를 들어 3번 건물에서 4번 건물이 관찰 가능해도 8번은 여전히 관찰이 가능하다. 즉, 새로운 관찰 가능한 건물이 등장해도 기존의 관찰 가능한 건물을 유지한다.&amp;nbsp;&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;특히 2번 건물을 보면 7보다 낮은 건물들을 제거해나가는 과정이 스택과 동일하다. 반대 방향도 똑같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 스택으로 잡아보고 자세히 알고리즘을 짜보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;오른쪽 -&amp;gt; 왼쪽 순회 (i=n-1 ~ 0)&lt;/li&gt;
&lt;li&gt;i번째 건물이 i+1번째 건물보다 낮다면 -&amp;gt; 할 거 없음&lt;/li&gt;
&lt;li&gt;i번째 건물이 i+1번째 건물보다 같거나 높다면 -&amp;gt; 오른쪽 방향으로 자신보다 낮은 건물들 제거&lt;/li&gt;
&lt;/ol&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_1757316408484&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 스택 22866 탑 보기 **/
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;stack&amp;gt;
using namespace std;

stack&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; s1, s2; //{건물 번호, 높이}
int n;
int building[100010];
pair&amp;lt;int, int&amp;gt; ans[100010];

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin &amp;gt;&amp;gt; n;
    for (int i = 0; i &amp;lt; n; i++)
        cin &amp;gt;&amp;gt; building[i];

    // 오른쪽 -&amp;gt; 왼쪽 순회
    // i번째 건물에서 오른쪽 방향으로 볼 수 있는 건물들
    // 7 x 4,8 8 6,8 8 x
    // 오른쪽에서부터 진행
    // i번째 건물이 i+1번째 건물보다 낮다면 -&amp;gt; continue
    // i번째 건물이 i+1번째 건물보다 같거나 높다면 -&amp;gt; 오른쪽 방향으로 자신보다 낮은 건물들 다 제거
    // 그냥 스택으로 하면 됨. 왜냐하면 한 방향으로만 삭제가 일어남
    int i;
    s1.push({n - 1, building[n - 1]});

    for (i = n - 2; i &amp;gt;= 0; i--)
    {
        if (building[i] &amp;gt;= building[i + 1])
            while (!s1.empty() &amp;amp;&amp;amp; building[i] &amp;gt;= s1.top().second)
                s1.pop();

        ans[i].first += s1.size();
        if (!s1.empty())
            ans[i].second = s1.top().first;

        // if (!s1.empty())
        //     cout &amp;lt;&amp;lt; s1.size() &amp;lt;&amp;lt; ' ' &amp;lt;&amp;lt; s1.top().first + 1 &amp;lt;&amp;lt; '\n';
        // else
        //     cout &amp;lt;&amp;lt; 0 &amp;lt;&amp;lt; ' ' &amp;lt;&amp;lt; 0 &amp;lt;&amp;lt; '\n';

        s1.push({i, building[i]});
    }

    // 왼쪽 -&amp;gt; 오른쪽 순회 (왼쪽 방향으로 볼 수 있는 것들)
    s2.push({0, building[0]});

    for (i = 1; i &amp;lt; n; i++)
    {
        if (building[i] &amp;gt;= building[i - 1])
            while (!s2.empty() &amp;amp;&amp;amp; building[i] &amp;gt;= s2.top().second)
                s2.pop();

        ans[i].first += s2.size();

        if (!s2.empty() &amp;amp;&amp;amp; (ans[i].second == 0 || abs(ans[i].second - i) &amp;gt;= abs(i - s2.top().first)))
            ans[i].second = s2.top().first;

        // if (!s2.empty())
        //     cout &amp;lt;&amp;lt; s2.size() &amp;lt;&amp;lt; ' ' &amp;lt;&amp;lt; s2.top().first + 1 &amp;lt;&amp;lt; '\n';
        // else
        //     cout &amp;lt;&amp;lt; 0 &amp;lt;&amp;lt; ' ' &amp;lt;&amp;lt; 0 &amp;lt;&amp;lt; '\n';

        s2.push({i, building[i]});
    }
    for (int i = 0; i &amp;lt; n; i++)
        if (ans[i].first == 0)
            cout &amp;lt;&amp;lt; 0 &amp;lt;&amp;lt; '\n';
        else
            cout &amp;lt;&amp;lt; ans[i].first &amp;lt;&amp;lt; ' ' &amp;lt;&amp;lt; ans[i].second + 1 &amp;lt;&amp;lt; '\n';
}

/*
10만개의 건물에 대해 순회
i번째 건물의 좌우 중 자신보다 큰고 가까운 건물 출력
완전탐색 : n^2 -&amp;gt; 불가능
max : 높이 7 -&amp;gt; 높이 7인 건물은 답이 X



반대로 왼쪽 방향만 생각해보면
왼쪽에서부터 진행하면서 똑같이

*/&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘/문제풀이</category>
      <category>C++</category>
      <category>문제풀이</category>
      <category>백준</category>
      <category>스택</category>
      <category>알고리즘</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/243</guid>
      <comments>https://jun-n.tistory.com/243#entry243comment</comments>
      <pubDate>Mon, 8 Sep 2025 16:27:10 +0900</pubDate>
    </item>
    <item>
      <title>소켓 통신 기술의 변화. 왜 멀티플렉싱인가?</title>
      <link>https://jun-n.tistory.com/242</link>
      <description>&lt;h1&gt;epoll과 멀티플렉싱&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 전통적인 웹소켓 통신&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blocking IO 기반의 멀티스레드 방식이다. 클라이언트가 accept()으로 요청하면 새로운 스레드를 생성하고, 생성된 스레드는 해당 클라이언트와 데이터 송수신을 전담한다. 이 과정에서 accept(), read(), write()와 같은 IO 함수들은 데이터가 준비될 때까지 해당 스레드를 Blocking 시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 특정 클라이언트의 느린 IO가 존재한다면 그 클라이언트의 해당 워커 스레드는 Block 된다. 즉, 일시정지된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Non-blocking I/O&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non Blocking IO는 IO 작업을 요청했을 때 그 작업이 끝날 때까지 기다리지 않고 즉시 다음 코드를 실행하는 방식이다. 조금 더 정확히 말하자면 제어권을 반환하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fcntl() 시스템 콜을 사용하여 특정 소켓의 FD를 Non-blocking 모드로 설정하면, read()나 write() 호출 시 당장 처리할 데이터가 없더라도 대기하지 않고 즉시EAGAIN이나 &lt;b&gt;EWOULDBLOCK&lt;/b&gt; 에러를 반환한다. 즉, 제어권을 즉시 반환하는 것이고 프로세스는 I/O를 기다리며 Block 되지 않고 다른 작업을 수행할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non-Blocking 방식에서는 ****하나의 스레드가 수천 개의 연결을 동시에 처리할 수 있다. 1번 연결에 데이터를 요청하고 바로 돌아와 2번 연결에 데이터를 요청하고, 3번 연결에서 온 데이터를 처리하는 식으로, 잠시도 쉬지 않고 일하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로세스는 I/O가 가능한지 확인하기 위해 무한 루프를 돌며 모든 소켓에 read()를 계속 시도해야 한다. 이를 Busy-waiting이라 하며, CPU 자원을 100% 점유하여 극심한 낭비를 초래하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. I/O 멀티플렉싱 - select, poll&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 어플리케이션이 계속 Polling 하는 것이 아니라, IO가 준비되면 커널이 알려주는 Event Notification 방식이 필요하다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멀티플렉싱은 여러 개의 신호를 하나의 채널로 합치는 기술이다. 이 개념을 차용한 I/O 멀티플렉싱은 하나의 스레드가 수많은 I/O 파일 디스크립터들을 동시에 감시하고 관리하는 기술이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 파일 디스크립터는 OS가 열려 있는 파일 혹은 I/O 채널을 식별하기 위해 부여하는 고유한 번호이다.&lt;/li&gt;
&lt;li&gt;유닉스 계열의 OS는 모든것을 파일로 관리한다. 따라서 웹서버가 새로운 클라이언트의 접속을 accept()하면 커널은 이 클라이언트와의 통신을 위한 전용 소켓을 만들고 여기에 파일 디스크립터 번호를 부여하게 된다.&lt;/li&gt;
&lt;li&gt;전용 소켓을 만든다는 말은 어떤 자원을 할당한다는 말이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I/O 멀티플렉싱을 사용하지 않는 Blocking 방식에서는 프로세스가 특정 FD 하나에서 데이터가 올 때 까지 Blocking 되어야 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, I/O 멀티플렉싱을 사용한다면 프로세스는 자신이 관리하는 FD 목록 중 어디서든 이벤트가 발생하면 실행 상태가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;select 와 poll은 I/O 멀티플렉싱을 구현하는 시스템 콜이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;select()&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;준비&lt;/b&gt;: 개발자는 감시할 FD들의 목록을 fd_set 에 저장한다. fd_set은 비트맵과 유사하며, 만약 5번, 8번 FD를 감시하고 싶다면 fd_set의 5번째, 8번째 비트를 1로 설정하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;호출&lt;/b&gt;: 준비된 fd_set을 select() 함수에 전달하여 커널을 호출한다. 이때 프로세스는 Block 상태가 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커널 작업&lt;/b&gt;: 커널은 fd_set에 설정된 모든 FD들을 하나씩 확인하며 I/O 이벤트가 발생하는지 감시한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;반환&lt;/b&gt;: 이벤트가 발생하면, 커널은 fd_set의 내용을 직접 수정하여 이벤트가 발생한 FD의 비트만 1로 남기고 나머지는 0으로 바꾼다. 그리고 대기하던 프로세스를 깨운다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확인&lt;/b&gt;: 깨어난 프로세스는 fd_set의 모든 비트를 0번부터 다시 검사하여, 어떤 FD의 비트가 1로 남아있는지 확인하고 해당 FD에 대한 I/O 작업을 수행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;감시할 수 있는 FD 개수가 FD_SETSIZE 로 제한된다.&lt;/li&gt;
&lt;li&gt;호출할 때마다 fd_set을 커널에 복사해야 하고, 커널과 어플리케이션 모두 전체 FD를 매번 스캔해야 하므로 비효율적이이다.&lt;/li&gt;
&lt;li&gt;호출 후 fd_set이 변경되므로, 다음 호출을 위해 다시 fd_set을 만들어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;poll()&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;poll()은 select()의 FD 개수 제한 문제를 해결한 시스템 콜이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;준비&lt;/b&gt;: pollfd 구조체 배열을 만들고, 감시할 FD 번호와 감시할 이벤트의 종류를 지정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;호출 및 반환&lt;/b&gt;: 배열을 poll() 함수에 전달한다. 커널은 이벤트가 발생한 구조체의 revents(return events) 필드에 발생한 이벤트의 종류를 기록하여 반환한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확인&lt;/b&gt;: 깨어난 프로세스는 pollfd 배열 전체를 순회하며 revents 필드가 채워진 구조체를 찾아 작업을 수행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단점&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FD 개수 제한은 해결했지만, select()와 마찬가지로 호출 시마다 전체 배열을 커널로 복사하고, 반환 후 전체 배열을 스캔 시간적 오버헤드가 남아있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Epoll&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;epoll은 기존 방식의 다음과 같이 해결한 가장 진보된 멀티플렉싱 기술이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;관심 FD 목록의 커널 내 저장&lt;/b&gt;: epoll_create()로 epoll 인스턴스를 커널에 생성하면, 커널은 이 인스턴스에 대한 FD 목록을 자체 공간에 유지한다. 어플리케이션은 epoll_ctl()을 통해 이 목록을 추가/삭제할 뿐, select/poll처럼 매번 전체 목록을 커널로 복사할 필요가 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이벤트 기반(Event-driven) 동작&lt;/b&gt;: epoll_wait() 호출 시, 커널은 select/poll처럼 전체 FD를 스캔하지 않는다. 대신 I/O가 준비된 FD가 발생할 때마다 해당 FD를 커널 내의 Ready List에 추가해둔다. epoll_wait() 은 이 Ready List가 비어있지 않으면 즉시 해당 목록의 내용만 어플리케이션에 전달하고 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조 덕분에 epoll은 감시하는 전체 FD의 수와 관계없이, 실제로 이벤트가 발생한 FD의 수에만 비례하는 성능을 보장한다.&lt;/p&gt;</description>
      <category>CS/Computer Network</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/242</guid>
      <comments>https://jun-n.tistory.com/242#entry242comment</comments>
      <pubDate>Tue, 19 Aug 2025 17:30:39 +0900</pubDate>
    </item>
    <item>
      <title>RDBMS와 NoSQL에 대한 고찰</title>
      <link>https://jun-n.tistory.com/241</link>
      <description>&lt;h1&gt;RDBMS, NoSQL&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. RDBMS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS는 데이터를 관계를 통해 관리하는 시스템이다. 여기서 관계는 테이블로 표현되며 2차원 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;주요 특징&lt;/b&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;b&gt;Schema-on-Write&lt;/b&gt; : 데이터를 저장하기 전 테이블의 구조를 명확히 정의해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 무결성 보장&lt;/b&gt; : PK, FK 등의 제약 조건을 통해 데이터의 중복을 방지하고 관계의 유효성을 보장한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQL (Structured Query Language)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트랜잭션 (Transaction)과 ACID 원칙&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS는 SQL을 사용할 수 있으며 데이터의 일관성과 신뢰성이 매우 높다. 하지만 그만큼 스키마를 미리 정의하기에 데이터 구조를 변경하기 어렵다. 또한 수평적 확장이 구조적으로 어렵고 JSON, XML 등의 비정형 데이터를 저장하고 처리하기에 비효율적이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RDBMS는 JOIN과 무결성이 핵심이다. 따라서 수평적 확장을 통해 데이터를 여러 노드에 분산시키게되면 여러 서버에 걸쳐 네트워크 통신이 필요하고 굉장히 큰 부하가 일어나게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RDBMS에서의 FK&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 대규모 트래픽을 다루는 서비스에서는 FK를 의도적으로 사용하지 않는 것이 트랜드이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FK 제약 조건이 걸려있으면 자식 테이블에 쓰기, 수정, 삭제가 일어날 때마다 참조 무결성 검증이 필요하다. 이 과정은 데이터베이스에 추가적인 부하를 주며, 대량의 쓰기 작업이 발생하는 서비스에서는 성능 저하의 주요 원인이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 데이터 변경 시 FK로 연결된 테이블 간에 잠금이 발생할 수 있는데, 이는 복잡한 트랜잭션에서 데드락의 원인이 되기도 한다. 샤딩 환경에서는 여러 데이터베이스 인스턴스에 걸쳐 FK 제약 조건을 유지하는 것이 매우 어렵다는 문제점도 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 서비스를 생각해보면 서비스당 하나의 데이터베이스가 핵심 원칙 중 하나이다. 이에 FK 유지가 거의 불가능하며 DB 레벨의 FK 대신 애플리케이션 레벨에서 느슨하게 결합된 방식으로 데이터의 관계를 관리한다. 주요 방식은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;API 호출을 통한 검증&lt;/li&gt;
&lt;li&gt;비정규화 : 주문 서비스에서 orders 테이블에 userId + userName 등을 함께 저장해 서비스 간 의존성을 줄인다.&lt;/li&gt;
&lt;li&gt;이벤트 기반 통신 : 사용자 서비스에서 사용자가 탈퇴하면 UserDeleted 이벤트를 발생시킨다. 주문 서비스는 이 이벤트를 구독하고 있다가, 사용자 정보를 비동기적으로 처리한다. 즉, 모든 데이터가 실시간으로 일치하지는 않지만 결국에는 일관성이 맞춰진다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제점을 바탕으로 FK를 사용하지 않는 것이 트랜드가 되었다. 즉, 데이터베이스 레벨의 무결성 보장을 포기하고 그 책임을 애플리케이션 레벨에서 처리하게 되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ORM 활용 : JPA 등을 통해 애플리케이션 코드 수준에서 관계를 관리할 수 있다.&lt;/li&gt;
&lt;li&gt;비즈니스 로직에서의 검증&lt;/li&gt;
&lt;li&gt;Soft Delete : 데이터를 물리적으로 삭제하는 대신 is_deleted 와 같은 값을 true로 변경하는 방식을 사용한다. 이를 통해 부모 데이터가 사라졌을 때 자식 데이터가 Orphan Data가 되는 것을 방지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 FK를 사용하지 않으면 데이터 구조 파악도 어렵고 데이터의 무결성을 보장할 수 없다. 따라서 정답은 없으며 상황에 맞게 선택해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FK 사용이 중요한 경우
&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;/li&gt;
&lt;li&gt;FK 미사용을 고려할 수 있는 경우
&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;MSA 구조의 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. NoSQL (Not Only SQL)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoSQL은 관계형 모델을 사용하지 않는 데이터베이스를 총칭한다. 정형화되지 않은 대용량의 데이터를 유연하고 빠르게 처리하기 위해 탄생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 CAP 이론을 알아야 NoSQL의 탄생을 이해할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CAP 이론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAP 이론은 분산 데이터베이스 시스템이 Consistency, Availability, Partition Tolerance 중 최대 두 가지의 속성만을 보장할 수 있다는 것을 설명하는 이론이다. 분산 시스템을 설계할 때 어떤 특성을 우선시하고 어떤 것을 희생해야 하는지에 대한 중요한 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 각 속성을 보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일관성 : 모든 노드는 항상 같은 데이터를 보여줘야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/li&gt;
&lt;li&gt;가용성은 지키고 일관성은 지키지 못하는 경우 서버가 응답하는 데이터가 항상 최신의 데이터라는 보장은 없을 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Partition Tolerance (분할 용인성) : 네트워크가 끊어져도 시스템은 동작해야 한다.
&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;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAP의 핵심은 P를 반드시 선택해야 하는 분산 환경에서, 우리는 C와 A 중 하나를 선택해야만 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스가 네트워크 문제로 A, B 파티션으로 나뉘었다고 생각해보자. 즉 P가 발생한 상황이다. 이때 파티션 A에 새로운 데이터 쓰기 요청이 들어왔다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CP 시스템 (일관성을 선택한 경우)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션 A는 B와 통신할 수 없으므로 새로운 데이터를 B에 즉시 전달하여 데이터를 일치시킬 수 없다. 하지만 일관성을 선택했기에, 즉 동기적으로 동작하기에 A는 B와 연결이 복구될 때까지 새로운 쓰기 요청을 거부하거나 응답하지 않아야 한다.&lt;/li&gt;
&lt;li&gt;즉, C를 지키기 위해 A를 희생할 수 밖에 없는 것이다.&lt;/li&gt;
&lt;li&gt;MongoDB, 전통적인 RDBMS가 여기에 속한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;AP 시스템 (가용성을 선택한 경우)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션 A는 일단 쓰기 요청을 받아 처리하고 응답한다. B 또한 자신의 버전으로 요청에 계속 응답한다.&lt;/li&gt;
&lt;li&gt;따라서 A, B는 서로 다른 데이터를 가지며 일관성을 지킬 수 없다.&lt;/li&gt;
&lt;li&gt;나중에 네트워크가 복구되면 서로 다른 데이터를 동기화하는 과정을 거쳐 최종적으로 일관성을 맞추게 된다. 이를 Eventual Consistency라 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 우리는 NoSQL을 이해할 준비가 되었다. CAP 이론에 따르면, P를 항상 가져가야 하는 입장에서 우리는 일관성과 가용성 중 하나를 포기해야만 한다. 하지만 RDBMS는 일관성과 가용성을 최우선으로 설계되었다. 단일 서버 환경을 상정하고 만들어진 모델이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 분산 환경에서 P는 포기할 수 없고, RDBMS의 무수한 무결성들 사이에서 C 또한 포기할 수 없다. 즉, CP 시스템을 선택할 수 밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 NoSQL은 대규모 분산 환경을 전제로 탄생했기에 P를 기본으로 깔고 간다. 또한 서비스가 멈추지 않는 것을 더 우선시하기에 AP 시스템으로 설계된 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 NoSQL과 RDBMS의 핵심적인 차이이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NoSQL의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Schema-on-Read : RDBMS와 달리 고정된 스키마가 없이 데이터를 읽어올 때 스키마가 해석된다.&lt;/li&gt;
&lt;li&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;Key-Value : Redis, &amp;hellip;&lt;/li&gt;
&lt;li&gt;Document Store : JSON, XML 등의 문서 저장. MongoDB, &amp;hellip;&lt;/li&gt;
&lt;li&gt;Column-Family Store : 행이 아닌 열을 기준으로 데이터를 저장&lt;/li&gt;
&lt;li&gt;Graph Store : 데이터와 그 관계를 노드, 엣지, 속성으로 표현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BASE 속성&lt;/b&gt;: ACID 대신 &lt;b&gt;BASE(Basically Available, Soft state, Eventually consistent)&lt;/b&gt; 속성을 따르는 경우가 많다. NoSQL은 AP 모델이기 많기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BASE 속성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BASE 속성은 ACID 원칙과 정반대에 서 있는 개념으로 NoSQL이 분산 환경에 적합함을 보이는 속성이다.즉, 가용성과 성능을 최우선으로 확보하기 위한 설계 철학이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Basically Available (기본적인 가용성)&lt;/li&gt;
&lt;/ol&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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Soft State (소프트 상태 / 유연한 상태)&lt;/li&gt;
&lt;/ol&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;li&gt;ACID에서는 한 번 Commit 된 데이터는 다른 트랜잭션에 의해 변경되기 전까지 그 상태가 영구적으로 유지된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Eventually Consistent (결과적 일관성)&lt;/li&gt;
&lt;/ol&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;h3 data-ke-size=&quot;size23&quot;&gt;NoSQL의 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점 또한 명확하다. 아직 표준화된 쿼리 언어가 없고 트랜잭션 기능이 제한적인 경우가 많다. 일관성도 많이 부족하다. 자세히 보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;데이터 일관성의 문제 (Eventually Consistent)&lt;/b&gt;: Eventual Consistency 특성 때문에 일시적 데이터 불일치를 허용한다. 따라서 금융 거래와 같은 경우 절대로 NoSQL을 사용해서는 안된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 중복과 업데이트의 어려움&lt;/b&gt;: 스키마가 없고 JOIN 또한 없기에 데이터의 중복을 야기할 수 있다. 따라서 업데이트 비용이 생각보다 더 크게 들 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복잡한 쿼리의 한계&lt;/b&gt;: JOIN 연산이 없거나 비효율적이기 때문에, 여러 컬렉션에 걸친 복잡한 데이터 분석을 작성하기 어렵다. 이는 애플리케이션 레벨에서의 복잡한 로직 추가를 야기한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과적 일관성과 UX&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;'품절된 상품 주문'과 같은 문제를 해결하기 위한 많은 패턴이 존재한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;보상 트랜잭션 (Compensating Transaction) / Saga 패턴&lt;/b&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;li&gt;이 보상 이벤트는 '주문 자동 취소', '고객에게 사과 알림 및 쿠폰 발송', '결제 자동 환불' 등의 반대되는 보상 조치를 트리거한다.&lt;/li&gt;
&lt;li&gt;사용자 입장에서는 주문이 취소되는 불쾌한 경험을 하지만, 적절한 보상(쿠폰 등)을 통해 브랜드 이미지를 관리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UX/UI를 통한 기대치 관리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;버튼 문구 변경&lt;/b&gt;: &quot;즉시 구매&quot;가 아닌 &quot;주문하기&quot; 또는 &quot;결제 진행&quot; 등의 문구를 사용하여, 클릭 즉시 모든 것이 확정되는 것이 아님을 암시한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;중간 상태 안내&lt;/b&gt;: 주문 요청 후 &quot;주문이 완료되었습니다&quot;라고 바로 보여주는 대신, &quot;주문 처리 중입니다. 재고를 확인하고 있습니다...&quot; 와 같은 중간 상태를 명확히 보여준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결과 알림&lt;/b&gt;: 최종적인 주문 성공 여부는 별도의 알림(푸시, 이메일)으로 알려주어, 사용자가 화면 앞에서 계속 기다리지 않도록 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;예약(Reservation) 또는 임대(Lease) 패턴&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;'주문' 버튼을 누르면 재고를 즉시 차감하는 대신, 해당 상품에 대해 짧은 시간(예: 10분) 동안 유효한 '예약'을 건다.&lt;/li&gt;
&lt;li&gt;이 예약 정보는 Redis와 같이 빠른 인메모리 저장소에 기록한다.&lt;/li&gt;
&lt;li&gt;사용자는 10분 안에 결제를 완료해야만 최종적으로 주문이 확정되고, 시간이 지나면 예약은 자동으로 해제되어 다른 사용자가 구매할 수 있게 된다.&lt;/li&gt;
&lt;li&gt;여기서 중요한 점은 사용자가 결제창을 지나는 모든 시간을 거친 뒤 최종 결제 버튼을 누른다는 것이다. 이때 품절 통보를 받게되면 사용자 경험 하락, 그렇다고 그 전에 트랜잭션으로 가두게되면 심각한 비효율이 발생한다. 이를 회피하는 것이 임대 패턴이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. MongoDB&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RDBMS (관계형 데이터베이스) 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS에서는 정규화 원칙에 따라 데이터를 별도의 테이블로 분리하여 저장한다. 따라서 'kim'이라는 사용자가 작성한 모든 글을 가져오려면, users 테이블과 posts 테이블을 user_id로 &lt;b&gt;JOIN&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB에서는 관련된 데이터를 하나의 &lt;b&gt;문서(Document)&lt;/b&gt; 안에 중첩된(nested) 형태로 함께 저장할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;_id&quot;: 1,
  &quot;username&quot;: &quot;kim&quot;,
  &quot;email&quot;: &quot;kim@example.com&quot;,
  &quot;posts&quot;: [
    {
      &quot;post_id&quot;: 101,
      &quot;title&quot;: &quot;첫 번째 글&quot;,
      &quot;content&quot;: &quot;안녕하세요.&quot;
    },
    {
      &quot;post_id&quot;: 102,
      &quot;title&quot;: &quot;두 번째 글&quot;,
      &quot;content&quot;: &quot;반갑습니다.&quot;
    }
  ]
}
{
  &quot;_id&quot;: 2,
  &quot;username&quot;: &quot;lee&quot;,
  &quot;email&quot;: &quot;lee@example.com&quot;,
  &quot;posts&quot;: [
    {
      &quot;post_id&quot;: 103,
      &quot;title&quot;: &quot;이 대리의 글&quot;,
      &quot;content&quot;: &quot;안녕하세요!&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 'kim' 사용자의 정보를 조회할 때, 별도의 JOIN 없이 하나의 문서를 읽는 것만으로 사용자의 모든 게시글 정보까지 한 번에 가져올 수 있어 매우 빠르고 효율적이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Redis (Remote Dictionary Server)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 In-Memory 데이터베이스이나 Key-Value 데이터 스토어이다. 모든 데이터를 메모리에 저장하여 압도적으로 빠른 속도를 자랑한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis의 주요 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;매우 빠른 속도&lt;/b&gt;: 데이터를 메모리에 저장하기 때문에 디스크 기반의 데이터베이스와는 비교할 수 없을 정도로 빠르다. 이 특징 때문에 주로 &lt;b&gt;캐시(Cache)&lt;/b&gt; 서버로 가장 많이 활용된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다양한 자료구조 지원&lt;/b&gt;: 단순한 Key-Value를 넘어 &lt;b&gt;Strings, Lists, Sets, Sorted Sets, Hashes&lt;/b&gt; 등 다양한 자료구조를 지원한다. 이는 개발자가 애플리케이션의 요구사항에 맞춰 데이터를 더 효율적으로 관리할 수 있게 해준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;영속성(Persistence) 지원&lt;/b&gt;: 인메모리 데이터베이스는 서버가 다운되면 데이터가 사라지는 단점이 있지만, Redis는 스냅샷(RDB)과 &lt;b&gt;AOF(Append Only File)&lt;/b&gt; 방식을 통해 데이터를 디스크에 저장하여 영속성을 확보할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;싱글 스레드(Single-Threaded)&lt;/b&gt;: Redis는 기본적으로 싱글 스레드로 동작한다. 이로 인해 Race Condition을 피할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다양한 활용성&lt;/b&gt;: 캐싱 외에도 실시간 랭킹 시스템, 세션 관리, 메시지 큐(Pub/Sub 기능 활용), 분산 락(Distributed Lock) 등 다방면에서 활용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 사용 시의 단점 및 주의사항&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;메모리 비용과 한계&lt;/b&gt;: 모든 데이터를 RAM에 저장하므로, 저장할 데이터의 양이 많아질수록 상당한 메모리 비용이 발생한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 유실 가능성&lt;/b&gt;: 영속성 기능을 제공하지만, 기본적으로는 휘발성 메모리를 사용한다. 스냅샷(RDB) 방식은 특정 시점 사이에 발생한 데이터 변경 사항이 유실될 수 있고, AOF 방식은 모든 쓰기 명령을 기록하므로 파일 크기가 커지고 복구 속도가 느려질 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;싱글 스레드의 한계&lt;/b&gt;: 싱글 스레드는 동시성 제어에 유리하지만, 반대로 말하면 한 번에 하나의 명령만 처리할 수 있다는 의미이다. 따라서 큰 자원을 잡아먹는 작업으로 인한 병목이 생길 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;복잡한 쿼리 불가능&lt;/b&gt;: Redis는 특정 Key에 대한 빠른 접근에 특화되어 있어, RDBMS처럼 데이터의 내용(Value)을 조건으로 검색하거나 복잡한 집계 연산을 수행하는 데는 적합하지 않다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>CS/Database System</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/241</guid>
      <comments>https://jun-n.tistory.com/241#entry241comment</comments>
      <pubDate>Tue, 19 Aug 2025 17:29:09 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 다익스트라 알고리즘에서의 경로 복원,  백준 11779번: 최소비용 구하기 2 C++ 풀이까지</title>
      <link>https://jun-n.tistory.com/240</link>
      <description>&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.encrypted.gg/1037&quot;&gt;https://blog.encrypted.gg/1037&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752384549858&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;[실전 알고리즘] 0x1D강 - 다익스트라 알고리즘&quot; data-og-description=&quot;네 반갑습니다. 이번에는 다익스트라 알고리즘을 해보겠습니다. 플로이드 알고리즘이랑 비슷하게 구현과 경로 복원 방법 모두 BOJ에 있는 문제를 가지고 직접 풀어볼거라 별도로 연습 문제 챕터&quot; data-og-host=&quot;blog.encrypted.gg&quot; data-og-source-url=&quot;https://blog.encrypted.gg/1037&quot; data-og-url=&quot;https://blog.encrypted.gg/1037&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/td1yF/hyZjugpDQf/4gXT12Db78elzvdztVkyv1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/O1wkJ/hyZjvTUAxh/W4IXlkOs0zupI4PwsrLPS1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/0htxy/hyZjvzDwss/c8SymzXvjUKCGIbPQhirFk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://blog.encrypted.gg/1037&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.encrypted.gg/1037&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/td1yF/hyZjugpDQf/4gXT12Db78elzvdztVkyv1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/O1wkJ/hyZjvTUAxh/W4IXlkOs0zupI4PwsrLPS1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/0htxy/hyZjvzDwss/c8SymzXvjUKCGIbPQhirFk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&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;[실전 알고리즘] 0x1D강 - 다익스트라 알고리즘&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;네 반갑습니다. 이번에는 다익스트라 알고리즘을 해보겠습니다. 플로이드 알고리즘이랑 비슷하게 구현과 경로 복원 방법 모두 BOJ에 있는 문제를 가지고 직접 풀어볼거라 별도로 연습 문제 챕터&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.encrypted.gg&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;바킹독님의 블로그를 참고하여 공부한 내용을 기록한 글입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;플로이드 알고리즘에서는 경로 복원을 위해 nxt[u][v]에 u -&amp;gt; v로 갈 때 u 바로 다음에 방문할 곳을 저장했다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;반면 다익스트라 알고리즘에서는 pre 테이블을 사용한다. 그리고 pre[u] 에는 시작점에서 u로 갈 때 u 직전에 방문해야 할 곳을 저장하면 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;두 알고리즘의 경로 복원 방식에 차이가 나는 이유가 뭘까? 이유는 알고리즘이 달라서일테고, 어떤 차이점때문에 플로이드 알고리즘에서는 nxt를 사용하고 다익스트라 알고리즘에서는 pre를 사용할까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;start -&amp;gt; u1 -&amp;gt; u2 -&amp;gt; v 와 같은 경로를 생각해보자. 다익스트라 알고리즘을 따라가다보면 u2까지 확정될테고, u2 -&amp;gt; v가 우선순위 큐에서 가장 짧아서 u2 -&amp;gt; v 경로가 확정 될 것이다. 이렇게 된 경우 start -&amp;gt; v 에서 v 이전에 u2를 방문하는 것은 명확하며, 구현적으로도 간단하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하지만 start -&amp;gt; v 에서 start 다음 u1, 그 다음 u2 이런 방식의 경로 저장은 정보 저장이 간단하지 않다. 추가적인 배열을 사용해서 역추적하며 따라가야 한다.&lt;/li&gt;
&lt;li&gt;즉, 다익스트라 알고리즘은 시작점에서 u1 u2 이런식으로 뻗어가며, 최종 v 까지의 경로를 확정하기에 pre를 사용하는 방식이 직관적이고 옳다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그렇다면 플로이드는 어땠을까? 플로이드 알고리즘은 u -&amp;gt; v와 u -&amp;gt; k -&amp;gt; v를 비교한다. u -&amp;gt; k -&amp;gt; v가 더 짧은 경로일 경우 u 바로 다음에 방문할 노드로 업데이트 하는 것이 훨씬 직관적이다.&amp;nbsp;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사실 플로이드에서 pre를 사용해 경로를 복원하는 방식은 엄청나게 비효율적이지는 않다.&lt;/li&gt;
&lt;li&gt;대신, 알고리즘의 흐름과 목적을 생각했을 때 pre를 사용하는 것은 자연스럽지 않은 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;플로이드에서는 원래 경로 복원에 자연스러운 nxt를 사용하고, 다익스트라에서는 알고리즘의 특성상 pre를 사용하는 편이 더 직관적이고 효율적이라는 것으로 알아두자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 경로 복원 알고리즘으로 돌아가자. pre[u]는 start -&amp;gt; u 에서 u 직전에 방문할 정점을 저장한다고 했다. 따라서 dist 테이블이 변경될 때 pre 테이블도 업데이트 하면 된다.&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;1225&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IJu2h/btsPguxtpji/Yx2aPWuERSfbLsKwLaLZq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IJu2h/btsPguxtpji/Yx2aPWuERSfbLsKwLaLZq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IJu2h/btsPguxtpji/Yx2aPWuERSfbLsKwLaLZq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIJu2h%2FbtsPguxtpji%2FYx2aPWuERSfbLsKwLaLZq1%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;1225&quot; height=&quot;441&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 1번 정점만 확정하고, 연결된 정점들을 우선순위 큐에 넣으면서 dist 테이블이 업데이트되고, pre[nxt]에는 cur을 넣게 된다.&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;cur : 현재 확정 중인 1번 노드이다.&lt;/li&gt;
&lt;li&gt;nxt : cur 노드와 연결된 노드이다.&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;바로 구현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/11779&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/11779&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1752386193122&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 다익스트라 11779 최소비용 구하기 2 **/
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;queue&amp;gt;
using namespace std;

int v, e, st, en;

vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; adj[1005];
const int INF = 0x3f3f3f3f;
int d[1005];   // 최단 거리 테이블
int pre[1005]; // 경로 복원 테이블

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin &amp;gt;&amp;gt; v &amp;gt;&amp;gt; e;
    fill(d, d + v + 1, INF);

    while (e--)
    {
        int u, v, w;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v &amp;gt;&amp;gt; w;
        adj[u].push_back({w, v});
    }

    priority_queue&amp;lt;pair&amp;lt;int, int&amp;gt;, vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;, greater&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;gt; pq;
    // 최소 힙

    cin &amp;gt;&amp;gt; st &amp;gt;&amp;gt; en;
    d[st] = 0;
    pq.push({d[st], st});

    while (!pq.empty())
    {
        auto cur = pq.top();
        pq.pop();

        // d[cur]과 우선순위 큐 정점이 다른 경우
        if (d[cur.second] != cur.first)
            continue;
        for (auto nxt : adj[cur.second])
        {
            if (d[nxt.second] &amp;lt;= d[cur.second] + nxt.first)
                continue;
            // cur을 거쳐 nxt로 가는게 더 가까운 경우
            d[nxt.second] = d[cur.second] + nxt.first;
            pq.push({d[nxt.second], nxt.second});

            // d가 업데이트 될 때 pre 업데이트
            pre[nxt.second] = cur.second;
        }
    }

    cout &amp;lt;&amp;lt; d[en] &amp;lt;&amp;lt; '\n';

    deque&amp;lt;int&amp;gt; ans;
    int cur = en;
    while (cur != st)
    {
        ans.push_front(cur);
        cur = pre[cur];
    }
    ans.push_front(cur);

    cout &amp;lt;&amp;lt; ans.size() &amp;lt;&amp;lt; '\n';
    for (auto x : ans)
        cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; ' ';
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최단 경로 테이블인 d가 업데이트 된다는 것은 현재 까지의 최단 경로 정보가 바뀐다는 것이고, 그 시점에서만 pre를 업데이트 해야 한다. 즉, d[cur]과 우선순위 큐에서 뽑은게 다른 경우 = 최단 경로가 아닌 경우에 업데이트하지 않게 조심해야 한다.&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘/알고리즘 지식</category>
      <category>다익스트라</category>
      <category>문제풀이</category>
      <category>백준</category>
      <category>알고리즘</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/240</guid>
      <comments>https://jun-n.tistory.com/240#entry240comment</comments>
      <pubDate>Sun, 13 Jul 2025 14:58:25 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 최단 거리 알고리즘 - 다익스트라, 백준 1753 C++ 풀이까지</title>
      <link>https://jun-n.tistory.com/239</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://blog.encrypted.gg/1037&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.encrypted.gg/1037&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1752230915483&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;[실전 알고리즘] 0x1D강 - 다익스트라 알고리즘&quot; data-og-description=&quot;네 반갑습니다. 이번에는 다익스트라 알고리즘을 해보겠습니다. 플로이드 알고리즘이랑 비슷하게 구현과 경로 복원 방법 모두 BOJ에 있는 문제를 가지고 직접 풀어볼거라 별도로 연습 문제 챕터&quot; data-og-host=&quot;blog.encrypted.gg&quot; data-og-source-url=&quot;https://blog.encrypted.gg/1037&quot; data-og-url=&quot;https://blog.encrypted.gg/1037&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/hgKIi/hyZjexn48I/eIE109SIysj9zXU3BL4g81/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/lES9x/hyZjuUBUYY/dtkvNkkhMDTZixHmPx7KIK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/un6kM/hyZjEbRHFS/t4zkhFqBmydbtOE3ytDdK0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://blog.encrypted.gg/1037&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.encrypted.gg/1037&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/hgKIi/hyZjexn48I/eIE109SIysj9zXU3BL4g81/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/lES9x/hyZjuUBUYY/dtkvNkkhMDTZixHmPx7KIK/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/un6kM/hyZjEbRHFS/t4zkhFqBmydbtOE3ytDdK0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&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;[실전 알고리즘] 0x1D강 - 다익스트라 알고리즘&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;네 반갑습니다. 이번에는 다익스트라 알고리즘을 해보겠습니다. 플로이드 알고리즘이랑 비슷하게 구현과 경로 복원 방법 모두 BOJ에 있는 문제를 가지고 직접 풀어볼거라 별도로 연습 문제 챕터&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.encrypted.gg&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바킹독님의 블로그를 참고하여 공부한 내용을 기록한 글입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Naive 다익스트라 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플로이드 알고리즘은 모든 점의 쌍 (All Pair)에 대한 최단 거리를 구하는 알고리즘이다.&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;알고리즘을 보자. 기본적으로 매 step마다 도달할 수 있는 정점 중 거리가 가장 가까운 정점을 구해서 그 거리를 확정하는 방식으로 동작한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0AoyD/btsPfIoVaRt/DjYaRv38LXwlXdH71zkIF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0AoyD/btsPfIoVaRt/DjYaRv38LXwlXdH71zkIF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0AoyD/btsPfIoVaRt/DjYaRv38LXwlXdH71zkIF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0AoyD%2FbtsPfIoVaRt%2FDjYaRv38LXwlXdH71zkIF0%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;404&quot; height=&quot;389&quot; data-origin-width=&quot;404&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 단계가 뭔지, 단계마다 도달할 수 있는 정점의 의미가 뭔지 위의 그래프를 예시로 생각해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;첫 단계는 도달할 수 있는 정점이 출발 단계인 1만 존재한다. 따라서 1 -&amp;gt; 1의 거리를 0으로 확정한다.&lt;/li&gt;
&lt;li&gt;1이 확정되었고, 1에서 도달할 수 있는 정점은 2 / 3 / 4가 존재한다. 가장 가까운 거리는 3이고 그와의 거리는 2로 확정한다.&lt;/li&gt;
&lt;li&gt;현재 확정된 정점은 1, 3 이다. 1, 3에서 도달할 수 있는 정점은 다음과 같다.&lt;br /&gt;a. 1 -&amp;gt; 2 (3)&lt;br /&gt;b. 1 -&amp;gt; 4 (5)&lt;br /&gt;c. 3 -&amp;gt; 4 (2 + 2)&lt;br /&gt;가장 짧은 거리는 1 -&amp;gt; 2이며 2로 거리를 확정한다.&lt;/li&gt;
&lt;li&gt;네 번째 step에서는 1, 2, 3이 방문상태이며 똑같이 도달할 수 있는 정점 중 가장 짧은 거리의 정점을 확정하면 된다.&lt;/li&gt;
&lt;/ol&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;991&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sOLN7/btsPf3GsPEQ/copTZmsiyGFm7cTCD7tzR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sOLN7/btsPf3GsPEQ/copTZmsiyGFm7cTCD7tzR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sOLN7/btsPf3GsPEQ/copTZmsiyGFm7cTCD7tzR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsOLN7%2FbtsPf3GsPEQ%2FcopTZmsiyGFm7cTCD7tzR0%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;991&quot; height=&quot;372&quot; data-origin-width=&quot;991&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 1, 2, 3번 정점까지 확정한 뒤 다음 정점을 계산할 때이다. 1 -&amp;gt; 4 까지 거리가 4이고 1 -&amp;gt; 5 까지 거리가 11이란걸 계산할 때 1번과 연결된 노드를 다 보고, 2번과 연결된 노드를 다 보는 방식으로 O(VE)로 구할 수 있겠지만 새 정점을 추가할 때 마다 미리 테이블에 거리를 계산해두면 O(V^2 + E)로 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 무슨말이냐면 일단 다시 처음으로 돌아서 1번 노드만 확정된 경우, 2 / 3 / 4 까지의 거리를 아래와 같이 미리 계산해둔다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nHFzw/btsPgyFTr0H/M0zxsuQs7nlptBVXkQrfSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nHFzw/btsPgyFTr0H/M0zxsuQs7nlptBVXkQrfSK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nHFzw/btsPgyFTr0H/M0zxsuQs7nlptBVXkQrfSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnHFzw%2FbtsPgyFTr0H%2FM0zxsuQs7nlptBVXkQrfSK%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;994&quot; height=&quot;372&quot; data-origin-width=&quot;994&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 최단 거리 테이블에서 최소값을 찾는다. d[3]이 2로 최소이므로 3번 정점을 확정할 수 있다. 그리고 3번 정점이 확정되었으니, 그 다음은 3번 노드에서 뻗어나가는 간선의 거리만을 새로 계산하면 된다. 결과는 아래와 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;372&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JcXVl/btsPgVgxZcb/SxzXfaykJQTdIN8kJKAKqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JcXVl/btsPgVgxZcb/SxzXfaykJQTdIN8kJKAKqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JcXVl/btsPgVgxZcb/SxzXfaykJQTdIN8kJKAKqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJcXVl%2FbtsPgVgxZcb%2FSxzXfaykJQTdIN8kJKAKqk%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;978&quot; height=&quot;372&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;372&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;알고리즘의 증명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고리즘을 다시 생각해보면, 첫 단계에서 1 -&amp;gt; 3이 가장 거리가 짧으므로 3번 정점을 확정할 수 있었다. 이게 왜 가능할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;귀류법으로 생각해서 중간에 다른 정점을 거쳐 거리 2보다 짧은 경로가 존재한다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 1 -&amp;gt; 3 보다 1 -&amp;gt; 2 -&amp;gt; 3이 더 짧은 것이다. 이 경우 중간에 음수 간선이 존재하는게 아닌 이상 1 -&amp;gt; 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&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;394&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b50Oco/btsPfJVGZ3O/j7f6kR9uYPFFObD6czInzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b50Oco/btsPfJVGZ3O/j7f6kR9uYPFFObD6czInzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b50Oco/btsPfJVGZ3O/j7f6kR9uYPFFObD6czInzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb50Oco%2FbtsPfJVGZ3O%2Fj7f6kR9uYPFFObD6czInzK%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;426&quot; height=&quot;394&quot; data-origin-width=&quot;426&quot; data-origin-height=&quot;394&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다익스트라 알고리즘에 따르면 첫 단계로 3번 노드를 확정시킨다. (1 -&amp;gt; 3 : 2)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 1 -&amp;gt; 2를 확정시키고 끝이나는데, 음수 간선이 존재하므로 사실은 1- &amp;gt; 2 -&amp;gt; 3이 더 짧은 거리이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;우선순위 큐를 이용한 현대의 다익스트라 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다익스트라 알고리즘은 매 번 가까운 정점을 뽑아내야 한다. 아래의 과정대로 구현하면 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;우선순위 큐에 (0, 시작점) 추가&lt;/li&gt;
&lt;li&gt;우선순위 큐에서 거리가 가장 작은 원소를 선택, 해당 거리가 최단 거리 테이블에 있는 값과 다르다면 해당 원소를 제거하고 3번 과정은 스킵&lt;/li&gt;
&lt;li&gt;선택된 정점 v에 대해, 현재 최단 거리 테이블에 있는 v의 이웃의 값보다 v를 경유해서 가는 것이 더 짧은 거리인 경우 최단 거리 테이블의 값 갱신. 우선순위 큐에 (거리, 이웃 정점) 추가&lt;/li&gt;
&lt;li&gt;우선순위 큐가 빌 때 까지 2~3 반복&lt;/li&gt;
&lt;/ol&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;1206&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tB07o/btsPfKAmV6I/0tTNWKKRkKQOoi2boLhyRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tB07o/btsPfKAmV6I/0tTNWKKRkKQOoi2boLhyRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tB07o/btsPfKAmV6I/0tTNWKKRkKQOoi2boLhyRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtB07o%2FbtsPfKAmV6I%2F0tTNWKKRkKQOoi2boLhyRK%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;1206&quot; height=&quot;417&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 정점만 확정된 초기 상태이다. 1번 원소가 선택되었고 해당 거리 (0)가 최단 거리 테이블 d[1] = 0과 다르지 않으므로 이웃인 2, 3, 4에 대해 현 시점의 최단 거리를 채워 넣는다. 여기서 최단 거리란 최단 거리 테이블 or 1번 정점을 경유하는 경우이다.&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;다음 차례에서는 (2, 3)을 뽑는다. d[3] = 2이므로 이웃 정점들을 본다. 3 -&amp;gt; 4가 존재하므로 d[3] + 2 = 4. 따라서 (4, 4)를 삽입하고 최단 거리 테이블 또한 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 우선순위 큐에는 4번 정점에 대한 노드가 두 개 존재하게 된다. 이래서 최단 거리 테이블과 우선순위 큐에서 뽑은 정점이 같은 값인지 확인하는 과정이 필요한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단계를 쭉 거치다보면 (5, 4)를 뽑게 될 것이고 d[4]는 5와 다르므로 (5, 4) 노드는 그냥 큐에서 제거하고 넘어가면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 우선순위 큐를 활용하는 경우 O(E log E)가 된다. 간선 1개당 최대 1개의 원소가 추가될 수 있기 때문이다. 바로 구현을 해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1753&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1753&lt;/a&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;pre id=&quot;code_1752235094363&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 다익스트라 1753 최단경로 **/
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;queue&amp;gt;
using namespace std;

int v, e, st;

vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt; adj[20005];
const int INF = 0x3f3f3f3f;
int d[20005]; // 최단 거리 테이블

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin &amp;gt;&amp;gt; v &amp;gt;&amp;gt; e &amp;gt;&amp;gt; st;
    fill(d, d + v + 1, INF);

    while (e--)
    {
        int u, v, w;
        cin &amp;gt;&amp;gt; u &amp;gt;&amp;gt; v &amp;gt;&amp;gt; w;
        adj[u].push_back({w, v});
    }

    priority_queue&amp;lt;pair&amp;lt;int, int&amp;gt;, vector&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;, greater&amp;lt;pair&amp;lt;int, int&amp;gt;&amp;gt;&amp;gt; pq;
    // 최소 힙

    d[st] = 0;
    pq.push({d[st], st});

    while (!pq.empty())
    {
        auto cur = pq.top();
        pq.pop();

        // d[cur]과 우선순위 큐 정점이 다른 경우
        if (d[cur.second] != cur.first)
            continue;
        for (auto nxt : adj[cur.second])
        {
            if (d[nxt.second] &amp;lt;= d[cur.second] + nxt.first)
                continue;
            // cur을 거쳐 nxt로 가는게 더 가까운 경우
            d[nxt.second] = d[cur.second] + nxt.first;
            pq.push({d[nxt.second], nxt.second});
        }
    }

    for (int i = 1; i &amp;lt;= v; i++)
    {
        if (d[i] == INF)
            cout &amp;lt;&amp;lt; &quot;INF\n&quot;;
        else
            cout &amp;lt;&amp;lt; d[i] &amp;lt;&amp;lt; '\n';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 시 다음 사항은 꼭 주의하자!&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;반드시 최소 힙으로 선언해야 한다. C++에서 최소 힙은 greater 이다.&lt;/li&gt;
&lt;li&gt;우선 순위 큐에 {거리, 정점 번호} 순으로 넣어 거리가 작은 것이 큰 우선순위를 가지게끔 설정해야 한다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>알고리즘/알고리즘 지식</category>
      <category>다익스트라</category>
      <category>백준</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/239</guid>
      <comments>https://jun-n.tistory.com/239#entry239comment</comments>
      <pubDate>Fri, 11 Jul 2025 21:01:27 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 최단 거리 알고리즘 - 플로이드, 백준 11404 C++ 풀이까지</title>
      <link>https://jun-n.tistory.com/238</link>
      <description>&lt;figure id=&quot;og_1751270618291&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;[실전 알고리즘] 0x1C강 - 플로이드 알고리즘&quot; data-og-description=&quot;안녕하세요, 이번에는 플로이드 알고리즘을 다루겠습니다. 이제 최단경로 알고리즘인 플로이드, 다익스트라 알고리즘만 다루고 나면 나름 길었던 그래프 파트가 끝납니다. 목차는 눈으로 한 번&quot; data-og-host=&quot;blog.encrypted.gg&quot; data-og-source-url=&quot;https://blog.encrypted.gg/1035&quot; data-og-url=&quot;https://blog.encrypted.gg/1035&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/biGH52/hyZfqddsu6/KYFJ3WKyjaLjb1IsFbOzX0/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/b8gg9H/hyZbouAVC7/BEM4LdQ7jUp3PoZLwnTsOk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/tqAab/hyZciOCydF/kQCDUE6oYpHKqJOrygvrr0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://blog.encrypted.gg/1035&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.encrypted.gg/1035&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/biGH52/hyZfqddsu6/KYFJ3WKyjaLjb1IsFbOzX0/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/b8gg9H/hyZbouAVC7/BEM4LdQ7jUp3PoZLwnTsOk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/tqAab/hyZciOCydF/kQCDUE6oYpHKqJOrygvrr0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&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;[실전 알고리즘] 0x1C강 - 플로이드 알고리즘&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;blog.encrypted.gg&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바킹독님의 블로그를 참고하여 공부한 내용을 기록한 글입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;729&quot; data-origin-height=&quot;666&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTbc5x/btsOYehBZyB/UJRqpWhqwTv9Z7qGOE4KWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTbc5x/btsOYehBZyB/UJRqpWhqwTv9Z7qGOE4KWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTbc5x/btsOYehBZyB/UJRqpWhqwTv9Z7qGOE4KWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTbc5x%2FbtsOYehBZyB%2FUJRqpWhqwTv9Z7qGOE4KWk%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;273&quot; height=&quot;666&quot; data-origin-width=&quot;729&quot; data-origin-height=&quot;666&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 그래프에서 직접 연결된 간선에 한해, 첫 단계의 최단 거리를 구해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YUuKo/btsOYNKrivg/BGPl0YTsiZyuZPbTWrgh7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YUuKo/btsOYNKrivg/BGPl0YTsiZyuZPbTWrgh7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YUuKo/btsOYNKrivg/BGPl0YTsiZyuZPbTWrgh7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYUuKo%2FbtsOYNKrivg%2FBGPl0YTsiZyuZPbTWrgh7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;240&quot; height=&quot;256&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;굉장히 쉽게 구할 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1에서 1로 가는 거리는 당연하게도 0이고 1에서 2는 바로 갈 수 있기에 그 가중치인 4를 적어주면 된다. 3에서 5의 경우 4를 거쳐서 간다면 8로 갈 수 있지만 현재는 하나의 간선만 고려하기에, 즉 바로 갈 수 있는 경로만 고려하기에 15로 구해진다.&lt;/p&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;즉, D[s][t]에 대해 D[s][t] / D[s][u] + D[u][t]를 비교하는 방식이며, u를 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;두 번째 단계의 테이블은 1번 정점을 거쳐서 갱신할 수 있는 거리는 갱신해버리면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/loA7D/btsOXkXaSP1/h0iVKjAFnrndhx2k98kfnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/loA7D/btsOXkXaSP1/h0iVKjAFnrndhx2k98kfnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/loA7D/btsOXkXaSP1/h0iVKjAFnrndhx2k98kfnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FloA7D%2FbtsOXkXaSP1%2Fh0iVKjAFnrndhx2k98kfnK%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;292&quot; height=&quot;314&quot; data-origin-width=&quot;608&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 3-&amp;gt;2를 생각해보면, 기존 무한대에서 1을 거치는 5로 업데이트 되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 문제로 구현해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 삼중 for문을 때려버려도 된다.&lt;/p&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.acmicpc.net/problem/11404&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/11404&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1751273924800&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 최단거리 11404 플로이드 **/
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;
using namespace std;

int n, m;
int dist[105][105];
const int INF = 0x3f3f3f3f;

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;

    for (int i = 1; i &amp;lt;= n; i++)
        fill(dist[i], dist[i] + n + 1, INF);

    for (int i = 0; i &amp;lt; m; i++)
    {
        int a, b, c;
        cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
        dist[a][b] = min(dist[a][b], c);
    }

    for (int i = 1; i &amp;lt;= n; i++)
        dist[i][i] = 0;

    // k번째 원소가 중간에 거치는 원소
    for (int k = 1; k &amp;lt;= n; k++)
        for (int i = 1; i &amp;lt;= n; i++)
            for (int j = 1; j &amp;lt;= n; j++)
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);

    for (int i = 1; i &amp;lt;= n; i++)
    {
        for (int j = 1; j &amp;lt;= n; j++)
        {
            if (dist[i][j] == INF)
                cout &amp;lt;&amp;lt; &quot;0 &quot;;
            else
                cout &amp;lt;&amp;lt; dist[i][j] &amp;lt;&amp;lt; ' ';
        }
        cout &amp;lt;&amp;lt; '\n';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 시 주의사항&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;INF는 0x7f7f7f7f로 int형의 최대 값으로 선언하게 되면, 두 INF를 더하는 과정에서 오버플로우가 생긴다.&lt;/li&gt;
&lt;li&gt;삼중 for 문에서 당연하게도 중간에 거쳐갈 노드 k는 가장 바깥으로 빼야 한다.&lt;/li&gt;
&lt;li&gt;adj를 인접행렬로 선언하면 바로 최단 거리 저장용 테이블로 사용할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;경로 복원&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;a에서 b까지 최단 거리로 가려면 a에서 어느 정점으로 가야하는지 저장해야 하는데, 이를 nxt 테이블이라 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nxt 테이블은 최단 거리 테이블을 채우면서 같이 채울 수 있다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ckki4/btsOYUJNno4/FQnGzYlvKf1R92kIr1C3Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ckki4/btsOYUJNno4/FQnGzYlvKf1R92kIr1C3Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ckki4/btsOYUJNno4/FQnGzYlvKf1R92kIr1C3Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCkki4%2FbtsOYUJNno4%2FFQnGzYlvKf1R92kIr1C3Bk%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;1766&quot; height=&quot;653&quot; data-origin-width=&quot;1766&quot; data-origin-height=&quot;653&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최단 거리 테이블에 2-&amp;gt;1 경로를 저장했고, 해당 경로의 중간에 거친 노드는 nxt 테이블의 [2][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;다음 단계부터가 중요하다. 다음 단계는 1번 노드를 거치는 최단 거리를 계산하는 단계인데,&amp;nbsp;2-&amp;gt;3을 생각해보면 2-&amp;gt;1-&amp;gt;3이 최단거리이므로 nxt 테이블에는 nxt[2][3]=1이 저장되면 될 것이다. 그대로 채워보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 1이 저장되는 이유는 1번 노드를 방문하는 단계니까 1을 넣는 것이 아니다. nxt[s][t]를 nxt[s][1]로 대체하는 것이다. 왜인지는 다음 단계에서 더 자세히 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgEBAG/btsOXfgVZe8/f4pgQduKee7KNkRe1iFF2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgEBAG/btsOXfgVZe8/f4pgQduKee7KNkRe1iFF2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgEBAG/btsOXfgVZe8/f4pgQduKee7KNkRe1iFF2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgEBAG%2FbtsOXfgVZe8%2Ff4pgQduKee7KNkRe1iFF2K%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;1760&quot; height=&quot;645&quot; data-origin-width=&quot;1760&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음 두 번째 단계는 2번 노드도 거치는 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 신경써야 할 부분은 nxt를 2로만 채우는 것이 아니라, s-&amp;gt;t 보다 s-&amp;gt;2 + 2-&amp;gt;t인 경우. 즉 이번 단계에서 갱신되는 노드에 대해 nxt[s][t]를 nxt[s][2]로 채워야 한다는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1775&quot; data-origin-height=&quot;611&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfJvzU/btsOWieaMTQ/itK71p6Y13hAKKdKZfVYF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfJvzU/btsOWieaMTQ/itK71p6Y13hAKKdKZfVYF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfJvzU/btsOWieaMTQ/itK71p6Y13hAKKdKZfVYF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfJvzU%2FbtsOWieaMTQ%2FitK71p6Y13hAKKdKZfVYF0%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;1775&quot; height=&quot;611&quot; data-origin-width=&quot;1775&quot; data-origin-height=&quot;611&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;nxt는 a에서 b로 갈 때 어느 정점으로 먼저 갈지를 저장하는 배열이다. 3-&amp;gt;5를 생각해보자. 이번 단계에서 3-&amp;gt;2, 2-&amp;gt;5가 더 효율적임을 알아서 갱신했고, 이에 따라 nxt 배열 또한 3에서 5를 갈 때 어디로 먼저 가면 될까를 저장해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장될 값은 1일테고, 이를 구현적으로는 nxt[3][2]로 표현하면 된다. 3에서 5로 갈 때 가장 먼저 거쳐야 할 값은 3에서 2로 갈 때 거치는 값 = 1인 것이다. 바로 다음 단계도 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1773&quot; data-origin-height=&quot;642&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NjFOT/btsOYe9TeXG/5SQ4uteEqk4wEmtemGaBS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NjFOT/btsOYe9TeXG/5SQ4uteEqk4wEmtemGaBS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NjFOT/btsOYe9TeXG/5SQ4uteEqk4wEmtemGaBS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNjFOT%2FbtsOYe9TeXG%2F5SQ4uteEqk4wEmtemGaBS0%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;1773&quot; height=&quot;642&quot; data-origin-width=&quot;1773&quot; data-origin-height=&quot;642&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번 노드를 거치는 경우는 아무 일도 없어서 갱신이 없다. 다음도 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;1757&quot; data-origin-height=&quot;656&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGrx6O/btsOZhEmUVs/AhbdysljlJiLp8CrCWXoo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGrx6O/btsOZhEmUVs/AhbdysljlJiLp8CrCWXoo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGrx6O/btsOZhEmUVs/AhbdysljlJiLp8CrCWXoo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGrx6O%2FbtsOZhEmUVs%2FAhbdysljlJiLp8CrCWXoo0%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;1757&quot; height=&quot;656&quot; data-origin-width=&quot;1757&quot; data-origin-height=&quot;656&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 3-&amp;gt;5가 특이하다. 이 전까지는 3-&amp;gt;1-&amp;gt;2-&amp;gt;5가 최단 거리였고, nxt에는 1이 저장되어 있었다. 4번 노드를 거치게 되는 경우를 생각해봐도 3-&amp;gt;1-&amp;gt;4-&amp;gt;5이므로 똑같이 nxt에도 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;이제 nxt 배열은 다 구했고, 이를 바탕으로 경로를 복원하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;똑같이 3-&amp;gt;5로 복원해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nxt[3][5] = 1이다. 따라서 3 -&amp;gt; 1 경로는 자명하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음은? 1에서 5까지 갈 때 그 다음에 갈 곳은 nxt[1][5]에 저장되어 있다. nxt[1][5]는 4이므로 3 -&amp;gt; 1 -&amp;gt; 4 까지는 구했고, 그 다음은 nxt[4][5] = 5로 3 -&amp;gt; 1 -&amp;gt; 4 -&amp;gt; 5의 경로 완성이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시, 정리해보자면 nxt[i][j]는 i-&amp;gt;j로 갈 때 가장 먼저 방문해야 할 곳이다. 따라서 k번째 노드를 경유하는게 더 최단 거리인 경우&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nxt[i][j] = nxt[i][k]로 갱신하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여기서 nxt[i][k]를 단순히 암기해서는 안된다. 무수한 알고리즘들을 외우는 방식은 코딩 테스트를 통과할 수 없고 원리를 이해해야 한다.&lt;/li&gt;
&lt;li&gt;nxt[i][j]는 i가 시작점이다. i -&amp;gt; ... -&amp;gt; k -&amp;gt; ... -&amp;gt; j 의 경로에서 i 바로 뒤에 위치한 노드는 nxt[i][k]로 구할 수 있기에 nxt[i][k]로 갱신하는 것이다.&lt;/li&gt;
&lt;li&gt;케이스를 나눠서 좀 더 명확히 이해해보자.&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 i -&amp;gt; k 사이에 노드가 없다면 nxt[i][k] = k 일 것이다.&lt;/li&gt;
&lt;li&gt;i -&amp;gt; u -&amp;gt; k 라면 nxt[i][k] = u 일텐데, 이 값은 언제 초기화되었을까? 가장 바깥쪽 loop, 즉 경유 노드가 u일 때 i -&amp;gt; k 경로를 구하면서 nxt[i][k] = u로 초기화되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;a href=&quot;https://www.acmicpc.net/problem/11780&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/11780&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 크게 어려운 부분은 없다.&lt;/p&gt;
&lt;pre id=&quot;code_1751351000357&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** 최단거리 11404 플로이드 **/
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;
using namespace std;

int n, m;
int dist[105][105];
int nxt[105][105];
const int INF = 0x3f3f3f3f;

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;

    for (int i = 1; i &amp;lt;= n; i++)
        fill(dist[i], dist[i] + n + 1, INF);

    for (int i = 0; i &amp;lt; m; i++)
    {
        int a, b, c;
        cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
        dist[a][b] = min(dist[a][b], c);
        // nxt도 초기화. 첫 단계이므로 a-&amp;gt;b 간선이 있으면 nxt[a][b] = b
        nxt[a][b] = b;
    }

    for (int i = 1; i &amp;lt;= n; i++)
        dist[i][i] = 0;

    // k번째 원소가 중간에 거치는 원소
    for (int k = 1; k &amp;lt;= n; k++)
        for (int i = 1; i &amp;lt;= n; i++)
            for (int j = 1; j &amp;lt;= n; j++)
            {
                // k 번째 원소를 거치는 경우가 더 최단 거리인 경우
                if (dist[i][j] &amp;gt; dist[i][k] + dist[k][j])
                {
                    // nxt[i][j] = i -&amp;gt; j 로 갈 때 가장 먼저 방문할 노드이므로 nxt[i][k] 저장
                    dist[i][j] = dist[i][k] + dist[k][j];
                    nxt[i][j] = nxt[i][k];
                }
            }

    for (int i = 1; i &amp;lt;= n; i++)
    {
        for (int j = 1; j &amp;lt;= n; j++)
        {
            if (dist[i][j] == INF)
                cout &amp;lt;&amp;lt; &quot;0 &quot;;
            else
                cout &amp;lt;&amp;lt; dist[i][j] &amp;lt;&amp;lt; ' ';
        }
        cout &amp;lt;&amp;lt; '\n';
    }

    for (int i = 1; i &amp;lt;= n; i++)
    {
        for (int j = 1; j &amp;lt;= n; j++)
        {
            if (dist[i][j] == 0 || dist[i][j] == INF)
            {
                cout &amp;lt;&amp;lt; &quot;0\n&quot;;
                continue;
            }

            vector&amp;lt;int&amp;gt; path;
            int st = i;
            while (st != j)
            {
                path.push_back(st);
                st = nxt[st][j];
            }
            path.push_back(j);
            cout &amp;lt;&amp;lt; path.size() &amp;lt;&amp;lt; ' ';

            for (auto x : path)
                cout &amp;lt;&amp;lt; x &amp;lt;&amp;lt; ' ';
            cout &amp;lt;&amp;lt; '\n';
        }
    }
}&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;</description>
      <category>알고리즘/알고리즘 지식</category>
      <category>C++</category>
      <category>백준</category>
      <category>알고리즘</category>
      <category>최단거리</category>
      <category>플로이드</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/238</guid>
      <comments>https://jun-n.tistory.com/238#entry238comment</comments>
      <pubDate>Mon, 30 Jun 2025 18:00:15 +0900</pubDate>
    </item>
    <item>
      <title>[C++] C++에서의 비교 함수 설정법과 람다 표현식</title>
      <link>https://jun-n.tistory.com/237</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;C++의 sort, stable_sort, set, map는 비교 함수를 설정할 수 있다. 정렬 기준을 설정하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 stable_sort는 동일한 값을 가진 원소들의 상대적 순서(삽입 순서)가 정렬 후에도 보존되는 정렬 알고리즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++에서 비교 함수가 true를 반환하면&amp;nbsp; 첫 번째 인자가 더 앞에 오게 한다. 즉, comp(a, b)가 true를 반환하면 a가 앞에 오는 것이다. 따라서 a&amp;gt;b를 비교 함수로 설정하면 a가 더 클 때 true이고 더 큰게 앞으로 오므로 내림차순 정렬이 된다.&lt;/p&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_1744179508007&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[캡처](매개변수) -&amp;gt; 반환타입 { 함수 본문 }&lt;/code&gt;&lt;/pre&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;매개변수 목록 ( ): 일반 함수의 매개변수와 동일&lt;/li&gt;
&lt;li&gt;반환 타입 -&amp;gt; 타입: 생략 가능, 컴파일러가 추론 가능&lt;/li&gt;
&lt;li&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;예시로 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1744179614087&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;

struct Person {
    std::string name;
    int age;
    float height;
};

int main() {
    std::vector&amp;lt;int&amp;gt; numbers = {5, 2, 8, 1, 9};
    
    // 1. 기본 내림차순 정렬
    std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
        return a &amp;gt; b;  // 내림차순: 큰 값이 앞에 오도록
    });
    
    // 2. 짝수가 홀수보다 앞에 오도록 정렬, 같은 종류 내에서는 오름차순
    std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
        if (a % 2 == b % 2) {  // 둘 다 짝수이거나 둘 다 홀수인 경우
            return a &amp;lt; b;      // 오름차순 정렬
        }
        return a % 2 == 0;     // a가 짝수면 true (a가 앞에 옴)
    });
    
    // 3. 외부 변수 캡처하여 정렬 (특정 값과의 거리 기준)
    int pivot = 5;
    std::sort(numbers.begin(), numbers.end(), [pivot](int a, int b) {
        return std::abs(a - pivot) &amp;lt; std::abs(b - pivot);  // pivot과 가까운 값이 앞에 오도록
    });
    
    // 4. 구조체 정렬
    std::vector&amp;lt;Person&amp;gt; people = {
        {&quot;Alice&quot;, 25, 165.5},
        {&quot;Bob&quot;, 30, 180.0},
        {&quot;Charlie&quot;, 25, 175.0}
    };
    
    // 나이 기준 오름차순, 같은 나이면 키 기준 내림차순
    std::sort(people.begin(), people.end(), [](const Person&amp;amp; a, const Person&amp;amp; b) {
        if (a.age == b.age) {
            return a.height &amp;gt; b.height;  // 키는 내림차순
        }
        return a.age &amp;lt; b.age;  // 나이는 오름차순
    });
    
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>프로그래밍 언어/C++\C</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/237</guid>
      <comments>https://jun-n.tistory.com/237#entry237comment</comments>
      <pubDate>Wed, 9 Apr 2025 15:21:01 +0900</pubDate>
    </item>
    <item>
      <title>[알고리즘] 투포인터 - 백준 2230: 수 고르기, 백준 1806: 부분합</title>
      <link>https://jun-n.tistory.com/236</link>
      <description>&lt;figure id=&quot;og_1742822419713&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;[실전 알고리즘] 0x14강 - 투 포인터&quot; data-og-description=&quot;안녕하세요, 이게 강의 목차를 16진수로 붙이니까 혼동을 주는데 이번 강의가 0x14강이니까 오리엔테이션은 빼고 20번째입니다. 아직 갈길이 좀 멀긴 하지만 꽤 많이 온 것 같습니다. 여러분들도 &quot; data-og-host=&quot;blog.encrypted.gg&quot; data-og-source-url=&quot;https://blog.encrypted.gg/1004&quot; data-og-url=&quot;https://blog.encrypted.gg/1004&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/D79nV/hyYvldgCqT/k6i8AtjlHN9ntuzE4ekeNk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/XX4DZ/hyYum4DWL1/6WaJRGumTTS4Od1E6GVOJk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bKuavt/hyYuhCBJ4g/lwSPb9tipSSHtmon26Zcw0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://blog.encrypted.gg/1004&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.encrypted.gg/1004&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/D79nV/hyYvldgCqT/k6i8AtjlHN9ntuzE4ekeNk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/XX4DZ/hyYum4DWL1/6WaJRGumTTS4Od1E6GVOJk/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bKuavt/hyYuhCBJ4g/lwSPb9tipSSHtmon26Zcw0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&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;[실전 알고리즘] 0x14강 - 투 포인터&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 이게 강의 목차를 16진수로 붙이니까 혼동을 주는데 이번 강의가 0x14강이니까 오리엔테이션은 빼고 20번째입니다. 아직 갈길이 좀 멀긴 하지만 꽤 많이 온 것 같습니다. 여러분들도&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.encrypted.gg&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바킹독님의 블로그를 참고하여 공부한 내용을 기록한 글입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;투포인터 알고리즘은 배열에서 원래 이중 for문으로 O(N^2)으로 처리되는 작업을 2개의 포인터의 움직임으로 O(N)에 해결하는 알고리즘이다. 어떻게 N이나 줄일 수 있냐면, 일반적인 이중 for문에서 i = 0일 때 j가 0부터 n-1까지 돌고, i = 1일 때 j가 0부터 n-1까지 도는 방식, 즉 각 i에 대해서 j가 0부터 n-1까지 도는 상황을 생각해보면, i = 0일 때 계산하면서 얻은 정보가 i = 1일 때에 전혀 쓰이지가 않는다. 그런데 투 포인터에서는 i = 0일 때 계산하면서 얻은 정보를 i = 1일 때 활용한다. 그 정보가 포인터의 이동으로 나타나는 것이다. 바로 문제로 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #333333; text-align: justify;&quot; data-ke-size=&quot;size20&quot;&gt;백준 2230: 수 고르기&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #333333; text-align: justify;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2230&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/2230&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수열에서 두 수를 골랐을 때 그 차이가 M 이상이면서 제일 작은 경우를 구하면 된다. 이전 이분 탐색에서 풀었던, 합이 0 되는 두 수 찾기와 거의 똑같다. lower_bound(~, ~, x + M)을 돌리면 된다. 이를 투 포인터로 풀어볼건데, 먼저 이중 for 문으로 풀어보자.&lt;/p&gt;
&lt;pre id=&quot;code_1742823085950&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;for(int i=0; i&amp;lt;n; i++)
	for(int j=i; j&amp;lt;n; j++)
    	if(a[j]-a[i] &amp;gt;= m)
        	ans = min(ans, a[j]-a[i]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매우 간단하지만 시간 복잡도가 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;먼저 i가 증가할수록 a[j] - a[i]가 m 이상이 되는 최초의 지점 j 또한 증가한다. 정렬된 배열이므로 당연하게 유추할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음으로, 각 i에 대해 a[j] - a[i]가 m 이상이 되는 최초의 지점 j를 찾았다면 j+1, j+2, ... 는 확인할 필요가 없다. 이것도 당연하다. 이렇게 당연한 두 직관을 가지고 투 포인터 풀이로 개선할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2 3 9 13 22, M = 6인 예시로 생각해보자. 먼저 두 포인터 st, en을 arr[0] = 2에 두고 en - st &amp;gt;= m인 최초의 en을 찾아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2(st) 3 9(en) 13 22, ans = 7이 된다. 지금 당장은 en 포인터를 뒤로 옮길 필요가 없다. 어차피 st 포인터의 값인 2에 대해서는 답이 될 수 있는 en이 존재하지 않기 때문이다. 따라서 st 포인터를 옮겨주자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2 3(st) 9(en) 13 22 -&amp;gt; en - st가 6 이상이다. 따라서 min = 6이 되고 min = M 이므로 답은 이미 나왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 뒤는 더 볼 필요가 없을 것 같고 대충 2 3 9(st) 13 22(en)이 되면 en이 끝까지 왔으므로 더 볼 필요도 없이 종료하면 된다. 바로 코드로 짜보자.&lt;/p&gt;
&lt;pre id=&quot;code_1742824099877&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** Two Pointer 2230: 수 고르기 **/
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;climits&amp;gt;
using namespace std;

int n, m;
long long arr[100100];
long long ans = LLONG_MAX;

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;

    for (int i = 0; i &amp;lt; n; i++)
        cin &amp;gt;&amp;gt; arr[i];

    sort(arr, arr + n);

    int en = 0;
    for (int st = 0; st &amp;lt; n; st++)
    {
        while (en &amp;lt; n &amp;amp;&amp;amp; arr[en] - arr[st] &amp;lt; m)
            en++;
        // en을 찾음, en = n-1 일 때 en++을 하고 종료하므로 en == n인지 체크해야 함.
        if (en == n)
            break;
        ans = min(ans, arr[en] - arr[st]);
    }
    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 위에서 생각해뒀던 로직을 그대로 구현하면 돼서 어려울 부분은&lt;/p&gt;
&lt;div style=&quot;background-color: #1f1f1f; color: #cccccc;&quot;&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #c586c0;&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;en&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;n&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;arr&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;en&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;arr&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;st&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;m&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;)&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;en&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #6a9955;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; // en을 찾음, en = n-1 일 때 en++을 하고 종료하므로 en == n인지 체크해야 함.&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #c586c0;&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;en&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;n&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;)&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #c586c0;&quot;&gt;break&lt;/span&gt;&lt;span style=&quot;color: #cccccc;&quot;&gt;;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도가 아닐까 싶다. 천천히 머리속으로 시뮬레이션을 돌려가면서 코드를 짜면 빼먹을 부분은 없다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;백준 1806: 부분합&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1806&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/1806&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수열에서 연속된 수들의 부분합 중 그 합이 S 이상이 되는 것 중, 가장 짧은 것의 길이를 구하면 된다. 아까랑 거의 똑같은데 조건만 좀 찾아보자. st pointer는 st ~ en 까지 더해서 합이 S 이상이 되면 st는 멈추고 en을 늘리면 된다. 바로 구현해보자.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현은 일단 ans에 몇 자리인지를 확인해야 하고 다른 변수를 둬서 st ~ en의 합도 저장해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;st ~ en 을 tmp에 저장한다고 하면, tmp를 구하고 en++을 하게되면 의도한 것 보다 한 칸 더 계산하게 된다. 아무튼 구현해보고 답이 틀리면 디버깅 해보면 바로 알 수 있을 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1742891126429&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/** Two Pointer 1806 부분합 **/
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;climits&amp;gt;
using namespace std;

int n, s;
int arr[100100];
int ans = INT_MAX;

int main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; s;

    for (int i = 0; i &amp;lt; n; i++)
        cin &amp;gt;&amp;gt; arr[i];

    int en = 0, tmp = arr[0];
    for (int st = 0; st &amp;lt; n; st++)
    {
        while (en &amp;lt; n &amp;amp;&amp;amp; tmp &amp;lt; s)
        {
            en++;
            if (en != n)
                tmp += arr[en];
        }
        if (en == n)
            break;
        ans = min(ans, en - st + 1);
        tmp -= arr[st];
    }
    if (ans == INT_MAX)
        ans = 0;
    cout &amp;lt;&amp;lt; ans &amp;lt;&amp;lt; '\n';
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘/알고리즘 지식</category>
      <author>조준왕</author>
      <guid isPermaLink="true">https://jun-n.tistory.com/236</guid>
      <comments>https://jun-n.tistory.com/236#entry236comment</comments>
      <pubDate>Tue, 25 Mar 2025 17:27:37 +0900</pubDate>
    </item>
  </channel>
</rss>