[{"content":"문제 게임 프로젝트의 수학 라이브러리에서 World Matrix의 회전 부분을 아래와 같이 구성했다.\n1 2 3 4 5 6 7 8 Float4x4 BuildWorld(const Transform\u0026amp; t) { return BuildScale(t.scale) * BuildRotationZ(t.rotation.z) // Roll * BuildRotationX(t.rotation.x) // Pitch * BuildRotationY(t.rotation.y) // Yaw * BuildTranslation(t.position); } 회전만 떼어내면 $R_z(r) \\cdot R_x(p) \\cdot R_y(\\theta)$ 순서다. 왜 하필 이 순서인가? 오일러 각은 어떤 순서로도 곱할 수 있는데, 이 배치가 FPS 카메라에 적합한 이유는 무엇인가?\n배경 규약 본 프로젝트의 수학 라이브러리는 다음을 따른다.\n좌표계: Left-handed 행렬 저장: row-major 벡터-행렬 곱: row-vector v * M (벡터가 행렬의 왼쪽에 옴) row-vector 규약에서 $v \\cdot A \\cdot B \\cdot C$는 왼쪽부터 순차 적용된다. 결합법칙에 의해\n$$ v \\cdot A \\cdot B \\cdot C = ((v \\cdot A) \\cdot B) \\cdot C $$가 성립하고, 가장 마지막에 곱해지는 행렬이 최종 결과에 직접 작용한다. 이 관찰이 이후 논의의 핵심이다.\n오해: \u0026ldquo;회전 행렬은 그 자체로 월드 축 회전이다\u0026rdquo; $R_y(\\theta)$는 행렬 그 자체로 보면 월드 Y축을 중심으로 도는 회전 행렬이다. $R_x(\\theta)$, $R_z(\\theta)$도 마찬가지다. 이 성질은 다른 행렬과의 곱에 무관하다.\n하지만 \u0026ldquo;각 행렬이 월드 축 회전\u0026quot;이라는 사실이 \u0026ldquo;해당 파라미터를 변화시켰을 때 결과 벡터가 월드 축 주위로 돈다\u0026quot;를 의미하지는 않는다. 행렬 곱은 비가환이기 때문이다. 파라미터 변화에 따른 궤적은 곱 순서에 따라 완전히 달라진다.\n핵심 명제 $f(\\theta) = v \\cdot A \\cdot R_y(\\theta) \\cdot B$ 형태에서 $\\theta$를 변화시킬 때 관찰되는 회전축은 \u0026ldquo;월드 Y축을 $B$로 변환한 축\u0026quot;이다. $B = I$일 때만 관찰 축이 월드 Y축과 일치한다.\n즉, 곱 체인의 맨 오른쪽(마지막) 슬롯에 놓인 회전 행렬만이 \u0026ldquo;파라미터를 바꿀 때 월드 축 회전으로 관찰되는\u0026rdquo; 성질을 가진다.\n수식으로 증명 카메라 forward 벡터를 $f_0 = (0, 0, 1)$이라 하자.\nCase 1: $M = R_z(r) \\cdot R_x(p) \\cdot R_y(\\theta)$ $$ f(\\theta) = f_0 \\cdot R_z(r) \\cdot R_x(p) \\cdot R_y(\\theta) $$결합법칙으로 묶으면\n$$ f(\\theta) = \\underbrace{\\left[ f_0 \\cdot R_z(r) \\cdot R_x(p) \\right]}_{u\\;(\\theta\\text{에 무관})} \\cdot R_y(\\theta) = u \\cdot R_y(\\theta) $$$u$는 $\\theta$에 무관한 상수 벡터다. $u \\cdot R_y(\\theta)$는 정의상 $u$를 월드 Y축 주위로 돌리는 궤적이다. 즉, $\\theta$가 변할 때 forward 벡터는 월드 Y축 기준의 원을 그린다.\nCase 2: $M = R_y(\\theta) \\cdot R_x(p) \\cdot R_z(r)$ $$ f(\\theta) = f_0 \\cdot R_y(\\theta) \\cdot R_x(p) \\cdot R_z(r) $$묶으면\n$$ f(\\theta) = \\underbrace{\\left[ f_0 \\cdot R_y(\\theta) \\right]}_{g(\\theta)} \\cdot \\underbrace{\\left[ R_x(p) \\cdot R_z(r) \\right]}_{B\\;(\\theta\\text{에 무관})} $$$g(\\theta)$는 월드 Y축 기준 원이지만, 그 결과를 다시 상수 변환 $B$에 통과시킨다. 원을 선형 변환하면 또 다른 원(또는 타원)이 되는데, 그 축은 \u0026ldquo;월드 Y축을 $B$로 변환한 축\u0026quot;이 된다. 일반적으로 이 축은 월드 Y축과 일치하지 않는다.\n일반 원리 임의의 $f(\\theta) = A \\cdot R_y(\\theta) \\cdot B$에서 $\\theta$ 파라미터 변화 시 관찰되는 회전축은\n$$ \\text{(관찰 축)} = (\\text{월드 Y축}) \\cdot B $$$B = I$, 즉 $R_y$가 곱 체인의 맨 오른쪽에 있을 때만 관찰 축이 월드 Y축이 된다.\n구체 예시: pitch $45°$, yaw $90°$ 같은 파라미터라도 곱 순서에 따라 결과 행렬이 다름을 수치로 확인해 보자. $R_x(45°)$와 $R_y(90°)$를 row-vector 규약으로 두면\n$$ R_x(45°) = \\begin{pmatrix} 1 \u0026 0 \u0026 0 \\\\ 0 \u0026 \\tfrac{\\sqrt{2}}{2} \u0026 \\tfrac{\\sqrt{2}}{2} \\\\ 0 \u0026 -\\tfrac{\\sqrt{2}}{2} \u0026 \\tfrac{\\sqrt{2}}{2} \\end{pmatrix} , \\quad R_y(90°) = \\begin{pmatrix} 0 \u0026 0 \u0026 -1 \\\\ 0 \u0026 1 \u0026 0 \\\\ 1 \u0026 0 \u0026 0 \\end{pmatrix} $$$R_x(45°) \\cdot R_y(90°)$ (pitch 후 yaw — yaw가 맨 오른쪽) $$ R_x(45°) \\cdot R_y(90°) = \\begin{pmatrix} 0 \u0026 0 \u0026 -1 \\\\ \\tfrac{\\sqrt{2}}{2} \u0026 \\tfrac{\\sqrt{2}}{2} \u0026 0 \\\\ \\tfrac{\\sqrt{2}}{2} \u0026 -\\tfrac{\\sqrt{2}}{2} \u0026 0 \\end{pmatrix} $$$f_0 = (0, 0, 1)$을 적용하면 $f = (0,0,1) \\cdot M$은 $M$의 3행과 같으므로\n$$ f = \\left( \\tfrac{\\sqrt{2}}{2},\\; -\\tfrac{\\sqrt{2}}{2},\\; 0 \\right) $$forward의 Y 성분은 $-\\tfrac{\\sqrt{2}}{2}$로 pitch $45°$ 만큼 정확히 아래를 본다. yaw가 어떻게 변해도 pitch 성분이 그대로 보존된다.\n$R_y(90°) \\cdot R_x(45°)$ (yaw 후 pitch — pitch가 맨 오른쪽) $$ R_y(90°) \\cdot R_x(45°) = \\begin{pmatrix} 0 \u0026 \\tfrac{\\sqrt{2}}{2} \u0026 -\\tfrac{\\sqrt{2}}{2} \\\\ 0 \u0026 \\tfrac{\\sqrt{2}}{2} \u0026 \\tfrac{\\sqrt{2}}{2} \\\\ 1 \u0026 0 \u0026 0 \\end{pmatrix} $$두 결과는 완전히 다르다. 동일한 pitch, yaw 값이라도 곱 순서에 따라 forward 벡터가 가리키는 방향이 달라진다.\nFPS 카메라에 $R_z \\cdot R_x \\cdot R_y$를 적용하는 이유 핵심 명제에 비추어 각 슬롯의 의미를 해석한다.\nYaw (Y) — 맨 오른쪽 FPS에서 마우스 좌우 조작은 지평선을 기울이지 않아야 한다. 이는 yaw 파라미터 변화 시 forward 벡터의 궤적이 월드 Y축 기준 원이어야 한다는 요구이고, 핵심 명제에 따라 $R_y$는 곱 체인의 맨 오른쪽에 와야만 한다. $R_z \\cdot R_x \\cdot R_y$에서 맨 오른쪽 슬롯을 차지하고 있으므로 이 요구가 충족된다.\nPitch (X) — 중간 pitch는 관찰 축 기준으로 보면 \u0026ldquo;월드 X축을 $R_y(\\theta)$로 변환한 축\u0026quot;이다. 엄밀히는 월드 X축이 아니지만, 일반적인 FPS 조작에서 roll = 0이고 yaw가 이미 적용된 상태에서 pitch를 움직이는 것은 \u0026ldquo;현재 바라보는 방향 기준으로 위/아래를 본다\u0026quot;는 자연스러운 행동에 대응한다. 수학적으로는 \u0026ldquo;yaw에 의해 회전된 X축\u0026rdquo; 기준 회전이지만, 이것이 바로 사용자가 기대하는 동작이다.\nRoll (Z) — 맨 왼쪽 roll은 캐릭터 고유의 기울기다. 피격 반응, 탑승물 기울기, 시네마틱 연출 등에서 설정되며, 이후 pitch/yaw가 그 기울어진 상태를 그대로 받아 적용된다. \u0026ldquo;roll $15°$로 기울어진 캐릭터가 위를 보고 옆을 본다\u0026quot;는 의미 구성이다.\n요약 모든 회전 행렬은 그 자체로는 월드 축 회전이다. 그러나 파라미터 변화 시 관찰되는 회전축은 오른쪽에 곱해지는 행렬들에 의해 재해석된다. $f(\\theta) = A \\cdot R_y(\\theta) \\cdot B$의 관찰 축은 \u0026ldquo;월드 Y축을 $B$로 변환한 축\u0026quot;이다. 맨 오른쪽 슬롯의 회전만이 \u0026ldquo;월드 축 회전\u0026quot;으로 관찰된다. FPS 카메라가 yaw를 맨 오른쪽에 두는 것은 마우스 좌우 조작 시 지평선 유지를 보장하기 위한 수학적 귀결이며, $R_z \\cdot R_x \\cdot R_y$ 순서는 여기에서 도출된다. ","permalink":"https://nansu0425.github.io/posts/fps-%EC%B9%B4%EB%A9%94%EB%9D%BC%EB%A5%BC-%EC%9C%84%ED%95%9C-%ED%9A%8C%EC%A0%84-%ED%96%89%EB%A0%AC-%EA%B3%B1-%EC%88%9C%EC%84%9C/","summary":"World Matrix의 회전 부분을 Rz * Rx * Ry로 구성하는 이유를 row-vector 규약에서 수식으로 유도한다","title":"FPS 카메라를 위한 회전 행렬 곱 순서"},{"content":"Cross Product Sign Method 삼각형의 각 변을 순회할 때, $P$가 모든 변의 같은 쪽에 있으면 내부에 있다.\n\u0026ldquo;같은 쪽\u0026quot;을 판정하는 도구가 2D cross product의 부호다.\n2D Cross Product 3D cross product에서 $z=0$일 때 $x$, $y$ 성분이 사라지고 $z$ 성분만 남는다.\n$$ \\vec{U} = (u_x,\\; u_y,\\; 0), \\quad \\vec{V} = (v_x,\\; v_y,\\; 0) $$$$ \\vec{U} \\times \\vec{V} = (0,\\; 0,\\; u_x v_y - u_y v_x) $$$z$ 성분 하나만 유의미하므로 스칼라처럼 사용한다.\n이 $z$ 값의 부호가 $\\vec{V}$가 $\\vec{U}$의 어느 쪽에 있는지를 알려준다.\n$\\vec{U}$에서 $\\vec{V}$로 반시계방향 회전 → $+Z$ 방향 → 양수 $\\vec{U}$에서 $\\vec{V}$로 시계방향 회전 → $-Z$ 방향 → 음수 삼각형에 적용 변의 방향 벡터 $\\vec{U} = \\overrightarrow{AB}$와 점까지의 벡터 $\\vec{V} = \\overrightarrow{AP}$의 cross product를 구한다. 세 변 모두에 대해 반복한다.\n$$ d_1 = \\overrightarrow{AB} \\times \\overrightarrow{AP} $$ $$ d_2 = \\overrightarrow{BC} \\times \\overrightarrow{BP} $$ $$ d_3 = \\overrightarrow{CA} \\times \\overrightarrow{CP} $$$d_1$, $d_2$, $d_3$의 부호가 모두 같으면 $P$는 내부에 있다.\n원리 CCW(반시계) 삼각형의 내부는 모든 변의 왼쪽이다.\n변 $\\overrightarrow{AB}$: $P$는 왼쪽 (양수) 변 $\\overrightarrow{BC}$: $P$는 왼쪽 (양수) 변 $\\overrightarrow{CA}$: $P$는 왼쪽 (양수) 전부 양수 → 내부.\nCW(시계) 삼각형이면 내부가 모든 변의 오른쪽이라 전부 음수가 된다. 어느 쪽이든 부호가 전부 같다 = 내부, 부호가 섞인다 = 외부.\n따라서 winding order(CW/CCW)에 관계없이 동작한다.\n구현 1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool IsPointInTriangle(float px, float py, float ax, float ay, float bx, float by, float cx, float cy) { float d1 = (bx - ax) * (py - ay) - (by - ay) * (px - ax); float d2 = (cx - bx) * (py - by) - (cy - by) * (px - bx); float d3 = (ax - cx) * (py - cy) - (ay - cy) * (px - cx); bool hasNeg = (d1 \u0026lt; 0.0f) || (d2 \u0026lt; 0.0f) || (d3 \u0026lt; 0.0f); bool hasPos = (d1 \u0026gt; 0.0f) || (d2 \u0026gt; 0.0f) || (d3 \u0026gt; 0.0f); return !(hasNeg \u0026amp;\u0026amp; hasPos); } hasNeg \u0026amp;\u0026amp; hasPos가 true이면 부호가 섞여 있으므로 외부다. 그 반대가 내부.\n","permalink":"https://nansu0425.github.io/posts/2d-%EC%A0%90%EC%9D%B4-%EC%82%BC%EA%B0%81%ED%98%95-%EB%82%B4%EB%B6%80%EC%97%90-%EC%9E%88%EB%8A%94%EC%A7%80-%ED%8C%90%EC%A0%95%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/","summary":"2D cross product의 부호를 이용하여 점이 삼각형 내부에 있는지 판정하는 방법","title":"2D 점이 삼각형 내부에 있는지 판정하는 방법"},{"content":"1. 서론 크래프톤 정글 게임테크랩 2기의 입학시험과 면접 모두 통과하여 최종 합격하게 됐습니다. 합격까지 거치는 과정과 정보 및 생각 등을 공유하려고 합니다. 이 프로그램에 관심 있으신 분들은 참고하시면 될 것 같습니다. 이 프로그램이 뭔지 아예 모르시는 분들은 크래프톤 정글 게임테크랩 2기 모집 | Notion 이 링크의 모집 글을 보시면 됩니다.\n2. 지원 계기 사실 저에게 이번 합격은 남다른 의미를 가집니다. 1기에 지원했다가 고배를 마셨기 때문입니다. 불합격했을 땐 \u0026lsquo;게임 프로그래머 취업 준비는 포기해야 할까?\u0026lsquo;라는 생각까지 들었습니다. 이미 졸업한 지 6개월이 지난 시점이었기에, 취업 준비 기간이 길어지는 것에 대한 압박감이 컸습니다. 그럼에도 불구하고 제가 다시 도전하기로 마음먹은 데에는 몇 가지 이유가 있었습니다.\n첫째, 그동안 쌓아온 게임 개발 관련 지식과 경험을 포기하기 아쉬웠습니다. 다른 분야로 전환한다고 해서 단기간에 성과를 낼 수 있을 거라는 확신도 없었습니다. 둘째, 게임테크랩 시험과 면접은 화려한 기술보다는 기본기의 중요성을 일깨워주었습니다. 불합격은 저의 부족한 학습 상태를 인정하고, 다시 기본부터 다져야 한다는 깨달음을 주었습니다.\n이후 저는 **C++**과 수학, 자료구조, 알고리즘 등 게임 프로그래밍의 핵심 기본기를 다시 공부했습니다. 처음부터 꼼꼼히 복습하며 제가 놓쳤던 부분을 채워나갔습니다. 그러던 중, 우연히 게임테크랩 2기 모집 공고를 보게 되었습니다. \u0026lsquo;이번이 마지막 기회다\u0026rsquo;라는 생각으로 다시 지원을 결심했습니다. 다행히 2기는 국비지원(KDT) 과정으로 참가비가 무료라는 점도 부담을 덜어주었습니다. 이번에도 떨어지면 게임 프로그래머의 꿈을 깨끗이 포기하려고 했습니다.\n하지만 결과는 합격이었습니다. 게임테크랩에 합격했다는 것은 저의 게임 프로그래머에 대한 꿈이 아직 유효하다는 것을 의미했습니다. 되돌아보니, 1기 불합격 이후 기본기를 재점검했던 시간이 합격의 결정적인 요인이었다고 생각합니다. 만약 게임 프로그래머 취업 목표가 있다면, 기본기를 확인하는 차원에서 게임테크랩 시험과 면접에 도전해보는 것도 괜찮을 것 같습니다.\n3. 지원서 지원서 폼은 제공됩니다. 그 폼에 맞춰 작성한 후 제출하면 됩니다. 저의 경우 1기 때 지원해본 경험이 있었기 때문에, 지원서는 모집 글을 본 후 다음 날에 바로 제출했었습니다. 빨리 제출한 또 다른 이유는 지원서가 합불에 영향을 크게 주지 않는 것 같은 생각이 들었기 때문입니다. 증빙서류를 요구하는 항목이 하나도 없었습니다. 예를 들어 학력란이 있는데, 재학증명서나 졸업증명서를 요구하지 않았습니다(물론 허위 사실을 기재하면 절대 안됩니다!). 증빙서류를 철저하게 요구하지 않는다는 건 중요하게 보지 않는다는 것과 같지 않을까? 하는 개인적인 생각입니다. 지원서의 핵심적인 항목과 제가 적었던 내용을 간략하게 소개하겠습니다.\n학력사항 및 전공 단국대 컴공 졸업 재직경험 네이버 클라우드 3개월 파트타임 근무(개발 포지션 아님) 1분 소개영상 몰입했던 경험 소개 프로젝트 리포지토리 개발 중이던 C++ 게임 서버 게임 제작 및 공부 경험 C++ 지뢰 찾기, 뱀 게임, 벽돌 깨기, 게임 서버 개발 블로그 소개 팀워크 경험 네이버 근무했을 때 경험 중 하나 소개 항목 별 특별한 팁은 없는 것 같습니다. 그냥 담백하고 솔직하게 적으면 될 것 같습니다. 혹시 겁을 먹는 사람들을 위해 다시 한번 말하지만, 지원서가 제 생각엔 별로 중요하지 않은 것 같습니다. 지원서는 빨리 작성하고 공부를 하시는 걸 추천합니다.\n4. 입학 시험 지원서를 제출하면 누구나 입학 시험에 응시할 수 있습니다. 지원서 제출 기간이 지나면 모든 지원자들에게 학습 자료가 메일로 갑니다. 시험 전까지 학습 자료를 열심히 공부한 후 입학 시험을 보시면 됩니다. 입학 시험의 세부 내용은 공개할 수 없지만 공부 방향성 정도는 말씀드릴 수 있을 것 같습니다.\nC++ 기초적인 문법과 동작은 모두 숙지해야 합니다. 객체지향 프로그래밍에 익숙해야 합니다. 수학 저의 경우 이득우의 게임 수학으로 준비했는데 추천합니다. 벡터 계산에 대한 이해도가 중요합니다. 자료구조 선형 및 비선형 자료구조를 C++(STL 없이)로 구현할 수 있어야 합니다. 언제 어떤 자료구조를 사용하는 게 좋을지 알아야 합니다. 입학 시험과 직접적으로 관련 있는 내용은 학습 자료에 있는 내용인데, 그건 입학 시험에 대한 스포이므로 생략했습니다. 학습 자료에 있는 내용을 처음 보더라도 위 항목들에 대한 공부가 돼있다면, 학습 자료를 공부하는데 무리가 없을 거라고 생각합니다.\n추가적인 팁으로, 입학 시험에서 AI를 활용해서 코딩해도 상관없습니다. 저의 경우 github copilot을 활용했습니다. AI를 잘 쓰는 것도 실력이라고 생각하기 때문에, AI를 시험에서 사용할 예정이라면 사용할 AI에 대한 숙련도를 높이는 것도 좋은 방법이라고 생각합니다.\n5. 면접 입학 시험에 합격하면, 시험 합격 메일이 오고 면접 스케줄을 잡습니다. 저는 가장 첫 타임에 스케줄을 잡았습니다. 면접 준비를 많이 하고 싶다면 마지막 타임에 스케줄을 잡는 게 맞지만, 짧고 굵게 집중해서 질질 끌지 않고 빨리 끝내고 싶었습니다. 면접은 저 포함 3명의 면접자와 면접관 2~3명으로 진행됩니다. 1시간 정도 진행됩니다. 저는 면접에선 긴장이 좀 됐습니다. 1기에 지원했을 때 입학 시험은 붙었지만 면접에서 떨어졌기 때문입니다. 2기 면접은 1기 면접을 복기하면서 준비했습니다. 입학 시험과 마찬가지로 면접 질문의 세부 사항을 공개하는 것은 불가능합니다. 준비 팁 정도만 몇 가지 정리하겠습니다.\n입학 시험에서 사용한 코드를 정말 이해하고 있는지 점검하세요. 컴퓨터 구조, 운영체제, 자료구조, 알고리즘 같은 CS 지식이 튼튼하면 유리합니다. Modern C++이 아닌 C++의 기초 문법이나 동작에 대한 이해를 평가합니다. 수학은 행렬과 벡터, 물리는 뉴턴 역학 위주로 학습하세요. 6. 결론 다음 주에 게임테크랩이 시작됩니다. 설레면서 긴장되는 감정이 공존합니다. 6개월 동안 게임 프로그래밍에만 몰입할 수 있는 시간과 시니어 프로그래머들의 피드백을 받을 수 있는 환경이 주어지는 건 흔치 않은 기회라고 생각됩니다. 저에게 주어진 이 기회를 감사하게 생각합니다. 이 과정이 끝났을 때 제가 얼마나 성장해있을지 기대가 됩니다.\n","permalink":"https://nansu0425.github.io/posts/%ED%81%AC%EB%9E%98%ED%94%84%ED%86%A4-%EC%A0%95%EA%B8%80-%EA%B2%8C%EC%9E%84%ED%85%8C%ED%81%AC%EB%9E%A9-2%EA%B8%B0-%ED%95%A9%EA%B2%A9/","summary":"크래프톤 정글 게임테크랩 2기","title":"크래프톤 정글 게임테크랩 2기 합격"},{"content":"안녕하세요. Direct3D 11 학습 노트 리포지토리를 만들게 되어 공유합니다.\nDirect3D 11은 방대한 내용 때문에 체계적인 학습이 중요하다고 생각했습니다. 학습 내용을 단순히 암기하기보다는, 저만의 지식 베이스(Knowledge Base) 를 구축하여 장기적으로 활용하고자 GitHub에 리포지토리를 만들었습니다.\n물론, Microsoft 공식 문서나 AI를 통해 정보를 얻을 수 있지만, 각각의 장단점이 있다고 생각했습니다. 공식 문서는 방대한 내용으로 인해 학습 경로를 잡기 어렵고, AI를 활용할 때도 미리 정리된 지식 기반이 있다면 더욱 정확하고 효율적인 답변을 얻을 수 있을 것입니다.\n현재 리포지토리에는 Direct3D 11의 기초 내용이 정리되어 있습니다. 앞으로 학습을 진행하며 내용을 지속적으로 추가해 나갈 예정입니다. 저 역시 학습자 입장이므로, 혹시 틀린 내용이 있다면 언제든지 피드백 부탁드립니다.\n아래 링크를 통해 리포지토리를 확인하실 수 있습니다.\nhttps://github.com/nansu0425/d3d11-notes\n","permalink":"https://nansu0425.github.io/posts/direct3d-11-%ED%95%99%EC%8A%B5-%EB%85%B8%ED%8A%B8-%EB%A6%AC%ED%8F%AC%EC%A7%80%ED%86%A0%EB%A6%AC/","summary":"\u003cstrong\u003eDirect3D 11 학습 노트 리포지토리\u003c/strong\u003e","title":"Direct3D 11 학습 노트 리포지토리"},{"content":"현재 Byteborne World라는 프로젝트를 진행하고 있습니다. C++ 게임 프로그래밍 프로젝트인데, 2D MMORPG를 목표로 진행하고 있습니다. 7월 초부터 작업했는데, 여러 클라이언트와 서버가 메시지를 통신할 수 있는 수준까지 구현했습니다. 네트워크 라이브러리로 Asio를 사용했고, 메시지 직렬화에는 Protobuf를 사용했습니다. 이 부분에 대한 문서를 프로젝트 github repository에 정리했습니다. 노션 블로그에 정리하려고 했는데 생각보다 불편한 점이 많아서(vscode에서 copilot으로 작업하는 게 너무 편해서!) 문서를 repository에 정리했습니다. 문서와 테스트 영상을 아래에 첨부했습니다. 피드백은 언제나 환영입니다!\nhttps://youtu.be/NMZF5mbRNIM?si=EoMhIAUM_o14R4vI\n문서 링크: https://github.com/nansu0425/ByteborneWorld/blob/master/docs/Asio%EC%99%80-Protobuf%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%86%B5%EC%8B%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C.md\n더미 클라이언트와 서버 통신을 구현했으니 이젠 게임 클라이언트를 구현하고 게임 클라이언트와 서버 통신을 구현해볼 생각입니다.\n","permalink":"https://nansu0425.github.io/posts/asio%EC%99%80-protobuf%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%A9%94%EC%8B%9C%EC%A7%80-%ED%86%B5%EC%8B%A0-%EC%8B%9C%EC%8A%A4%ED%85%9C/","summary":"Asio와 Protobuf를 이용한 메시지 통신 시스템 구현","title":"Asio와 Protobuf를 활용한 메시지 통신 시스템"},{"content":"1. 문제 설명 정수 값을 가지는 노드로 이루어진 연결 리스트가 Palindrome인지 판별하는 문제입니다. 자세한 문제 설명은 문제 링크를 참조하세요.\nhttps://leetcode.com/problems/palindrome-linked-list/description/\n2. 배열을 이용한 투 포인터 풀이 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 /** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ #include \u0026lt;array\u0026gt; class Solution { public: bool isPalindrome(ListNode* head) { ListNode* cur = head; while (cur) { m_arr[m_size] = cur-\u0026gt;val; ++m_size; cur = cur-\u0026gt;next; } int left = 0; int right = m_size - 1; while (left \u0026lt; right) { if (m_arr[left] != m_arr[right]) { return false; } ++left; --right; } return true; } private: std::array\u0026lt;int, 100\u0026#39;000\u0026gt; m_arr = {}; size_t m_size = 0; }; 가장 단순하게 떠올릴 수 있고 쉬운 풀이인 것 같습니다. 과정은 다음과 같습니다.\n연결 리스트를 순회하여 모든 값을 배열에 기록합니다. 첫 번째 원소에서 오른쪽으로 이동하는 left 포인터와 마지막 원소에서 왼쪽으로 이동하는 right 포인터를 준비합니다. left와 right를 이동시키면서 가운데에서 만날 때까지 left와 right 값이 같은지 확인합니다. 한번이라도 다르면 Palindrome이 아니고, 가운데에서 만날 때까지 같다면 Palindrome입니다. 연결 리스트를 순회하므로 시간 복잡도는 O(n)이 되고, 사용하는 배열 크기는 고정돼 있으므로 O(1) 공간 복잡도가 됩니다.\n3. 연결 리스트 절반을 거꾸로 뒤집어서 확인하는 풀이 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 /** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode() : val(0), next(nullptr) {} * ListNode(int x) : val(x), next(nullptr) {} * ListNode(int x, ListNode *next) : val(x), next(next) {} * }; */ class Solution { public: bool isPalindrome(ListNode* head) { ListNode* slow = head; ListNode* fast = head; // slow를 리스트의 중간으로 이동, 짝수개면 길이/2 + 1까지 이동 while ((fast != nullptr) \u0026amp;\u0026amp; (fast-\u0026gt;next != nullptr)) { slow = slow-\u0026gt;next; fast = fast-\u0026gt;next-\u0026gt;next; } ListNode* rev = makeReverse(slow); while (rev) { if (head-\u0026gt;val != rev-\u0026gt;val) { return false; } head = head-\u0026gt;next; rev = rev-\u0026gt;next; } return true; } private: ListNode* makeReverse(ListNode* head) { ListNode* prev = nullptr; ListNode* cur = head; while (cur) { ListNode* next = cur-\u0026gt;next; cur-\u0026gt;next = prev; prev = cur; cur = next; } return prev; } }; 리스트를 수정해야 하는 풀이입니다. 리스트나 포인터 연습하기엔 더 좋은 풀이입니다. 과정은 다음과 같습니다.\n노드 한 개씩 이동하는 slow 포인터, 노드 두 개씩 이동하는 fast 포인터를 준비합니다. slow와 fast로 이동시키다가 fast가 먼저 끝에 다다르면, slow 포인터는 가운데에 위치하게 됩니다. slow 포인터가 가리키는 노드 이후의 연결 리스트는 연결 방향을 모두 거꾸로 바꿉니다. 연결 방향이 바뀌면, 마지막 노드부터 가운데 노드까지 순회할 수 있게 됩니다. 첫 번째 노드를 가리키는 포인터와 마지막 노드를 가리키는 포인터를 가운데 노드까지 이동시키면서 두 포인터의 노드 값이 같은지 확인합니다. 가운데 노드까지 값이 같으면 Palindrome, 한 번이라도 다르면 Palindrome이 아닙니다. slow와 fast를 이용한 순회, 그리고 첫 번째 노드와 마지막 노드부터 시작하는 순회를 하므로 시간 복잡도는 O(n)입니다. 포인터만 사용하고 추가적인 메모리 할당을 하지 않으므로 O(1) 공간 복잡도가 됩니다.\n🔍 링크 https://www.youtube.com/@nansu0425 https://x.com/nansu0425 ","permalink":"https://nansu0425.github.io/posts/leetcode-234-palindrome-linked-list-%ED%92%80%EC%9D%B4/","summary":"LeetCode 234. Palindrome Linked List 풀이","title":"LeetCode 234. Palindrome Linked List 풀이"},{"content":"https://github.com/nansu0425/MmorpgServer\n1. 패킷 코드 자동 생성의 필요성 현재 제 프로젝트에선 패킷 직렬화 라이브러리로 protobuf를 채택했습니다. 사실 처음엔 flatbuffers를 선택했었는데, 조금 사용해보니 작성해야 할 코드량이 많고 사용법이 조금 까다로웠습니다. 또한 visual studio의 syntax highliting 지원이 없었고, 자료도 많아 보이지 않았습니다. 단순히 속도 때문에 도입하긴 했는데 mmorpg 서버에선 protobuf를 많이 쓰는 것 같아서 기존 flatbuffers 코드를 걷어내고 protobuf로 바꿨습니다(바꾸길 정말 잘 했다는 생각이 들었습니다). 패킷의 payload 구조를 proto 파일에 정의하는 방식으로 사용하고 있습니다. proto 파일 변경 사항이 생길 때마다 패킷 관련 코드를 수정해야 하는데, 이 과정이 실수가 생길 여지가 굉장히 크다고 느꼈습니다. 일관성 유지 및 생산성을 위해 proto 파일 정의에 맞는 패킷 코드를 자동으로 생성해주는 스크립트를 만들기로 했습니다. 스크립트 제작 과정에서 다음 강의를 참고했습니다.\nhttps://www.inflearn.com/course/%EC%96%B8%EB%A6%AC%EC%96%BC-3d-mmorpg-4\n2. PacketGenerator Tools 솔루션에 PacketGenerator 파이썬 프로젝트를 만들었습니다. 이 프로젝트의 스크립트와 템플릿을 이용해서 패킷 관련 코드를 자동 생성합니다. PacketGnerator.py 실행 시 핵심 동작은 다음과 같습니다.\nproto 파일들이 존재하는 디렉토리를 구합니다. 그 디렉토리에서 패킷 코드 생성에 필요한 proto 파일들을 구분합니다. ProtoParser가 key는 proto 파일 이름, value는 패킷 데이터 리스트인 패킷 딕셔너리를 만듭니다. jinja2 템플릿 엔진과 패킷 딕셔너리를 활용해서 Templates의 패킷 코드가 필요한 모든 프로젝트에 패킷 코드를 생성합니다. 3. GenPacket.bat PacketGenerator는 패킷 코드 템플릿을 이용해서 패킷 코드를 생성하는 역할을 하는데, 저기서 하지 않는 것이 있습니다. proto 파일을 protoc.exe로 컴파일하는 부분이 없습니다. GenPacket.bat은 proto 파일의 소스 코드 생성과 PacketGenerator 실행까지 포함된 윈도우 배치 파일 스크립트입니다. 즉, 이 GenPacket.bat만 실행하면 proto 파일에 정의된 내용을 반영해야 하는 모든 패킷 코드를 생성 합니다. 이것도 Tools에 만들었습니다. 아래는 현재 사용 중인 GenPackete.bat의 스크립트 입니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 @echo off REM -------------------------------------------------- REM GenPacket.bat REM - 정의된 Protocol을 바탕으로 Packet 관련 코드를 자동 생성 REM -------------------------------------------------- REM 현재 스크립트 위치 set SCRIPT_DIR=%~dp0 REM repository의 루트 디렉터리 for %%I in (\u0026#34;%SCRIPT_DIR%..\u0026#34;) do set ROOT_DIR=%%~fI REM protoc.exe 경로 set PROTOC=%ROOT_DIR%\\Server\\vcpkg_installed\\x64-windows-static\\tools\\protobuf\\protoc.exe REM .proto 파일들이 있는 디렉터리 set PROTO_DIR=%ROOT_DIR%\\Shared\\Protocol REM 임시 출력 디렉터리 set OUT_DIR=%SCRIPT_DIR%Build REM 최종 복사 대상 디렉터리 set COPY_DIRS=\u0026#34;%ROOT_DIR%\\Server\\Protocol\\Payload\u0026#34; REM 1) 출력 폴더 초기화 if exist \u0026#34;%OUT_DIR%\u0026#34; ( rd /s /q \u0026#34;%OUT_DIR%\u0026#34; ) mkdir \u0026#34;%OUT_DIR%\u0026#34; echo ============================================ echo Generating C++ sources from .proto files... echo Proto dir : %PROTO_DIR% echo Temp out : %OUT_DIR% echo Targets : %COPY_DIRS% echo Using protoc: %PROTOC% echo ============================================ REM 2) .proto→CPP 코드 생성 for /R \u0026#34;%PROTO_DIR%\u0026#34; %%F in (*.proto) do ( echo [PROTO] Compiling %%~fF \u0026#34;%PROTOC%\u0026#34; -I=\u0026#34;%PROTO_DIR%\u0026#34; --cpp_out=\u0026#34;%OUT_DIR%\u0026#34; \u0026#34;%%~fF\u0026#34; if errorlevel 1 ( echo [ERROR] Failed to compile %%~fF exit /b 1 ) ) REM 3) 생성된 파일을 각 복사 대상에 배포 for %%D in (%COPY_DIRS%) do ( echo [COPY] -\u0026gt; %%~D xcopy \u0026#34;%OUT_DIR%\\*\u0026#34; \u0026#34;%%~D\\\u0026#34; /E /Y /I if errorlevel 1 ( echo [WARNING] Copy to %%~D may have failed. ) ) REM 4) 임시 출력 폴더 정리 echo [CLEAN] Removing temp dir %OUT_DIR% rd /s /q \u0026#34;%OUT_DIR%\u0026#34; REM 5) PacketGenerator.py 실행 echo ============================================ echo Generating packet code... echo ============================================ set PACKET_GEN_DIR=%SCRIPT_DIR%PacketGenerator cd /d \u0026#34;%PACKET_GEN_DIR%\u0026#34; python PacketGenerator.py if errorlevel 1 ( echo [ERROR] Failed to run PacketGenerator.py exit /b 1 ) echo [SUCCESS] Packet code generation completed. echo ============================================ echo All packet generation tasks completed successfully. echo ============================================ 4. 자동 생성 예시 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 syntax = \u0026#34;proto3\u0026#34;; import \u0026#34;Common.proto\u0026#34;; package proto; message ClientToWorld_EnterRoom { int64 id = 1; string password = 2; } message ClientToWorld_Chat { int64 id = 1; string message = 2; } 현재 proto 파일은 세 개(Common.proto, ToClient.proto, ToWorld.proto)가 있는데, 어떤 식으로 패킷 코드가 생성되는지 설명하기 위해 ToWorld.proto를 예시로 들겠습니다. 이 proto 파일은 클라이언트에서 World 서버로 전송하는 패킷의 payload 구조를 정의합니다. 이 proto 파일과 관련된 패킷 코드를 자동 생성하기 위해 GenPacket.bat를 실행했을 때 생성된 코드의 예시는 아래와 같습니다.\n4.1. proto → 소스 코드 Protocol 프로젝트의 Payload 폴더에 ToWorld.proto의 컴파일된 소스 코드가 생성됩니다.\n4.2. Id.h 1 2 3 4 5 6 7 8 9 10 11 12 13 ///////// 생략 ///////// enum class PacketId : Int16 { Invalid = 0, {%- for proto_file, packets in proto_parser.packet_dict.items() %} {%- for packet in packets %} {{ packet.payload_type }} = {{ packet.packet_id }}, {%- endfor %} {%- endfor %} }; ///////// 생략 ///////// 1 2 3 4 5 6 7 8 9 10 11 12 ///////// 생략 ///////// enum class PacketId : Int16 { Invalid = 0, WorldToClient_EnterRoom = 1000, WorldToClient_Chat = 1001, ClientToWorld_EnterRoom = 1002, ClientToWorld_Chat = 1003, }; ///////// 생략 ///////// ToWorld.proto에 정의된 payload 타입들이 패킷 id가 부여된 형태로 PacketId에 추가됩니다.\n4.3. Dispatcher.h 1 2 3 4 5 6 7 8 ///////// 생략 ///////// #include \u0026#34;Protocol/Packet/Id.h\u0026#34; {%- for proto_file, packets in proto_parser.packet_dict.items() %} #include \u0026#34;Protocol/Payload/{{ proto_file }}.pb.h\u0026#34; {%- endfor %} ///////// 생략 ///////// 1 2 3 4 5 6 7 ///////// 생략 ///////// #include \u0026#34;Protocol/Packet/Id.h\u0026#34; #include \u0026#34;Protocol/Payload/ToClient.pb.h\u0026#34; #include \u0026#34;Protocol/Payload/ToWorld.pb.h\u0026#34; ///////// 생략 ///////// ToWrold.proto를 컴파일하여 생성된 ToWorld.ph.h 헤더 파일을 포함하는 #include 문을 추가합니다.\n4.4. Sender.h 1 2 3 4 5 6 7 8 9 10 ///////// 생략 ///////// // payload 타입별로 Send 함수를 오버로딩 {%- for proto_file, packets in proto_parser.packet_dict.items() %} {%- for packet in packets %} static void Send(const SharedPtr\u0026lt;core::Session\u0026gt;\u0026amp; target, const {{ packet.payload_type }}\u0026amp; payload) { Send(target, payload, PacketId::{{ packet.payload_type }}); } {%- endfor %} {%- endfor %} ///////// 생략 ///////// 1 2 3 4 5 6 7 8 9 ///////// 생략 ///////// // payload 타입별로 Send 함수를 오버로딩 static void Send(const SharedPtr\u0026lt;core::Session\u0026gt;\u0026amp; target, const WorldToClient_EnterRoom\u0026amp; payload) { Send(target, payload, PacketId::WorldToClient_EnterRoom); } static void Send(const SharedPtr\u0026lt;core::Session\u0026gt;\u0026amp; target, const WorldToClient_Chat\u0026amp; payload) { Send(target, payload, PacketId::WorldToClient_Chat); } static void Send(const SharedPtr\u0026lt;core::Session\u0026gt;\u0026amp; target, const ClientToWorld_EnterRoom\u0026amp; payload) { Send(target, payload, PacketId::ClientToWorld_EnterRoom); } static void Send(const SharedPtr\u0026lt;core::Session\u0026gt;\u0026amp; target, const ClientToWorld_Chat\u0026amp; payload) { Send(target, payload, PacketId::ClientToWorld_Chat); } ///////// 생략 ///////// ToWorld.proto에 정의된 payload 타입 별 Send 함수를 추가합니다. 각 Send 함수는 패킷 id를 인자로 받는 Send 함수 템플릿을 호출하는데, 자동화 코드를 생성하면서 패킷 id 부분을 payload 타입에 맞게 설정합니다. 따라서 패킷 전송 시 패킷 id를 수동으로 설정할 필요가 없고 payload만 전달하면 됩니다.\n4.5. Handler.h 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 /* {{ project_name }}/Packet/Handler.h */ #pragma once #include \u0026#34;Protocol/Packet/Dispatcher.h\u0026#34; namespace {{ project_namespace }} { class {{ proto_file }}_PacketHandler : public proto::PacketDispatcher { public: static {{ proto_file }}_PacketHandler\u0026amp; GetInstance() { static {{ proto_file }}_PacketHandler sInstance; return sInstance; } protected: // 모든 패킷 핸들러 등록 {{ proto_file }}_PacketHandler() { RegisterAllHandlers(); } virtual void RegisterAllHandlers() override { using namespace proto; {% for packet in proto_parser.packet_dict[proto_file] %} RegisterHandler\u0026lt;{{ packet.payload_type }}\u0026gt;(\u0026amp;Handle_{{ packet.payload_type }}, PacketId::{{ packet.payload_type }}); {%- endfor %} } private: // 모든 페이로드 핸들러 {%- for packet in proto_parser.packet_dict[proto_file] %} static Bool Handle_{{ packet.payload_type }}(const SharedPtr\u0026lt;core::Session\u0026gt;\u0026amp; owner, const proto::{{ packet.payload_type }}\u0026amp; payload); {%- endfor %} }; } // namespace {{ project_namespace }} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 /* WorldServer/Packet/Handler.h */ #pragma once #include \u0026#34;Protocol/Packet/Dispatcher.h\u0026#34; namespace world { class ToWorld_PacketHandler : public proto::PacketDispatcher { public: static ToWorld_PacketHandler\u0026amp; GetInstance() { static ToWorld_PacketHandler sInstance; return sInstance; } protected: // 모든 패킷 핸들러 등록 ToWorld_PacketHandler() { RegisterAllHandlers(); } virtual void RegisterAllHandlers() override { using namespace proto; RegisterHandler\u0026lt;ClientToWorld_EnterRoom\u0026gt;(\u0026amp;Handle_ClientToWorld_EnterRoom, PacketId::ClientToWorld_EnterRoom); RegisterHandler\u0026lt;ClientToWorld_Chat\u0026gt;(\u0026amp;Handle_ClientToWorld_Chat, PacketId::ClientToWorld_Chat); } private: // 모든 페이로드 핸들러 static Bool Handle_ClientToWorld_EnterRoom(const SharedPtr\u0026lt;core::Session\u0026gt;\u0026amp; owner, const proto::ClientToWorld_EnterRoom\u0026amp; payload); static Bool Handle_ClientToWorld_Chat(const SharedPtr\u0026lt;core::Session\u0026gt;\u0026amp; owner, const proto::ClientToWorld_Chat\u0026amp; payload); }; } // namespace world WorldServer에 ToWorld.proto에 정의된 payload를 처리하는 핸들러 클래스를 생성합니다. 핸들러 클래스가 헤더 파일에 생성되면, cpp에 각 핸들러의 로직만 직접 작성하면 됩니다.\n","permalink":"https://nansu0425.github.io/posts/%ED%8C%A8%ED%82%B7-%EC%BD%94%EB%93%9C-%EC%9E%90%EB%8F%99-%EC%83%9D%EC%84%B1-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%A0%9C%EC%9E%91/","summary":"패킷 코드 자동 생성 스크립트 만들기","title":"패킷 코드 자동 생성 스크립트 제작"},{"content":"1. 테스트 영상 https://www.youtube.com/watch?v=kUdA2jdxJvQ\nhttps://github.com/nansu0425/MmorpgServer\n2. 영상 설명 현재 진행 중인 Unity2dMmorpg의 서버 테스트 아직 Unity 클라이언트는 없는 상황이고, 콘솔 Dummy Client로 테스트 서버에 room이 하나 있고, 서버는 그 room에 있는 모든 player에게 100ms마다 패킷을 broadcast 클라이언트는 1000개의 session을 서버에 연결 연결된 각 session은 player를 만들고 room에 입장 → 총 1000개의 player가 서버 room에 입장 session의 연결이 끊어지면, room에서 player 퇴장 → player 제거 → session 제거 과정을 거친다 ","permalink":"https://nansu0425.github.io/posts/1000%EA%B0%9C%EC%9D%98-player%EA%B0%80-%EC%9E%88%EB%8A%94-room%EC%97%90-%EC%9D%BC%EC%A0%95-%EC%A3%BC%EA%B8%B0%EB%A1%9C-broadcast/","summary":"1000개의 player가 있는 room에 일정 주기로 broadcast","title":"1000개의 player가 있는 room에 일정 주기로 broadcast"},{"content":" 문제 링크: 5525번: IOIOI\n1. Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include \u0026lt;iostream\u0026gt; #include \u0026lt;string\u0026gt; int main() { std::ios_base::sync_with_stdio(false); std::cin.tie(nullptr); // 입력 int N, M; std::cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; std::string S; S.reserve(M); std::cin \u0026gt;\u0026gt; S; int answer = 0; int count = 0; // Pn을 탐색 for (int i = 0; i \u0026lt; M - 2;) { if ((S[i] == \u0026#39;I\u0026#39;) \u0026amp;\u0026amp; (S[i + 1] == \u0026#39;O\u0026#39;) \u0026amp;\u0026amp; (S[i + 2] == \u0026#39;I\u0026#39;)) { ++count; // Pn을 포함하고 있는 경우 if (count \u0026gt;= N) { ++answer; } i += 2; } else { count = 0; ++i; } } std::cout \u0026lt;\u0026lt; answer; return 0; } 2. Review 처음엔 슬라이딩 윈도우 방식으로 접근 했습니다. Pn을 포함하는지 검사할 때 Pn의 모든 문자열을 비교했습니다. 그런 방식으로 코드를 작성했을 때 N, M의 제약 조건이 없는 서브태스크에서 시간 초과가 났습니다. 이 방식은 O(M * N)의 시간 복잡도 였습니다. 시간 초과가 나지 않게 만들기 위해 O(M) 시간 복잡도 알고리즘으로 최적화 했습니다. Pn을 포함하는지 확인할 때, Pn의 모든 문자열을 검사하는 방식이 아니라 연속되는 IOI 문자열이 있을 때마다 IOI 문자열의 개수를 세는 방식을 사용합니다. 연속되는 IOI의 개수가 N 이상이면, Pn을 포함한다고 판단할 수 있습니다. 이렇게 최적화 했을 때, 8ms로 모든 서브태스크를 통과했습니다.\n","permalink":"https://nansu0425.github.io/posts/%EB%B0%B1%EC%A4%80-%EC%8B%A4%EB%B2%841-5525-ioioi/","summary":"백준 실버1 5525 - IOIOI","title":"백준 실버1 5525 - IOIOI"},{"content":" 문제 링크: 1389번: 케빈 베이컨의 6단계 법칙\n1. 코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; #include \u0026lt;queue\u0026gt; #include \u0026lt;algorithm\u0026gt; int main() { std::ios_base::sync_with_stdio(false); std::cin.tie(nullptr); int N, M; std::cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; std::vector\u0026lt;std::vector\u0026lt;int\u0026gt;\u0026gt; userGraph(N + 1); // 친구 관계 입력 for (int i = 0; i \u0026lt; M; ++i) { int u, v; std::cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; userGraph[u].push_back(v); userGraph[v].push_back(u); } std::vector\u0026lt;int\u0026gt; distance(N + 1); std::queue\u0026lt;int\u0026gt; userQueue; int minSum = N * N; int ansUser = 0; for (int user = 1; user \u0026lt;= N; ++user) { std::fill(distance.begin() + 1, distance.end(), -1); distance[user] = 0; userQueue.push(user); // BFS로 최단 거리 업데이트 while (!userQueue.empty()) { int u = userQueue.front(); userQueue.pop(); for (int v : userGraph[u]) { if (distance[v] == -1) { distance[v] = distance[u] + 1; userQueue.push(v); } } } int sum = 0; // 케빈 베이컨의 수 계산 for (int i = 1; i \u0026lt;= N; ++i) { sum += distance[i]; } // 케빈 베이컨의 수가 가장 작은 경우 if (sum \u0026lt; minSum) { minSum = sum; ansUser = user; } } std::cout \u0026lt;\u0026lt; ansUser; return 0; } 2. 분석 BFS를 활용해 최단 거리를 구하는 것이 핵심인 문제였습니다. 최단 거리를 구하기 위해 userGraph, distance, userQueue를 사용합니다. 유저 A로부터 다른 모든 유저의 최단 거리를 구하려고 할 때 다음과 같이 진행됩니다.\n모든 유저의 distance를 -1로 설정합니다. 유저 A의 distance는 0으로 설정하고 userQueue에 유저 A를 넣습니다 BFS 반복문에 진입합니다. userQueue에서 유저 한 명을 꺼내고, userGraph를 이용해서 꺼낸 유저에 연결된 유저들을 조사합니다. 연결된 유저의 distance가 -1이면 최단 거리를 계산하고, -1이 아니면 최단 거리를 이미 계산한 것으로 간주합니다. 최단 거리를 계산할 때 유저 u → 유저 v라고 하면, 유저 v의 최단 거리를 distance[u] + 1로 계산합니다. 계산 후 유저 v는 큐에 넣습니다. 큐에 유저가 없을 때까지 BFS 반복문이 진행됩니다. 위와 같은 과정으로 최단 거리를 구한 후 케빈 베이컨 수를 계산합니다. 가장 작은 케빈 베이컨 수라면 정답에 해당하는 유저를 업데이트합니다. 시간 복잡도를 계산하겠습니다. 유저 한 명의 케빈 베이컨 수를 계산할 때 userGraph에 BFS를 수행합니다. BFS를 수행하면, 모든 정점을 한 번씩 방문하고 방문 시 연결된 간선을 순회합니다. 따라서 유저 한 명의 케빈 베이컨 수를 구할 때 시간 복잡도는 O(N + M)입니다. 모든 유저의 케빈 베이컨 수를 구하기 때문에 모든 유저에 대해 BFS를 수행합니다. 이것까지 고려하면 최종 시간복잡도는 O(N * (N + M))입니다.\n","permalink":"https://nansu0425.github.io/posts/%EB%B0%B1%EC%A4%80-1389-%EC%BC%80%EB%B9%88-%EB%B2%A0%EC%9D%B4%EC%BB%A8%EC%9D%98-6%EB%8B%A8%EA%B3%84-%EB%B2%95%EC%B9%99/","summary":"백준 1389 \u003cstrong\u003e케빈 베이컨의 6단계 법칙\u003c/strong\u003e","title":"백준 1389 케빈 베이컨의 6단계 법칙"},{"content":" 문제 링크: 30804번: 과일 탕후루\n1. 코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include \u0026lt;iostream\u0026gt; #include \u0026lt;unordered_map\u0026gt; int gFruits[200\u0026#39;000] = {}; int main() { std::ios_base::sync_with_stdio(false); std::cin.tie(nullptr); // 탕후루 정보 입력 int N; std::cin \u0026gt;\u0026gt; N; for (int i = 0; i \u0026lt; N; ++i) { std::cin \u0026gt;\u0026gt; gFruits[i]; } int maxLen = 1; std::unordered_map\u0026lt;int, int\u0026gt; fruitCount; int left = 0; // right을 한 칸씩 오른쪽으로 이동 for (int right = 0; right \u0026lt; N; ++right) { ++fruitCount[gFruits[right]]; // 과일 종류가 세 개가 되면, 두 종류가 될 때까지 left를 오른쪽으로 이동 while (fruitCount.size() \u0026gt; 2) { --fruitCount[gFruits[left]]; if (fruitCount[gFruits[left]] == 0) { fruitCount.erase(gFruits[left]); } ++left; } // 최대 길이 업데이트 maxLen = std::max(maxLen, right - left + 1); } std::cout \u0026lt;\u0026lt; maxLen; return 0; } 2. 분석 투 포인터를 활용한 문제였습니다. 탕후루 배열의 left, right을 0번 인덱스에서 시작합니다. right를 오른쪽으로 이동시키면서 최대 길이를 업데이트합니다. 업데이트하기 전 left 이동이 일어날 수 있는데, left ~ right 범위의 과일 종류가 두 가지를 넘어가면 두 가지 종류가 될 때까지 left를 이동시킵니다. 탕후루 배열의 길이 N만큼 순차적으로 순회하므로 시간 복잡도는 O(N)이 됩니다.\n","permalink":"https://nansu0425.github.io/posts/%EB%B0%B1%EC%A4%80-30804-%EA%B3%BC%EC%9D%BC-%ED%83%95%ED%9B%84%EB%A3%A8/","summary":"백준 30804 과일 탕후루 ****","title":"백준 30804 과일 탕후루"},{"content":" 문제 링크: 18111번: 마인크래프트\n1. 코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include \u0026lt;iostream\u0026gt; #include \u0026lt;algorithm\u0026gt; int g_heightFreq[257] = {}; int g_numRows = 0; int g_numCols = 0; int g_numBlocks = 0; int CalculateTime(int p_height) { int time = 0; int numBlocks = g_numBlocks; for (int height = 0; height \u0026lt;= 256; ++height) { const int freq = g_heightFreq[height]; int diff = height - p_height; // 제거 if (diff \u0026gt; 0) { time += 2 * diff * freq; numBlocks += diff * freq; } // 설치 else if (diff \u0026lt; 0) { time -= diff * freq; numBlocks += diff * freq; } } return (numBlocks \u0026lt; 0) ? -1 : time; } int main() { std::ios_base::sync_with_stdio(false); std::cin.tie(nullptr); std::cin \u0026gt;\u0026gt; g_numRows \u0026gt;\u0026gt; g_numCols \u0026gt;\u0026gt; g_numBlocks; int maxHeight = 0; int minHeight = 256; // 높이 정보 기록 for (int i = 0; i \u0026lt; g_numRows * g_numCols; ++i) { int height; std::cin \u0026gt;\u0026gt; height; ++g_heightFreq[height]; maxHeight = std::max(maxHeight, height); minHeight = std::min(minHeight, height); } int minTime = 500 * 500 * 256 * 2; int optimalHeight = 0; // 최소/최대 높이 범위의 땅 고르기 작업 시 걸리는 시간 계산 for (int h = maxHeight; h \u0026gt;= minHeight; --h) { const int time = CalculateTime(h); // 블록이 부족한 경우 if (time == -1) { continue; } // 기존 작업 시간보다 더 짧은 경우 if (time \u0026lt; minTime) { minTime = time; optimalHeight = h; } } std::cout \u0026lt;\u0026lt; minTime \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; optimalHeight; return 0; } 2. 분석 처음엔 parametric search 문제인줄 알았습니다. 땅 고르기 높이가 올라갈 수록, 작업 시간이 감소할 것이라고 예상했습니다. 만약 작업 시간이 정말 이런 단조 감소 함수라면, parametric search로 접근해서 해결할 수 있을 것 입니다. 하지만 땅 고르기 높이가 올라간다고 해서 작업 시간이 반드시 줄어들지 않습니다. 반례를 몇 가지 생각해볼 수도 있겠지만 문제에 이미 힌트가 있습니다. 문제 설명 중 작업 시간이 같을 시 높이가 더 높은 경우를 출력하라고 나와 있습니다. 이 문구에서 땅 고르기 높이가 올라간다고 해서 작업 시간이 반드시 줄어드는 것이 아니라는 것을 어느 정도 암시하고 있습니다. parametric search로 접근하지 못 한다는 것을 깨닫고 brute-force로 접근했습니다. brute-force도 처음에는 비효율적으로 접근했는데, CalculateTime 과정에서 맵의 모든 좌표를 순회하는 방식으로 시간을 계산했습니다. 이런 식이면 최악의 경우 맵이 500 * 500 크기일 때, CalculateTime 호출 시 250’000번의 반복이 발생합니다. 이 문제를 해결하고 개선한 최종 코드가 현재 코드입니다. CalculateTime에서 모든 좌표를 순회하는 방식이 아닌, 0~256까지 높이에 대해 각 높이에 해당하는 좌표의 개수를 활용하여 작업 시간을 계산합니다. 이때는 맵 크기에 무관하게 항상 257번의 반복만 합니다. 최종 코드의 시간 복잡도를 계산하겠습니다. 모든 높이에 해당하는 좌표의 개수를 기록할 때 O(M * N), 땅 고르기의 최소 작업 시간을 찾을 때 O((maxHeight - minHeight) * 257) 이므로, O(M * N + (maxHeight - minHeight) * 257)이 최종 시간 복잡도가 됩니다.\n","permalink":"https://nansu0425.github.io/posts/%EB%B0%B1%EC%A4%80-18111-%EB%A7%88%EC%9D%B8%ED%81%AC%EB%9E%98%ED%94%84%ED%8A%B8/","summary":"백준 18111 \u003cstrong\u003e마인크래프트\u003c/strong\u003e","title":"백준 18111 마인크래프트"},{"content":"1. 개요 1.1. 문제 방향 없는 그래프가 주어졌을 때, 연결 요소 (Connected Component)의 개수를 구하는 프로그램을 작성하시오.\n1.2. 입력 첫째 줄에 정점의 개수 N과 간선의 개수 M이 주어진다. (1 ≤ N ≤ 1,000, 0 ≤ M ≤ N×(N-1)/2) 둘째 줄부터 M개의 줄에 간선의 양 끝점 u와 v가 주어진다. (1 ≤ u, v ≤ N, u ≠ v) 같은 간선은 한 번만 주어진다.\n1.3. 출력 첫째 줄에 연결 요소의 개수를 출력한다.\n문제 링크: 11724번: 연결 요소의 개수\n2. 코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include \u0026lt;iostream\u0026gt; #include \u0026lt;vector\u0026gt; std::vector\u0026lt;int\u0026gt; g_graph[1\u0026#39;001] = {}; bool g_isVisited[1\u0026#39;001] = {}; int g_connComponentCount = 0; void Dfs(int v) { // 방문 처리 g_isVisited[v] = true; // 방문한 적 없는 모든 연결 정점에 대해 Dfs for (const int\u0026amp; w : g_graph[v]) { if (!g_isVisited[w]) { Dfs(w); } } } int main() { std::ios_base::sync_with_stdio(false); std::cin.tie(nullptr); int N = -1; int M = -1; std::cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; // 간선 입력 for (int i = 0; i \u0026lt; M; ++i) { int u = -1; int v = -1; std::cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; g_graph[u].push_back(v); g_graph[v].push_back(u); } // 모든 정점을 순회하며 연결 요소 개수 업데이트 for (int v = 1; v \u0026lt;= N; ++v) { if (g_isVisited[v]) { continue; } Dfs(v); ++g_connComponentCount; } std::cout \u0026lt;\u0026lt; g_connComponentCount; return 0; } 3. 분석 그래프에서 연결 요소는 서로 도달 가능한 정점들의 부분 집합을 의미합니다. 무방향 그래프에서 연결 요소의 개수를 찾기 위해 사용한 전략은 다음과 같습니다.\n모든 정점 순회를 시작합니다. 정점을 방문했는지 확인하고 방문한 적 있다면 다음 정점으로 넘어가고, 방문한 적 없다면 Dfs를 수행합니다. Dfs를 수행하여 도달 가능한 모든 정점에 대해 방문 처리 합니다. 이 과정이 하나의 연결 요소의 모든 정점을 방문하는 것이므로 Dfs가 끝나면 연결 요소 개수를 1 증가시킵니다. 모든 정점 순회가 끝났을 땐 그래프의 연결 요소 개수 세기가 끝납니다. 각 정점을 1번만 방문하고, 정점 방문 시 연결된 간선을 순회하므로 시간 복잡도는 O(V + E)가 됩니다.\n","permalink":"https://nansu0425.github.io/posts/%EB%B0%B1%EC%A4%80-11724-%EC%97%B0%EA%B2%B0-%EC%9A%94%EC%86%8C%EC%9D%98-%EA%B0%9C%EC%88%98/","summary":"백준 11724 \u003cstrong\u003e연결 요소의 개수\u003c/strong\u003e","title":"백준 11724 연결 요소의 개수"},{"content":"1. 배경 현재 진행하고 있는 C++ 프로젝트에서 custom allocator와 custom deleter를 사용하는 std::unique_ptr을 사용하고 싶었습니다. 그래서 custom allocator/deleter를 사용하는 UniquePtr타입과 UniquePtr을 생성하는 MakeUnique 함수를 만들었습니다. 그 코드는 아래와 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 template\u0026lt;typename TObject\u0026gt; struct UniquePtrDeleter { // 소멸자 호출 및 객체 해제 void operator()(TObject* pObject) const { pObject-\u0026gt;~TObject(); SmartPointerAllocator\u0026lt;TObject\u0026gt;().deallocate(pObject, 1); } }; // SmartPointerAllocator로 객체를 할당, 해제하는 std::unique_ptr template\u0026lt;typename TObject\u0026gt; using UniquePtr = std::unique_ptr\u0026lt;TObject, UniquePtrDeleter\u0026lt;TObject\u0026gt;\u0026gt;; // SmartPointerAllocator로 객체를 할당, 해제하는 std::unique_ptr 생성 template\u0026lt;typename TObject, typename... Args\u0026gt; UniquePtr\u0026lt;TObject\u0026gt; MakeUnique(Args\u0026amp;\u0026amp;... args) { // 객체 할당 및 생성자 호출 TObject* pObject = SmartPointerAllocator\u0026lt;TObject\u0026gt;().allocate(1); new (pObject) TObject(std::forward\u0026lt;Args\u0026gt;(args)...); return UniquePtr\u0026lt;TObject\u0026gt;(pObject); } 2. 문제 처음엔 이렇게 코드를 작성해서 사용했지만 얼마 안 가 문제를 발견했습니다. 부모 클래스 타입 UniquePtr는 자식 클래스 타입 UniquePtr를 받을 수 없습니다. UniqePtr\u0026lt;Base\u0026gt; ptr = MakeUnique\u0026lt;Drived\u0026gt;(); 이런 코드가 불가능하다는 의미입니다. 이 코드가 불가능한 이유는, UniquePtrDeleter의 템플릿 파라미터에 전달되는 타입이 다르기 때문입니다. 전달되는 타입이 어떻게 다른지 std::unique_ptr로 풀어서 보면, std::unique_ptr\u0026lt;Base, UniquePtrDeleter\u0026lt;Base\u0026gt;\u0026gt; ptr = UniquePtr\u0026lt;Derived, UniquePtrDeleter\u0026lt;Derived\u0026gt;\u0026gt;; 이렇게 됩니다. 이 문제를 해결하기 위해선 UniquePtrDeleter를 객체 타입과 무관하게 사용할 수 있도록 객체 타입에 대한 의존성을 제거해야 합니다.\n3. 개선 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 class UniquePtrDeleter { private: using DeleterFunction = void (*)(void*); public: UniquePtrDeleter() : _deleterFunction(DeleteNothing) {} explicit UniquePtrDeleter(DeleterFunction deleterFunction) : _deleterFunction(deleterFunction) {} // std::unique_ptr이 deleter를 호출할 때 이 메서드가 호출된다 void operator()(void* pObject) const { _deleterFunction(pObject); } // TObject 타입에 맞는 UniquePtrDeleter 생성 template\u0026lt;typename TObject\u0026gt; static UniquePtrDeleter Create() { return UniquePtrDeleter(DeleteObject\u0026lt;TObject\u0026gt;); } private: // 아무것도 하지 않는 Deleter static void DeleteNothing(void* pObject) noexcept {} // TObject 타입 객체 해제 작업을 수행 template\u0026lt;typename TObject\u0026gt; static void DeleteObject(void* pObject) noexcept { // 소멸자 호출 후 객체 해제 static_cast\u0026lt;TObject*\u0026gt;(pObject)-\u0026gt;~TObject(); SmartPointerAllocator\u0026lt;TObject\u0026gt;().deallocate(static_cast\u0026lt;TObject*\u0026gt;(pObject), 1); } private: DeleterFunction _deleterFunction = nullptr; }; // SmartPointerAllocator로 객체를 할당, 해제하는 std::unique_ptr template\u0026lt;typename TObject\u0026gt; using UniquePtr = std::unique_ptr\u0026lt;TObject, UniquePtrDeleter\u0026gt;; // SmartPointerAllocator로 객체를 할당, 해제하는 std::unique_ptr 생성 template\u0026lt;typename TObject, typename... Args\u0026gt; UniquePtr\u0026lt;TObject\u0026gt; MakeUnique(Args\u0026amp;\u0026amp;... args) { // 메모리 할당 SmartPointerAllocator\u0026lt;TObject\u0026gt; allocator; TObject* pObject = allocator.allocate(1); try { // 생성자 호출 new (pObject) TObject(std::forward\u0026lt;Args\u0026gt;(args)...); } catch (...) { // 생성자 호출 중 예외 발생 시 메모리 해제 allocator.deallocate(pObject, 1); throw; } return UniquePtr\u0026lt;TObject\u0026gt;(pObject, UniquePtrDeleter::Create\u0026lt;TObject\u0026gt;()); } type erasure 기법을 활용하여 UniquePtrDeleter의 객체 타입을 추상화 했습니다. 이 기법은 컴파일 타임에 결정되는 타입을 런타임에 추상화하기 위해 사용할 수 있습니다. 이것이 가능한 이유는, UniquePtrDeleter::Create\u0026lt;TObject\u0026gt;로 타입이 추상화된 UniquePtrDeleter를 생성하지만 내부적으로는 타입에 맞는 DeleterFunction에 대한 포인터를 들고 있기 때문입니다. Create는 함수 템플릿이기 때문에 컴파일 타임에 안전하게 타입에 맞는 DeleterFunction을 설정합니다. 약간의 trade-off가 존재 합니다. 런타임에 DeleterFunction에 대한 포인터를 유지해야 하기 때문에 std::unqieu_ptr의 크기가 늘어납니다. 64비트 환경이라면 객체에 대한 포인터 하나와 추가적으로 DeleterFunction에 대한 포인터를 갖게 되므로 16B 크기가 됩니다.\n","permalink":"https://nansu0425.github.io/posts/type-erasure-%EA%B8%B0%EB%B2%95%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%B4-unique_ptr%EC%9D%98-custom-deleter-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0/","summary":"type erasure 기법을 활용해 unique_ptr의 custom deleter 문제 해결","title":"type erasure 기법을 활용해 unique_ptr의 custom deleter 문제 해결"},{"content":"1. 문제 정리 1.1 문제 개요 세준이는 양수와 \u0026rsquo;+\u0026rsquo;, \u0026rsquo;-\u0026rsquo; 연산자 그리고 괄호를 사용해 하나의 식을 만들었습니다.\n그러나 나중에 세준이는 식에서 모든 괄호를 지워버렸습니다.\n이제 세준이는 원래의 식에 괄호를 적절히 추가하여 최소 결과값을 만들어 내고자 합니다.\n즉, 괄호의 배치를 잘 조정하여 식의 계산 결과가 최소가 되는 값을 구하는 문제입니다.\n1.2 입력 조건 입력 형식: 한 줄로 구성된 수식 수식 구성: 0부터 9까지의 숫자 '+', '-' 기호 제약 사항: 식의 첫 번째와 마지막 문자는 반드시 숫자이다. 연속해서 두 개 이상의 연산자가 등장하지 않는다. 한 숫자가 5자리를 초과하지 않는다. 숫자는 선행하는 0을 포함할 수 있다. 식의 길이는 최대 50자이다. 괄호를 적절히 추가하여 계산했을 때의 최소 결과값을 출력합니다. 1.4 예제 예제 1 입력: 55-50+40 출력: 35 예제 2 입력: 10+20+30+40 출력: 100 예제 3 입력: 00009-00009 출력: 0 2. 코드 설명 2.1 C++ 코드 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include \u0026lt;iostream\u0026gt; #include \u0026lt;string\u0026gt; int main() { std::string expression; std::cin \u0026gt;\u0026gt; expression; int sum = 0; bool isMinus = false; // 처음 \u0026#39;-\u0026#39;가 나왔는지 여부를 판단하는 플래그 std::string number; // 현재까지 누적된 숫자를 저장하는 문자열 // 식을 한 글자씩 순회하며 처리 for (char c : expression) { // 연산자 \u0026#39;+\u0026#39; 또는 \u0026#39;-\u0026#39;를 만났을 때 if (c == \u0026#39;+\u0026#39; || c == \u0026#39;-\u0026#39;) { // \u0026#39;-\u0026#39; 이후부터 등장하는 숫자는 빼줌 if (isMinus) { sum -= std::stoi(number); } // 아직 \u0026#39;-\u0026#39;가 나오지 않은 상태이고, 현재 연산자가 \u0026#39;+\u0026#39;인 경우 else if (c == \u0026#39;+\u0026#39;) { sum += std::stoi(number); } // 처음 \u0026#39;-\u0026#39; 연산자가 등장한 경우 else if (c == \u0026#39;-\u0026#39;) { isMinus = true; sum += std::stoi(number); } number.clear(); // 다음 숫자 입력을 위해 초기화 } // 현재 문자가 숫자인 경우, number에 누적 else { number.push_back(c); } } // 마지막에 남은 숫자 처리 if (isMinus) { sum -= std::stoi(number); } else { sum += std::stoi(number); } std::cout \u0026lt;\u0026lt; sum; return 0; } 2.2 코드 동작 설명 입력 및 변수 초기화 expression: 입력받은 수식을 저장하는 문자열입니다. sum: 최종 결과값을 누적할 변수입니다. isMinus: 처음 '-' 연산자가 등장했는지 여부를 나타내는 불리언 변수입니다. number: 현재까지 연속된 숫자를 문자열 형태로 누적하기 위한 변수입니다. 문자열 순회 및 처리 식을 한 글자씩 순회하면서, 연산자('+' 또는 '-')를 만났을 때: 현재까지 누적된 문자열 number를 정수로 변환하여 처리합니다. isMinus 플래그에 따라, 이미 '-'가 등장한 경우: 해당 숫자를 빼준다. 그렇지 않은 경우, 현재 연산자가 '+'라면 더해준다. 현재 연산자가 '-'라면 첫 번째 \u0026lsquo;-\u0026lsquo;로 간주하고, 해당 숫자를 더한 후 isMinus를 true로 설정합니다. 연산자 처리 후, number를 초기화하여 다음 숫자 처리를 준비합니다. 숫자일 경우: number에 해당 숫자를 계속 누적합니다. 마지막 숫자 처리 및 결과 출력 반복문이 종료된 후, 남아있는 number에 대해 같은 조건에 따라 처리합니다. 최종적으로 누적된 sum 값을 출력합니다. 3. 알고리즘 설명 3.1 핵심 아이디어 및 전략 핵심 아이디어:\n입력된 식에서 첫 번째 '-' 이후의 모든 연산은 결과값을 최소화하기 위해 빼야 합니다.\n더하기가 많을 수록 결과가 커지기 때문에, 첫 번째 '-'가 등장한 후부터 모든 숫자들을 앞에서 더한 합에서 빼도록 괄호 처리해야 합니다.\n전략:\n첫 번째 '-' 전까지의 숫자들: 이 구간에서는 '+'만 있으므로 모든 숫자를 단순히 더합니다. 첫 번째 '-' 이후부터의 숫자들: 이때부터 등장하는 모든 숫자는 이전에 더한 숫자들의 합에서 빼줍니다. 이를 위해 식을 한 번 순회하면서, '+'나 '-'가 등장할 때마다 현재까지 누적된 숫자를 정수로 변환하고, isMinus 플래그를 사용하여 해당 숫자를 더할지 빼줄지 결정합니다. 3.2 시간 복잡도 및 공간 복잡도 분석 시간 복잡도:\n식의 길이를 n이라 할 때, 한 번의 순회(O(n))로 모든 문자를 처리합니다.\n공간 복잡도:\n사용되는 변수들은 몇 개의 정수형, 불리언형, 그리고 문자열 변수 정도이므로, 추가적인 공간 복잡도는 O(1)입니다.\n","permalink":"https://nansu0425.github.io/posts/%EB%B0%B1%EC%A4%80-1541-%EC%9E%83%EC%96%B4%EB%B2%84%EB%A6%B0-%EA%B4%84%ED%98%B8/","summary":"백준 1541 잃어버린 괄호","title":"백준 1541 잃어버린 괄호"},{"content":" 본 게시물의 내용은 AI의 답변을 제 방식으로 재정리한 것입니다. 원문 링크: https://chatgpt.com/share/67e24682-d234-8010-933c-ab004cdd0ad0\n1. 유망 플랫폼과 장르 분석 1.1. 진입하기 좋은 플랫폼 PC (Steam) 1인 인디 게임 개발자가 가장 쉽게 진입할 수 있는 플랫폼 콘솔 닌텐도 Switch가 인디 친화적 모바일 사용자 풀이 거대하지만 경쟁이 극심하다 마케팅 비용과 라이브서비스 운영 부담이 크다 초기 자본이 적은 1인 개발자는 PC/콘솔 위주로 시작하고 성공하면 모바일로 확장하는 전략 권장된다 1.2. 인디 인기 장르 동향 로그라이크 2010년대 중반부터 지금까지 지속적으로 강세 Vampire Survivors: 간단한 조작과 반복 플레이 중독성으로 성공 시뮬레이션/경영 스타듀밸리는 여유롭고 힐링되는 게임 플레이의 높은 수요로 인해 성공 다만 2023년 들어 웰빙/힐링 스타일 게임 출시가 안정세에 접어들었다 샌드박스 도시 건설 게임인 Townscaper는 팬데믹 기간 저자극 게임 수요로 성공 복잡한 목표 없이 창의적 놀이 플랫폼 자체만으로도 충분한 수요가 있다 스토리 중심/비주얼 노벨 Helltaker: 퍼즐 게임으로, 독특한 캐릭터와 밈 덕분에 성공 강렬한 스토리/캐릭터성은 입소문을 탈 수 있게 하는 요인 1.3. 시장 트렌드와 유리한 조합 PC/콘솔 + 로그라이크 or 시뮬레이션 PC는 여전히 인디게임 주 무대 로그라이크의 플레이 반복성은 콘텐츠 생산력이 한정된 1인 개발자에게 이상적 시뮬레이션 및 경영은 꾸준한 수요가 있고 업데이트를 통해 오래 서비스 가능 PC로 시작해 성과를 거둔 후 콘솔로 확장하는 전략이 이상적 모바일 + 캐주얼 or 퍼즐/방치형 단순한 장르 도전에 적합 모바일은 1인 개발로 성공할 확률이 낮으므로, 코어 게이머층을 노린 PC 인디로 먼저 입지를 다지는 것이 안정적인 전략 2. 성공한 1인 개발 게임 사례 분석 (2020년 이후) 2.1. Phasmophobia (2020) 장르 4인 협동 공포 게임 (유령 추적) 개발/마케팅 별다른 홍보 없이 스트리머들의 입소문으로 인기를 얻었다 Early Access 출시 후 예상 밖 성공에 Early Access 기간을 연장하고 콘텐츠 확장 성공 요인 공포 게임에 협동 요소 접목 마이크 음성 인식 등 스트리머들이 콘텐츠로 활용하기 좋은 시스템 코로나 시기 SNS/Twitch 바이럴 지속적인 업데이트 2.2. Vampire Survivors (2021) 장르 로그라이크 bullet-hell 액션 개발/마케팅 PC 출시 이후 모바일/콘솔 출시 취미로 시작한 프로젝트로, 초기에는 게임잼 수준 간단한 웹게임 Early Access 출시 후 유튜브 입소문을 타고 Steam 차트 석권 급성장한 후에도 직원 1~2명 정도 소규모로 운영 성공 요인 원초적으로 재미있는 게임 루프 복잡한 조작 없이 한 손으로 플레이하는 단순함 레트로한 감성에도 불구하고, 수백 대의 몬스터를 쓸어버리는 쾌감 3천 원대로 낮게 책정하고 꾸준히 콘텐츠를 추가하여 좋은 가성비로 입소문 2.3. Bright Memory (2020) 장르 1인칭 슈팅 + 핵앤슬래시 개발/마케팅 Unreal Engine으로 개발한 하이브리드 FPS 개발자가 회사에서 일할 때 외에 틈틈이 혼자 제작한 에피소드 형식 게임 19년에 초소형 챕터를 선공개한 후 피드백을 받아 개선한 완전판 출시 퍼블리셔의 도움으로 글로벌 마케팅이 이루어졌다 성공 요인 최신 엔진 기술 활용과 니치한 수요 공략 AAA급에 버금가는 비주얼과 타격감 구현 신작 갈증이 있던 하드코어 FPS 팬층과 기술 매니아들을 사로잡았다 1인 개발 화제성이 홍보 포인트가 됐다 기술력과 화제성을 겸비하면 1인 개발작도 콘솔 시작 진입 가능 2.4. Townscaper (2020) 장르 캐주얼 도시 건설 (샌드박스) 개발/마케팅 프로토타입을 Early Access로 공개하여 유저 피드백과 입소문을 얻었다 목표 없이 아기자기한 도시를 짓는 경험에 초점을 맞췄다 전세계적으로 힘든 시기였기에, 힐링 게임의 수요가 높았다 SNS에서 마을 스크린샷이 공유되면서 바이럴 PC와 Switch에서 판매 후 모바일로도 출시됐다 성공 요인 극단적으로 편안하고 진입장벽이 낮은 게임성 Reddit 등지에 개발 과정을 공유하고, 이용자들 마을들을 리트윗하는 등 커뮤니티 소통을 잘했다 플레이어 스스로 즐거움을 찾도록 한 디자인이 잘 먹혔다 2.5. Helltaker 장르 퍼즐 + 데이트 시뮬레이션 개발/마케팅 1~2시간이면 끝나는 짧은 분량의 무료 게임 캐릭터 디자인과 유머로 커뮤니티에서 폭발적 반응 별도 광고 없이도 출시 직후 수백만 다운로드 굿즈 판매와 DLC 판매로 수익 창출 성공 요인 매우 강렬한 컨셉과 스타일 귀여우면서 매력적인 악마 캐릭터들, 짧지만 센스있는 대사와 음악 게임 팬아트와 밈이 유행하면서 자발적인 바이럴 마케팅 무료 배포로 진입장벽을 완전히 낮췄다 초기에 수익보다는 팬층 확보를 먼저하고 IP 가치를 높여 향후 수익화를 도모하는 전략 2.6. 공통점 잘 시도하지 않는 독창성을 가지고 있거나, 기존 장르라도 차별화된 재미의 핵심을 정확히 포착했다 커뮤니티를 적극적으로 활용하거나 출시 후 입소문/스트리머 효과를 극대화하여 마케팅 비용 절감 1인이 감당 가능한 적정 개발 규모와 범위를 유지하여 컨텐츠 양보단 완성도에 집중 3. 전략적 개발 프로세스 제안 1단계: 게임 아이디어 기획과 트렌드 분석 초기 기획 단계에서 1인 개발 규모에 맞는 MVP(Minimum Viable Product)를 선별해야 한다 Vampire Survivors: **“자동으로 공격하는 캐릭터가 적들을 물리치는 단순한 구조”라는 핵심 아이디어로 프로토타입을 만든 후 콘텐츠를 지속적으로 추가하는 식으로 개발 트렌드 분석 후 수요가 있지만 충족되지 않은 틈새를 찾아야 한다 Phasmophobia: 당시 협동 공포 장르가 드물었던 점을 노렸다 Helltaker: 퍼즐 게임에 매력적인 캐릭터를 결합하면 성공 가능성이 높다고 판단 일반적인 2D 플랫포머 같은 이미 유사 게임이 포화된 장르는 피하거나, 반드시 해야 한다면 비주얼/스토리 등 차별화가 필요하다. 아이디어를 커뮤니티에 공유해서 초기 피드백을 활용할 수 있다 2단계: 적합한 엔진 및 툴 선택 Unity 및 Unreal Engine 가장 대중적인 선택지 풍부한 Asset과 레퍼런스 Game Maker 및 Godot 2D 도트 그래픽 위주면 고려 가능 그래픽/사운드 등 혼자 처리하기 힘든 작업은 에셋이나 외주 권장 핵심 개발 역량을 게임성에 집중 3단계: 개발 일정 수립 및 범위 관리 현실적인 마일스톤 설정하고 지키려고 하되, 1인 개발 장점을 살린 융통성도 필요 성공한 1인 개발자들은 공통적으로 우선순위 결정과 절제를 잘했다 스타듀밸리 개발자는 개발 기간 동안 콘텐츠 아이디어가 폭주했지만, 핵심이 아닌 기능은 과감히 잘라내고 핵심 게임 루프에 집중 개발 중기 이후에 폴리싱(다듬기) 단계를 별도로 두어 버그 수정과 UX 개선에 충분한 시간 투자 게임은 출시 초 첫 인상이 중요하므로 처음부터 컨텐츠 양에 욕심내기보다 완성도를 높이는 것이 장기적으로 유리 일정이 지연될 시 우선 순위가 낮은 요소는 과감히 제외하고 Early Access 이후 유저 피드백을 받으며 업데이트 지속적으로 개발을 이어가기 위해선 동기부여가 필요하므로 목표 달성마다 커뮤니티에 공유하거나 데모를 공개하여 유저의 반응을 얻는다 4단계: 플레이테스트 및 커뮤니티 구축 유저가 재밌어하는지 확인하려면 빈번한 플레이테스트 필요 1인 개발의 경우 QA팀이 따로 없기 때문에 외부로부터 피드백을 받아야 한다 Itch.io, Steam의 비공개 플레이테스트, Discord 커뮤니티 Phasmophobia: Early Access로 출시되자마자 수많은 유저 의견이 쏟아졌고, 개발자는 이를 반영하여 UI/난이도 개선 테스트 단계의 핵심은 유저가 겪는 불편을 빠르게 개선하는 민첩성 버그 리포트 및 건의사항에 개발자가 직접 대응하면 커뮤니티 충성도가 올라간다 공식 커뮤니티를 만들고 팬들과 소통 Helltaker: 팬아트를 리트웟하고 작은 업데이트라도 꾸준히 공유 스타듀밸리: 공식 포럼과 Reddit에서 유저들이 팁을 공유하고 모드를 만들면서 커뮤니티 주도적 확산 5단계: 마케팅 및 출시 전략 Steam 출시 전 위시리스트를 최대한 확보 Steam Next Fest 같은 인디게임 체험 행사에 데모 출품 데모가 있으면 피드백을 받을 수 있고, 행사 기간 받은 위시리스트가 향후 출시 시점 알고리즘 노출에 큰 도움이 된다 SNS 무료이면서 영향력 있는 마케팅 수단 개발 과정에서 재미있는 순간의 영상이나 사진을 만들어 유뷰트, 트위터, 인스타그램, 틱톡 등에 공유하면 예상치 못한 바이럴이 일어날 수 있다 Bright Memory: 개발 초기에 레딧과 중국 SNS에 공개한 영상이 화제가 돼 투자 유치와 퍼블리셔 연결로 이어졌다 커뮤니티 홍보 문구보다는 진정성있는 개발 스토리를 전하는 것이 중요 출시 시점 세일 및 스트리머 협업을 통해 초기 유입을 극대화 Phasmophobia: 성공은 공포 게임 스트리머의 역할이 컸다 Vampire Survivors: 유튜버들의 플레이 영상 덕에 판매 폭증 예산 부족 시 수익을 나누는 조건으로 퍼블리셔와 협업하는 것도 고려 Bright Memory: 퍼블리셔 도움으로 콘솔 진출 출시 후 버그 패치는 신속히 하고, 플레이어 피드백을 수용해 일정 주기로 콘텐츠 업데이트나 이벤트를 제공하여 유저 풀 유지 스타듀밸리: 무료 업데이트로 장기 흥행 Vampire Surviors: 저렴한 DLC를 꾸준히 출시 개발 프로세스 요약 작게 시작하여 지속적으로 키워나가는 것이 핵심 처음부터 완벽한 대작을 만들기보다, 핵심이 탄탄한 게임을 일단 선보이고 사용자와 함께 발전시키는 접근이 현실적이고 성공 확률도 높다 적절한 아이디어, 도구 활용, 커뮤니티 소통, 효과적인 마케팅까지 한 흐름으로 관리 4. 결론 및 종합 전략 플랫폼 진입 장벽이 낮고 글로벌 유통이 가능한 Steam 시장부터 공략 성공 시 콘솔로 확장 장르 로그라이크, 시뮬레이션, 스토리 위주 어드벤처, 협동 공포 이미 포화된 장르는 피하고 반드시 명확한 차별화 요소를 넣는다 개발 프로세스 과욕을 버리고 핵심 재미에 집중한 기획 절제 자신에게 맞는 엔진/에셋 활용으로 생산성 극대화 현실적인 일정 관리와 범위 조절로 완성도 확보 중간중간 커뮤니티 피드백으로 게임성 검증 및 보완 커뮤니티/마케팅 개발 초기부터 소통 채널들을 확보 팬 형성에 힘쓰고 데모 공개와 Steam Next Fest 참가로 입소문과 위시리스트를 모은다 출시 시 스트리머/SNS 등 모든 홍보 채널 동원 출시 후 꾸준한 업데이트와 투명한 소통으로 유저 충성도를 높인다 결론 혼자의 한계를 인정하면서도, 혼자이기에 할 수 있는 민첩하고 독창적인 개발을 하는 것이 1인 개발 성공의 요체 거대한 팀은 시도하기 어려운 실험적인 게임도 1인 개발자는 만들어낼 수 있고, 커뮤니티와 직접 호흡하며 빠르게 발전시킬 수 있다 ","permalink":"https://nansu0425.github.io/posts/2025%EB%85%84-1%EC%9D%B8-%EA%B2%8C%EC%9E%84-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EC%84%B1%EA%B3%B5-%EC%A0%84%EB%9E%B5-%EC%A0%95%EB%A6%AC/","summary":"2025년 1인 게임 개발자 성공 전략 정리","title":"2025년 1인 게임 개발자 성공 전략 정리"},{"content":" 프로젝트 Gtihub: https://github.com/nansu0425/CMakeTest\nOverview Visual Studio 2022에서 크로스플랫폼 C++ 앱을 빌드하고 디버깅할 수 있는 CMake 프로젝트 구성 예제입니다. 원격 리눅스(Ubuntu 24.04)에 연결하여 로컬 윈도우(Windows 11)에서 개발하는 환경을 기준으로 구성했습니다. 빌드 configuration 구성을 수정하고 싶다면 CMakePresets.json를 수정하면 됩니다. vcpkg를 이용해 의존성 관리를 합니다. CMake 실행 시 vcpkg.json에 명시된 의존성 패키지를 자동으로 설치합니다. 빌드된 프로그램은 Asio 라이브러리를 이용해 비동기로 Hello, World!를 출력합니다.\nPrerequisites 아래 링크를 참고하여 프로젝트에 필요한 도구 설치와 설정을 진행하세요.\nhttps://learn.microsoft.com/ko-kr/cpp/build/cmake-projects-in-visual-studio?view=msvc-170 https://learn.microsoft.com/ko-kr/cpp/build/get-started-linux-cmake?view=msvc-170 Demo https://www.youtube.com/watch?v=Yn2SrqHXhnQ ","permalink":"https://nansu0425.github.io/posts/%ED%81%AC%EB%A1%9C%EC%8A%A4%ED%94%8C%EB%9E%AB%ED%8F%BC-cmake-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%98%88%EC%A0%9C/","summary":"크로스플랫폼 CMake 프로젝트 예제","title":"크로스플랫폼 CMake 프로젝트 예제"},{"content":"1. 테스트 영상 https://www.youtube.com/watch?v=x-H8K8AElgU\n로컬호스트 서버로 진행 클라이언트는 1000개의 세션 생성 각 세션은 1초마다 서버에 ping 요청 ping 응답을 받은 세션은 요청 후 응답을 받을 때까지 걸린 시간을 마이크로초 단위로 출력 2. 세부 사항 2.1. 테스트 환경 OS Windows 11 CPU 11th Gen Intel(R) Core(TM) i5-11500 @ 2.70GHz 2.71 GHz 코어: 6개 논리 프로세서: 12개 RAM 16GB 2.2. 프로그램 설정 클라이언트 소켓 입출력 스레드: 4개 세션 관리 스레드: 1개 메시지 처리 스레드: 4개 비동기 작업 스레드: 3개 서버 소켓 입출력 스레드: 4개 세션 관리 스레드: 3개 메시지 처리 스레드: 4개 비동기 작업 스레드: 1개 3. 프로젝트 코드 https://github.com/nansu0425/PattyCore\n","permalink":"https://nansu0425.github.io/posts/%EC%84%B8%EC%85%98-1000%EA%B0%9C-ping-%ED%85%8C%EC%8A%A4%ED%8A%B8/","summary":"세션 1000개 Ping 테스트 기록","title":"세션 1000개 Ping 테스트"},{"content":"1. UDP 소켓을 사용하는 클라이언트 https://gist.github.com/nansu0425/f23c3770e3be5aab3e6d7c3720146214\n클라이언트 코드는 위와 같다. 전체적인 코드의 흐름은 UDP 소켓으로 daytime 서버에 데이터그램을 전송한 후 서버가 daytime 정보를 담은 데이터그램을 클라이언트로 보내면 그것을 수신한 후 출력하는 것이다. 만약 서버가 없다면 어떤 문제가 생길까?\n2. 서버가 없을 때 출력 receive_from: 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다 [system:10054 at ... in function \u0026#39;receive_from\u0026#39;] 윈도우 환경에서 로컬 호스트로 테스트했을 때 위와 같이 출력된다. receive_from에서 예외가 발생한다. 에러 코드를 보면 system:10054인데, https://www.boost.org/doc/libs/1_87_0/libs/system/doc/html/system.html 이 글을 읽어보면 10054는 OS 레벨에서 반환한 에러 코드를 의미한다는 것을 알 수 있다.\n3. 윈도우 소켓 에러 코드 찾아보니 10054는 윈도우 소켓의 에러 코드 중 WSAECONNRESET에러의 값이다. https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2 에서 에러의 설명을 보면 원격 호스트의 문제로 연결이 끊긴 것이라고 한다. 하지만 여기서 의문이 생긴다. UDP는 TCP와 달리 비연결지향형 프로토콜로 연결이라는 개념이 없다. receive_from에서 애초에 연결이 없는 상태인데 연결을 끊다는 것이 무슨 의미이고 원격 호스트에 문제가 있는지 어떻게 아는 걸까?\n4. 패킷 분석 통신 중에 주고받는 패킷을 본다면 어떤 식으로 동작하는지 힌트를 얻을 수 있을 것이라고 생각했다.\nWireshark로 클라이언트 코드를 실행하고 패킷을 캡처했다. DAYTIME 프로토콜 요청을 먼저 보내는데 이것은 send_to를 할 때 전송하는 데이터그램일 것이다. 이후 ICMP 메시지를 받는데 Destination unreachable 타입 메시지를 받는다. 즉 send_to를 했을 때 클라이언트는 데이터그램이 목적지에 도달하지 못 했다는 것을 알게 된다. 윈도우 소켓은 데이터그램을 보냈을 때 Destination unreachable ICMP 메시지를 받으면 데이터그램 수신을 시도할 때 10054 에러가 발생한다는 것을 알 수 있다.\n5. 10054 에러를 무시한다면 https://gist.github.com/nansu0425/3daacfdff53a9b7e683b09bab7dae862\nreceive_from을 시도했을 때 10054 에러가 발생하면 receive_from을 다시 시도하도록 코드를 수정했다. 코드를 실행해보니 receive_from 첫 시도에서만 10054 에러가 발생하고 다음 시도에서는 정상적으로 수신 대기를 한다. 하지만 이 코드에서 주의할 점은 서버가 클라이언트의 요청을 받아야 클라이언트에게 응답을 하는 구조인데 만약 클라이언트 send_to 이후에 서버 측 소켓이 생성된다면, 클라이언트는 서버로부터 어떤 응답을 받지 못하고 기다리기만 할 것이다. 최초 send_to 이후로 클라이언트는 추가적인 요청을 하지 않고, 서버는 소켓을 생성하기 전에 클라이언트가 보낸 요청에 대해선 알 수 없기 때문이다.\n","permalink":"https://nansu0425.github.io/posts/udp-%EC%86%8C%EC%BC%93%EC%9C%BC%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B7%B8%EB%9E%A8-%EC%A0%84%EC%86%A1-%EC%8B%9C-system-10054-%EC%97%90%EB%9F%AC/","summary":"UDP 소켓으로 데이터그램 전송 시 system:10054 에러","title":"UDP 소켓으로 데이터그램 전송 시 system:10054 에러"},{"content":" 본 게시물의 내용은 AI에게 질문한 답변을 정리한 것입니다.\n❓ 질문 io_context::run 함수를 호출한 스레드가 하는 일이 io_context에 등록된 비동기 작업이 끝났을 때 핸들러를 호출하는 것인지 아니면 요청한 비동기 작업 전체를 처리하는 것인지 궁금해\n💬 답변 boost::asio::io_context::run 함수를 호출한 스레드가 하는 작업은 다음과 같습니다:\n1. 핸들러 실행 io_context::run을 호출한 스레드는 io_context에 등록된 완료된 비동기 작업의 핸들러를 실행합니다. 즉, 비동기 작업이 완료되면 등록된 콜백(핸들러)이 실행되는데, 이 핸들러 호출은 io_context::run을 호출한 스레드에서 이루어집니다.\n2. 작업 실행 여부 스레드가 실제 비동기 작업(예: 네트워크 데이터 수신, 타이머 대기 등)을 수행하지는 않습니다. 비동기 작업 자체는 일반적으로 운영 체제나 관련 시스템 콜 수준에서 처리되며, 완료된 작업의 결과만 io_context로 반환됩니다. 반환된 작업은 io_context의 내부 큐에 저장되고, io_context::run을 호출한 스레드가 큐에 있는 핸들러를 순차적으로 실행합니다.\n동작 과정 요약 사용자가 io_context에 비동기 작업을 등록 (async_* 함수 사용). 비동기 작업은 운영 체제나 다른 스레드가 처리. 작업이 완료되면 관련 핸들러가 io_context의 작업 큐에 등록. io_context::run을 호출한 스레드가 이 작업 큐를 순회하면서 핸들러를 실행. 주요 특징 비동기 작업 수행: async_read와 같은 작업은 Boost.Asio가 내부적으로 운영 체제의 비동기 I/O 기능(예: epoll, kqueue, IOCP)을 사용하여 처리합니다. 이 작업은 io_context::run을 호출한 스레드와는 독립적으로 동작합니다. 핸들러 실행 전담: io_context::run을 호출한 스레드는 완료된 작업의 핸들러만 실행합니다. 멀티스레드: io_context::run을 여러 스레드에서 호출하면, 등록된 핸들러들이 여러 스레드에서 병렬로 처리될 수 있습니다. 단, 동일한 핸들러는 한 번에 하나의 스레드에서만 실행됩니다. 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include \u0026lt;boost/asio.hpp\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; void handler(const boost::system::error_code\u0026amp; ec) { if (!ec) { std::cout \u0026lt;\u0026lt; \u0026#34;Timer expired!\\n\u0026#34;; } } int main() { boost::asio::io_context io_context; boost::asio::steady_timer timer(io_context, std::chrono::seconds(2)); timer.async_wait(handler); // io_context::run 호출 (핸들러 실행 전담) std::thread t([\u0026amp;io_context]() { io_context.run(); }); t.join(); // 스레드 대기 return 0; } 실행 흐름:\n타이머 비동기 작업이 async_wait로 등록됩니다. 타이머는 별도의 시스템 리소스에서 시간이 경과하는지 확인합니다. 시간이 만료되면 io_context로 완료 신호가 전달됩니다. io_context::run을 호출한 스레드가 handler를 호출합니다. 결론 io_context::run을 호출한 스레드는 비동기 작업의 실행 자체를 처리하지 않으며, 완료된 작업의 핸들러를 실행하는 데만 관여합니다. 비동기 작업은 운영 체제 또는 Boost.Asio의 내부 메커니즘에 의해 처리됩니다.\n","permalink":"https://nansu0425.github.io/posts/io_context%EC%9D%98-run-%ED%98%B8%EC%B6%9C-%EC%8A%A4%EB%A0%88%EB%93%9C%EB%8A%94-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%9E%91%EC%97%85%EC%9D%98-%ED%95%B8%EB%93%A4%EB%9F%AC%EB%A7%8C-%EC%B2%98%EB%A6%AC%ED%95%9C%EB%8B%A4/","summary":"io_context의 run 호출 스레드는 비동기 작업의 핸들러만 처리한다","title":"io_context의 run 호출 스레드는 비동기 작업의 핸들러만 처리한다"},{"content":" 본 게시물의 내용은 AI에게 질문한 답변을 정리한 것입니다.\n❓ 질문 완료된 비동기 작업과 핸들러가 io_context의 큐에 어떤식으로 저장되는지 알려줘. 그리고 핸들러를 처리하는 이벤트 루프에 대해서도 알려줘.\n💬 답변 Boost.Asio의 io_context는 비동기 작업과 핸들러를 관리하는 핵심 컴포넌트로, 이벤트 루프의 역할을 합니다. 이 과정은 크게 두 가지로 나뉩니다: 비동기 작업과 핸들러가 io_context의 내부 큐에 저장되는 방식, 그리고 핸들러를 순차적으로 실행하는 이벤트 루프의 작동 방식입니다.\n1. 완료된 비동기 작업과 핸들러의 저장 io_context는 내부적으로 **작업 큐(task queue)**와 **완료 핸들러 큐(completion handler queue)**를 관리합니다.\n비동기 작업의 처리 흐름: 비동기 작업 등록:사용자가 async_* 함수(예: async_read, async_wait)를 호출하면, io_context는 이 작업의 핸들러와 필요한 메타데이터를 저장하고, 비동기 작업 자체를 운영 체제나 비동기 I/O 메커니즘에 위임합니다. 작업 완료 시 큐에 등록:작업이 완료되면, Boost.Asio는 핸들러를 완료 핸들러 큐에 추가합니다. 이 핸들러는 작업 결과(예: 데이터 수신, 타이머 완료 등)를 포함하여 실행 가능한 상태로 저장됩니다. 핸들러 호출 대기:완료된 핸들러는 io_context::run을 통해 실행되기 전까지 큐에 대기합니다. 내부 큐의 구성: 작업 큐: 비동기 작업과 관련된 메타데이터가 저장됩니다. 작업이 완료되지 않은 상태에서 대기하는 작업입니다. 완료 핸들러 큐: 작업 완료 시 실행할 준비가 된 핸들러들이 저장됩니다. 이 큐는 이벤트 루프가 순차적으로 처리합니다. 2. 핸들러 실행 및 이벤트 루프 핸들러를 실행하는 과정은 이벤트 루프(Event Loop)로 작동합니다. 이벤트 루프는 io_context::run 메서드에서 실행됩니다.\n이벤트 루프의 작동 방식: 핸들러 가져오기: io_context의 완료 핸들러 큐에서 준비된 핸들러를 하나 가져옵니다. 핸들러 실행: 가져온 핸들러를 호출합니다. 핸들러는 비동기 작업의 결과를 처리하거나 추가 작업을 등록할 수 있습니다. 반복 실행: 완료 핸들러 큐에 남은 핸들러가 있다면 다음 핸들러를 가져와 실행합니다. 큐가 비어 있으면 io_context::run은 차단(block) 상태로 들어가, 새로운 작업이 완료될 때까지 대기합니다. 이벤트 루프의 흐름: 1 2 3 4 5 6 7 8 while (!io_context.done()) { if (completion_handler_queue.has_handlers()) { auto handler = completion_handler_queue.get_next_handler(); handler(); // 핸들러 실행 } else { wait_for_new_work(); // 새로운 작업이 완료될 때까지 대기 } } 3. 핸들러 실행의 순차성 io_context는 단일 스레드에서 실행될 경우, 완료 핸들러를 등록된 순서대로 순차적으로 실행합니다. 다중 스레드에서 io_context::run을 호출하면, 완료 핸들러 큐에 있는 작업들이 여러 스레드에서 병렬로 실행될 수 있지만, 각 핸들러는 단일 스레드에서 실행됩니다(핸들러 자체는 경쟁 상태가 없음). 4. 이벤트 루프와 io_context의 역할 이벤트 루프의 역할:io_context의 이벤트 루프는 비동기 작업 완료 후 핸들러를 실행하는 메커니즘입니다. 이는 전형적인 이벤트 루프 패턴을 따릅니다: 작업 큐 관찰 완료된 작업 처리 대기와 실행 반복 io_context의 역할:io_context는 이벤트 루프를 관리하며, 비동기 작업의 등록, 핸들러 실행, 대기 등을 수행합니다. 간단한 그림으로 설명 비동기 작업 등록 (async_* 호출):\n[작업 등록] -\u0026gt; [작업 큐] -\u0026gt; (OS 또는 비동기 메커니즘) 작업 완료 및 핸들러 준비:\n(작업 완료) -\u0026gt; [완료 핸들러 큐] -\u0026gt; (대기) 이벤트 루프에서 핸들러 실행:\n[완료 핸들러 큐] -\u0026gt; [핸들러 실행] -\u0026gt; (반복) 예제와 함께 보는 이벤트 루프 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include \u0026lt;boost/asio.hpp\u0026gt; #include \u0026lt;iostream\u0026gt; #include \u0026lt;thread\u0026gt; void timerHandler1(const boost::system::error_code\u0026amp;) { std::cout \u0026lt;\u0026lt; \u0026#34;Timer 1 expired!\\n\u0026#34;; } void timerHandler2(const boost::system::error_code\u0026amp;) { std::cout \u0026lt;\u0026lt; \u0026#34;Timer 2 expired!\\n\u0026#34;; } int main() { boost::asio::io_context io_context; // 두 개의 타이머 등록 boost::asio::steady_timer timer1(io_context, std::chrono::seconds(2)); timer1.async_wait(timerHandler1); boost::asio::steady_timer timer2(io_context, std::chrono::seconds(1)); timer2.async_wait(timerHandler2); // 이벤트 루프 실행 io_context.run(); return 0; } 실행 흐름:\ntimer2가 먼저 완료 -\u0026gt; timerHandler2 실행. timer1이 완료 -\u0026gt; timerHandler1 실행. 출력:\nTimer 2 expired! Timer 1 expired! 이 프로그램의 io_context.run()은 이벤트 루프를 실행하여 핸들러를 순차적으로 실행합니다.\n","permalink":"https://nansu0425.github.io/posts/io_context%EC%9D%98-%EB%82%B4%EB%B6%80-%ED%81%90%EC%99%80-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84/","summary":"io_context의 내부 큐와 이벤트 루프","title":"io_context의 내부 큐와 이벤트 루프"},{"content":"1. 작업 중이던 코드 https://gist.github.com/nansu0425/725bf8c01a928b00c651448d967cd03e\n스레드를 관리하는 클래스를 만들고 있었다. 클래스 이름은 ThreadManager고 위 코드는 이 클래스의 메서드 중 하나인 Launch메서드다. 이 메서드를 구현하던 중 스택 유효 범위 관련 문제를 겪었다. 스택이 동작하는 방식은 크게 어렵지 않아서 이런 부분에서 실수할 것이라는 생각은 하지 못 했는데 아직 기본기가 부족한 것 같다. 코드의 맥락을 좀 더 설명해보면, Launch 메서드의 주요 동작은 callback을 수행하는 스레드를 만드는 것이다 . 멀티스레드 환경에서 수행돼서 3번 라인에서 락을 건다. _threads는 std::thread 타입의 스레드를 관리하는 std::vector다. 람다 함수는 callback을 호출한다. callback 호출 전후 코드는 thread local storage를 초기화하고 정리하는 코드다. 위 코드에서 무슨 문제가 발생할까?\n2. 문제 분석 이 코드를 실행하면 callback을 호출하는 부분에서 프로그램이 죽는다. 그리고 죽는 것을 여러 번 시도해보면 에러 내용이 바뀔 때도 있다. 이런 상황이 발생하는 이유는 이 글의 제목에서 언급하듯이 스택의 유효 범위를 간과했기 때문이다. 스레드 생성 시 전달하는 람다 함수는 callback 파라미터의 참조를 캡처한다. 따라서 callback을 호출할 때 callback 파라미터는 유효해야 한다. 하지만 스레드를 생성하고 Launch 메서드는 종료되는데 그때 스택 프레임을 정리하면서 callback 파라미터를 정리한다. Launch의 스택 프레임이 정리되기 전에 생성한 스레드가 callback을 호출한다면 제대로 동작할 수도 있지만 보통 스레드가 생성되고 그 스레드가 시작되기까지 시간이 걸린다. 즉 위 상황에선 callback을 호출할 때 callback 파라미터는 이미 유효한 상태가 아닐 가능성이 매우 높다.\n3. 수정 코드 https://gist.github.com/nansu0425/2ccf37be0bb1b8dd7350bbd6ef7e5ade\n람다가 callback을 캡처할 때 참조가 아닌 값을 캡처한다. 그러면 callback을 복사하게 되고 파라미터의 유효성은 람다의 callback에 영향을 주지 못 한다. 이런 스택 유효 범위 문제는 코드가 단순하면 쉽게 보이는데 약간 복잡해지면 은근히 실수하기 쉬운 것 같다. 스택의 데이터를 사용하는 것에 대한 경각심이 생겼다.\n","permalink":"https://nansu0425.github.io/posts/%EC%A0%95%EB%A7%90-%EA%B8%B0%EB%B3%B8%EC%A0%81%EC%9D%B8-%EA%B2%83%EC%9D%B4%EC%A7%80%EB%A7%8C-%EA%B0%80%EB%81%94-%EB%86%93%EC%A7%80%EB%8A%94-%EC%8A%A4%ED%83%9D-%EC%9C%A0%ED%9A%A8-%EB%B2%94%EC%9C%84/","summary":"C++ 작업 중 스택 유효 범위 관련 오류를 겪고 그것에 대해 정리한 글","title":"정말 기본적인 것이지만 가끔 놓지는 스택 유효 범위"},{"content":"1. 권장 버전 확인 언리얼 엔진을 처음 사용하다 보니 개발 환경을 설정하는 것에서 문제가 계속 터진다. 블루프린트만 사용하다가 C++을 사용해보려고 했는데 Editor 빌드에서 계속 실패했다. 반나절 정도 씨름하다가 알게된 원인은 MSVC 버전 문제였다. 현재 언리얼 엔진 5.0.3, Visual studio 2022를 사용 중이다. Visual studio를 설치할 때 최신 버전의 빌드 도구를 설치했는데 최신 버전은 호환되지 않는 것 같다. 언리얼 엔진 공식 문서를 확인해보면 MSVC 권장 버전은 14.38.33130이다.\n2. MSVC 권장 버전 설치 Visual Studio 설치 관리자에서 수정 버튼을 누른 후 개별 구성 요소 탭을 들어간다. 그 후 msvc를 검색하면 여러 버전의 msvc 목록이 나오는데 14.38.33130 버전을 포함하는 것을 고른 후 수정 버튼을 눌러 다운 받는다.\n3. 언리얼 엔진 빌드 설정 다운 받은 MSVC 버전을 언리얼 엔진 빌드에서 사용하도록 설정해줘야 한다. \\AppData\\Roaming\\Unreal Engine\\UnrealBuildTool 경로에 들어가면 BuildConfiguration 이름의 XML 파일이 있다. 그 파일을 수정해야 하는데 아래와 같이 바꿔준다.\n1 2 3 4 5 6 \u0026lt;Configuration xmlns=\u0026#34;https://www.unrealengine.com/BuildConfiguration\u0026#34;\u0026gt; \u0026lt;WindowsPlatform\u0026gt; \u0026lt;CompilerVersion\u0026gt;14.38.33130\u0026lt;/CompilerVersion\u0026gt; \u0026lt;ToolchainVersion\u0026gt;14.38.33130\u0026lt;/ToolchainVersion\u0026gt; \u0026lt;/WindowsPlatform\u0026gt; \u0026lt;/Configuration\u0026gt; 여기까지 해주면 MSVC 호환성 문제로 빌드 실패하는 문제는 해결된다.\n","permalink":"https://nansu0425.github.io/posts/msvc-%ED%98%B8%ED%99%98%EC%84%B1-%EB%AC%B8%EC%A0%9C%EB%A1%9C-%EC%9D%B8%ED%95%9C-%EB%B9%8C%EB%93%9C-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0/","summary":"MSVC 호환성 문제로 인한 빌드 문제를 해결하는 방법 정리","title":"MSVC 호환성 문제로 인한 빌드 문제 해결"},{"content":"1. 데모 영상 https://www.youtube.com/watch?v=UlvL4TSa6PY\n2. 핵심 구현 Space를 누르면 Player가 바라보는 방향으로 Projectile이 발사된다. Projectile이 발사되면 화면 왼쪽 위에 남은 Ammo 개수와 생성된 Projectile 이름이 출력된다. Ammo는 20개고 다 떨어지면 5초 후 Level을 처음 상태로 되돌린다. Rack과 Barrels는 Projectile과 충돌하면 운동량이 변한다. 3. 후기 언리얼 엔진으로 게임을 만들어보고 싶어서 강의를 보기 시작했다. 강의는 프로젝트를 따라 하며 학습하는 방식으로 구성돼 있는데, Warehouse Wrecking 프로젝트가 강의의 첫 프로젝트다. 이틀 정도 걸린 것 같고 Blueprint, BSP, Actor를 다루는 기초적인 방법을 배웠다. 프로젝트를 Github에 기록하고 싶었는데 크기가 너무 커서 올리지 못했다. Github는 에셋같은 바이너리 파일이 아니라 소스코드를 보관하는 용도란 것을 잊고 있었다. 알아보니 큰 데이터를 보관하기 위한 Git LFS(Large File Storage)라는 것도 알게 됐는데 요금 때문에 선뜻 사용하지 못했다. 언리얼 엔진 프로젝트의 보관 및 공유 방법을 찾아야 할 필요성을 느꼈다.\n4. 관련 링크 강의 링크: https://www.udemy.com/course/unrealcourse/ ","permalink":"https://nansu0425.github.io/posts/warehouse-wrecking-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%95%99%EC%8A%B5/","summary":"Unreal Engine 강의의 Warehouse Wrecking 프로젝트를 따라하며 학습한 후 정리","title":"Warehouse Wrecking 프로젝트 학습"},{"content":"1. std::remove 동작 remove함수는 컨테이너의 특정 원소를 제거할 때 사용한다. remove를 사용하기 위해선 algorithm 헤더 파일이 필요하다. 아래는 vector의 원소를 제거하는 예제 코드다.\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;vector\u0026gt; #include \u0026lt;algorithm\u0026gt; int main() { std::vector\u0026lt;int\u0026gt; values = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; std::vector\u0026lt;int\u0026gt;::iterator last = std::remove(values.begin(), values.end(), 4); return 0; } values의 원소 중 4를 제거했다. remove 후에는 남은 원소들의 마지막 원소 다음 위치의 반복자를 반환한다. 따라서 last는 9 다음 원소를 가리키게 된다. remove 사용 시 주의할 점은 제거 후 컨테이너의 원소 개수는 변하지 않는다는 것이다. remove 동작은 제거할 원소 위치에 뒤에 있는 원소들을 앞으로 복사하여 덮어쓰는 것이다. 이런 동작으로 인해 last가 가리키는 원소의 값을 확인해보면 9다. last는 values[9]를 가리키게 되는데 values[8]에 9를 덮어쓴 후 여전히 원래 값을 가지고 있기 때문이다. 원소 제거 후 컨테이너의 크기까지 줄이고 싶다면 erase함수를 사용해야 한다.\n2. std::erase 동작 erase는 원소 제거 후 컨테이너의 원소 개수를 제거한 원소 수만큼 줄인다. capacity는 변하지 않는다.\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;vector\u0026gt; int main() { std::vector\u0026lt;int\u0026gt; values = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; std::vector\u0026lt;int\u0026gt;::iterator erased = values.erase(values.begin() + 5); return 0; } values의 5번 인덱스에 해당하는 원소를 제거한다. erase도 remove처럼 반복자를 반환하는데 제거된 원소가 있던 위치의 반복자를 반환한다. 제거된 원소의 위치는 5번 인덱스이므로 erased는 values[5]를 가리키는 반복자다. 제거 후엔 remove처럼 제거된 원소 뒤의 원소들을 앞으로 복사하여 덮어쓴다. 따라서 erased가 가리키는 위치에 저장된 값은 6이다. erase는 원소 개수를 줄이므로 values의 size는 9가 된다.\n3. std::remove와 std::erase 활용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include \u0026lt;vector\u0026gt; #include \u0026lt;algorithm\u0026gt; int main() { std::vector\u0026lt;int\u0026gt; values = {4, 5, 8, 2, 6, 3, 7, 1, 0, 2, 3, 6, 1, 8}; std::vector\u0026lt;int\u0026gt;::iterator erased = values.erase(std::remove_if(values.begin(), values.end(), [](const int \u0026amp;value) { return (value \u0026gt; 4); }), values.end()); return 0; } 위 코드는 remove_if를 이용해 4보다 큰 원소들을 제거 후, 남은 원소들 개수에 맞게 컨테이너의 원소 개수를 줄인다. 이때 erased는 제거 후 남은 원소들 중 마지막 원소의 다음 원소를 가리킨다. erase는 반복자를 이용해 특정 위치나 범위를 지정하는 것만 가능하고 값이나 특정 조건을 이용해 제거할 원소를 지정하는 것은 불가능하다. 조건식으로 제거할 원소를 지정하기 위해 remove_if를 사용했고 컨테이너 원소 개수를 줄이기 위해 erase를 사용했다.\n","permalink":"https://nansu0425.github.io/posts/std-erase%EC%99%80-std-remove-%EB%B9%84%EA%B5%90/","summary":"std::erase와 std::remove 함수를 비교한 후 정리하는 글","title":"std::erase와 std::remove 비교"},{"content":"1. 데모 영상 https://www.youtube.com/watch?v=pJdUsPuMGEw\n2. 핵심 구현 너비와 높이를 설정하면 크기에 맞는 Field가 중앙에 그려진다. Field 초기화 시 모든 Tiles를 초기화한다. 미리 설정된 지뢰 개수에 따라 무작위로 Tile을 골라 지뢰가 있는 Tile로 설정한다. 지뢰가 없는 Tile의 경우 자신을 둘러싼 이웃 Tiles를(8개) 확인 후 지뢰의 개수를 저장한다. 지뢰를 제외한 모든 Tiles가 드러나면(왼쪽 클릭) Player가 승리한다. 지뢰가 아닌 Tile이 드러나면 초기화 때 저장된 주변 지뢰의 개수가 표시된다. 만약 이웃 Tiles에 지뢰가 없는 Tile이 드러난 경우, 이웃 Tiles가 드러난다 (재귀적으로 수행) 지뢰가 있을 것 같다고 생각되는 곳에 마우스 오른쪽을 클릭하면 Flag가 설정된다. 지뢰를 누르면 클릭한 지뢰는 빨간색 Tile로 표시되고 나머지 지뢰가 드러난다. 3. 관련 링크 프로젝트 코드 https://github.com/nansu0425/CppGameProgramming/tree/main/Minesweeper 강의 https://www.youtube.com/watch?v=DkjOZ_Hlqbs\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=48 https://www.youtube.com/watch?v=_ss9u40AOSI\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=49 https://www.youtube.com/watch?v=2n29D6V0bZo\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=50 https://www.youtube.com/watch?v=aiWM7i7Lnj4\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=51\u0026t=0s 사용 프레임워크 https://github.com/planetchili/chili_framework ","permalink":"https://nansu0425.github.io/posts/%EC%A7%80%EB%A2%B0-%EC%B0%BE%EA%B8%B0-minesweeper/","summary":"지뢰 찾기 프로젝트를 진행하고 정리하는 글","title":"지뢰 찾기 (Minesweeper)"},{"content":"1. 소개 Red-Black 트리는 BST 종류 중 하나로 스스로 균형을 잡는 Self-Balanced BST다. BST에서 균형은 성능을 높이기 위해 매우 중요하다. 탐색과 삽입, 삭제 등 주요 연산의 시간 복잡도는 균형이 잡혔을 때 $O(\\log n)$이지만 한쪽으로 편향될수록 $O(n)$에 가까워진다. Red-Black 트리 외에 대표적인 Self-Balanced BST로 AVL 트리가 있다. AVL 트리는 Red-Black 트리에 비해 엄격하게 균형을 유지해서 탐색에선 AVL트리가 더 장점이 있다. 대신 균형을 유지하기 위한 오버헤드가 Red-Black 트리보다 커서 삽입/삭제 같은 트리를 수정하는 연산은 Red-Black 트리에 비해 느릴 가능성이 높다. AVL 트리는 예전에 한 번 구현해 봤다. Red-Black 트리는 AVL 트리에 비해 구현 난이도가 높아 보여서 구현을 미뤘었는데 이번에 도전해 봤다. 유튜브 쉬운 코드 채널의 Red-Black 트리 강의를 바탕으로 구현했다.\n2. 구조 2.1. Node struct 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /*------------* * Node * *------------*/ template\u0026lt;typename K, typename V\u0026gt; struct Node { enum class Color { red, black, doubleBlack, nilBlack, }; K\tkey; V\tvalue; Node*\tpRight = nullptr; Node*\tpLeft = nullptr; Color\tcolor = Color::red; Node(K key, V value); }; key는 트리에서 Node를 식별하기 위해 사용하고 value가 실질적인 데이터를 저장한다. 자식을 가리키기 위한 포인터 두 개가 존재하고 Node의 색깔을 저장하기 위한 변수가 있다. black에 extra black이 붙으면 doubleBlack이 되고 삭제될 Node에 붙으면 nilBlack이 된다. red에 extra black이 붙을 땐 black으로 처리하면 돼서 따로 redBlack값을 만들진 않았다.\n2.2. RedBlackTree class 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 /*--------------------* * RedBlackTree * *--------------------*/ template\u0026lt;typename K, typename V\u0026gt; class RedBlackTree { private: using\tNode = Node\u0026lt;K, V\u0026gt;; enum class Rotation { none, right, rightLeft, left, leftRight, }; public: ~RedBlackTree(); void\tInsert(K key, V value); void\tDelete(K key); void\tClear(); bool\tViolatesProperties(); private: Node*\tInsert(K key, V value, Node* pCurrent); Node*\tDelete(K key, Node* pCurrent); void\tClear(Node* pCurrent); Node*\tAddExtraBlack(Node* pCurrent); Node*\tRemoveExtraBlack(); Node*\tHandleDoubleBlack(Node* pGrandParent); bool\tViolatesProperties(Node* pCurrent, bool isParentRed, int numberBlacks, int\u0026amp; maxNumberBlacks); static Node*\tRotateRight(Node* pRoot); static Node*\tRotateLeft(Node* pRoot); static Node*\tRotateRightLeft(Node* pRoot); static Node*\tRotateLeftRight(Node* pRoot); static Node*\tHandleConsecutiveReds(Node* pGrandParent, Rotation rotation); static Node*\tGetSuccessor(Node* pRoot); private: Node*\tm_pRoot = nullptr; Node*\tm_pDoubleBlack = nullptr; bool\tm_isConsecutiveReds = false; bool\tm_isConsecutiveRedsRight = false; bool\tm_isRotated = false; }; 삽입/삭제는 오버로딩 메서드가 존재하는데 public의 메서드를 호출하면 private에 있는 메서드를 이용해 재귀적으로 작업을 수행한다. Node의 색깔을 다루는 메서드들이 존재하고 균형을 잡기 위한 회전 연산들도 존재한다. 회전 연산은 AVL 트리에서 사용하는 것과 같은 것들이다. 멤버 변수엔 루트에 대한 포인터, double black Node에 대한 포인터가 있다. 그리고 Red Node가 연속으로 존재하는지 확인할 수 있는 멤버와 연속으로 존재하는 Red Node의 자식이 오른쪽인지 왼쪽인지 판단하기 위한 멤버도 있다. 삽입 시 회전 처리를 한 번 하면 더 이상 할 필요가 없는데 그것을 확인하기 위해서 회전 여부를 저장하는 멤버가 있다. debug 모드에선 삽입/삭제 시 ViolatesProperties 메서드로 마지막에 Red-Black 트리 속성을 위반했는지 확인한다. 실제 구현 코드는 아래 깃허브 코드에 있다.\n3. 관련 링크 전체 구현 코드: https://github.com/nansu0425/CppDataStructuresAndAlgorithmDesignPrinciples/tree/main/Lesson02/RedBlackTree 참고 강의 https://www.youtube.com/watch?v=2MdsebfJOyM\u0026t=529s https://www.youtube.com/watch?v=6drLl777k-E\u0026t=27s ","permalink":"https://nansu0425.github.io/posts/red-black-%ED%8A%B8%EB%A6%AC-%EA%B5%AC%ED%98%84/","summary":"Red-Black 트리를 구현한 것에 대한 기록","title":"Red-Black 트리 구현"},{"content":"1. 데모 영상 https://www.youtube.com/watch?v=BNcL2K8Jx-Y\n2. 핵심 구현 공은 벽돌(깨야하는 사각형), 벽(회색 사각형), 패들(흰색 사각형)과 충돌 시 튕겨져 나간다 공이 튕길 때 속도 벡터의 x 혹은 y 부호가 반대로 바뀐다 공이 패들 아래로 떨어지면 왼쪽 위의 똥 하나가 감소하고 똥이 남아있으면 공은 리스폰된다 패들에서 공이 튕길 때 패들 중심의 x좌표와 공 중심 x좌표 사이 거리가 멀어질수록 튕기는 공의 속도 벡터 각도는 감소한다 패들의 속도 벡터를 튕겨져 나가는 공의 속도 벡터에 더한다 튕겨져 나가는 공의 속도 벡터 최소 각도를 설정할 수 있다 게임 종료 조건 왼쪽 위의 모든 똥이 없어진다 모든 벽돌이 깨진다 3. 관련 링크 프로젝트 코드: https://github.com/nansu0425/CppGameProgramming/tree/main/BrickBreaker 강의 https://www.youtube.com/watch?v=UqN9svru_rg\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=40 https://www.youtube.com/watch?v=jZOeRp8Nm20\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=41 https://www.youtube.com/watch?v=rvWFlowWSkg\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=42 https://www.youtube.com/watch?v=5miKCudxFUs\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=43 사용 프레임워크: https://github.com/planetchili/chili_framework ","permalink":"https://nansu0425.github.io/posts/%EB%B2%BD%EB%8F%8C-%EA%B9%A8%EA%B8%B0-brick-breaker/","summary":"벽돌 깨기 프로젝트를 끝내고 정리하는 글","title":"벽돌 깨기 (Brick Breaker)"},{"content":" 좋은 자료를 발견할 때마다 추가할 예정\n1. 최적화 RVO에 대한 글: https://sigcpp.github.io/2020/06/08/return-value-optimization 인라인 함수 https://blog.naver.com/PostView.naver?blogId=ycpiglet\u0026logNo=222818854483 https://stackoverflow.com/questions/9370493/inline-function-members-inside-a-class 2. STL map이 RB 트리로 구현된 이유에 대한 의견들: https://stackoverflow.com/questions/5288320/why-is-stdmap-implemented-as-a-red-black-tree 3. Boost C++ Boost를 사용한 비동기 프로그래밍: https://booiljung.github.io/#programming/cplusplus/cplusplus_boost_asio_programming/01_preface_ko/ Boost.Asio를 이용한 MMO 네트워크 프로그래밍: https://www.youtube.com/playlist?list=PLzRO3TKhPcjflN266bAMpGRci6YfYScq5 4. 개념 Aggregate, POD, Trivially copyable class, Standard-layout class: https://stackoverflow.com/questions/4178175/what-are-aggregates-and-trivial-types-pods-and-how-why-are-they-special ","permalink":"https://nansu0425.github.io/posts/%EC%B0%B8%EA%B3%A0%ED%95%98%EA%B8%B0-%EC%A2%8B%EC%9D%80-%EC%9E%90%EB%A3%8C%EB%93%A4-%EC%A0%95%EB%A6%AC/","summary":"내가 생각했을 때 참고하기 좋은 C++ 관련 자료들 정리","title":"참고하기 좋은 자료들 정리"},{"content":"1. 데모 https://www.youtube.com/watch?v=e2wGySOvH1Y\u0026amp;list=LL\u0026amp;index=1\n2. 규칙 뱀은 주기적으로 이동하고 방향키로 뱀의 이동 방향을 변경할 수 있다 뱀이 먹이(빨간색)를 먹으면 길이, 속도가 증가하고 또 다른 먹이가 임의의 위치에 생성된다 주기적으로 장애물(회색)이 임의의 위치에 생성되고 뱀이 장애물과 충돌 시 게임은 종료된다 뱀이 벽(파란색)과 충돌하면 게임이 종료된다 뱀이 자기 자신과 충돌하면 게임이 종료된다 3. 관련 링크 프로젝트 코드: https://github.com/nansu0425/CppGameProgramming/tree/main/SnakeGame 참고 강의 https://www.youtube.com/watch?v=KTyemy82Zqk\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=28\u0026t=0s https://www.youtube.com/watch?v=DoO_NEksf3c\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=29 https://www.youtube.com/watch?v=aE9fGMRRe6U\u0026list=PLqCJpWy5FohcehaXlCIt8sVBHBFFRVWsx\u0026index=30 사용 프레임워크 https://github.com/planetchili/chili_framework ","permalink":"https://nansu0425.github.io/posts/%EB%B1%80-%EA%B2%8C%EC%9E%84-snake-game/","summary":"C++로 만든 뱀 게임 소개 및 기록","title":"뱀 게임 (Snake Game)"},{"content":"1. auto 리턴 1 2 3 4 5 template\u0026lt;typename L, typename R\u0026gt; ReturnType getSum(const L\u0026amp; left, const R\u0026amp; right) { return left + right; } 두 인자의 합을 반환하는 getSum 템플릿 함수가 있다. getSum의 반환 타입인 ReturnType은 left + right가 반환하는 값의 타입이 돼야 한다. 하지만 getSum은 함수 템플릿이기 때문에 파라미터 타입에 따라 ReturnType이 바뀐다. 모든 타입에 대해 일반화하기 위해선 getSum의 반환 타입을 어떻게 명시해야 할까?\n1 2 3 4 5 template\u0026lt;typename L, typename R\u0026gt; auto getSum(const L\u0026amp; left, const R\u0026amp; right) { return left + right; } 리턴 타입에 auto 키워드를 사용하면 컴파일러가 리턴 타입을 추론한다. C++14부터 컴파일러가 함수의 리턴 타입을 추론할 수 있다. 이때 주의할 점은 auto 키워드로 타입을 추론할 땐, const와 레퍼런스가 사라진다. 따라서 반환 값의 복사본을 전달한다. 만약 레퍼런스 타입으로 반환해야 하는 경우 이 방법은 최선이 아니다.\n2. decltype(auto) 리턴 1 2 3 4 5 template\u0026lt;typename L, typename R\u0026gt; decltype(auto) getSum(const L\u0026amp; left, const R\u0026amp; right) { return left + right; } decltype의 경우 const와 레퍼런스를 유지한다. decltype(auto)를 decltype(left + right)로 취급한다고 생각하면 된다.\n1 2 // decltype(getSum(left, right)) sum = getSum(left, right); 동일 decltype(auto) sum = getSum(left, right); 이것은 함수 리턴 뿐만 아니라 변수 초기화 시 타입 추론에도 사용 가능하다.\n3. 후행 리턴 타입 함수 리턴 타입 추론 기능은 C++14부터 지원한다고 했다. C++14 이전인 C++11에서 함수 템플릿의 리턴 타입 추론을 구현하려면 어떻게 해야할까?\n1 2 3 4 5 template\u0026lt;typename L, typename R\u0026gt; decltype(left + right) getSum(const L\u0026amp; left, const R\u0026amp; right) { return left + right; } auto 키워드 대신 decltype(return 표현식)을 생각해볼 수 있을 것이다. 하지만 이 방법은 컴파일 오류가 발생한다. decltype 내에 있는 left와 right는 정의되기 전이기 때문에 컴파일러가 식별할 수 없다.\n1 2 3 4 5 template\u0026lt;typename L, typename R\u0026gt; auto getSum(const L\u0026amp; left, const R\u0026amp; right) -\u0026gt; decltype(left + right) { return left + right; } 후행 리턴 타입(trailing return type)이라는 것을 활용하면 이 문제를 해결할 수 있다. decltype 내에 있는 left와 right는 정의된 후에 사용되기 때문에 컴파일러가 식별 가능하다. 또한 리턴 타입은 자동으로 추론되는 것이 아닌 decltype의 결과를 사용한다. decltype 내의 표현식을 잘못 입력하면 함수의 리턴 타입이 잘못 될 수 있기 때문에 주의가 필요하다.\n4. 참고 자료 전문가를 위한 C++ 5판, 12.3.4 함수 템플릿의 리턴 타입, 663p ~ 665p ","permalink":"https://nansu0425.github.io/posts/%ED%95%A8%EC%88%98-%ED%85%9C%ED%94%8C%EB%A6%BF%EC%9D%98-%EB%A6%AC%ED%84%B4-%ED%83%80%EC%9E%85-%EC%B6%94%EB%A1%A0/","summary":"C++ 함수 템플릿의 리턴 타입 추론에 대한 정리","title":"함수 템플릿의 리턴 타입 추론"},{"content":"1. 개요 홍정모 교수님의 자료구조 강의 중 이진 탐색 트리 쪽을 공부했다. 강의 마지막에 미니 프로젝트로 영어 사전 만드는 것을 추천했다. 그래서 공부한 이진 탐색 트리를 활용하는 연습을 해볼겸 간단한 영어 사전 프로그램을 만들기로 했다. 영어 사전 프로그램은 단어와 뜻 정보가 있는 텍스트 파일을 읽어 들인다. 그리고 그 데이터를 이진 탐색 트리 형태로 저장한다. 프로그램에 사용자가 단어를 입력하면 트리에서 단어를 탐색 후 뜻을 출력한다.\n코드 링크: https://github.com/nansu0425/HongLabDataStructuresStudy/tree/main/MyEnglishDictionary\n2. 구조 2.1. String 문자열을 다루기 위한 클래스이다. C++에서 문자열을 다룰 땐 보통 std::string 클래스를 활용한다. 하지만 굳이 따로 문자열 클래스를 만든 이유는 문자열 관련 구현 연습을 해보고 싶어서 만들어봤다. m_pBuf 멤버 변수가 문자열이 저장된 배열을 가리키는 포인터다. 생성 시 문자열을 위한 메모리를 동적 할당하는데 g_bufSize 전역 변수의 크기만큼 할당한다. 동적 리소스에 대한 포인터를 소유하고 있으므로 복사, 이동 관련 생성자와 대입 연산자를 구현했다. 복사 시 깊은 복사를 하고 이동 시 이전 객체와 모든 멤버를 스왑한다. 이진 탐색 트리에서 단어 비교가 필요하기 때문에 비교 연산자를 구현했다. getLine 메서드는 입력 스트림으로부터 한 줄 입력받는다. 파일에서 입력받는 것을 편하게 하기 위해 추가했다.\n2.2. BinarySearchTree 이진 탐색 트리를 구현한 클래스 템플릿이다. 트리의 노드는 Node 구조체로 정의했고 키와 값은 Pair 구조체 형태로 저장한다. insert와 remove 메서드는 재귀적으로 삽입, 삭제를 수행한다. balance, rotateLeft, rotateRight메서드는 AVL 트리 형태로 균형을 맞추기 위해 존재한다. 트리의 탐색, 삽입, 삭제 연산의 시간 복잡도는 트리 높이에 의해 결정되므로 성능을 위해선 균형을 맞춰 높이를 최소화해야 한다.\n2.2. EnglishDictionary 영어 사전을 구현한 클래스다. 멤버는 단어 탐색 트리인데, 단어를 key, 단어의 뜻을 value로 저장한다. setWordSearchTree메서드의 파라미터에 단어와 의미 정보가 있는 파일의 이름을 넘기면 파일을 읽고 단어 탐색 트리에 저장한다. 단어, 의미 정보 파일은 단어 한 줄과 의미 한 줄의 두 줄 단위 데이터로 기록된 텍스트 파일이다. search메서드는 단어를 인자로 넘기면 트리에서 탐색 후 뜻을 반환한다.\n3. 동작 main 함수의 프로그램 로직이다. 처음에 텍스트 파일 이름을 인자로 넘겨서 EnglishDictionary 객체를 만든다. 객체 생성 후에는 사용자에게 단어를 입력받고 의미를 탐색해서 출력하는 과정을 반복한다.\n4. 성능 홍정모 교수님 강의에서 제공해준 텍스트 파일 데이터는 169,888 줄의 양이다. 이 텍스트 파일을 사용하면 프로그램을 실행했을 때 트리가 완성되기까지 1415초 정도가 소요되고 단어를 검색할 땐 712ms 정도가 걸린다. 메모리는 133MB를 사용한다. 프로젝트를 진행하면서 해시 테이블 진도를 같이 나갔는데 영어 사전을 해시 테이블로 저장한다면 이진 탐색 트리 영어 사전과 성능 차이가 얼마나 날지 궁금하다. 해시 테이블의 경우 속도는 더 빠르지만 메모리는 더 많이 사용할 것 같다. 나중에 해시 테이블로 사전을 구현해 봐야겠다.\n","permalink":"https://nansu0425.github.io/posts/%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%ED%8A%B8%EB%A6%AC%EB%A5%BC-%EC%9D%91%EC%9A%A9%ED%95%9C-%EC%98%81%EC%96%B4-%EC%82%AC%EC%A0%84/","summary":"이진 탐색 트리를 응응하여 영어 사전을 구현한 것에 대한 정리","title":"이진 탐색 트리를 응용한 영어 사전"},{"content":"1. 힙과 우선순위 큐 개념 힙은 heap property를 만족하는 완전 이진 트리를 말한다. heap property에 따라 두 가지 종류로 힙을 분류한다. heap property는 힙에서 부모 노드와 자식 노드 사이에 존재하는 조건인데, 부모 노드가 자식 노드보다 커야 한다는 heap property를 만족하는 힙은 최대 힙, 반대로 부모 노드가 자식 노드보다 작아야 한다는 heap property를 만족하는 힙은 최소 힙이라고 한다. 트리는 재귀적인 구조이기 때문에 최대 힙의 루트 노드에 가장 큰 값이 오고, 최소 힙의 루트 노드엔 가장 작은 값이 오게 된다. 트리를 구현할 때 연결 리스트 혹은 배열로 구현할 수 있는데 완전 이진 트리는 보통 배열에 저장한다. 배열의 인덱스와 노드 번호를 매핑시키면 노드를 배열에 연속적으로 저장할 수 있기 때문이다. 또한 배열은 중간 인덱스에서 삽입, 삭제하는 연산이 비효율적이지만, 힙의 삽입, 삭제는 중간이 아닌 마지막 노드에서 일어나기 때문에 배열에 구현해도 삽입, 삭제가 비효율적이지 않다. 힙의 가장 큰 장점은 최댓값 혹은 최솟값을 찾는 연산의 효율성이다. 힙을 쓰지 않고 최댓값 혹은 최솟값을 구하기 위해선 순차 탐색 혹은 정렬을 해야 하는데 순차 탐색은 시간 복잡도가 $O(n)$, 정렬은 빨라도 $O(n\\log{n})$이다. 반면 힙에서 어떤 노드를 삽입, 삭제 후 루트 노드에 최댓값 혹은 최솟값이 설정되는 연산의 시간 복잡도는 $O(\\log{n})$이다. 이런 시간 복잡도가 나올 수 있는 이유는 힙의 노드를 올바른 곳에 위치시키는 과정은 heap property를 만족할 때까지 부모 노드, 자식 노드를 비교 후 스왑하는 것인데, 이 과정은 트리 높이에 비례하기 때문이다. 완전 이진 트리의 높이를 $h$, 트리 노드 개수를 $n$이라고할 때, $2^h$는 $n$의 근사치다. 이것을 생각하면 $h$는 $\\log{n}$에 거의 비례하기 때문에 트리 높이에 비례하는 연산은 시간 복잡도가 $O(\\log{n})$이 된다. 우선순위 큐를 구현할 때 보통 힙으로 구현하는데, 힙의 최댓값, 최솟값을 효율적으로 찾을 수 있는 특성이 우선순위 큐를 구현하기에 딱 맞는 특성이기 때문이다. 우선순위 큐는 값들을 넣었을 때 꺼내는 순서가 우선순위 순서로 나오는 자료구조이다. 최소힙, 최대힙과 마찬가지로 우선순위 큐도 최대 우선순위 큐, 최소 우선순위 큐로 나누기도 한다.\n2. 구현 구조 구현 코드: https://github.com/nansu0425/HongLabDataStructuresStudy/tree/main/MyPriorityQueue\n위 그림은 내가 구현한 우선순위 큐 PriorityQueue의 구조다. PriorityQueue는 클래스 템플릿으로 PriorityQueue\u0026lt;int\u0026gt;를 인스턴스화하면 int 데이터를 저장하는 우선순위 큐가 된다. 생성자 중 배열을 받는 생성자가 있는데 배열을 받으면 배열 원소를 복사하여 우선순위 큐에 집어넣는다. 위 그림에선 int배열 { 3, 4, 0, 2, 1 }을 넘긴 경우다. 힙을 구현할 때 보통 배열 하나만 사용한다. 하지만 이 우선순위 큐는 배열을 두 개 사용하는데 최소힙과 최대힙 모두 사용하는 우선순위 큐를 구현했기 때문이다. 이 우선순위 큐는 가장 낮은 우선순위 값과 가장 높은 우선순위 값 모두 $O(\\log{n})$ 시간 복잡도로 꺼낼 수 있다. 왼쪽 아래 배열은 최대힙이고, 오른쪽 아래 배열은 최소힙이다. 배열은 실제 데이터를 갖고 있는 게 아니라 연결 리스트 노드에 대한 포인터다. 포인터를 사용한 이유는 배열에 실제 데이터를 저장해버리면 배열이 두 개이므로 똑같은 데이터가 두 개가 돼 비효율적이기 때문이다. 그리고 두 배열의 데이터를 동기화하는 것이 번거로울 것이다. 노드는 이중 연결 리스트 형태로 연결돼 있는데 삽입, 삭제를 효율적으로 하기 위함이다. 노드는 Node\u0026lt;T\u0026gt; 형태의 구조체 템플릿으로 만든다. T는 노드가 가리키는 데이터의 타입인데 위 그림에선 QueueData\u0026lt;int\u0026gt;가 템플릿 인자로 들어갔다. QueueData\u0026lt;T\u0026gt; 구조체 템플릿은 우선순위를 따지는 데이터에 대한 포인터와 해당 데이터를 소유하고 있는 최대힙, 최소힙 배열 원소의 인덱스 정보를 갖고 있다. 인덱스 정보를 갖고 있는 이유는 삽입, 삭제 연산 시 필요하기 때문이다. 삽입의 경우 힙 배열이 가득 찼을 때 삽입하려는 값이 힙의 가장 낮은 우선순위의 값보다 우선순위가 높은 경우 우선순위가 가장 낮은 값을 삽입 값으로 교체하도록 구현했다. 이 교체를 할 때 최소힙의 루트 노드 값을 변경 후 해당 노드에 해당하는 최대힙 노드의 위치가 변경돼야 하므로 교체되는 최소힙 노드가 최대힙 배열에선 어디 인덱스에 존재하는지 알아야 한다. 삭제의 경우도 비슷한데, 예를 들어 가장 우선순위가 높은 값을 꺼낸다고 할 때 최대힙에서 삭제되는 노드가 최소힙에선 몇 번 인덱스에 해당하는 노드인지 알아야 최소힙을 힙 구조로 다시 재배열할 수 있다.\n3. 동작 테스트 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include \u0026lt;iostream\u0026gt; #include \u0026#34;PriorityQueue.h\u0026#34; int main() { const int arrSize = 7; int arr[arrSize] = { 3, 2, 1, 6, 5, 7, 4 }; auto fPriorityComparator = [](const int\u0026amp; lower, const int\u0026amp; higher)-\u0026gt;bool { return lower \u0026lt; higher; }; // 우선순위 큐 생성 PriorityQueue\u0026lt;int\u0026gt; pq(arr, arrSize, 10, fPriorityComparator); std::cout \u0026lt;\u0026lt; pq \u0026lt;\u0026lt; std::endl; // Max heap: 7 6 4 2 5 1 3 // Min heap: 1 2 3 6 5 7 4 // 삽입 pq.insert(8); pq.insert(0); std::cout \u0026lt;\u0026lt; pq \u0026lt;\u0026lt; std::endl; // Max heap: 8 7 4 6 5 1 3 2 0 // Min heap: 0 1 3 2 5 7 4 8 6 // 9 삽입 후 힙 배열은 가득 찬다 pq.insert(9); pq.insert(10); pq.insert(11); // -1은 가장 낮은 우선순위 값보다 우선순위가 낮으므로 값 교체가 일어나지 않는다 pq.insert(-1); std::cout \u0026lt;\u0026lt; pq \u0026lt;\u0026lt; std::endl; // Max heap: 11 9 10 8 7 4 3 2 6 5 // Min heap: 2 5 3 6 9 7 4 8 10 11 // 가장 낮은 우선순위 값을 꺼낸다 for (int count = 0; count \u0026lt; 3; ++count) { std::cout \u0026lt;\u0026lt; pq.extract(HeapType::MIN) \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl; // 2 3 4 // 가장 높은 우선순위 값을 꺼낸다 for (int count = 0; count \u0026lt; 3; ++count) { std::cout \u0026lt;\u0026lt; pq.extract(HeapType::MAX) \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } std::cout \u0026lt;\u0026lt; std::endl \u0026lt;\u0026lt; std::endl; // 11 10 9 std::cout \u0026lt;\u0026lt; pq \u0026lt;\u0026lt; std::endl; // Max heap: 8 7 6 5 // Min heap: 5 6 7 8 return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Max heap: 7 6 4 2 5 1 3 Min heap: 1 2 3 6 5 7 4 Max heap: 8 7 4 6 5 1 3 2 0 Min heap: 0 1 3 2 5 7 4 8 6 Max heap: 11 9 10 8 7 4 3 2 6 5 Min heap: 2 5 3 6 9 7 4 8 10 11 2 3 4 11 10 9 Max heap: 8 7 6 5 Min heap: 5 6 7 8 ","permalink":"https://nansu0425.github.io/posts/%EC%B5%9C%EB%8C%80-%ED%9E%99%EA%B3%BC-%EC%B5%9C%EC%86%8C-%ED%9E%99-%EB%AA%A8%EB%91%90-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90-%EA%B5%AC%ED%98%84/","summary":"최대 힙과 최소 힙을 이용해서 구현한 우선순위 큐에 대해 정리","title":"최대 힙과 최소 힙 모두 사용하는 우선순위 큐 구현"},{"content":"1. 이진 트리 순회 (Binary Tree Traversal) 이진 트리는 비선형 자료구조이기 때문에 순차적으로 방문할 수 없다. 탐색 방법에 따라 노드를 방문하는 순서가 달라지는데, 이진 트리 순회 방법은 전위(Preorder), 중위(Inorder), 후위(Postorder) 순회 세 가지로 분류된다. 루트 노드의 자식 노드는 서브 트리의 루트 노드로 볼 수 있다. 이런 재귀적 특성을 갖는 구조 때문에 재귀적 방법의 순회는 매우 쉽게 구현할 수 있다. 재귀적 방법이 아닌 반복문을 사용하는 반복적(Iterative) 순회도 가능하다. 트리 순회를 공부하면서 재귀적 순회를 구현 완료 후 반복적 순회 구현을 시도했는데, 간단할 줄 알았지만 생각 이상으로 정말 어려웠다(후위 순회 구현은 실패). 방법을 알고난 지금은 단순하다고 느끼지만 모르면 떠올리기 쉽지 않은 내용인 것 같아 이 글에 정리한다.\n2. 전위 순회 (Preorder Traversal) 전위 순회의 탐색 순서는 루트 노드를 먼저 방문 후 서브 트리를 탐색하는 것이다. 코드에선 왼쪽 서브트리 탐색 후 오른쪽 서브트리를 탐색한다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void IterPreorder() { if (!root_) return; Stack\u0026lt;Node*\u0026gt; s; s.Push(root_); // 스택에 방문할 노드가 있으면 반복 while (!s.IsEmpty()) { // 스택에서 노드를 꺼낸다 Node* cur = s.Top(); s.Pop(); // 루트 노드를 방문 Visit(cur); // 오른쪽 자식노드 있으면 스택에 넣는다 if (cur-\u0026gt;right) { s.Push(cur-\u0026gt;right); } // 왼쪽 자식노드 있으면 스택에 넣는다 if (cur-\u0026gt;left) { s.Push(cur-\u0026gt;left); } } } 3. 중위 순회 (Inorder Traversal) 탐색 순서는 왼쪽 서브트리 → 루트 노드 → 오른쪽 서브트리이다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 void IterInorder() { if (!root_) return; Stack\u0026lt;Node*\u0026gt; s; Node* cur = root_; // cur이 nullptr고 스택이 비면 반복 종료 while (cur || !s.IsEmpty()) { // cur이 nullptr가 아니면 스택에 cur 노드를 넣고 // 왼쪽 자식 노드로 내려간다 while (cur) { s.Push(cur); cur = cur-\u0026gt;left; } // cur이 nullptr면 스택에서 노드를 꺼낸 후 그 노드로 이동 cur = s.Top(); s.Pop(); // cur의 왼쪽 서브트리는 탐색이 끝났으므로 cur을 방문 Visit(cur); // cur의 오른쪽 서브트리 탐색을 위해 오른쪽 자식 노드로 이동 cur = cur-\u0026gt;right; } } 4. 후위 순회 (Postorder Traversal) 순회 순서는 왼쪽 서브트리 → 오른쪽 서브트리 → 루트 노드이다. 스택을 두 개 사용한다. 이런 식으로 해결하는 것은 처음에 떠올리지 못 했다. 단순하지만 기발한 방법인 것 같다. 첫 번째 반복에선 방문하지 않고 s2 스택에 모든 노드를 넣는다. 두 번째 반복에선 s2에서 노드를 꺼내면서 방문한다. s1의 역할은 s2에 노드가 올바른 순서로 들어가도록 하는 것이다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void IterPostorder() { if (!root_) return; Stack\u0026lt;Node*\u0026gt; s1, s2; s1.Push(root_); // s1에 노드가 있으면 반복 while (!s1.IsEmpty()) { // s1에 있는 노드를 꺼내서 s2에 넣는다 s2.Push(s1.Top()); s1.Pop(); // s2에 방금 넣은 노드가 cur Node* cur = s2.Top(); // cur의 왼쪽 자식 노드가 있으면 s1에 넣는다 if (cur-\u0026gt;left) { s1.Push(cur-\u0026gt;left); } // cur의 오른쪽 자식 노드가 있으면 s1에 넣는다 if (cur-\u0026gt;right) { s1.Push(cur-\u0026gt;right); } } // s2에 노드가 있으면 노드를 꺼내서 방문 while (!s2.IsEmpty()) { Visit(s2.Top()); s2.Pop(); } } ","permalink":"https://nansu0425.github.io/posts/%EC%9E%AC%EA%B7%80%EC%A0%81-%EB%B0%A9%EB%B2%95%EC%9D%84-%EC%93%B0%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%9D%B4%EC%A7%84-%ED%8A%B8%EB%A6%AC-%EC%88%9C%ED%9A%8C/","summary":"재귀적 방법을 쓰지 않고 반복문을 이용해 전위, 중위, 후위 탐색하는 방법을 다룬다","title":"재귀적 방법을 쓰지 않는 이진 트리 순회"},{"content":"1. 소개 📌 코드 링크: https://github.com/nansu0425/HongLabDataStructuresStudy/tree/main/Calculator\n간단한 계산기를 C++로 구현했다. 홍정모 교수님의 자료구조 강의 중 스택과 큐를 공부했는데 공부한 것을 활용한 간단한 프로젝트를 해보면 좋을 것 같아서 하게 됐다. 계산기는 C 스타일 문자열(const char*) 형태로 식을 주면 그 식을 읽고 계산한다. 몇 가지 특징은 다음과 같다.\n계산기에 사용되는 스택과 큐는 클래스 템플릿으로 구현했다. 양의 정수, 실수를 피연산자로 사용할 수 있고 자릿수 제한은 없다. 연산자와 피연산자 사이 띄어쓰기가 없거나 많아도 계산 가능하다. 식 계산에 필요한 메모리 크기는 자동으로 설정된다. 사용 가능한 연산자는 +, -, *, / 네 가지다. 괄호 () 사용 가능하다. 아래는 구현한 계산기를 테스트하는 짧은 코드다.\n1 2 3 4 5 Calculator cal; cal.SetExpression(\u0026#34; 5 + (4 - 9.3 / 3)* 10\u0026#34;); cal.PrintInfixExpression(); std::cout \u0026lt;\u0026lt; \u0026#34;= \u0026#34; \u0026lt;\u0026lt; cal.EvaluateExpression() \u0026lt;\u0026lt; std::endl; 위 코드를 실행했을 때 출력문은 5 + ( 4 - 9.3 / 3 ) * 10 = 14\n2. 동작 흐름 SetExpression(const char*)메서드의 인자로 식이 적힌 문자열을 넘기면 식을 순차적으로 탐색하면서 피연산자와 연산자를 새로운 문자열 토큰으로 분리한다. 분리된 토큰은 중위 표기식 큐에 순서대로 집어 넣는다. EvaluateExpression() 를 호출하면 식 계산을 시작한다. 먼저 하는 것은 중위 표기식에 있는 토큰을 꺼내서 순서를 바꿔 후위 표기식 큐에 집어 넣는 것이다. 중위 표기식에서 후위 표기식을 바꾸는 것은 연산자 토큰을 저장하는 스택을 이용해 진행한다. 중위 표기식 큐에서 꺼낸 토큰이 피연산자면 바로 후위 표기식 큐에 넣고, 만약 연산자 토큰이면 연산자 우선순위를 판단해서 연산자 스택에 있는 토큰들을 후위 표기식 큐에 넣거나 그대로 둔 뒤 중위 표기식에서 꺼냈던 연산자 토큰을 스택에 넣는다. 이 과정을 반복하면 후위 표기식이 완성된다. 후위 표기식이 완성되면, 후위 표기식에서 토큰을 하나씩 꺼내는데 토큰이 피연산자면 실수 형태로 형변환을 한 후 피연산자 스택에 들어간다. 연산자 토큰을 꺼내면 해당 연산자에 따라 피연산자 스택에서 피연산자 2개를 꺼내서 계산 후 계산 결과를 다시 스택에 넣는다. 이 과정을 반복하면 후위 표기식 큐에서 모든 토큰을 꺼냈을 때 스택엔 식의 최종 결과 값을 가진 요소 하나만 남게 된다. EvaluateExpression()은 스택에 남은 그 값을 반환한다.\n3. 후기 구현 난이도를 올리고 싶어서 표준 라이브러리의 스택, 큐, 문자열을 사용하지 않았다. 그리고 스스로 생각하는 연습을 하기 위해 API를 참고하는 것 외에는 다른 코드를 거의 참고하지 않고 구현했다. 그렇게 어려운 알고리즘이나 자료구조가 사용된 것도 아닌데 참고하는 것을 최소화하고 바닥부터 코드를 짜다보니 자잘한 곳(메모리 복사, 해제, 인덱싱 등)에서 계속 버그가 터져서 내가 생각했던 것보다 구현이 오래 걸렸다. 아직 C++ 코드의 정교함이 떨어지는 것 같다. 또한 다른 코드를 거의 참고하지 않다 보니 이 방법이 최선인가에 대해 고민하는 과정에서 시간을 많은 잡아먹었다. 하지만 그 고민하는 과정이 언젠간 도움이 될 것이라고 믿는다. 진짜 실력은 무엇이 최선인지 모르는 상황에서 남들보다 더 좋은 선택을 내릴 수 있는 능력이라고 생각하기 때문이다.\n","permalink":"https://nansu0425.github.io/posts/%EC%8A%A4%ED%83%9D%EA%B3%BC-%ED%81%90%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EC%97%AC-%EB%A7%8C%EB%93%A0-%EA%B3%84%EC%82%B0%EA%B8%B0/","summary":"스택과 큐를 활용하여 만든 계산기에 대한 기록","title":"스택과 큐를 활용하여 만든 계산기"},{"content":"1. 서론 클래스 템플릿을 이용하여 자료구조를 구현하고 있었는데 선언과 정의 코드를 헤더 파일에 전부 넣고 있었다. 그렇게 해야 오류가 나지 않는다는 단순한 이유로 그렇게 하고 있었는데 왜 이렇게 하는지에 대해 제대로 생각해본 적은 없던 것 같다. 일반적인 클래스를 구현할 때 처럼 선언과 정의를 분리하는 것이 정리된 것 같은 느낌이 들어서 더 좋은데 왜 굳이 이렇게 해야하는지 궁금해졌다. 이것이 최선인지에 대한 생각을 기록하려 한다.\n2. 선언과 정의를 분리할 경우 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #pragma once template\u0026lt;typename T\u0026gt; class TemplateTest { public: TemplateTest(T data) : m_data(data) { } T GetData(); private: T\tm_data; }; 1 2 3 4 5 6 7 #include \u0026#34;TemplateTest.h\u0026#34; template\u0026lt;typename T\u0026gt; T TemplateTest\u0026lt;T\u0026gt;::GetData() { return m_data; } TemplateTest라는 클래스의 GetData 메서드의 선언과 정의를 헤더 파일과 소스 파일에 분리했다. main 함수에서 GetData 메서드를 호출하면 Visual Studio에선 LNK2019 링크 에러가 발생한다. LNK2019는 선언은 있는데 정의를 찾지 못했을 때 발생하는 링크 에러다. GetData의 정의가 존재하는데 왜 정의를 찾지 못했다고 나올까? 이유는 컴파일 과정에서 템플릿 정의를 발견했지만 컴파일하지 않았기 때문이다. 템플릿은 템플릿 파라미터에 인자가 주어져 인스턴스화될 경우에만 컴파일된다. TemplateTest.cpp에선 정의만 존재하고 인스턴스화하는 코드는 없기 때문에 정의는 컴파일되지 않아 정의와 관련된 어떤 코드도 생성되지 않는다. main에서 GetData를 호출할 때 컴파일 에러가 발생하지 않았다. GetData에 대한 선언은 헤더 파일에 존재하기 때문이다. 따라서 링크 과정에서 GetData의 정의를 찾을 수 있다고 간주할 것이다. 하지만 링크 과정에서 정의를 찾지 못해 링크 에러가 발생하게 된다.\n3. 명시적 인스턴스화 1 2 3 4 5 6 7 8 9 #include \u0026#34;TemplateTest.h\u0026#34; template\u0026lt;typename T\u0026gt; T TemplateTest\u0026lt;T\u0026gt;::GetData() { return m_data; } template int TemplateTest\u0026lt;int\u0026gt;::GetData(); 아래 줄에 어떤 코드를 추가했다. 저 코드는 명시적으로 템플릿을 인스턴스화한 것이다. 템플릿 파라미터엔 int를 넘겨줬다. 이렇게 명시적으로 인스턴스화하면 GetData의 정의 코드가 컴파일돼 링크 에러없이 프로그램이 성공적으로 실행된다. 하지만 이 방법은 좋은 방법이 아닌 것 같다. 템플릿을 사용하는 가장 큰 이유는 모든 타입에 대해 하나의 코드로 일반화하는 것인데, 사용할 타입이 늘어나면 타입에 맞춰 명시적 인스턴스화 코드를 추가해야 한다. 코드를 누락하거나 타입을 햇갈리면 바로 링크 에러를 마주할 것이다. 이렇게 되면 템플릿을 쓰는 의미가 없다. 하지만 명시적 인스턴스화 자체가 나쁜 건 아닌 것 같다. 이것을 선언과 정의를 분리하는 용도가 아닌 템플릿을 강제로 컴파일시켜 내제된 오류를 찾아내는 용도로 쓸 수 있을 것 같다. 명시적으로 인스턴스화 하지 않으면 클래스 템플릿을 인스턴스화 하더라도 메서드 중 한번도 호출되지 않는 메서드는 컴파일되지 않는다. 호출되지 않는 메서드는 컴파일 오류가 있어도 컴파일을 거치지 않아 컴파일 에러가 발생하지 않는다. 이렇게 선택적으로 인스턴스화 하는 것을 선택적 인스턴스화라고 한다. 반면 명시적 인스턴스화는 클래스 템플릿의 모든 메서드를 컴파일하므로 메서드의 잠재된 컴파일 오류를 확인할 수 있다.\n4. 최선의 방법 템플릿 클래스의 정의와 선언은 헤더 파일 안에 같이 넣는 게 맞는 것 같다. 아무리 생각해도 다른 방법들은 문제가 생길 여지가 많다. 어떤 글에선 템플릿 정의가 있는 cpp 파일을 include하는 방법도 소개했는데 추천하지 않는다고 했고 내가 생각해도 그건 정말 좋지 않은 방법같다. 프로그램 규모가 조금만 커져도 헤더 파일과 소스 파일 내용이 섞이면서 링크 관련 문제가 우수수 나올 것 같다.\n","permalink":"https://nansu0425.github.io/posts/%ED%81%B4%EB%9E%98%EC%8A%A4-%ED%85%9C%ED%94%8C%EB%A6%BF%EC%9D%98-%ED%97%A4%EB%8D%94-%ED%8C%8C%EC%9D%BC-%EC%86%8C%EC%8A%A4-%ED%8C%8C%EC%9D%BC-%EB%B6%84%EB%A6%AC/","summary":"클래스 템플릿을 헤더 파일과 소스 파일에 선언, 정의를 분리하는 것에 대한 생각","title":"클래스 템플릿의 헤더 파일, 소스 파일 분리"},{"content":"1. 서론 원형 배열 기반의 데크(Deque)를 구현하고 있었는데 배열의 인덱스 이동을 처리하는 과정에서 깔끔하지 못 하게 처리하는 상황이 있었다. 아주 단순하고 기본적인 부분이지만 정말 확실하게 기억하기 위해 정리를 하려고 한다. 이번 글에선 원형 배열에서 인덱스를 증가시키거나 감소시킬 때 사용할 수 있는 코드를 기록할 것이다.\n2. 원형 배열(Circular Array) 배열의 첫 번째 위치와 마지막 위치가 연결된 것처럼 구현하면 그것을 원형 배열이라고 한다. 연결돼 있다는 것은 0번 인덱스에서 인덱스를 감소시키면 배열의 마지막 인덱스가 되고, 배열의 마지막 위치에서 인덱스를 증가시키면 0번 인덱스가 되는 것을 의미한다. 원형 배열은 원형 큐(Circular Queue), 원형 데크(Circular Deque) 등을 구현할 때 사용된다.\n3. 인덱스 증가 현재 원소의 인덱스를 저장하는 변수를 index, 배열의 크기를 size라고 할 때 다음 원소의 인덱스를 index에 저장하고 싶은 경우 아래와 같이 코드를 짤 수 있다.\n1 index = (index + 1) % size; 배열의 크기를 넘어서지 않는다면 index는 1 증가할 것이다. 만약 현재 인덱스가 마지막 원소의 인덱스라면 index + 1과 size가 같게 되므로 나머지가 0이 되어 index에는 0이 저장된다.\n4. 인덱스 감소 변수는 인덱스 증가 예제와 같을 때 현재 원소에서 이전 원소의 인덱스로 index의 값을 바꾸고 싶을 때 아래와 같이 코드를 작성할 수 있다.\n1 index = (index - 1 + size) % size; 현재 원소가 첫 번째 원소가 아니라면 index의 값은 1 감소한다. 만약 현재 원소가 첫 번째 원소면 index - 1은 -1이 되고 size - 1이 index에 저장된다. size - 1은 배열 마지막 자리의 인덱스이다.\n","permalink":"https://nansu0425.github.io/posts/%EC%9B%90%ED%98%95-%EB%B0%B0%EC%97%B4-%EA%B5%AC%ED%98%84-%EC%8B%9C-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%9D%B4%EB%8F%99/","summary":"원형 배열을 구현할 때 인덱스 이동을 어떻게 처리할지에 대한 설명","title":"원형 배열 구현 시 인덱스 이동"},{"content":"1. 배열과 희소 다항식 희소 다항식은 차수가 큰 항이 있지만 항의 개수는 얼마 없는 다항식을 말한다. 다항식을 배열로 구현할 경우 각 항의 차수를 index에 대응시키는 방법이 있다. 하지만 이 방법은 희소 다항식에 어울리지 않는 방법이다. 만약 $x^{99} + x^2$을 구현하려고 한다면 최고 차항의 차수가 99이므로 원소 100개 크기의 배열이 필요하게 되는데, 실제로 의미있는 원소는 2개 밖에 안된다. 다른 방법으로는 각 항의 정보를 갖고 있는 구조체의 배열 형태로 구현하는 것이 있다. 새로운 항을 추가할 때마다 원소 하나를 추가하면 되므로 불필요하게 낭비되는 메모리를 줄일 수 있다.\n2. Term과 Polynomial https://gist.github.com/nansu0425/745be77936d22423a492ace81e1b3f25\n항의 정보는 Term구조체 형태로 저장된다. 계수와 지수 정보를 갖고 있다. Polynomial은 다항식을 표현하기 위한 클래스다. 주요 기능으로는 항 추가, 다항식 덧셈이 있다. m_terms 는 항의 배열을 가리키는 포인터고 초기 크기를 설정할 수 있다. 배열의 크기인 m_capacity 를 넘어가면 배열 크기는 2배로 증가한다. 다항식은 내림차순 정렬이 되도록 항을 추가할 때 삽입 정렬을 한다.\n3. Polynomial 구현 https://gist.github.com/nansu0425/10d349e86c81ee4cc9218af88ea14e8c\n4. 구현 테스트 https://gist.github.com/nansu0425/8572a2af5bfa0a203eb380e8364fb1d9\n출력 결과는 다음과 같다\n1 2 3 4 5 6 7 8 9 Capacity: 8 numTerms: 6 10x^1000 + x^100 - 3x^5 + 5x^2 + 6x + 8 Capacity: 8 numTerms: 5 10x^32 + 13x^7 - 6x^3 - 8x + 6 Capacity: 16 numTerms: 9 10x^1000 + x^100 + 10x^32 + 13x^7 - 3x^5 - 6x^3 + 5x^2 - 2x + 14 ","permalink":"https://nansu0425.github.io/posts/%EB%B0%B0%EC%97%B4%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%9D%AC%EC%86%8C-%EB%8B%A4%ED%95%AD%EC%8B%9D-%EA%B5%AC%ED%98%84-%EC%97%B0%EC%8A%B5/","summary":"C++를 이용해 희소 다항식을 배열 형태로 구현 연습해본 코드 기록","title":"배열을 이용한 희소 다항식 구현 연습"},{"content":"1. C++ 20 Visual Studio의 C++20 환경에서 표준 라이브러리 모듈을 import하는 방법으로 MS에선 헤더 유닛(header units)을 권장 1.1. 프로젝트 생성 프로젝트 이름: Cpp20\n1.2. 예제 코드 생성 main.cpp\n1 2 3 4 5 6 7 8 import \u0026lt;iostream\u0026gt;; int main() { std::cout \u0026lt;\u0026lt; \u0026#34;Hello World!\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } HeaderUnits.h\n1 2 3 #pragma once import \u0026lt;iostream\u0026gt;; 1.3. 언어 표준 설정 프로젝트 속성 \u0026gt; 일반 \u0026gt; C++ 언어 표준: ISO C++20 표준 (/std:c++20)\n1.4. 헤더 단위 컴파일 설정 HeaderUnits.h 속성 \u0026gt; 일반 \u0026gt; 항목 형식: C/C++ 컴파일러\n설정 후 적용 클릭\nC/C++ \u0026gt; 고급 \u0026gt; 컴파일 옵션: C++ 헤더 단위로 컴파일(/exportHeader )\n1.5. 설정 확인 f5를 눌렀을 때 Hello World! 출력 → iostream import 성공 다른 표준 라이브러리 모듈이 필요하면 해당 모듈을 HeaderUnits.h에 추가 2. C++ 23 C++ 23에선 표준 라이브러리를 import할 수 있는 named 모듈(std, std.compat) 제공 2.1. 프로젝트 생성 프로젝트 이름: Cpp23\n2.2. 예제 코드 생성 main.cpp\n1 2 3 4 5 6 7 8 import std; int main() { std::cout \u0026lt;\u0026lt; \u0026#34;Hello World!\u0026#34; \u0026lt;\u0026lt; std::endl; return 0; } 2.3. 언어 표준 설정 프로젝트 속성 \u0026gt; 일반 \u0026gt; C++ 언어 표준: 미리 보기 - 최신 C++ 초안의 기능(/std:c++latest)\n2.4. 표준 라이브러리 모듈 빌드 설정 C/C++ \u0026gt; 언어 \u0026gt; ISO C++ 23 표준 라이브러리 모듈 빌드: 예\n2.5. 설정 확인 f5를 눌렀을 때 Hello World! 출력 → std import 성공 STL 라이브러리의 std namespace 내에 있는 모든 함수 사용 가능 import std.compat → std namespace 내의 함수 뿐만 아니라 C 표준 라이브러리의 전역 namespace 함수도 사용 가능 3. 참고 자료 https://learn.microsoft.com/en-us/cpp/build/compare-inclusion-methods?view=msvc-170 named 모듈 import가 헤더 유닛보다 빠르다 named 모듈은 내부 구현과 매크로를 외부로 노출하지 않는다 C++ 23 사용할 수 있다면 named 모듈 사용 권장 https://learn.microsoft.com/en-us/cpp/cpp/tutorial-import-stl-named-module?view=msvc-170 https://en.cppreference.com/w/cpp/standard_library#Importing_modules 전문가를 위한 C++ 5판 ","permalink":"https://nansu0425.github.io/posts/cpp-%ED%91%9C%EC%A4%80-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-stl-%EB%AA%A8%EB%93%88-import-%EB%B0%A9%EB%B2%95/","summary":"Visual Studio 2022에서 C++ 표준 라이브러리 모듈 방법을 C++20과 C++23 기준으로 정리","title":"C++ 표준 라이브러리(STL) 모듈 import 방법"},{"content":"1. 동작 https://gist.github.com/nansu0425/ba0135eb954a8f14eb9ce16ffc2f30c8\n2. 특징 반복문에서 매 반복마다 leftIdx부터 rightIdx까지의 문자열 순서가 유지되도록 swap을 두 번 수행한다 rightIdx는 고정돼 있고 leftIdx는 재귀 호출할 때 마다 하나 씩 증가한다 3. 성능 시간 복잡도 출력해야 하는 순열 개수는 문자열의 문자 개수를 $n$이라고 할 때 $_{n}P_{n} = n!$ 문자열 출력 시 모든 문자 $n$개를 순차 탐색한다 따라서 시간 복잡도는 $O(n * n!)$ 공간 복잡도 함수의 스택 프레임은 leftIdx가 1 증가할 때마다 쌓인다 leftIdx는 rightIdx까지 증가하므로 쌓이는 스택 프레임의 최대 개수는 rightIdx와 leftIdx의 차에 비례한다 rightIdx를 $r$, leftIdx를 $l$이라고 할 때 공간 복잡도는 $O(r - l)$ 4. 참고 자료 https://www.geeksforgeeks.org/permutation/ https://honglab.co.kr/courses/data-structures ","permalink":"https://nansu0425.github.io/posts/%EB%AC%B8%EC%9E%90%EC%97%B4%EC%9D%98-%EB%AA%A8%EB%93%A0-%EC%88%9C%EC%97%B4-%EC%B6%9C%EB%A0%A5/","summary":"문자열의 모든 순열을 출력하는 알고리즘에 대한 동작, 특징, 성능에 대한 정리","title":"문자열의 모든 순열 출력"},{"content":"1. 순열의 수 서로 다른 원소 $n$개 중 $r$개를 골라 특정 순서로 나열하는 방법의 수 $$ \\begin{split} _nP_r \u0026= n\\times(n - 1)\\times(n-2)\\times...\\times(n - (r - 1)) \\\\ \u0026= \\frac{n!}{(n - r)!} \\end{split} $$2. 조합의 수 서로 다른 원소 $n$개 중 순서 상관없이 $r$개를 고르는 방법의 수 $$ \\begin{split} {_n}C{_r} \\times {_r}P{_r} \u0026= {_n}P{_r} \\\\ {_n}C{_r} \u0026= \\frac{_nP_r}{_rP_r} \\\\ {_n}C{_r} \u0026= \\frac{_nP_r}{r!} \\end{split} $$","permalink":"https://nansu0425.github.io/posts/%EC%88%9C%EC%97%B4%EA%B3%BC-%EC%A1%B0%ED%95%A9-%EA%B8%B0%EC%B4%88-%EA%B3%B5%EC%8B%9D/","summary":"순열의 수와 조합의 수를 구하는 기초적인 공식 정리","title":"순열과 조합 기초 공식"},{"content":"1. 반복적 방법 1.1. 동작 https://gist.github.com/nansu0425/3c800c768b678168c202a426484cb7a1\n1.2. 특징 key값을 찾을 때까지 반복문은 탐색 범위를 반으로 줄이는 작업을 반복한다\n정렬된 상태에서 수행 가능\nkey값을 갖는 원소가 여러 개 있을 때 원소들 중 어떤 원소의 인덱스를 반환할지 예측하기 힘들다\n→ key값의 원소들 중 가장 작은 인덱스를 찾고 싶다면, 이진 탐색 후 반환된 인덱스에서 작은 인덱스 방향으로 다른 값이 나올 때까지 탐색하면 된다\n1.3. 성능 최상의 경우 시간 복잡도 key값을 가진 원소가 배열의 가운데에 있을 때 가운데 원소와 key 값을 한 번의 비교 후 탐색 완료 → $O(1)$ 최악의 경우 시간 복잡도 탐색 범위를 더 이상 반으로 나눌 수 없을 때까지 줄여야 찾을 수 있는 인덱스에 key가 존재하는 경우 배열의 원소 개수 $N$을 2로 나눌 수 있는 횟수가 증가하면 수행 시간이 증가한다 → $O(\\log_2N)$ 공간 복잡도 원소 개수가 늘어나도 알고리즘 수행에 필요한 메모리 크기는 같다 → $O(1)$ 2. 재귀적 방법 2.1. 동작 https://gist.github.com/nansu0425/d626b019b97fb9a80ed3d7f032767cab\n2.2. 특징 재귀 호출할 때마다 탐색 범위를 반으로 줄인다 탐색이 끝나면 반환 시작 1.2. 특징 의 2, 3번과 동일한 특징을 갖는다 2.3. 성능 시간 복잡도 1.3. 성능 의 시간 복잡도와 같다 최상의 경우 공간 복잡도 key값을 가진 원소가 배열의 가운데에 있을 때 탐색 범위 크기에 상관없이 재귀 호출은 일어나지 않는다 → $O(1)$ 촤악의 경우 공간 복잡도 탐색 범위를 반으로 나눌 수 없을 때까지 줄여야 찾을 수 있는 인덱스에 key가 존재하는 경우 leftIdx와 rightIdx의 차를 2로 나눌 수 있는 횟수가 증가할 때 재귀 호출 횟수가 증가 leftIdx를 $l$, rightIdx를 $r$이라고 할 때 공간 복잡도는 $O(\\log_2(r - l))$ ","permalink":"https://nansu0425.github.io/posts/%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89-%EC%A0%95%EB%A6%AC/","summary":"이진 탐색의 반복, 재귀적 방법의 동작, 특징, 성능 정리","title":"이진 탐색 정리"},{"content":"1. 일차항의 합 $$ \\begin{split} \\sum_{k = 1}^{n} k \u0026= 1 + 2 + 3 + ... + n \\\\ \u0026= \\frac{n(n + 1)}{2} \\end{split} $$2. 이차항의 합 $$ \\begin{split} \\sum_{k=1}^{n}k^2 \u0026= 1^2 + 2^2 + 3^2 + ... + n^2 \\\\ \u0026= \\frac{n(n+1)(2n + 1)}{6} \\end{split} $$3. 삼차항의 합 $$ \\begin{split} \\sum_{k=1}^{n}k^3 \u0026= 1^3 + 2^3 + 3^3 + ... + n^3 \\\\ \u0026= \\left\\{ \\frac {n(n+1)} {2} \\right\\}^2 \\end{split} $$","permalink":"https://nansu0425.github.io/posts/%EA%B8%B0%EC%B4%88%EC%A0%81%EC%9D%B8-%EC%88%98%EC%97%B4%EC%9D%98-%ED%95%A9-%EA%B3%B5%EC%8B%9D/","summary":"일차항, 이차항, 삼차항의 수열의 합을 구하는 공식 정리","title":"기초적인 수열의 합 공식"},{"content":"1. 버블 정렬 1.1. 동작 https://gist.github.com/nansu0425/209a5f74b19d51212611931a259bd71c\n1.2. 특징 큰 값 부터 정렬된다 원소 하나를 정렬하기 위해서 swap이 여러 번 일어날 수 있다 stable 정렬이다 1.3. 성능 최상의 경우 시간 복잡도\n이미 정렬된 상태일 때 최상의 경우 정렬되지 않은 영역을 순차적으로 탐색 후 swap이 한번도 일어나지 않으면 정렬 종료 원소 개수 $n$이 증가할 때 수행 시간이 증가한다 시간 복잡도: $O(n)$ 최악의 경우 시간 복잡도\n내림차순으로 정렬된 경우 인덱스 $k$의 자리를 정렬된 상태로 만들기 위해선 $k$번의 비교가 필요하다 마지막 인덱스 자리부터 인덱스 1번 자리까지 정렬된 상태로 만들면 모든 원소가 정렬된다 원소 개수를 $n$이라고 할 때 비교 횟수는 $$ \\sum_{k=1}^{n - 1}k = \\frac{n(n-1)}{2} $$ 시간 복잡도: $O(n^2)$ 공간 복잡도\n원소 개수가 늘어나도 필요한 메모리 크기는 같다 공간 복잡도: $O(1)$ 2. 선택 정렬 2.1. 동작 https://gist.github.com/nansu0425/1fda7bf785a346b2d596546d367a3bcc\n2.2. 특징 작은 값부터 정렬된다 한 원소를 정렬할 때 swap은 한 번만 일어난다 unstable 정렬이다 ex) $(2, a), (2, b), (1, c)$일 때 숫자 기준으로 선택 정렬하면 $(1, c), (2, b), (2, a)$가 된다 2.3. 성능 시간 복잡도\n최솟값을 찾을 때 정렬되지 않은 원소들 모두 탐색해야 한다 원소가 이미 정렬된 상태여도 그 원소가 정렬된 상태인지 확인하기 위해 최솟값 탐색을 한다 → 최악의 경우와 최상의 경우 시간 복잡도가 같다 원소 개수가 $n$개 일 때 비교 횟수는 $(n- 1) + (n -2) + ... + 1$ 이므로 $$ \\sum_{k=1}^{n - 1}k = \\frac{n(n-1)}{2} $$ 시간 복잡도: $O(n^2)$ 공간 복잡도\n원소 개수가 늘어나도 필요한 메모리 크기는 같다 공간 복잡도: $O(1)$ 3. 삽입 정렬 3.1. 동작 https://gist.github.com/nansu0425/65502b86c70601cba73e76088f4cbed0\n3.2. 특징 작은 값부터 정렬된다 stable 정렬이다 3.3 성능 최상의 경우\n이미 원소들이 정렬된 경우 각 원소를 정렬된 자리에 있는지 한번 씩만 확인 후 수행 종료 원소 개수 $n$이 증가하면 수행 시간이 증가 시간 복잡도: $O(n)$ 최악의 경우\n내림차순으로 정렬된 경우 원소 개수가 $n$개이고 인덱스 1번부터 $n - 1$번 원소의 삽입 위치를 찾기 위한 총 비교 횟수는 $1 + 2 + 3 + ... + (n - 1)$이므로 $$ \\sum_{k=1}^{n - 1}k = \\frac{n(n-1)}{2} $$ 시간 복잡도: $O(n^2)$ 공간 복잡도\n원소 개수가 늘어나도 필요한 메모리 크기는 같다 공간 복잡도: $O(1)$ ","permalink":"https://nansu0425.github.io/posts/%EB%B2%84%EB%B8%94-%EC%84%A0%ED%83%9D-%EC%82%BD%EC%9E%85-%EC%A0%95%EB%A0%AC-%EC%A0%95%EB%A6%AC/","summary":"버블, 선택, 삽입 정렬의 동작, 특징, 성능 정리","title":"버블, 선택, 삽입 정렬 정리"},{"content":"1. 나의 시도 1.1. bitset 이용 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const int n = 10; std::bitset\u0026lt;n\u0026gt; a(5); std::bitset\u0026lt;n\u0026gt; b(10); for (int i = 0; i \u0026lt; n; ++i) { // bit가 다르면 a[i]와 b[i]를 flip if (a[i] != b[i]) { a[i] = !a[i]; b[i] = !b[i]; } } bitset은 비트 개수를 템플릿 인자로 받는 C++의 비트열 자료구조 bit의 비교 횟수는 비트열의 bit 개수 $N$에 비례한다 → 시간 복잡도: $O(N)$ 1.2. shift 연산과 xor 연산 활용 1 2 3 4 5 6 7 8 9 10 11 12 int a = 5; int b = 10; for (int i = 0; i \u0026lt; sizeof(int) * 8; ++i) { // a와 b의 같은 자리의 bit를 비교 후 다르면 각 bit를 flip if ((a \u0026amp; (1 \u0026lt;\u0026lt; i)) ^ (b \u0026amp; (1 \u0026lt;\u0026lt; i))) { a ^= (1 \u0026lt;\u0026lt; i); b ^= (1 \u0026lt;\u0026lt; i); } } shift 연산은 대부분 프로세서에서 상수 시간에 계산 bit의 비교 횟수는 비교 데이터의 bit 개수 $N$에 비례 → 시간 복잡도: $O(N)$ 2. 검색 후 찾은 방법 참고 사이트: https://www.geeksforgeeks.org/swap-two-numbers-without-using-temporary-variable/ 2.1. 덧셈, 뺄셈 활용 1 2 3 4 5 6 7 8 9 int a = x; int b = y; // a = x + y, b = y a += b; // a = x + y, b = x + y - y =\u0026gt; x b = a - b; // a = x + y - x =\u0026gt; y, b = x a -= b; 세 번의 산술 연산만 필요 → 시간 복잡도: $O(1)$ 오버플로우 위험 존재 2.2. 곰셉, 나눗셈 활용 1 2 3 4 5 6 7 8 9 int a = x; int b = y; // a = x * y, b = y a *= b; // a = x * y, b = x * y / y =\u0026gt; x b = a / b; // a = x * y / x =\u0026gt; y, b = x a /= b; 세 번의 산술 연산만 필요 → 시간 복잡도: $O(1)$ 오버플로우 위험 존재 0 나누기 위험 존재 2.3. XOR 비트 연산 활용 1 2 3 4 5 6 7 8 9 int a = x; int b = y; // a = x ^ y, b = y a ^= b; // a = x ^ y, b = y ^ x ^ y =\u0026gt; x b ^= a; // a = x ^ y ^ x =\u0026gt; y, b = x a ^= b; 같은 데이터로 xor 연산 두 번하면 원래 데이터로 돌아오는 성질 이용 세 번의 비트 연산만 필요 → 시간 복잡도: $O(1)$ ","permalink":"https://nansu0425.github.io/posts/%EC%9E%84%EC%8B%9C-%EB%B3%80%EC%88%98-%EC%82%AC%EC%9A%A9-%EC%97%86%EC%9D%B4-swap/","summary":"임시 변수 사용 없이 swap하는 몇 가지 방법 정리","title":"임시 변수 사용 없이 swap"},{"content":"1. 2진수를 정수 데이터로 저장 1.1 10진법 정수 → 2진법 정수 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 10진법 정수를 입력받는다 int dec; std::cin \u0026gt;\u0026gt; dec; int bin = 0; int powerOfTen = 1; // 10진법 정수를 2진법 정수로 변환하여 bin에 저장 while (dec \u0026gt; 0) { bin += (dec % 2) * powerOfTen; powerOfTen *= 10; dec /= 2; } // 입력받았던 정수를 2진법 정수로 출력 std::cout \u0026lt;\u0026lt; bin \u0026lt;\u0026lt; std::endl; 1.2 2진법 정수 → 10진법 정수 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 2진법 정수를 입력받는다 int bin; std::cin \u0026gt;\u0026gt; bin; int dec = 0; int powerOfTwo = 1; // 2진법 정수를 10진법 정수로 변환하여 dec에 저장 while (bin \u0026gt; 0) { dec += (bin % 10) * powerOfTwo; powerOfTwo *= 2; bin /= 10; } // 입력받았던 정수를 10진법 정수로 출력 std::cout \u0026lt;\u0026lt; dec \u0026lt;\u0026lt; std::endl; 2. 2진수를 bitset에 저장 2.1 bistset이란? \u0026lt;bitset\u0026gt; 헤더에 선언 존재 고정된 크기의 비트열을 추상화한 자료구조 비트 한 개를 1 bit에 저장 → bool은 1 byte 크기이므로 bool 타입의 컨테이너나 배열을 사용하는 것보다 효율적이다 템플릿 매개 변수로 비트열의 크기를 정한다 2.2 10진법 정수 → 2진법 bitset 1 2 3 4 5 6 7 8 9 // 10진법 정수를 입력받는다 int dec; std::cin \u0026gt;\u0026gt; dec; // 입력받은 정수를 생성자의 인자로 받으면서 크기가 10인 bitset을 만든다 std::bitset\u0026lt;10\u0026gt; bits(dec); // 입력받은 정수를 2진법으로 출력 std::cout \u0026lt;\u0026lt; bits \u0026lt;\u0026lt; std::endl; bitset을 출력할 때는 bitset의 크기만큼 자릿수를 채워서 출력한다 2.3 2진법 bitset → 10진법 정수 1 2 3 4 5 6 7 8 9 // 2진수를 string 타입 문자열로 입력받는다 std::string bin; std::cin \u0026gt;\u0026gt; bin; // 입력받은 문자열을 생성자의 인자로 넘기면서 크기가 10인 bitset을 만든다 std::bitset\u0026lt;10\u0026gt; bits(bin); // unsigned long 타입의 10진수 정수 형태로 바꿔서 출력한다 std::cout \u0026lt;\u0026lt; bits.to_ulong() \u0026lt;\u0026lt; std::endl; 만약 2진수가 음수(2의 보수)로 들어온다면 아래와 같이 처리해야 한다 1 2 3 4 5 6 7 8 9 10 11 12 13 // 음수인 2진수를 string 타입 문자로 입력받는다 std::string bin; std::cin \u0026gt;\u0026gt; bin; // 입력받은 문자열을 생성자의 인자로 넘기면서 크기가 10인 bitset을 만든다 std::bitset\u0026lt;10\u0026gt; bits(bin); // 모든 비트를 뒤집는다 -\u0026gt; 1의 보수 bits.flip(); // 1의 보수를 unsigned long 타입 정수로 바꾼후 1을 더한다 -\u0026gt; 2의 보수 // unsigned long은 음수가 될 수 없으므로 int로 형변환 후 -1을 곱한다 std::cout \u0026lt;\u0026lt; -(static_cast\u0026lt;int\u0026gt;(bits.to_ulong()) + 1) \u0026lt;\u0026lt; std::endl; bitset을 바로 음수로 표현할 수는 없어서 양의 정수로 바꾼 후 -1을 곱한다 ","permalink":"https://nansu0425.github.io/posts/2%EC%A7%84%EC%88%98-10%EC%A7%84%EC%88%98-%EB%B3%80%ED%99%98/","summary":"C++에서 2진수와 10진수를 변환하는 방법 정리","title":"2진수 ↔ 10진수 변환"},{"content":"1. setw와 setfill 출력 스트림 동작을 조작할 수 있는 있는 manipulator 객체 \u0026lt;iomanip\u0026gt; 헤더에 선언이 존재 setw 숫자 데이터 출력 시 필드의 너비 지정 바로 다음 출력에만 적용 setfill 숫자의 자릿수가 필드 너비보다 작을 때 필드를 채울 문자 지정 한번 설정 후 계속 적용 2. 예제 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;iostream\u0026gt; #include \u0026lt;iomanip\u0026gt; int main() { int sec = 0; int min = 0; int hour = 0; std::cin \u0026gt;\u0026gt; sec; hour = sec / 3600; min = (sec % 3600) / 60; sec = (sec % 3600) % 60; std::cout \u0026lt;\u0026lt; std::setfill(\u0026#39;0\u0026#39;); std::cout \u0026lt;\u0026lt; std::setw(2) \u0026lt;\u0026lt; hour \u0026lt;\u0026lt; \u0026#34;:\u0026#34; \u0026lt;\u0026lt; std::setw(2) \u0026lt;\u0026lt; min \u0026lt;\u0026lt; \u0026#34;:\u0026#34; \u0026lt;\u0026lt; std::setw(2) \u0026lt;\u0026lt; sec \u0026lt;\u0026lt; std::endl; return 0; } 초를 입력받고 시:분:초 형식으로 출력하는 코드 setfill: 필드를 채울 문자로 ‘0’ 설정 setw: 필드 너비를 2로 지정, 다음 출력에만 적용되므로 출력할 때 마다 설정 ","permalink":"https://nansu0425.github.io/posts/%EC%A0%95%EC%88%98-%EC%B6%9C%EB%A0%A5-%EC%8B%9C-%EC%9E%90%EB%A6%BF%EC%88%98-%EB%A7%9E%EC%B6%94%EA%B8%B0-setw-setfill/","summary":"C++에서 정수 출력 시 setw, setfill을 이용해 자릿수 맞추기","title":"정수 출력 시 자릿수 맞추기 (setw, setfill)"},{"content":"1. cin 호출 후 getline 호출 1 2 3 4 5 std::string str1; std::string str2; std::cin \u0026gt;\u0026gt; str1; std::getline(std::cin, str2); 위 코드를 실행 후 hello world를 입력하면 str1에는 “hello”가 저장되고 str2에는 “ wolrd”(띄어쓰기가 문자열에 들어갔다)가 저장된다. 또한 hello 입력 후 엔터를 눌러 줄바꿈을 한 후 world를 입력하면 str1에는 “hello”가 저장되고 str2에는 “”(빈 문자열)이 저장된다. 이런 결과를 보고 내린 결론은 cin은 공백 문자를 버퍼에서 지우지 않는다는 것이다. 버퍼에 남아있던 공백 문자가 getline에 영향을 줬다. 띄어쓰기가 있을 때는 띄어쓰기가 포함되고 줄바꿈이 있을 때는 줄바꿈 문자를 버퍼에서 읽고 이후에 입력을 받지 않고 getline이 종료된다. 따라서 이런 경우 getline전에 ignore 나 get같은 함수를 이용해 입력 버퍼에서 공백 문자를 없애는 작업을 해야 한다.\n2. getline 호출 후 cin 호출 1 2 3 4 5 std::string str1; std::string str2; std::getline(std::cin, str1); std::cin \u0026gt;\u0026gt; str2; cin호출 후 getline을 호출했을 때와 다르게 getline이 cin에서 입력받는 문자열에 영향을 주지 않는다. getline을 연속해서 호출해도 다음 문자열에 영향을 주지 않는데 이것을 생각해보면 getline은 cin과 다르게 공백 문자를 버퍼에 남기지 않는다. 그리고 cin또한 연속 호출할 때는 문자열에 공백 문자가 포함되지 않는다. cin은 버퍼에 공백 문자를 남기긴 하지만 단지 무시할 뿐이다.\n","permalink":"https://nansu0425.github.io/posts/cin%EA%B3%BC-getline-%EC%88%9C%EC%84%9C/","summary":"C++의 cin, getline 호출 순서에 대한 정리","title":"cin과 getline 순서"},{"content":"1. 스트림 개념 C++ 에서 제공하는 입출력 매커니즘 스트림은 출발지 또는 목적지가 있고 한 방향으로 데이터를 전달한다 문자 기반 스트림 인스턴스 정리 스트림 클래스 데이터 흐름 버퍼링 여부 cin istream standard input \u0026raquo; 프로그램 O cout ostream standard output \u0026laquo; 프로그램 O cerr ostream standard error \u0026laquo; 프로그램 X clog ostream standard error \u0026laquo; 프로그램 O wide character 사용 시 wcin, wcout, wcerr, wclog사용 2. 에러 검사 입출력 시 에러는 언제나 발생할 수 있다 에러가 한번 발생해서 error bits가 설정되면 이후의 입출력 연산은 실패한다 스트림 사용 전에 사용 가능한 상태인지 미리 검사해야 한다 입출력 스트림의 상태를 검사할 수 있는 메서드 메서드 true 반환 조건 eof() 스트림이 end-of-file에 도달해 eofbit가 설정된 경우 bad() 복구하기 힘든 시스템 레벨의 입출력 문제가 발생하여 badbit가 설정된 경우 fail() 복구 가능한 입출력 문제가 발생하여 failbit가 설정되거나 badbit가 설정된 경우 good() eofibt, badbit, failbit 모두 설정되지 않아 goodbit가 설정된 경우 입출력 스트림 객체를 조건식으로 사용 시 !fail()과 같다 good()의 결과는 !fail() \u0026amp;\u0026amp; !eof() 와 같다 ","permalink":"https://nansu0425.github.io/posts/%EB%AC%B8%EC%9E%90-%EA%B8%B0%EB%B0%98-%EC%9E%85%EC%B6%9C%EB%A0%A5-%EC%8A%A4%ED%8A%B8%EB%A6%BC-%EA%B8%B0%EC%B4%88/","summary":"C++에서 문자 기반 입출력 스트림의 개념과 에러 검사 정리","title":"문자 기반 입출력 스트림 기초"},{"content":"1. 개념 I/O 스트림의 포맷은 특정 locale 설정에 맞춰져 있다 std::locale class 객체를 이용해 I/O 스트림의 locale 설정을 할 수 있다 특정 국가의 locale 값은 OS마다 다르다 POSIX에서 미국 설정 값: en_US Windows에서 미국 설정 값: en-US std::locale 생성자에 공백 스트링을 인자로 넘기면 사용자 환경에 맞는 locale 객체를 생성한다 2. imbue 활용 스트림의 imbue 메서드에 std::locale 객체를 넘기면 스트림의 locale이 설정된다 아래 코드를 실행하면 wcout, wcin의 모든 데이터 포맷은 사용자 시스템 locale에 맞춰진다 1 2 std::wcout.imbue(std::locale(\u0026#34;\u0026#34;)); std::wcin.imbue(std::locale(\u0026#34;\u0026#34;)); 3. 글로벌 로케일 활용 std::locale::global 함수를 호출하면 애플리케이션의 글로벌 locale 설정을 변경한다 std::locale의 default 생성자는 글로벌 locale의 복제본를 반환한다 아래 코드를 실행하면 글로벌 locale 설정이 입출력 스트림에 적용된다 1 std::locale::global(std::locale(\u0026#34;\u0026#34;)); 4. 주의할 점 입출력의 locale 설정은 입출력 함수(wcout, wcin, ..) 사용 전에 끝내야 한다 입출력 함수를 사용하다가 locale 설정을 변경하려고 하면 문제가 생길 가능성이 높다 ","permalink":"https://nansu0425.github.io/posts/%EC%9E%85%EC%B6%9C%EB%A0%A5-locale-%EC%84%A4%EC%A0%95-%EB%B0%A9%EB%B2%95/","summary":"C++에서 imbue, 글로벌 로케일을 이용해 입출력 locale 설정하는 방법","title":"입출력 locale 설정 방법"},{"content":" Visual Studio 2022 기준 locale 설정까지 해서 wide character 타입으로 입출력해도 한글이 깨지는 상황이 있다 이런 경우 코드 파일이 Unicode(UTF-8 with signature) - Codepage 65001로 인코딩돼 있는지 확인 signature(BOM) 있는 UTF-8 이어야 한다 Unicode(UTF-8 without signature) - Codepage 65001 인코딩의 경우 한글이 있으면 깨지거나 컴파일 오류 발생 예를 들어 main.cpp 파일의 인코딩 방식을 확인하고 싶다면 아래와 같이 하면 된다 File \u0026gt; Save main.cpp As… 클릭 Save 버튼의 화살표 버튼 클릭 Save with Encoding… 클릭 다른 방식으로 인코딩돼 있다면 Unicode(UTF-8 with signature) - Codepage 65001 로 설정 ","permalink":"https://nansu0425.github.io/posts/%ED%95%9C%EA%B8%80-%EC%9E%85%EC%B6%9C%EB%A0%A5-%EA%B9%A8%EC%A7%80%EB%8A%94-%ED%98%84%EC%83%81/","summary":"Visual Studio 2022에서 한글 입출력 깨지는 현상 해결 방법","title":"한글 입출력 깨지는 현상"},{"content":"About me.\n","permalink":"https://nansu0425.github.io/about/","summary":"about","title":"About"}]