<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>cobin_dev.log</title>
        <link>https://velog.io/</link>
        <description>사용자의 입장에서 생각하며 문제를 해결하는 백엔드 개발자입니다✍</description>
        <lastBuildDate>Sun, 15 Jun 2025 12:18:08 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>cobin_dev.log</title>
            <url>https://velog.velcdn.com/images/cobin_dev/profile/61a45a7e-12e3-4ba2-8880-0376fe296f8f/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. cobin_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/cobin_dev" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Kafka Listener의 LazyInitializationException 트러블슈팅]]></title>
            <link>https://velog.io/@cobin_dev/Kafka-Listener%EC%9D%98-LazyInitializationException-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@cobin_dev/Kafka-Listener%EC%9D%98-LazyInitializationException-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</guid>
            <pubDate>Sun, 15 Jun 2025 12:18:08 GMT</pubDate>
            <description><![CDATA[<h2 id="오류-상황">오류 상황</h2>
<p>Kafka로 주문 완료 메시지를 주고받는 스마트ETA 시스템을 개발 중이었다.<br>Spring Boot + JPA 기반의 애플리케이션에서 Kafka 메시지를 수신한 후, <code>Order</code> 엔티티를 조회하고 관련 <code>Store</code> 정보를 활용해 지연 통계를 갱신하는 로직을 구현했다.<br>하지만 메시지 수신 처리 도중 예상치 못한 예외가 발생했다.</p>
<h2 id="오류-내용">오류 내용</h2>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/c123523f-e0b7-4f3f-a9b7-bdb62b9ad9cd/image.png" alt=""></p>
<p>Kafka 메시지 리스너에서 예외 발생:</p>
<pre><code>org.hibernate.LazyInitializationException: Could not initialize proxy [com.example.delivery.store.domain.Store#1] - no session</code></pre><p>관련 로그 중 일부:</p>
<pre><code>DeliveryCompletedListener.listen(DeliveryCompletedEvent event) → processCompletedOrder(order)
→ order.getStore().getName() 호출 시 예외 발생</code></pre><h2 id="오류-코드">오류 코드</h2>
<p><code>OrderService.java</code> 일부</p>
<pre><code class="language-java">/**
     * 배달을 완료하는 메서드
     * @param orderId 주문 ID
     * @param deliveredAt 배달 완료 시각
     */
    public void completeDelivery(Long orderId, LocalDateTime deliveredAt) {
        Optional&lt;Order&gt; order = orderRepository.findById(orderId);
        if(order.isEmpty()) throw new RuntimeException(&quot;주문을 찾을 수 없습니다.&quot;);

        Order o = order.get();
        o.completeDelivery(deliveredAt);
        orderRepository.save(o);

        // Kafka 메시지 발행
        DeliveryCompletedEvent event = new DeliveryCompletedEvent(o.getId(), deliveredAt);
        producer.send(&quot;delivery-status&quot;, event);
    }</code></pre>
<p><code>DeliveryCompletedListener.java</code> 일부</p>
<pre><code class="language-java">    /**
     * Kafka로부터 배달 완료 이벤트를 수신하고 처리하는 리스너
     *
     * - 토픽: delivery-status
     * - 그룹 ID: smarteta-group
     * - 메시지 형식: DeliveryCompletedEvent (주문 ID, 배달 완료 시간 포함)
     *
     * 처리 로직:
     * 1. 전달받은 주문 ID로 Order를 조회
     * 2. 해당 Order의 배달 완료 시간(deliveredAt)을 업데이트
     * 3. StoreDelaySummaryService를 통해 지연 통계를 갱신
     */
    @KafkaListener(topics = &quot;delivery-status&quot;, groupId = &quot;smarteta-group&quot;)
    public void listen(DeliveryCompletedEvent event) {
        try {
            log.info(&quot;배달 완료 메시지 수신: {}&quot;, event);

            Optional&lt;Order&gt; optionalOrder = orderRepository.findById(event.orderId());
            if (optionalOrder.isEmpty()) {
                log.warn(&quot;주문 ID={} 에 해당하는 주문을 찾을 수 없습니다.&quot;, event.orderId());
                return;
            }

            Order order = optionalOrder.get();
            order.completeDelivery(event.deliveredAt());
            summaryService.processCompletedOrder(order);

            log.info(&quot;주문 처리 완료 → 주문 ID: {}&quot;, order.getId());
        }catch (Exception e){
            log.error(&quot; Kafka 메시지 처리 중 예외 발생&quot;, e);
        }
    }</code></pre>
<p><code>Order.java</code>의 일부</p>
<pre><code class="language-java">/* 주문 도메인 */
@Table(name = &quot;orders&quot;)
public class Order {

    // 주문 아이디
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // 유저 ID
    private Long userId;

    // 매장
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;store_id&quot;)
    private Store store;

    // 매장과 목적지까지의 거리
    private double distanceKm;
</code></pre>
<blockquote>
<p>하나의 EC2 안에 Kafka, MySQL, 그리고 Spring 서버(Docker 컨테이너)가 포함되어 있음<br>Spring 서버는 Kafka 메시지를 수신하여 JPA를 통해 DB 조회 후 지연 통계 업데이트 로직을 실행함</p>
</blockquote>
<hr>
<h2 id="현재-흐름">현재 흐름</h2>
<ol>
<li><p>Kafka Listener가 비동기적으로 메시지를 수신함</p>
</li>
<li><p>Listener는 orderRepository.findById(event.getOrderId()) 로 Order 객체를 가져옴</p>
</li>
<li><p>이 Order는 내부에 Store를 갖고 있지만, 아직 store는 프록시 상태</p>
</li>
<li><p>store.getName()을 호출하는 순간 JPA가 Store를 로딩하려 함</p>
</li>
<li><p>문제는 이 시점엔 이미 Hibernate 세션이 닫혀 있음 (Kafka Listener는 트랜잭션 바깥에서 실행됨)
→ 따라서 DB에 접근할 수 없어 LazyInitializationException 예외 발생</p>
</li>
</ol>
<hr>
<h2 id="예상-원인">예상 원인</h2>
<h3 id="lazyinitializationexception이란">LazyInitializationException이란?</h3>
<p>JPA는 <code>@ManyToOne(fetch = LAZY)</code> 관계에서 실제 연관 객체(<code>Store</code>)를 처음부터 조회하지 않고, 프록시 객체로 감싸 놓는다.<br>이 프록시는 실제 데이터가 필요한 시점(<code>getName()</code> 호출 등)에 DB에 접근해 데이터를 가져오려 한다.</p>
<p>하지만 이 시점에 <strong>JPA 세션(Session)</strong> 이 이미 닫혀 있으면 프록시가 초기화되지 못하고 예외가 발생한다.</p>
<hr>
<h3 id="lazyinitializationexception-발생원인">LazyInitializationException 발생원인</h3>
<p>Kafka 메시지를 수신하는 메서드 <code>listen()</code> 내부에서는 트랜잭션이 자동으로 생성되지 않는다.<br>따라서 <code>orderRepository.findById(orderId)</code>로 조회한 Order는 프록시 상태로 존재하며,<br>해당 Order에 연결된 <code>Store</code>를 조회하려고 할 때 세션이 이미 닫혀 예외가 발생했다.</p>
<p>즉,</p>
<pre><code class="language-java">Order order = orderRepository.findById(...).get();
order.getStore().getName(); // 여기서 LazyInitializationException 발생</code></pre>
<hr>
<h2 id="실제-해결-방법">실제 해결 방법</h2>
<h3 id="transactional-추가">@Transactional 추가</h3>
<pre><code class="language-java">@Transactional
public void listen(DeliveryCompletedEvent event) {
    ...
}</code></pre>
<p>Kafka 메시지를 처리하는 메서드에 <code>@Transactional</code>을 붙이면,<br>Spring은 해당 메서드 수행 중 JPA 세션을 유지시켜 프록시 초기화가 가능하도록 한다.
<img src="https://velog.velcdn.com/images/cobin_dev/post/9694c21d-b295-40df-b9ef-90de0c9762d0/image.png" alt=""></p>
<p><strong>결과적으로 LazyInitializationException이 해결되었다.</strong></p>
<hr>
<h2 id="트랜잭션-없이-세션이-닫히는-기준은">트랜잭션 없이 세션이 닫히는 기준은?</h2>
<ul>
<li>JPA의 세션(Session)은 트랜잭션 단위로 열리고 닫힌다.</li>
<li>트랜잭션이 없을 경우, Spring Data JPA가 리포지토리 메서드 수행 중 <strong>임시로 세션을 열고 바로 닫는다.</strong></li>
<li>따라서 리포지토리에서 반환된 엔티티는 더 이상 DB 접근이 불가능한 &quot;프록시 객체&quot;로 남게 된다.</li>
</ul>
<hr>
<h2 id="🧾-참고">🧾 참고</h2>
<ul>
<li><a href="https://cantcoding.tistory.com/78">JPA/ could not initialize proxy - no Session</a></li>
<li><a href="https://stackoverflow.com/questions/345705/hibernate-lazyinitializationexception-could-not-initialize-proxy">Stackoverflow</a></li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot + Kafka 연동 (with Docker)]]></title>
            <link>https://velog.io/@cobin_dev/Spring-Boot-Kafka-%EC%97%B0%EB%8F%99-with-Docker</link>
            <guid>https://velog.io/@cobin_dev/Spring-Boot-Kafka-%EC%97%B0%EB%8F%99-with-Docker</guid>
            <pubDate>Sat, 14 Jun 2025 13:33:15 GMT</pubDate>
            <description><![CDATA[<p>배달 관련 프로젝트를 진행하며 도커와 카프카 도입이 필요했다.</p>
<h2 id="1-kafka를-도입한-이유">1. Kafka를 도입한 이유</h2>
<ul>
<li>배달 ETA는 <strong>주문 상태 변화</strong>에 따라 유동적으로 조정된다.</li>
<li>Kafka는 이런 <strong>이벤트 기반 아키텍처</strong>에서 핵심 역할을 하는 <strong>메시지 브로커</strong>이기 때문</li>
</ul>
<hr>
<h2 id="2-kafka-환경-구성-docker-compose">2. Kafka 환경 구성 (Docker Compose)</h2>
<pre><code class="language-yaml">version: &#39;3.8&#39;

services:
  zookeeper:
    image: bitnami/zookeeper:latest
    container_name: zookeeper
    ports:
      - &quot;2181:2181&quot;  # Zookeeper는 Kafka를 위한 메타데이터 저장소 역할
    environment:
      - ALLOW_ANONYMOUS_LOGIN=yes

  kafka:
    image: bitnami/kafka:3.5.1
    container_name: kafka
    ports:
      - &quot;9092:9092&quot;  # Kafka 브로커가 외부로 노출되는 포트
    environment:
      - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181  # Zookeeper 연결 설정
      - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092  # 내부용 Listener
      - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092  # 외부용 Listener
      - ALLOW_PLAINTEXT_LISTENER=yes  # 인증 없이 허용 (개발용)
      - KAFKA_ENABLE_KRAFT=no  # KRaft 모드 비활성화 → Zookeeper 기반 Kafka 사용
    depends_on:
      - zookeeper  # Kafka는 Zookeeper가 먼저 실행되어야 함
</code></pre>
<h3 id="실행하기-실행-명령어">실행하기 (실행 명령어)</h3>
<pre><code class="language-bash">docker compose up -d</code></pre>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/348964bc-4410-408c-9464-26583ba24aff/image.png" alt=""></p>
<p>도커에서 잘 실행되고 있다!</p>
<hr>
<h2 id="3-spring-boot-설정">3. Spring Boot 설정</h2>
<h3 id="gradle-의존성-추가">Gradle 의존성 추가</h3>
<pre><code class="language-groovy">implementation &#39;org.springframework.kafka:spring-kafka&#39;</code></pre>
<hr>
<h3 id="applicationyml-설정">application.yml 설정</h3>
<pre><code class="language-yaml">spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: smarteta-group
      auto-offset-reset: earliest</code></pre>
<hr>
<h2 id="4-kafka-프로듀서리스너-코드">4. Kafka 프로듀서/리스너 코드</h2>
<h3 id="producer-kafka-메시지를-보내는-역할">Producer: Kafka 메시지를 보내는 역할</h3>
<pre><code class="language-java">@Component
public class DeliveryEventProducer {

    private final KafkaTemplate&lt;String, String&gt; kafkaTemplate;

    public DeliveryEventProducer(KafkaTemplate&lt;String, String&gt; kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }

    public void send(String topic, String message) {
        kafkaTemplate.send(topic, message);
        System.out.println(&quot;보낸 카프카 메시지 → &quot; + message);
    }
}</code></pre>
<hr>
<h3 id="listener-kafka-메시지를-받는-역할">Listener: Kafka 메시지를 받는 역할</h3>
<pre><code class="language-java">@Component
public class DeliveryEventListener {

    @KafkaListener(topics = &quot;test-topic&quot;, groupId = &quot;smarteta-group&quot;)
    public void listen(String message) {
        System.out.println(&quot;받은 카프카 메시지: &quot; + message);
    }
}</code></pre>
<hr>
<h3 id="테스트용-controller">테스트용 Controller</h3>
<pre><code class="language-java">@RestController
@RequestMapping(&quot;/kafka&quot;)
public class KafkaTestController {

    private final DeliveryEventProducer producer;

    public KafkaTestController(DeliveryEventProducer producer) {
        this.producer = producer;
    }

    @GetMapping(&quot;/send&quot;)
    public ResponseEntity&lt;String&gt; send() {
        producer.send(&quot;test-topic&quot;, &quot;스프링과 카프카 연동 완료!&quot;);
        return ResponseEntity.ok(&quot;Sent!&quot;);
    }
}</code></pre>
<hr>
<h2 id="5-postman으로-테스트">5. Postman으로 테스트</h2>
<h3 id="요청-정보">요청 정보</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>Method</td>
<td>GET</td>
</tr>
<tr>
<td>URL</td>
<td><a href="http://localhost:8080/kafka/send">http://localhost:8080/kafka/send</a></td>
</tr>
</tbody></table>
<hr>
<h3 id="결과-콘솔">결과 (콘솔)</h3>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/51a3a495-354a-40f4-947f-d1241c32620b/image.png" alt=""></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1953번 팀배분]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-1953%EB%B2%88-%ED%8C%80%EB%B0%B0%EB%B6%84</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-1953%EB%B2%88-%ED%8C%80%EB%B0%B0%EB%B6%84</guid>
            <pubDate>Mon, 28 Oct 2024 12:32:08 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/1953">백준 1953번 팀배분 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/3accf76c-47db-40d8-8987-b1a80633bba8/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/a4e15a42-661a-4233-86f5-67cb1147d885/image.png" alt=""></p>
<hr>
<p>처음에 문제를 보고 유니온 파인드가 떠올랐다. 그런데 잘보니 유니온이 아니라 다른 팀이 되야하는 연산을 해야하므로 유니온 파인드로 풀기는 어려웠다.</p>
<p>이후 아래 코드를 구현했다.</p>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int N;
    static int [] parent;
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;
        StringBuilder sb = new StringBuilder();

        st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());

        parent = new int[N+1];
        Arrays.fill(parent, 1); // 1과 -1로 구분

        for(int i=1; i&lt;=N; i++){
            st = new StringTokenizer(br.readLine());
            int total = Integer.parseInt(st.nextToken());
            int nowTeam = parent[i];
            for(int k=0; k&lt;total; k++){
                int dislike = Integer.parseInt(st.nextToken());
                parent[dislike] = - nowTeam;
            }
        }

        List&lt;Integer&gt; firstTeam = new ArrayList&lt;&gt;();
        List&lt;Integer&gt; secondTeam = new ArrayList&lt;&gt;();
        for(int i=1; i&lt;=N; i++){
            if(parent[i] == 1) firstTeam.add(i);
            else secondTeam.add(i);
        }

        Collections.sort(firstTeam);
        Collections.sort(secondTeam);

        sb.append(firstTeam.size()).append(&quot;\n&quot;);
        for(int num : firstTeam) sb.append(num).append(&quot; &quot;);
        sb.append(&quot;\n&quot;);

        sb.append(secondTeam.size()).append(&quot;\n&quot;);
        for(int num : secondTeam) sb.append(num).append(&quot; &quot;);

        System.out.print(sb);
    }
}</code></pre>
<p>그런데 이 코드는 통과할 수 없다. 반례를 구하기 어려워 내가 직접 반례를 찾아야했다.</p>
<pre><code>4
2 2 3
1 1
1 1
1 1
</code></pre><p>위와 같은 입력을 하면 내가 작성한 코드에서는 아래와 같이 출력된다.</p>
<pre><code>1
4 
3
1 2 3 </code></pre><p>1번은 분명 2, 3번과 같은팀이 되면 안되는데 말이다,,,,</p>
<p>이유는 간단하다. 내가 1번을 보고 2번과 3번을 -1로 초기화 했을 떄 4번은 1인 상태이다. 그런데 4번이 1을 싫어한다는 것을 보는 순간 4번의 값(1)에 -를 붙임 -1을 1의 값으로 설정하게 되어 배열이 <code>-1 -1 -1 1</code> 이 되어버리는 것이다.</p>
<p>따라서 우리는 DFS를 통해 풀어야한다. 왜냐하면 하나가 결정되면 cascade로 결정을 해주고, 또 결정된 것을 cascade로 싫어하는 사람을 -한 값으로 저장해야하기 때문이다. 이러한 과정에서 중요한 것은 visited를 사용해야한다는 것이다. 이미 팀을 배정받은 사람이 다시 팀을 배정받으면 모든 규칙이 엉망이 되어버리기 때문이다.</p>
<h2 id="풀이-방법">풀이 방법</h2>
<h3 id="1-싫어하는-사람들을-인접-리스트-배열로-양방향-저장하기">1. 싫어하는 사람들을 인접 리스트 배열로 양방향 저장하기</h3>
<p>싫어하는 사람들을 인접 리스트 배열로 저장해야한다. 이때 양방향으로 저장해야함에 주의하자.</p>
<pre><code class="language-java">        for(int i=1; i&lt;=N; i++){
            st = new StringTokenizer(br.readLine());
            int total = Integer.parseInt(st.nextToken());
            for(int k=0; k&lt;total; k++){
                int dislike = Integer.parseInt(st.nextToken());
                arr[i].add(dislike);
                arr[dislike].add(i);
            }
        }
</code></pre>
<h3 id="2-1번은-1번팀에-고정하고-dfs-돌리기">2. 1번은 1번팀에 고정하고, DFS 돌리기</h3>
<p>어차피 2개의 팀밖에 주어지지 않으므로 1번은 1번팀에 고정한다.
이후 DFS를 돌린다.</p>
<pre><code class="language-java">team[1] = 1; // 1번은 1번 팀
        for(int i=1; i&lt;=N; i++){
            if(!visited[i]) DFS(i, team[i]);
        }</code></pre>
<pre><code class="language-java">static void DFS(int num, int nowTeam){
        visited[num] = true;
        for(int n : arr[num]){
            if(!visited[n]) {
                team[n] = - nowTeam;
                DFS(n, - nowTeam);
            }
        }
    }</code></pre>
<h3 id="3-순서대로-출력하기">3. 순서대로 출력하기</h3>
<p>이제 각 팀의 인원을 계산한 뒤 오름차 순으로 출력한다.</p>
<pre><code class="language-java">int firstTeam = 0; // 첫번째 팀 인원수
        for(int i=1; i&lt;=N; i++){
            if(team[i] == 1) firstTeam++;
        }
        sb.append(firstTeam).append(&quot;\n&quot;);

        int secondTeam = N - firstTeam; // 두번째 팀 인원수

        // 첫번째 팀 출력
        for(int i=1; i&lt;=N; i++){
            if(team[i] == 1) sb.append(i).append(&quot; &quot;);
        }
        sb.append(&quot;\n&quot;);

        sb.append(secondTeam).append(&quot;\n&quot;);
        // 두번째 팀 출력
        for(int i=1; i&lt;=N; i++){
            if(team[i] == -1) sb.append(i).append(&quot; &quot;);
        }

        System.out.print(sb);</code></pre>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;

public class p1953 {
    static int N;
    static int [] team;
    static List&lt;Integer&gt; [] arr;
    static boolean[] visited;

    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st;
        StringBuilder sb = new StringBuilder();

        st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());

        arr = new List[N+1];
        team = new int[N+1];
        visited = new boolean[N+1];
        for(int i=1; i&lt;=N; i++) arr[i] = new ArrayList&lt;&gt;();


        for(int i=1; i&lt;=N; i++){
            st = new StringTokenizer(br.readLine());
            int total = Integer.parseInt(st.nextToken());
            for(int k=0; k&lt;total; k++){
                int dislike = Integer.parseInt(st.nextToken());
                arr[i].add(dislike);
                arr[dislike].add(i);
            }
        }

        team[1] = 1; // 1번은 1번 팀
        for(int i=1; i&lt;=N; i++){
            if(!visited[i]) DFS(i, team[i]);
        }

        int firstTeam = 0; // 첫번째 팀 인원수
        for(int i=1; i&lt;=N; i++){
            if(team[i] == 1) firstTeam++;
        }
        sb.append(firstTeam).append(&quot;\n&quot;);

        int secondTeam = N - firstTeam; // 두번째 팀 인원수

        // 첫번째 팀 출력
        for(int i=1; i&lt;=N; i++){
            if(team[i] == 1) sb.append(i).append(&quot; &quot;);
        }
        sb.append(&quot;\n&quot;);

        sb.append(secondTeam).append(&quot;\n&quot;);
        // 두번째 팀 출력
        for(int i=1; i&lt;=N; i++){
            if(team[i] == -1) sb.append(i).append(&quot; &quot;);
        }

        System.out.print(sb);

    }
    static void DFS(int num, int nowTeam){
        visited[num] = true;
        for(int n : arr[num]){
            if(!visited[n]) {
                team[n] = - nowTeam;
                DFS(n, - nowTeam);
            }
        }
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 16472번 고냥이]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-16472%EB%B2%88-%EA%B3%A0%EB%83%A5%EC%9D%B4</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-16472%EB%B2%88-%EA%B3%A0%EB%83%A5%EC%9D%B4</guid>
            <pubDate>Fri, 06 Sep 2024 14:44:19 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/16472">백준 16472번 고냥이 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/4ccfd928-c449-4057-b4e2-c0cfb516eb69/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/7fd6af55-ac88-4d2e-a5c0-0281485e46ad/image.png" alt=""></p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<p>먼저 문제를 잘 읽어야한다. 사실 나는 처음에 문제를 잘못 읽어서 틀렸기 때문이다.
입력으로 받은 문자열 종류 최대 개수 안에서 최대 문자열 길이를 출력해야한다.</p>
<h3 id="1-알파벳-제일-최근-출현-배열-만들기">1. 알파벳 제일 최근 출현 배열 만들기</h3>
<p>먼저 알파벳 최근 출현 배열을 만들어야한다. 그 이유는 새로 검사한 알파벳을 추가했을 때 현재 인식 중인 알파벳의 종류에는 없는 알파벳인 경우 문제에서 주어진 <code>N</code>을 초과하게된다. 따라서 이 경우 제일 알파벳 종류 중 1개를 제거해야하는데, 이 때 출현이 가장 오래된 알파벳을 제거할 것이기 때문에 알파벳이 나오면 해당 알파벳의 최근 출현 위치를 기록할 배열이 필요하다.
배열의 길이는 알파벳의 개수만큼 만들면 된다.<code>int</code>배열의 기본 값인 <code>0</code>으로 초기화 되면 마지막 출현 위치가 헷갈릴 수 있으므로 <code>-1</code>로 초기화해주었다.</p>
<h3 id="2-현재-출현-알파벳-종류를-세기-위한-set-만들기">2. 현재 출현 알파벳 종류를 세기 위한 Set 만들기</h3>
<p>현재 인식 중인 알파벳의 종류를 세기 위해서 <code>set</code>을 사용한다. </p>
<h3 id="3-다음-알파벳을-추가했을-때-최대-알파벳-종류의-개수를-넘어가면-문자열-길이-업데이트-하기">3. 다음 알파벳을 추가했을 때 최대 알파벳 종류의 개수를 넘어가면 문자열 길이 업데이트 하기</h3>
<p>새로운 알파벳을 검사했을 때 3가지 경우로 나뉜다.</p>
<h4 id="3-1-현재-인식가능한-알파벳에-종류에-포함되어-있지-않은데-이-알파벳을-인식하면-최대-인식-종류-개수를-넘는-경우">3-1. 현재 인식가능한 알파벳에 종류에 포함되어 있지 않은데, 이 알파벳을 인식하면 최대 인식 종류 개수를 넘는 경우</h4>
<p>1) 정답과 비교하여 최대 인식 길이를 업데이트 해준다.
2) 최종 출현 위치가 가장 낮은(일찍인) 문자를 찾아야한다. 알파벳 출현 배열에서 -<code>1</code>이 아닌 가장 작은 값을 찾으면 그 알파벳일 것이다. 
3) 현재 인식 길이를 <code>현재 위치 - 찾은 알파벳의 최종 출현 위치</code>로 업데이트 한다.
4) 찾은 알파벳의 최종 출현 위치 배열에서의 값을 <code>-1</code>로 업데이트 한다.
5) 인식 가능한 문자 <code>set</code> 에서 찾은 알파벳을 제거한다.
6) 새 문자를 인식 가능한 문자 <code>set</code>에 넣는다.</p>
<h4 id="3-2-현재-인식가능한-알파벳에-종류에-포함되어-있지-않은데-이-알파벳을-인식해도-최대-인식-종류-개수를-넘지-않는-경우">3-2. 현재 인식가능한 알파벳에 종류에 포함되어 있지 않은데, 이 알파벳을 인식해도 최대 인식 종류 개수를 넘지 않는 경우</h4>
<p>1) 새 문자를 인식 가능한 문자 <code>set</code>에 넣는다.
2) 현재 인식 가능한 문자 종류 플래그를 1 증가시킨다.
3) 현재 인식 길이를 <code>+1</code> 해준다.
4) 최대 인식 길이를 업데이트 해준다.</p>
<h4 id="3-3-현재-인식가능한-알파벳에-종류에-포함된-경유">3-3. 현재 인식가능한 알파벳에 종류에 포함된 경유</h4>
<p>1) 새 문자를 인식 가능한 문자 <code>set</code>에 넣는다.
2) 현재 인식 길이를 <code>+1</code> 해준다.
3) 최대 인식 길이를 업데이트 해준다.</p>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class p16472 {
    static int [] alpha;
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        String line = br.readLine();
        alpha = new int [&#39;z&#39;-&#39;a&#39;+1]; // 각 알파벳의 마지막 출현 위치 배열

        Arrays.fill(alpha,-1); // 마지막 출현 위치를 -1로 초기화

        // 찾기
        Set&lt;Character&gt; set = new HashSet&lt;&gt;(); // 현재 인식가능한 알파벳들을 넣어둔 집합
        int result = 1; // 정답
        int nowKind = 1; // 현재 인식하고 있는 문자열의 종류 (첫번째꺼를 넣으므로 초기값은 1)
        int nowLen = 1; // 현재 인식하고 있는 문자열의 길이 (첫번째꺼를 넣으므로 초기값은 1)

        // 첫번째 꺼 미리 넣어두기
        set.add(line.charAt(0));
        alpha[line.charAt(0)-&#39;a&#39;] = 0;

        for(int i=1; i&lt;line.length(); i++){
            char next = line.charAt(i);
            //최종 출현 위치 업데이트
            if(!set.contains(next) &amp;&amp; nowKind == n){ // 포함되어 있지 않은데 인식 가능 종류 개수를 넘으면
                result = Math.max(nowLen, result); // 최대 길이 업데이트
                char minAlpha = findMin(); // 가장 출현 위치가 낮은 문자 찾기

                nowLen = i - alpha[minAlpha - &#39;a&#39;]; //현재 인식 길이 업데이트
                alpha[minAlpha-&#39;a&#39;] = -1; // 가장 낮은 출현위치를 -1로 설정
                set.remove(minAlpha); // 인식 가능한 문자열에서 지우기

                set.add(next); //새로운 문자 인식 가능 집합에 넣기
            }else{
                if(!set.contains(next)) nowKind++; // 현재 인식 가능 종류에 없었으면
                set.add(next);
                nowLen++;
                result = Math.max(nowLen, result);
            }
            alpha[next-&#39;a&#39;] = i; //최종 출현 위치 업데이트
        }
        System.out.println(result);
    }

    // 가장 출현 위치가 낮은 문자를 찾는 메서드
    static char findMin() {
        int min = Integer.MAX_VALUE;
        char c = &#39;a&#39;; //임시 값 ( 아무 값이나 넣어둠)
        for(int i=0 ;i&lt;alpha.length; i++){
            if(alpha[i]!=-1 &amp;&amp; alpha[i] &lt; min){
                min = alpha[i];
                c = (char)(&#39;a&#39; + i);
            }
        }
        return c;
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1987번 알파벳]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-1987%EB%B2%88-%EC%95%8C%ED%8C%8C%EB%B2%B3</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-1987%EB%B2%88-%EC%95%8C%ED%8C%8C%EB%B2%B3</guid>
            <pubDate>Mon, 19 Aug 2024 06:37:45 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/1987">백준 1987번 알파벳 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/ef5ee17f-9dbb-4e26-984d-7d77a01f594a/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/417ad6e2-b052-4a98-858a-181361a941f3/image.png" alt=""></p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<p>간단한 DFS 문제다. <code>visited[]</code> 배열을 각 문자열에 대해서 만들고 이미 방문한 문자를 방문하지 않게 하는 방식으로 풀어나가면 된다.</p>
<h3 id="1-문자열에-대해-visited-배열-만들기">1. 문자열에 대해 visited 배열 만들기</h3>
<p>문제에서 대문자들로 배열이 주어지므로 아스키 코드로 65번부터 90번까지의 문자이다. 따라서 <code>visited[]</code> 배열의 크기를 26으로 만들고 각각 인덱스를 <code>문자 - 65</code>로 생각하면된다.</p>
<h3 id="2-dfs-구현하기">2. DFS 구현하기</h3>
<p>일반적인 DFS와 똑같이 구현하면 된다.</p>
<pre><code class="language-java">static void DFS(int r, int c, boolean[] visited) {
    int idx = arr[r][c] - 65; // 현재 문자의 아스키 코드
    if(!visited[idx]) { // 방문한 적 없으면
        visited[idx] = true;
        for(int i=0 ;i&lt;4; i++) {
            int newX = c + dx[i];
            int newY = r + dy[i];
            if(check(newX, newY)) {
                DFS(newY, newX, visited);
            }
        }
        // 나올 때는 visited를 false로 바꿔줌
        visited[idx] = false;
    }else { // 방문 했으면
        // max 업데이트
        int cnt = 0;
        for(int i=0; i&lt;26; i++) {
            if(visited[i]) {
                cnt++;
            }
        }
        if(max &lt; cnt) max = cnt;
    }
}</code></pre>
<h3 id="3-max-업데이트-후-출력하기">3. max 업데이트 후 출력하기</h3>
<p>만약 이미 방문한 문자라면 현재 <code>visited[]</code> 배열에서 방문한 것들을 센 후 <code>max</code>와 비교하여 <code>max</code>를 업데이트 해준다. 여기서 <code>max</code>는 최대 몇칸을 갈 수 있는지를 나타낸다.</p>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">package 백준;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Arrays;
import java.util.StringTokenizer;

public class p1987 {
    static int R, C;
    static int [] dx = {-1, 0, 0, 1};
    static int [] dy = {0, 1, -1, 0};
    static char [][] arr;
    static int max = 1;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine());

        R = Integer.parseInt(st.nextToken());
        C = Integer.parseInt(st.nextToken());

        arr = new char [R][C];

        for(int i=0; i&lt;R; i++) {

            String line = br.readLine();
            for(int j=0; j&lt;C; j++) {
                arr[i][j] = line.charAt(j);
            }
        }


        boolean [] visited = new boolean[26];
        DFS(0, 0, visited);
        bw.write(max+&quot;&quot;);
        bw.flush();
        bw.close();
        // DFS
    }    

    static void DFS(int r, int c, boolean[] visited) {
        int idx = arr[r][c] - 65; // 현재 문자의 아스키 코드
        if(!visited[idx]) { // 방문한 적 없으면
            visited[idx] = true;
            for(int i=0 ;i&lt;4; i++) {
                int newX = c + dx[i];
                int newY = r + dy[i];
                if(check(newX, newY)) {
                    DFS(newY, newX, visited);
                }
            }
            // 나올 때는 visited를 false로 바꿔줌
            visited[idx] = false;
        }else { // 방문 했으면
            // max 업데이트
            int cnt = 0;
            for(int i=0; i&lt;26; i++) {
                if(visited[i]) {
                    cnt++;
                }
            }
            if(max &lt; cnt) max = cnt;
        }
    }

    static boolean check(int x, int y) {
        return y&lt;R &amp;&amp; y&gt;=0 &amp;&amp; x&lt;C &amp;&amp; x&gt;=0;
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 28107번 회전초밥]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-28107%EB%B2%88-%ED%9A%8C%EC%A0%84%EC%B4%88%EB%B0%A5</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-28107%EB%B2%88-%ED%9A%8C%EC%A0%84%EC%B4%88%EB%B0%A5</guid>
            <pubDate>Sun, 18 Aug 2024 12:41:23 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/28107">백준 28107번 회전초밥 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/a91c809c-f0b4-4317-aeef-ca70442cb85a/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/6093d3f3-6f19-4b04-bd28-dfb046607d68/image.png" alt=""></p>
<hr>
<p>간단한 문제라고 생각한다. 특별한 알고리즘 기법이 들어가기보다는 문제 조건대로 큐를 사용해서 구현하면 된다.
또한 초밥 리스트 배열과 손님별 먹은 초밥 수 배열을 사용해야겠다는 아이디어만 있으면 충분히 풀 수 있다.</p>
<h2 id="풀이-방법">풀이 방법</h2>
<h3 id="1-초밥-리스트-배열-만들기">1. 초밥 리스트 배열 만들기</h3>
<p>먼저 그래프를 구현하는 것처럼 초밥 리스트 배열을 구현했다. <code>1 ~ N</code> 손님들은 각 순서대로 우선순위가 있기 때문에 우선순위 큐 대신 그냥 큐를 사용했다. 따라서 초밥마다 먹을 사람을 링크드 리스트에 저장하는 형식이다.</p>
<pre><code class="language-java">// 초밥 리스트 배열 초기화
LinkedList&lt;Integer&gt;[] sushi = new LinkedList[200_001];
for (int i = 1; i &lt;= 200_000; i++) {
    sushi[i] = new LinkedList&lt;&gt;();
}</code></pre>
<h3 id="2-초밥-리스트-배열-채우기">2. 초밥 리스트 배열 채우기</h3>
<p>이제 주어진 손님 리스트에 따라 초밥 리스트를 채운다.</p>
<pre><code class="language-java">// 초밥 리스트 배열 채우기
for (int i = 1; i &lt;= N; i++) {
    st = new StringTokenizer(br.readLine());
    int dishes = Integer.parseInt(st.nextToken());
    for (int j = 0; j &lt; dishes; j++) {
        int now = Integer.parseInt(st.nextToken());
        sushi[now].add(i);
    }
}</code></pre>
<h3 id="3-요리사가-만든-초밥을-먹을-사람-구하기">3. 요리사가 만든 초밥을 먹을 사람 구하기</h3>
<p>이제 우선순위가 저장되었으므로 요리사가 초밥을 만들 때마다 초밥을 먹을 사람을 정해주어야한다. 따라서 요리사가 초밥을 만들면 초밥 리스트에서 해당 초밥의 리스트를 확인하여 사이즈가 0보다 크면 해당 사람을 리스트에서 삭제하고, 손님별 먹은 초밥 수 배열에서 해당 손님을 +1 한다.</p>
<pre><code class="language-java">// 요리사가 초밥을 만드는 과정
st = new StringTokenizer(br.readLine());
for (int i = 1; i &lt;= M; i++) {
    int now = Integer.parseInt(st.nextToken()); // 요리사가 현재 만든 초밥
    if (sushi[now].size() &gt; 0) { // 초밥을 먹을 사람이 있으면
        int eatPerson = sushi[now].remove(0);
        eat[eatPerson]++;
    }
}</code></pre>
<h3 id="4-각-사람이-먹은-초밥의-개수-출력하기">4. 각 사람이 먹은 초밥의 개수 출력하기</h3>
<p>이제 손님별 먹은 초밥 수 배열을 순서대로 출력한다.</p>
<pre><code class="language-java">for (int i = 1; i &lt;= N; i++) {
    sb.append(eat[i]).append(&quot; &quot;);
}
</code></pre>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.LinkedList;
import java.util.StringTokenizer;

public class p28107 {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringBuilder sb = new StringBuilder();

        // 손님 수와 초밥 수 입력받기
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken()); // 손님 수
        int M = Integer.parseInt(st.nextToken()); // 초밥 수

        // 손님별 먹은 초밥 수 배열 초기화
        int[] eat = new int[N + 1];
        // 초밥 리스트 배열 초기화
        LinkedList&lt;Integer&gt;[] sushi = new LinkedList[200_001];
        for (int i = 1; i &lt;= 200_000; i++) {
            sushi[i] = new LinkedList&lt;&gt;();
        }

        // 초밥 리스트 배열 채우기
        for (int i = 1; i &lt;= N; i++) {
            st = new StringTokenizer(br.readLine());
            int dishes = Integer.parseInt(st.nextToken());
            for (int j = 0; j &lt; dishes; j++) {
                int now = Integer.parseInt(st.nextToken());
                sushi[now].add(i);
            }
        }

        // 요리사가 초밥을 만드는 과정
        st = new StringTokenizer(br.readLine());
        for (int i = 1; i &lt;= M; i++) {
            int now = Integer.parseInt(st.nextToken()); // 요리사가 현재 만든 초밥
            if (sushi[now].size() &gt; 0) { // 초밥을 먹을 사람이 있으면
                int eatPerson = sushi[now].remove(0);
                eat[eatPerson]++;
            }
        }

        for (int i = 1; i &lt;= N; i++) {
            sb.append(eat[i]).append(&quot; &quot;);
        }

        bw.write(sb.toString());
        bw.flush();
        bw.close();
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1106번 호텔]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-1106%EB%B2%88-%ED%98%B8%ED%85%94</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-1106%EB%B2%88-%ED%98%B8%ED%85%94</guid>
            <pubDate>Fri, 16 Aug 2024 12:34:58 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/1106">백준 1106번 호텔 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/4ef3b14b-0c1c-4051-b6c5-f1b36c3800d9/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/f1b9e0af-dae2-4b0c-a37a-bef76fc29b03/image.png" alt=""></p>
<hr>
<p>보자마자 DP 문제라는 것을 알았다. 그리고 DP중에서도 냅색 문제라고 생각했다. 그런데 일반적인 냅색 문제와는 달랐다.</p>
<blockquote>
<p><strong>일반적인 냅색 문제는 <code>0-1</code> 냅색문제로 짐이 1번 들어가거나 0번들어가거나 둘 중 하나의 경우인 상황이다.</strong><br>
그러나 이 문제를 보면 짐(여기서는 도시)이 1번이 아닌 여러번 들어갈 수 있다.<br>
따라서 <strong>이 문제는 <code>0-1</code> 냅색 문제를 응용해야한다는 것을 느꼈다.</strong></p>
</blockquote>
<h2 id="풀이-방법">풀이 방법</h2>
<h3 id="1-dp-배열을-어떻게-만들지-구상하기">1. DP 배열을 어떻게 만들지 구상하기</h3>
<p>먼저 DP 배열을 어떻게 만들지 구상해야한다. 기존의 냅색 문제는 DP 배열을 두가지로 만들 수 있다.</p>
<blockquote>
<h4 id="1차원-배열">1차원 배열</h4>
</blockquote>
<ul>
<li><strong>dp[i]</strong></li>
<li><strong>행:</strong> 최대 허용 가능한 무게</li>
<li><strong>dp[i]:</strong> 는 무게가 최대 i까지 허용될 때 모든 짐을 고려하여 얻을 수 있는 최대 효용</li>
</ul>
<blockquote>
<h4 id="2차원-배열">2차원 배열</h4>
</blockquote>
<ul>
<li><strong>dp[i][j]</strong></li>
<li><strong>열:</strong> 각 짐들의 개수(각 짐)</li>
<li><strong>행:</strong> 최대 허용 가능한 무게</li>
<li><strong>dp[i][j]:</strong> 는 무게가 최대 j까지 허용될 때 짐 i개(예를 들어 1번짐부터 i번짐)를 고려했을 때 얻을 수 있는 최대 효용 </li>
</ul>
<h4 id="그렇다면-현재-문제에서는-dp배열을-어떻게-만들어야할까">그렇다면 현재 문제에서는 dp배열을 어떻게 만들어야할까?</h4>
<p>이번 문제에서는 1차원 배열을 만들고 각 행은 최대 허용 가능한 무게를, dp[i]의 값은 i가 최대 허용 가능 무게일 때 최소 비용이다.</p>
<p>이 문제에서 주어진 조건을 바꿔서 말하면 다음과 같다.</p>
<blockquote>
<h3 id="1차원-배열-1">1차원 배열</h3>
</blockquote>
<ul>
<li><strong>dp[i]</strong></li>
<li><strong>행</strong> 고객 수</li>
<li><strong>dp[i]</strong> 고객수가 i명을 유치하기 위한 최소 비용</li>
</ul>
<h4 id="배열의-길이는-몇으로-해야할까">배열의 길이는 몇으로 해야할까?</h4>
<p>언뜻보면 <code>C</code>라고 생각할 수 있다. 하지만 결과적으로 <code>C+100</code>으로 해주어야한다. 배열에서 각 인덱스의 의미는 고객 수이다. <code>i</code>가 100이면 100명의 고객을 유치할 때의 최소 비용이 dp[i]이고, <code>i</code>가 101이면 101명의 고객을 유치할 때의 최소 비용이 dp[i]이다.</p>
<p>예를 들어 A도시는 3명을 유치하는 데에 5의 비용이 들고, B도시는 1명을 유치하는 데에 10의 비용이 든다고 가정해보자.
<code>dp[1] = 10</code> 이고 <code>dp[2] = 20</code> 인데 <code>dp[3] = 5</code>이다.
정확히 i명을 유치하는 데에 필요한 비용이므로 C명을 유치하는 데에 필요한 비용은 dp[C]이지만 최소 C명을 유치하는 비용은 dp[C+a] 일 수 있다.</p>
<h4 id="그럼-여기서-a는-어떻게-정할까">그럼 여기서 a는 어떻게 정할까?</h4>
<p>최대 사람 수가 100이므로 <code>C+100</code>을 해주면 더이상 유치할 수 없는 경우까지 안전하게 계산해준다. </p>
<h3 id="2-dp-배열을-어떻게-채울지-구상하기">2. DP 배열을 어떻게 채울지 구상하기</h3>
<p>이제 DP배열을 만들었으니 어떻게 채울지를 결정해야한다.
기존의 <code>0-1</code>냅색 문제의 1차원 배열은 아래와 같은 식으로 채워주었다.</p>
<pre><code class="language-java">for(int i=1; i&lt;=N; i++){
    for(int w=Limit; w&gt;=value[i]; w--){
        dp[w] = Math.Max(dp[w], dp[w - weigth[i]]+ value[i]);
    }
}</code></pre>
<p>이번에는 최소를 구해주어야하므로 Min을 사용해야한다. 그런데 <code>0-1</code> 냅색 문제에서는 따로 배열을 초기화하지 않았는데 그 이유는 배열을 선언하면 0으로 초기화 되기 때문이다. 그리고 <code>Math.max()</code>를 사용하므로 0으로 초기화해주는 것이 맞다.</p>
<p>하지만 이번에는 <code>Math.min()</code>을 사용해야하므로 배열들을 <code>MAX_VALUE</code>로 초기화해주어야한다. 또한 <code>dp[0] = 0</code>으로 초기화해준다. 이 의미는 0명을 유치하기 위한 비용은 0이라는 의미이다.</p>
<p>이제 초기화는 했으니 <code>dp[1]</code>부터 채워나가야한다.
<code>dp[i]</code>는 i명을 유치하기 위한 최소 비용이므로 <code>dp[i]</code>와 <code>dp[i-현재 도시의 유치 가능한 사람 수]+ 현재 도시의 사람들을 유치하는 데에 드는 비용</code> 중 최소값을 <code>dp[i]</code>로 설정해야한다.</p>
<blockquote>
<p><strong>그런데 이 때 주의할 점이 있다. 바로 <code>dp[i-현재 도시의 유치 가능한 사람 수]</code>가 우리가 초기화한 <code>MAX_VALUE</code>가 아닌 경우에만 위 점화식을 사용해야한다는 것이다.</strong><br>
<strong>왜냐하면 우리가 초기화한 <code>MAX_VALUE</code>는 해당 사람을 유치하는 방법이 없다는 것을 의미하기 때문이다. 따라서 <code>i-현재 도시의 유치 가능한 사람 수</code>명을 유치하는 방법은 존재하지 않은 것이므로 계산할 수 없다.</strong></p>
</blockquote>
<p>따라서 코드는 아래와 같이 된다.</p>
<pre><code class="language-java">            // DP 배열 채우기
            for (int j = v; j &lt; C + 100; j++) {
            //만약 dp배열에서 현재 사람 수를 뺀 사람 명수의 비용이 초기 비용이 아니면 (초기 비용으로 초기화한 것은 만족시킬 수 없는 상태를 나타냄으로)
                if(dp[j-v] != Long.MAX_VALUE){ 
                //현재 배열 값과 현재 배열에서 v명을 빼고 i번째 사람을 넣었을 때의 비용 중 작은 것으로 결정
                    dp[j] = Math.min(dp[j], dp[j - v] + c); 
                }
            }</code></pre>
<h3 id="3-dp-배열을-통해-답을-어떻게-도출할지-생각하기">3. DP 배열을 통해 답을 어떻게 도출할지 생각하기</h3>
<p>그렇다면 이렇게 DP배열을 만들었을 때 답은 어떻게 구할까?
아까 말했듯 <code>dp[C]</code>는 답이 될 수도 있고 아닐 수도 있다. 따라서 <code>C ~ C+99</code>까지의 <code>dp[i]</code> 중 가장 작은 것을 답으로 설정해야한다.</p>
<p>이번 문제는 DP의 응용이어서 상당히 오랜 시간이 걸렸지만, 실제 코테에서는 DP를 심화하여 내기가 어려움으로 너무 걱정하진 않아도 될 것 같다.</p>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.*;

public class p1106_2 {
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st;

        // 입력 받기
        st = new StringTokenizer(br.readLine());
        int C = Integer.parseInt(st.nextToken()); // 목표 인원 수
        int N = Integer.parseInt(st.nextToken()); // 도시 수

        long[] dp = new long[C + 100]; //DP 배열  -&gt; 최대 손님은 100명인데 103명을 하는게 더 최소일 수도 있고, 104명을 하는게 더 최소일 수도 있어서 C에 100을 더함
        // dp[i] =  i명의 사람을 유치하기 위한 최소 비용
        Arrays.fill(dp, Long.MAX_VALUE); // 초기화를 최대값으로 함 -&gt; 나중에 min을 할 것이기 때문에 음수 값으로 하면 안됨

        // dp[0]을 0으로 초기화
        dp[0] = 0; // 0명을 유치하기 위한 비용은 0임

        // 비용과 인원 데이터 받기
        for (int i = 0; i &lt; N; i++) {
            st = new StringTokenizer(br.readLine());
            int c = Integer.parseInt(st.nextToken()); // 비용
            int v = Integer.parseInt(st.nextToken()); // 모집 가능한 인원 수

            // DP 배열 채우기
            for (int j = v; j &lt; C + 100; j++) {
                if(dp[j-v] != Long.MAX_VALUE){ //만약 dp배열에서 현재 사람 수를 뺀 사람 명수의 비용이 초기 비용이 아니면 (초기 비용으로 초기화한 것은 만족시킬 수 없는 상태를 나타냄으로)
                    dp[j] = Math.min(dp[j], dp[j - v] + c); //현재 배열 값과 현재 배열에서 v명을 빼고 i번째 사람을 넣었을 때의 비용 중 작은 것으로 결정
                }
            }
        }

        // 답 찾기
        long min = Long.MAX_VALUE;
        for (int i = C; i &lt; C + 100; i++) { //C명 이상을 유치하는 비용 중 가장 작은 비용을 값으로 설정
            min = Math.min(dp[i], min);
        }

        // 출력
        bw.write(min + &quot;\n&quot;);
        bw.flush();
        bw.close();
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 17352번 여러분의 다리가 되어 드리겠습니다!]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-17352%EB%B2%88-%EC%97%AC%EB%9F%AC%EB%B6%84%EC%9D%98-%EB%8B%A4%EB%A6%AC%EA%B0%80-%EB%90%98%EC%96%B4-%EB%93%9C%EB%A6%AC%EA%B2%A0%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-17352%EB%B2%88-%EC%97%AC%EB%9F%AC%EB%B6%84%EC%9D%98-%EB%8B%A4%EB%A6%AC%EA%B0%80-%EB%90%98%EC%96%B4-%EB%93%9C%EB%A6%AC%EA%B2%A0%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Thu, 15 Aug 2024 12:24:38 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/17352">백준 17352번 여러분의 다리가 되어 드리겠습니다! 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/32b467f4-8787-4993-8ea7-86d3b17b586e/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/39a3c6a3-734b-44c5-93d9-2862a1005acc/image.png" alt=""></p>
<hr>
<p>전형적인 유니온 파인드 문제다. 유니온 파인드를 사용해서 풀었다.</p>
<h2 id="풀이-방법">풀이 방법</h2>
<h3 id="1-대표노드-배열-생성하기">1. 대표노드 배열 생성하기</h3>
<p>유니온 파인드는 대표노드 배열이 필요하다. 따라서 대표노드 배열을 <code>N+1</code> 길이로 생성해야한다.</p>
<h3 id="2-유니온-연산과-파인드-연산-구현하기">2. 유니온 연산과 파인드 연산 구현하기</h3>
<p>유니온 파인드 문제는 사실 유니온 연산과 파인드 연산을 구현하면 80프로는 끝난다. 따라서 유니온 연산과 파인드 연산을 구현해준다.</p>
<h3 id="3-첫-노드와-연결되어-있지-않은-노드를-찾아-출력하기">3. 첫 노드와 연결되어 있지 않은 노드를 찾아 출력하기</h3>
<p>이 문제에서는 다리가 1개만 잘렸고, 어떤 노드끼리 연결할지는 아무거나 출력하라고 했으므로 기준을 첫번째 노드로 잡는다. 왜냐하면 결국 어딘가는 연결되어있지 않기 때문에 1번노드가 포함된 그룹과 1번노드가 포함되지 않은 그룹으로 나뉠 것이기 때문이다. 따라서 1번노드와 대표노드가 같지 않은 노드를 찾으면 해당 노드를 출력하고 종료한다.</p>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.StringTokenizer;

public class p17352 {
    static int [] arr;
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine());
        StringBuilder sb = new StringBuilder();

        int N = Integer.parseInt(st.nextToken()); // 섬의 개수
        arr = new int[N+1];

        for(int i=1; i&lt;N+1; i++) arr[i] = i;

        for(int i=0; i&lt;N-2; i++){
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            union(a, b);
        }

        int parent = find(1);
        sb.append(1).append(&quot; &quot;);
        for(int i=1; i&lt;N+1; i++){
            if(find(i)!= parent){
                sb.append(i).append(&quot; &quot;);
                break;
            }
        }

        for(int i= 1; i&lt;N+1; i++){
            arr[i] = find(i);
        }

        bw.write(sb.toString());
        bw.flush();
        bw.close();
    }

    static void union(int a, int b){
        int aa = find(a);
        arr[aa] = find(b);
    }

    static int find(int num){
        if(arr[num]  == num) return num;
        return arr[num] = find(arr[num]);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 11265번 끝나지 않는 파티]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-11265%EB%B2%88-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EB%8A%94-%ED%8C%8C%ED%8B%B0</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-11265%EB%B2%88-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EB%8A%94-%ED%8C%8C%ED%8B%B0</guid>
            <pubDate>Wed, 14 Aug 2024 06:35:23 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/11265">백준 11265번 끝나지 않는 파티 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/20e154b9-7b5f-429f-9940-710a93ae8873/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/3ee45af1-f254-474b-917d-8b4bbd0e8675/image.png" alt=""></p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<h3 id="처음-접근-방식">처음 접근 방식</h3>
<p>처음에는 BFS로 접근했었다. 그래프 문제인건 확실했고, 최단거리를 통해서 갈 수 있는지 여부를 확인해야했기 때문이다.
아래가 내가 처음에 작성한 코드이다.</p>
<pre><code class="language-java">package algo;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.StringTokenizer;

public class p11265 {
    static List&lt;Node&gt; [] arr;
    static int N = 0;
    static long cnt = 0;
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st;
        StringBuilder sb = new StringBuilder();


        //1 입력 받기

        // 1-1. N과 M 입력 받기
        st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());     // 파티장의 개수(1~)
        int M = Integer.parseInt(st.nextToken());    // 사람 수 (1~)

        // 1-2. 진입 배열 만들기
        arr = new List[N+1];

        //초기화
        for(int i=1; i&lt;N+1; i++) arr[i] = new ArrayList&lt;&gt;();

        // 진입 배열로 만들기
        for(int t = 1; t&lt;=N; t++) {
            st = new StringTokenizer(br.readLine());
            for(int j=1; j&lt;N+1; j++) {
                int now = Integer.parseInt(st.nextToken());
                if(now!=0) {
                    arr[j].add(new Node(now, t));                    
                }
            }
        }

        // 손님들 배열 만들기
        Person [] p = new Person[M+1];
        for(int i = 1; i&lt;=M; i++) {
            st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            int time = Integer.parseInt(st.nextToken());
            p[i] = new Person(start, end, time);
        }

        //손님들 배열 순회
        for(int i=1; i&lt;M+1; i++) {
            if(calculate(p[i], new boolean[N+1], p[i].time) &lt;= p[i].time) {
                sb.append(&quot;Enjoy other party\n&quot;);
            }else {
                sb.append(&quot;Stay here\n&quot;);
            }
        }
        bw.write(sb.toString());
        bw.flush();
        bw.close();
    }


// 걸리는 시간을 계산하는 메서드
static long calculate(Person p, boolean[] visited, long maxTime) {
    int start = p.start; // 목적지
    int end = p.end; // 현재 위치
    long time = 0; // 걸리는 시간

    //end -&gt; start로 가야함
    Queue&lt;Person&gt; q = new LinkedList&lt;&gt;();

    for(Node n : arr[end]) {
        q.add(new Person(start, n.from, time + n.weight));
    }

    long min = Long.MAX_VALUE;

    while(!q.isEmpty()) {
        Person poll = q.poll();
        visited[poll.end] = true;

        // 만약 도착했으면
        if(poll.start == poll.end) {
            // min보다 작으면 업데이트
            if(min &gt; poll.time) min = poll.time;
        }else { //도착하지 않았으면
            for(Node n : arr[end]) { //자기 자신으로 진입가능한 것들 중에서
                if(!visited[n.from] &amp;&amp; maxTime &gt;= (poll.time + n.weight)) {
                    q.add(new Person(start, n.from, poll.time + n.weight));
                }
            }
        }
    }
    return min;
    }
}
class Node{
    int weight;
    int from;

    public Node(int weight, int next) {
        this.weight = weight;
        this.from = next;
    }
}

class Person{
    int start;
    int end;
    long time;

    public Person(int start, int end, long time) {
        this.start = start;
        this.end = end;
        this.time = time;
    }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/cd3b97b2-1f46-4bf3-87ae-03989c02275b/image.png" alt="">
그런데 시간초과가 났다. 
위 코드를 보면 <code>calculate()</code>에서 아래 코드 부분이 <code>O(N * N)</code>이다. </p>
<pre><code class="language-java">while(!q.isEmpty()) {
        Person poll = q.poll();
        visited[poll.end] = true;

        // 만약 도착했으면
        if(poll.start == poll.end) {
            // min보다 작으면 업데이트
            if(min &gt; poll.time) min = poll.time;
        }else { //도착하지 않았으면
            for(Node n : arr[end]) { //자기 자신으로 진입가능한 것들 중에서
                if(!visited[n.from] &amp;&amp; maxTime &gt;= (poll.time + n.weight)) {
                    q.add(new Person(start, n.from, poll.time + n.weight));
                }
            }
        }
    }</code></pre>
<blockquote>
<p>여기서는 <code>N</code>이 500이다. 그런데 <code>calculate()</code>를 <code>M</code>만큼 반복하므로 <code>O(500 * 500 * 100,000)</code> 이다. 따라서 <code>200,000,000</code>을 넘으므로 시간초과가 나는 것이다.</p>
</blockquote>
<h3 id="최종-해결-방안">최종 해결 방안</h3>
<p>결국 나는 <strong>플로이드 워셜 알고리즘</strong>을 사용해야한다는 것을 알았다. <code>O(V^3)</code>이긴 하지만 <code>V</code>가 <code>500</code>이므로 충분히 가능하다. 따라서 플로이드 워셜 알고리즘을 사용하여 해결했다.</p>
<h3 id="1-입력을-받아-2차원-거리-배열-만들기">1. 입력을 받아 2차원 거리 배열 만들기</h3>
<p>플로이드 워셜은 2차원 배열을 선언해야한다. 이후 행과 열을 각각 출발 노드와 목적지 노드로 생각하며 문제를 해결한다. 따라서 2차원 배열을 선언하였다.</p>
<p>또한 각 배열의 초기값은 -1로 초기화하고(일반적으로 무한대로 하지만 여기서는 음수 값이 주어지지 않으므로 편의상 -1로 했다), 행과 열이 같으면 자기 자신 노드로 가는 경우이므로 0으로 초기화했다.</p>
<p>이후 2차원 배열에 거리 입력 값을 받았다.</p>
<pre><code class="language-java">//1 입력 받기

        // 1-1. N과 M 입력 받기
        st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());     // 파티장의 개수(1~)
        int M = Integer.parseInt(st.nextToken());    // 사람 수 (1~)

        long [][] arr = new long [N+1][N+1];

        // 자기 자신은 0으로 초기화하고 나머지는 -1로 초기화
        for(int i=1; i&lt;=N; i++) {
            for(int j=1; j&lt;=N; j++) {
                if(i==j) arr[i][j] = 0;
                else arr[i][j] = -1;
            }
        }

        // 각 거리 입력 받기
        for(int i=1; i&lt;N+1; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j=1; j&lt;N+1; j++) {
                arr[i][j] = Long.parseLong(st.nextToken());
            }
        }</code></pre>
<h3 id="2-플로이드-워셜을-사용하여-거리-배열-업데이트-하기">2. 플로이드 워셜을 사용하여 거리 배열 업데이트 하기</h3>
<p>플로이드 워셜은 아래와 같은 3중 for문으로 이루어진다.</p>
<pre><code class="language-java">for(1 ~ N) { //K에 대해서 (중간 노드)
    for(1 ~ N){ // S에 대해서(출발 노드)
        for(1 ~ N){ // E에 대해서(목적지 노드)
        }
    }
}</code></pre>
<p>따라서 위 구조를 따라서 아래 코드처럼 거리 배열을 업데이트하였다.</p>
<pre><code class="language-java">//플로이드워셜로 거리 배열 업데이트
        for(int k=1; k&lt;=N; k++) {    // K
            for(int s=1; s&lt;=N; s++) {    // S
                for(int e=1; e&lt;=N; e++) {    // E
                    arr[s][e] = Math.min(arr[s][e], arr[s][k] + arr[k][e]);
                }                
            }
        }</code></pre>
<h3 id="3-각-손님에-대해-거리-배열을-참조하여-파티-참석-여부-출력하기">3. 각 손님에 대해 거리 배열을 참조하여 파티 참석 여부 출력하기</h3>
<p>플로이드 워셜 알고리즘을 통해서 거리배열이 완성되었으므로 이제 각 손님에 대해서 해당 파티가 참석 가능한지 출력하는 코드를 작성했다.</p>
<pre><code class="language-java">//손님들에 따라 가능한지 여부 출력하기
        for(int i=0; i&lt;M; i++) {
            st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            long limitTime = Long.parseLong(st.nextToken());

            if(limitTime &gt;= arr[start][end]) { //시간 안에 가는 경우
                sb.append(&quot;Enjoy other party\n&quot;);
            }else { //시간 안에 못가는 경우
                sb.append(&quot;Stay here\n&quot;);
            }
        }</code></pre>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">package algo;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.StringTokenizer;

public class p11265_2 {
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st;
        StringBuilder sb = new StringBuilder();


        //1 입력 받기

        // 1-1. N과 M 입력 받기
        st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());     // 파티장의 개수(1~)
        int M = Integer.parseInt(st.nextToken());    // 사람 수 (1~)

        long [][] arr = new long [N+1][N+1];

        // 자기 자신은 0으로 초기화하고 나머지는 -1로 초기화
        for(int i=1; i&lt;=N; i++) {
            for(int j=1; j&lt;=N; j++) {
                if(i==j) arr[i][j] = 0;
                else arr[i][j] = -1;
            }
        }

        // 각 거리 입력 받기
        for(int i=1; i&lt;N+1; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j=1; j&lt;N+1; j++) {
                arr[i][j] = Long.parseLong(st.nextToken());
            }
        }

        //플로이드워셜로 거리 배열 업데이트
        for(int k=1; k&lt;=N; k++) {    // K
            for(int s=1; s&lt;=N; s++) {    // S
                for(int e=1; e&lt;=N; e++) {    // E
                    arr[s][e] = Math.min(arr[s][e], arr[s][k] + arr[k][e]);
                }                
            }
        }

        //손님들에 따라 가능한지 여부 출력하기
        for(int i=0; i&lt;M; i++) {
            st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            long limitTime = Long.parseLong(st.nextToken());

            if(limitTime &gt;= arr[start][end]) { //시간 안에 가는 경우
                sb.append(&quot;Enjoy other party\n&quot;);
            }else { //시간 안에 못가는 경우
                sb.append(&quot;Stay here\n&quot;);
            }
        }

        bw.write(sb.toString());
        bw.flush();
        bw.close();
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 19951번 태상이의 훈련소 생활]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-19951%EB%B2%88-%ED%83%9C%EC%83%81%EC%9D%B4%EC%9D%98-%ED%9B%88%EB%A0%A8%EC%86%8C-%EC%83%9D%ED%99%9C</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-19951%EB%B2%88-%ED%83%9C%EC%83%81%EC%9D%B4%EC%9D%98-%ED%9B%88%EB%A0%A8%EC%86%8C-%EC%83%9D%ED%99%9C</guid>
            <pubDate>Mon, 12 Aug 2024 12:38:08 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/19951">백준 19951번 태상이의 훈련소 생활 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/9f228231-cc8f-48d0-8ca5-3e98617052e4/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/0d65517c-ef10-40f1-82a2-8505c962b8cf/image.png" alt=""></p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<p>처음엔 문제를 쉽게 생각해서 1차원 배열에 연병장 높이들을 넣은 뒤 각 작업마다 연산을 해주려고 했다. 그런데 문제 조건을 보면 <code>N</code>과 <code>M</code>이 최대 <code>100,000</code>이기 때문에 1초를 초과한다.
M은 작업의 수라서 무조건 반복해야하기 때문에 N을 줄이는 방식을 택해야한다.
바로 누적합이다.</p>
<h3 id="1-연병장-배열과-누적합-배열-입력-받기">1. 연병장 배열과 누적합 배열 입력 받기</h3>
<p>먼저 연병장 높이들을 받기 위해 1차원 배열을 생성하여 연병장 높이들을 받아준다. 이후 누적합 배열에 시작과 끝을 입력 받는다.
이 때 주의해야하는 점이 우리는 누적합을 추후에 계산할 것이기 때문에 예를 들어 1 ~ 5까지 4만큼 더하기 위해서는 누적합 배열의 1번 인덱스에 4를 더해주어야한다. 또한 6번 인덱스에 4를 빼주어야한다. 누적합 배열을 순회하면서 이전 인덱스의 값을 현재 인덱스의 값에 더해줄 것이기 때문에 이런 방식으로 값을 주어야한다.</p>
<h3 id="2-누적합-적용하기">2. 누적합 적용하기</h3>
<p>누적합 배열은 1번에서 완성되었으므로 누적합 배열을 돌면서 이전 인덱스의 값을 현재 인덱스에 더해준다.</p>
<h3 id="3-출력하기">3. 출력하기</h3>
<p>마지막으로 누적합 배열에서 각 인덱스에 해당하는 값들을 공백으로 나누어 출력하면 된다.</p>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.StringTokenizer;

public class p19951 {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);
        StringBuilder sb = new StringBuilder();

        int N = Integer.parseInt(st.nextToken()); // 연병장 길이
        int M = Integer.parseInt(st.nextToken()); // 작업 개수

        int[] arr = new int[N+1]; // 연병장 배열
        int[] sum = new int[N+1]; // 누적합 배열

        st = new StringTokenizer(br.readLine());

        // 연병장 입력 받기
        for(int i=1; i&lt;N+1; i++){
            arr[i] = Integer.parseInt(st.nextToken());
        }
        // 작업 입력 받기
        for(int i=0; i&lt;M; i++) {
            st = new StringTokenizer(br.readLine());

            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            int weight = Integer.parseInt(st.nextToken());
            sum[start] += weight;
            if (end + 1 &lt;= N) {
                sum[end + 1] -= weight;
            }
        }

        // 누적합 적용하기
        for(int i=1; i&lt;=N; i++) {
            sum[i] += sum[i-1];
            sb.append((arr[i] + sum[i])).append(&quot; &quot;);
        }


        bw.write(sb.toString().trim()); // 마지막 공백 제거
        bw.flush();
        bw.close();
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 2225번 합분해]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-2225%EB%B2%88-%ED%95%A9%EB%B6%84%ED%95%B4</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-2225%EB%B2%88-%ED%95%A9%EB%B6%84%ED%95%B4</guid>
            <pubDate>Sat, 10 Aug 2024 09:17:02 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/2225">백준 2225번 합분해 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/d966fb6f-1fd6-4c72-9b1c-d8a0a61d212f/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/8a2b94e8-b49a-41d8-ad9f-d240a5a37a0f/image.png" alt=""></p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<h3 id="1-패턴-파악하기">1. 패턴 파악하기</h3>
<p>이 문제는 잘 보면 패턴을 파악할 수 있다. <code>b</code>개의 숫자를 더해서 <code>a</code>를 만드는 방식은 <code>b-1</code>개의 숫자로 <code>c</code>를 만들어 놓고 <code>c</code>와 마지막 숫자 <code>d</code>를 더하면 된다.
즉 마지막 숫자를 <code>0 ~ a</code> 중에서 하나로 잡고 <code>b-1</code>개의 숫자로 <code>b-a</code>를 만드는 방법들을 다 더해주면 된다.
전에 계산한 값들을 통해 다음 값을 계산할 수 있으므로 DP문제에 해당한다.</p>
<h3 id="2-dp-배열-만들고-채우기">2. DP 배열 만들고 채우기</h3>
<p>2차원 DP 배열을 만든다. </p>
<pre><code class="language-java">int [][] arr = new int[num+1][target+1]; // DP 배열 arr[i][j] -&gt; i를 개를 더해서 j를 만들기</code></pre>
<p>나는 위와 같이 선언해주었다. <code>i</code>는 몇개를 더할지를 의미하고 <code>j</code>는 더해서 몇을 만들지를 의미한다.</p>
<p>배열을 만든 뒤 배열을 채워주어야한다.</p>
<h4 id="2-1-1개로-만드는-경우를-1로-초기화">2-1. 1개로 만드는 경우를 1로 초기화</h4>
<p>몇을 만들든 1개로 만드는 경우는 자기 자신을 더하는 경우밖에 없으므로 1로 초기화한다.</p>
<pre><code class="language-java">//1개로 만드는 경우
for(int i=0 ;i&lt;=target; i++){
    arr[1][i] = 1; //1으로 초기화
}</code></pre>
<h4 id="2-2-전의-계산한-값을-이용하도록-코드-작성">2-2. 전의 계산한 값을 이용하도록 코드 작성</h4>
<p>설명보다는 아래 코드를 보는 것이 도움이 될 것이다.
<code>i</code>는 몇개를 더할지를 의미하고 <code>j</code>는 무엇을 만들지를 의미한다. <code>k</code>는 마지막 수를 의미한다.
따라서 <code>i</code>는 1개부터 더할 수 있으므로 1부터 시작해서 주어진 최대 개수까지 반복하고, <code>j</code>는 0부터 <code>target</code>까지 만드는 경우를 모두 계산하기 위해 0부터 <code>target</code>까지 반복했다.
<code>k</code>는 마지막 수이므로 0부터 최대 현재 더할 수 있는 최대값인 <code>j</code>까지의 값을 가질 수 있으므로 0부터 <code>j</code>까지 반복했다.</p>
<pre><code class="language-java">// DP 적용
        for(int i=1; i&lt;=num; i++){ // i개를 더해서
            for(int j=0; j&lt;=target; j++){ // j를 만듦
                // arr[i][j] = arr[i-1]들 다 더하기
                for(int k = 0; k&lt;=j; k++){
                    arr[i][j] = (arr[i][j] +  arr[i-1][j-k]) % 1_000_000_000;
                }
            }
        }</code></pre>
<h3 id="3-답-출력하기">3. 답 출력하기</h3>
<p>이제 DP 배열이 완성되었으므로 우리가 구하려는 값인 <code>arr[num][target]</code>을 출력해주면 된다. 이는 <code>num</code>개를 더해 <code>target</code>을 만드는 방법의 개수이다.</p>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.*;
import java.util.StringTokenizer;

public class p2225 {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw  = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st= new StringTokenizer(br.readLine());
        int target = Integer.parseInt(st.nextToken()); // 만들 수
        int num = Integer.parseInt(st.nextToken()); // 더할 개수
        int [][] arr = new int[num+1][target+1]; // DP 배열 arr[i][j] -&gt; i를 개를 더해서 j를 만들기

        //1개로 만드는 경우
        for(int i=0 ;i&lt;=target; i++){
            arr[1][i] = 1; //1으로 초기화
        }

        // DP 적용
        for(int i=1; i&lt;=num; i++){ // i개를 더해서
            for(int j=0; j&lt;=target; j++){ // j를 만듦
                // arr[i][j] = arr[i-1]들 다 더하기
                for(int k = 0; k&lt;=j; k++){
                    arr[i][j] = (arr[i][j] +  arr[i-1][j-k]) % 1_000_000_000;
                }
            }
        }

        bw.write(arr[num][target]+&quot;&quot;);
        bw.flush();
        bw.close();
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1213번 팰린드롬 만들기]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-1213%EB%B2%88-%ED%8C%B0%EB%A6%B0%EB%93%9C%EB%A1%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-1213%EB%B2%88-%ED%8C%B0%EB%A6%B0%EB%93%9C%EB%A1%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 10 Aug 2024 06:01:59 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/1213">백준 1213번 팰린드롬 만들기 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/f196cdba-fc78-468f-a5c7-7d6d9aee45b5/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/b8a9c821-66e4-46e2-926c-0f37f6fe875d/image.png" alt=""></p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<p>백준을 풀다보면 팰린드롬 문제를 많이 만난다. 팰린드롬이란 뒤집어도 같은 문자열을 말한다. 따라서 홀수개인 알파벳이 0개이거나 1개만 있어야한다. 이것만 알고 있다면 이번 문제는 어렵지 않아 쉽게 풀 수 있다.</p>
<p>전반적인 흐름은 입력받은 알파벳들을 <code>출현 횟수 / 2</code> 만큼 A부터 더하고 Z까지 완료하면 만든 문자열을 뒤집어서 변수에 저장한 뒤
홀수인 알파벳이 1개이면 <code>현재 문자열 + 홀수인 알파벳 + 뒤집은 문자열</code>을 출력하고
홀수인 알파벳이 없으면 <code>현재 문자열 + 뒤집은 문자열</code>을 출력한다.</p>
<h3 id="1-알파벳을-배열로-입력-받기">1. 알파벳을 배열로 입력 받기</h3>
<p>대문자 알파벳만 주어지는데 대문자 <code>A</code>는 아스키코드로 65이고 <code>Z</code>는 90 이다. 따라서 26개짜리 int 배열을 선언하고 각 인덱스를 A부터 Z라고 생각하고 각 값은 해당 알파벳의 출현 숫자로 생각하면 된다. </p>
<blockquote>
<p><strong>따라서 배열을 입력 받을 때 문자열의 각 요소를 char로 바꾼 뒤 65를 뺀 인덱스 값을 +1 하도록 했다.</strong></p>
</blockquote>
<br>

<h3 id="2-출현-횟수가-홀수인-알파벳의-개수-세기-및-홀수일-경우-해당-알파벳-기록해두기">2. 출현 횟수가 홀수인 알파벳의 개수 세기 및 홀수일 경우 해당 알파벳 기록해두기</h3>
<p>홀수인 알파벳은 1개까지는 허용된다. 따라서 홀수인 알파벳이 생겼을 경우 기록해두고 int 변수를 잡아서 홀수인 알파벳의 개수를 세야한다. 만약 홀수인 알파벳이 1개를 초과하면 <code>I&#39;m Sorry Hansoo</code>를 출력하고 종료한다.
홀수인 알파벳이 1개만 있다면 두가지 경우로 나뉜다.</p>
<h4 id="1-해당-알파벳의-출현-횟수가-1인-경우">1. 해당 알파벳의 출현 횟수가 1인 경우</h4>
<p>만약 출현횟수가 1인 경우 해당 알파벳이 어떤 알파벳인지 변수에 저장만 해둔다.</p>
<h4 id="2-해당-알파벳의-출현-횟수가-1을-초과하는-홀수일-경우">2. 해당 알파벳의 출현 횟수가 1을 초과하는 홀수일 경우</h4>
<p>만약 출현횟수가 3 이상인 홀수라면 1개를 빼고는 현재 만들고 있는 문자열에 더해주어야한다. 예를 들어 5번 출현했으면 1개를 뺸 4개에서 2를 나는 2개를 더해주어야한다. 4개를 안더하고 2개만 더하는 이유는 어차피 뒤집은 문자열을 나중에 더할 것이기 때문에 <code>지금 더한 횟수 * 2</code>만큼 최종 문자열에 출현하기 때문이다.</p>
<br>


<h3 id="3-출현-횟수가-짝수인-알파벳의-경우-출현횟수--2-만큼-더하기">3. 출현 횟수가 짝수인 알파벳의 경우 출현횟수 / 2 만큼 더하기</h3>
<p>출현 횟수가 짝수이면 <code>출현횟수 / 2 만큼</code> 현재 만들고 있는 문자열에 더해주면 된다.</p>
<br>


<h3 id="4-z까지-완료한-뒤-현재-만든-문자열을-뒤집어서-변수에-저장해두기">4. Z까지 완료한 뒤 현재 만든 문자열을 뒤집어서 변수에 저장해두기</h3>
<p>A ~ Z까지 완료한 뒤 팰린드롬을 만들기 위해 현재까지 만든 문자열을 뒤집고 변수에 저장해두어야한다. 나중에 더해주어야하기 때문이다.</p>
<br>


<h3 id="5-출현-횟수가-홀수인-알파벳이-있으면-현재-문자열에-더해주기">5. 출현 횟수가 홀수인 알파벳이 있으면 현재 문자열에 더해주기</h3>
<p>만약 출현 횟수가 홀수인 알파벳이 있으면 2번에서 1개를 빼고는 더해주었으므로 홀수인 알파벳 1개를 현재 문자열에 더해준다.</p>
<br>


<h3 id="6-현재까지-만든-문자열에-아까-저장한-뒤집은-문자열을-더한-뒤-출력하기">6. 현재까지 만든 문자열에 아까 저장한 뒤집은 문자열을 더한 뒤 출력하기</h3>
<p>현재까지 만든 문자열에 아까 저장한 뒤집은 문자열을 더한 뒤 출력하면 정답이 나온다.</p>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.*;

public class p1213 {
    public static void main(String[] args) throws IOException {
        int [] alphabets = new int[26]; // idx 0은 65인 A부터 시작
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        // 입력 받기
        String line = br.readLine();
        String[] inputs = line.split(&quot;&quot;);
        for (String s : inputs){
            char c = s.charAt(0);
            int idx = c-65;
            alphabets[idx] ++;
        }
        int noPair = 0; // 홀수인 것의 개수
        char noPairAlphabet = 0;
        for(int i=0; i&lt;26; i++){
            int alphabetCnt = alphabets[i];
            char alpha = (char) (i+65);
            if(alphabetCnt % 2 != 0){
                if(alphabetCnt / 2 &gt; 0){ //홀수이지만 1개는 아닌 경우
                    for(int j=0; j&lt;alphabetCnt / 2; j++){
                        sb.append(alpha);
                    }
                }
                noPairAlphabet = (char) (i+65);
                noPair++;
                if(noPair &gt;1){ // 홀수개인 알파벳이 1개를 초과하는 경우
                    bw.write(&quot;I&#39;m Sorry Hansoo&quot;);
                    bw.flush();
                    bw.close();
                    return;
                }
            }else {
                // 짝수개인 경우
                for(int j=0; j&lt;alphabetCnt / 2; j++){
                    sb.append(alpha);
                }
            }
        }

        //현재까지의 sb 뒤집기
        String reverse = sb.reverse().toString();
        // 다시 원래대로 뒤집기
        sb.reverse().toString();
        // 만약 홀수개인 것이 1개 남았으면
        if(noPair == 1){
            sb.append(new String(String.valueOf(noPairAlphabet)));
        }
        sb.append(reverse);

        bw.write(sb.toString());
        bw.flush();
        bw.close();
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 21608 상어 초등학교]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-21608-%EC%83%81%EC%96%B4-%EC%B4%88%EB%93%B1%ED%95%99%EA%B5%90</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-21608-%EC%83%81%EC%96%B4-%EC%B4%88%EB%93%B1%ED%95%99%EA%B5%90</guid>
            <pubDate>Thu, 08 Aug 2024 11:34:21 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/21608">백준 21608번 상어 초등학교 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/e968f901-d09f-4ad3-8653-59ef18f39302/image.png" alt=""></p>
<p>오늘은 문제가 길어서 입출력만 올렸다.</p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<p>문제를 보면 입력값의 범위가 최대 20이므로 굉장히 작은 것을 알 수 있다. 따라서 매번 완전탐색을 해도 제한시간을 넘지 않는다는 것을 생각했다.</p>
<h3 id="1-좋아하는-사람들을-리스트-배열로-저장">1. 좋아하는 사람들을 리스트 배열로 저장</h3>
<p>먼저 문제를 보자마자 든 생각은 방향이 있는 그래프라고 생각했다. 따라서 인접리스트 배열처럼 각 사람의 인덱스에 있는 리스트에 그 사람이 좋아하는 사람들을 추가하였다.</p>
<h3 id="2-자리를-바꿀-순서의-사람마다-교실을-한칸식-검사-후-우선순위-큐에-사용하기">2. 자리를 바꿀 순서의 사람마다 교실을 한칸식 검사 후 우선순위 큐에 사용하기</h3>
<p>자리를 순서의 사람마다 2차원 배열 교실을 검사한다. 1칸씩 잡고, 그 자리가 비어있으면 그 자리로부터 4방향 탐색을 통해 인접한 사람의 개수와 인접한 비어있는 자리의 개수를 구한다. 이후 이 값들을 클래스로 만들어 우선순위 큐에 저장한다.</p>
<h3 id="3-우선순위-큐에서-우선순위-적용-후-우선순위-큐-비우기">3. 우선순위 큐에서 우선순위 적용 후 우선순위 큐 비우기</h3>
<p>각 사람마다 자리를 정해주기 위해서 우선순위 큐에서 우선순위가 가장 높은 자리를 하나 뽑는다. 이때 문제에서 제시한 우선순위가 적용되도록 우선순위큐를 <code>좋아하는 인접 개수 &gt; 비어있는 인접 개수 &gt; 행 &gt; 열</code> 우선순위로 비교하도록 한다.
또한 한 사람은 한자리밖에 못앉으므로 자리를 뽑고난 뒤 큐를 초기화한다(비운다).</p>
<h3 id="4-자리배치가-끝난-후-점수-다시-계산하기">4. 자리배치가 끝난 후 점수 다시 계산하기</h3>
<p>자리배치가 끝난 뒤 2차원 배열인 교실을 돌면서 4방향 탐색을 통해 점수를 계산한다. </p>
<blockquote>
<p>자리배치 시에 계산한 인접점수를 사용하면 되지 않겠냐고 생각할 수 있지만 내가 좋아하는 사람이 나보다 자리 정하는 순서가 낮으면서 나의 자리와 인접하게 앉을 수 있으므로 점수는 바뀔 수 있다. 
*<em>따라서 자리배치 후 다시 점수를 계산해주어야한다. *</em></p>
</blockquote>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class p21608 {
    static int total;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine());

        total = Integer.parseInt(st.nextToken()); // 가로 및 세로 줄
        int totalPerson = total * total; //전체 학생 수
        List&lt;Integer&gt; [] likes = new List[totalPerson+1]; // 좋아하는 학생 인접 리스트
        int [] seq = new int[totalPerson]; // 자리 지정 순서 배열
        int [][] classroom = new int[total][total]; // 교실 현재 자리 2차원 배열
        int result = 0; // 정답

        int [] dx = {-1, 0, 0, 1};
        int [] dy = {-0, -1, 1, 0};
        // A형 PQ  우선순위: adj &gt; vacant &gt; row &gt; col
        PriorityQueue&lt;A&gt; pq = new PriorityQueue&lt;&gt;((o1, o2) -&gt; {
            if(o1.adjNum != o2.adjNum) return o2.adjNum - o1.adjNum;
            if(o1.vacantNum != o2.vacantNum) return o2.vacantNum - o1.vacantNum;
            if(o1.row != o2.row) return o2.row - o1.row;
            return o2.col - o1.col;
        });

        // 좋아하는 학생 인접 리스트 초기화
        for(int i=0; i&lt;=totalPerson; i++){
            likes[i] = new ArrayList&lt;&gt;();
        }
        // 좋아하는 학생 인접 리스트 입력 받기
        for(int t = 0; t&lt;totalPerson; t++){
            st = new StringTokenizer(br.readLine());
            int now = Integer.parseInt(st.nextToken());
            seq[t] = now;
            for(int a = 0; a&lt;4; a++){
                likes[now].add(Integer.parseInt(st.nextToken()));
            }
        }
        for(int s = 0; s&lt;totalPerson; s++){ //모든 seq에 있는 사람들에 대해
            int p = seq[s]; //현재 자리를 정하는 사람
            //1. 교실에서 한칸씩 검사
            for(int i =0; i&lt;total; i++){
                for(int j=0; j&lt;total; j++){
                    int nowX = j;
                    int nowY = i;
                    if(classroom[nowY][nowX]!=0) continue;
                    List&lt;Integer&gt; likeFour = likes[p];
                    int vac = 0; // 비어있는 인접한 개수
                    int adj = 0; // 좋아하는 인접한 개수
                    for(int k=0; k&lt;4; k++){
                        int newX = j+dx[k];
                        int newY = i+dy[k];
                        if(check(newX, newY)){ //범위 안에 있으면
                            if(classroom[newY][newX] == 0) vac++; // 비어있으면
                            else if(likeFour.contains(classroom[newY][newX])) adj++; //좋아하는 인접한 사람이면
                        }
                    }
                    // 4방 탐색후 PQ에 넣기
                    pq.add(new A(adj, vac, nowY, nowX));
                }
            }
            A poll = pq.poll(); //앉을 사람
            classroom[poll.row][poll.col] = p; //착석
            pq.clear();
        }

        //정답 구하기
        for(int i=0; i&lt;total; i++){
            for(int j=0; j&lt;total; j++){
                int nowPerson = classroom[i][j];
                int cnt = 0; // 좋아하는 인접한 개수
                List&lt;Integer&gt; likeFour = likes[nowPerson];
                for(int k=0; k&lt;4; k++){
                    int newX = j+dx[k];
                    int newY = i+dy[k];
                    if(check(newX, newY)){ //범위 안에 있으면
                        if(likeFour.contains(classroom[newY][newX])) cnt++; //좋아하는 인접한 사람이면
                    }
                }
            result += calResult(cnt);
            }
        }
        bw.write(result+&quot;&quot;);
        bw.flush();
        bw.flush();
    }
    // 범위 내에 있는지 체크
    static boolean check(int x, int y){
        return x&gt;=0 &amp;&amp; x&lt;total &amp;&amp; y&gt;=0 &amp;&amp; y&lt;total;
    }
    static int calResult(int num){
        switch (num){
            case 0:
                return 0;
            case 1:
                return 1;
            case 2:
                return 10;
            case 3:
                return 100;
            default:
                return 1000;
        }
    }

}
class A{
    int adjNum; // 좋아하는 사람 인접 개수
    int vacantNum; // 비어있는 자리 인접 개수
    int row; // 행
    int col;// 열

    public A(int adjNum, int vacantNum, int row, int col) {
        this.adjNum = adjNum;
        this.vacantNum = vacantNum;
        this.row = row;
        this.col = col;
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 3164번 패턴]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-3164%EB%B2%88-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-3164%EB%B2%88-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Wed, 07 Aug 2024 08:54:40 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/3164">백준 3164번 패턴 문제 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/5571a058-7b4e-4db9-bf5e-c965c30e5573/image.png" alt="">
<img src="https://velog.velcdn.com/images/cobin_dev/post/7f4333eb-8e93-42cc-b9fc-db1f287d8beb/image.png" alt=""></p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<p>문제를 보고 바로 든 생각은 좌표 대신 사각형으로 생각해서 풀어야겠다는 것이었다. 실제로 회색 상자의 개수를 구하는 것이므로 그렇게 푸는게 편할 것 같기도 했고, 더 직관적일 것이라 생각했다.</p>
<h3 id="1-입력-받은-좌표들-변형좌표가-아닌-사각형으로-생각하기-위해">1. 입력 받은 좌표들 변형(좌표가 아닌 사각형으로 생각하기 위해)</h3>
<p>입력 받은 좌표들을 사각형 중 가장 왼쪽 아래에 포함된 크기 1짜리 사각형과 가장 오른쪽 위에 포함된 크기 1짜리 사각형의 인덱스로 바꾸어주었다. 인덱스로 생각하므로 첫번째 사각형은 0부터 시작한다고 생각하고 계산하였다.</p>
<h3 id="2-규칙-찾기">2. 규칙 찾기</h3>
<p>규칙을 보니 사각형으로 생각했을 때 <code>(i,i)</code> 사각형에서부터 사각형의 세로 최소 인덱스와 가로 최소 인덱스를 빼고 1을 더하면 된다는 생각을 했다. 따라서 가로와 세로를 따로 따로 구하도록 코드를 구현했다.</p>
<blockquote>
<p><strong>또한 중요한 것이 i와 세로(가로) 최대 인덱스 중 작은 것에서 빼야한다는 것이다.</strong> 만약 i가 세로 최대 인덱스보다 크다면 <code>세로 최대인덱스 - 세로 최소 인덱스 +1</code>을 해주어야한다는 말이다.</p>
</blockquote>
<h3 id="3-중복된-개수-빼기">3. 중복된 개수 빼기</h3>
<p>만약 <code>(i,i)</code>에서 개수를 구할 때 세로에서도 <code>(i,i)</code>를 포함하고 가로에서도 <code>(i,i)</code>를 포함하기 때문에 <code>세로 개수 &gt; 0</code> 이고 <code>가로 개수 &gt; 0</code> 이면 중복된 1개를 빼주어야한다.</p>
<p>사실 아이디어만 있다면 구현은 간단하므로 설명이 이해가 안된다면 어떤 식으로 푸는지 대략적으로 생각해보고 구현하면 된다.</p>
<h3 id="4-long으로-변수-잡기중요">4. long으로 변수 잡기(중요)</h3>
<blockquote>
<p>생각해보면 <code>0 0 1,000,000 1,000,000</code> 을 넣으면 <code>int</code>로 선언 시에 오버플로우가 발생한다. 따라서 <code>result</code> 변수는 <code>long</code>으로 선언해야한다. </p>
</blockquote>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">package Solution;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.StringTokenizer;

public class p3164_1 {
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine()); //한 줄 읽기

        int x1 = Integer.parseInt(st.nextToken()); // 주어지는 사각형을 이루는  사각형 중 제일 왼쪽 아래 1칸짜리 사각형의 x인덱스(0부터 시작)
        int y1 = Integer.parseInt(st.nextToken()); // 주어지는 사각형을 이루는  사각형 중 제일 왼쪽 아래 1칸짜리 사각형의 y인덱스(0부터 시작)
        int x2 = Integer.parseInt(st.nextToken()) -1; // 주어지는 사각형을 이루는  사각형 중 제일 오른쪽 위  1칸짜리 사각형의 x인덱스(0부터 시작)
        int y2 = Integer.parseInt(st.nextToken()) -1; // 주어지는 사각형을 이루는  사각형 중 제일 오른쪽 위  1칸짜리 사각형의 y인덱스(0부터 시작)

        int min = Math.min(x1, y1); //탐색을 위한 최소값
        int max = Math.max(x2, y2); // 탐색을 위한 최대값

        long result = 0; //최종 결과 long으로 해야함

        for(int i=min; i&lt;=max; i++) { //최소부터 최대 까지 탐색
            if(i % 2 == 1) { // 인덱스가 홀수번째이면(홀수 번째이면 색칠됨)
                int column = 0; // 세로
                int row = 0; // 가로

                // 세로 계산
                if(i&gt;= x1 &amp;&amp; i&lt;= x2 &amp;&amp; i &gt;= y1) { // 가로의 범위 안에 있고 세로의 최소범위보다 크면
                    int tmp = Math.min(i, y2);
                    if(tmp&gt;0) column = tmp - y1 +1;    // 양수개인 경우만 더하기
                }

                // 가로 계산
                if(i&gt;= y1 &amp;&amp; i&lt;= y2 &amp;&amp; i &gt;= x1) { // 세로의 범위 안에 있고 가로의 최소범위보다 크면
                    int tmp = Math.min(i, x2);
                    if(tmp&gt;0) row = tmp - x1 +1; // 양수개인 경우만 더하기        
                }

                //중복 제거
                if(row &gt;0 &amp;&amp; column&gt;0) result--; // 만약 가로에서도 계산하고 세로에서도 계산하면 두번 계산하므로 1개 빼주기
                // 결과 업데이트
                result = result + row + column;                            
            }
        }
        bw.write(result+&quot;&quot;);
        bw.flush();
        bw.close();
    }

}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Arrays.toString(배열) VS 배열.toString()]]></title>
            <link>https://velog.io/@cobin_dev/Arrays.toString%EB%B0%B0%EC%97%B4-VS-%EB%B0%B0%EC%97%B4.toString</link>
            <guid>https://velog.io/@cobin_dev/Arrays.toString%EB%B0%B0%EC%97%B4-VS-%EB%B0%B0%EC%97%B4.toString</guid>
            <pubDate>Wed, 24 Jul 2024 05:14:03 GMT</pubDate>
            <description><![CDATA[<p>[Movie.java]</p>
<pre><code class="language-java">public class Movie {
    private int id;
    private String title;
    private String director;
    private String genre;
    private int runningTime;

    public Movie() {
    }

    public Movie(int id, String title, String director, String genre, int runningTime) {
        super();
        this.id = id;
        this.title = title;
        this.director = director;
        this.genre = genre;
        this.runningTime = runningTime;
    }

    @Override
    public String toString() {
        return &quot;Movie [id=&quot; + id + &quot;, title=&quot; + title + &quot;, director=&quot; + director + &quot;, genre=&quot; + genre + &quot;, runningTime=&quot;
                + runningTime + &quot;]&quot;;
    }


}
</code></pre>
<p>[MovieTest.java]</p>
<pre><code class="language-java">public class MovieTest {
    public static void main(String[] args) {

        IMovieManager mg = MovieManagerImpl.getInstance();
        // 영화들 삽입
        mg.add(new Movie(1, &quot;제목1&quot;, &quot;감독1&quot;, &quot;장르1&quot;, 100));
        mg.add(new Movie(2, &quot;제목2&quot;, &quot;감독2&quot;, &quot;장르2&quot;, 200));
        mg.add(new Movie(3, &quot;제목3&quot;, &quot;감독3&quot;, &quot;장르3&quot;, 300));
        mg.add(new SeriesMovie(4, &quot;제목4&quot;, &quot;감독4&quot;, &quot;장르4&quot;, 400, 4, &quot;ep4&quot;));
        mg.add(new SeriesMovie(5, &quot;제목5&quot;, &quot;감독5&quot;, &quot;장르5&quot;, 500, 5, &quot;ep5&quot;));



        // 리스트 가져오기
        Movie[] list = mg.getList();
        System.out.println(&quot;***********************getList()***********************&quot;);
        System.out.println(Arrays.toString(list));
        System.out.println(list.toString());
        System.out.println();

    }
}</code></pre>
<p>위 코드를 작성하다가 궁금한 점이 생겼다. 코드를 보면 <code>Movie</code>에서 <code>toString()</code> 메서드를 <code>override</code>하고 있다. </p>
<blockquote>
<p><strong>그렇다면</strong> <code>System.out.println(Arrays.toString(list));</code> 와 <code>System.out.println(list.toString());</code><strong>의 출력값은 같을까?</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/55441608-05e4-419e-b7d2-17e7a5bd3c37/image.png" alt=""></p>
<p>그렇지 않음을 볼 수 있다. 윗줄의 출력이 <code>System.out.println(Arrays.toString(list));</code>의 결과이고, 아랫줄의 출력이 <code>System.out.println(list.toString());</code>의 결과이다.</p>
<blockquote>
<p>둘다 <code>toString()</code>메서드를 호출했는데 왜 이런 차이점이 발생할까?</p>
</blockquote>
<hr>
<h3 id="arraystostringarr">Arrays.toString(arr)</h3>
<p><code>Arrays.toString(arr)</code>는 <code>java.util.Arrays</code> 클래스에 정의된 정적 메서드이다. 이 메서드는 배열의 각 요소에 대해 <code>toString()</code> 메서드를 호출하고, 이를 문자열로 변환하여 반환한다. 즉, *<em>배열의 모든 요소를 문자열로 변환하여 <code>[element1, element2, ...]</code> 형식의 문자열을 반환한다. *</em></p>
<p><strong>또한 <code>override</code>를 하였을 경우 <code>override</code>된 메서드가 호출된다.</strong></p>
<h3 id="arrtostring">arr.toString()</h3>
<p><code>arr.toString()</code>은 배열 객체 자체의 <code>toString()</code> 메서드를 호출하는 것이다. 자바에서 배열의 기본 <code>toString()</code> 메서드는 배열의 요소를 문자열로 변환하지 않고, 단순히 배열의 타입과 해시 코드를 포함하는 문자열을 반환한다. 이는 <code>Object</code> 클래스에서 상속된 기본 <code>toString()</code> 메서드를 사용하기 때문이다. </p>
<p><strong>자바에서 배열 객체의 자체의 <code>toString()</code>은 <code>override</code>할 수 없다.</strong> 배열은 자바의 기본 데이터 타입으로, 배열 클래스(<code>java.lang.Object</code>를 상속받은 배열 클래스)는 내부적으로 <code>JVM</code>에 의해 관리되기 때문이다.</p>
<p><strong>따라서 배열 객체 자체의 <code>toString()</code>을 출력하면 무조건 배열타입과 해시코드를 포함하는 문자열이 반환된다.</strong></p>
<p>결론적으로 내가 어떤 클래스에 <code>toString()</code>을 오버라이드 했고, 해당 클래스 배열을 오버라이드 한 메서드를 통해 출력하고 싶다면 <code>Arrays.toString()</code>을 사용해야한다.</p>
<h3 id="참고">참고</h3>
<ul>
<li><a href="https://www.geeksforgeeks.org/arrays-tostring-in-java-with-examples/">https://www.geeksforgeeks.org/arrays-tostring-in-java-with-examples/</a></li>
<li>자바의 정석 3판(남궁 성,2016)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[자바에서 래퍼클래스는 어떻게 저장될까]]></title>
            <link>https://velog.io/@cobin_dev/%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C-%EB%9E%98%ED%8D%BC%ED%81%B4%EB%9E%98%EC%8A%A4%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%80%EC%9E%A5%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@cobin_dev/%EC%9E%90%EB%B0%94%EC%97%90%EC%84%9C-%EB%9E%98%ED%8D%BC%ED%81%B4%EB%9E%98%EC%8A%A4%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%A0%80%EC%9E%A5%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Tue, 23 Jul 2024 01:17:56 GMT</pubDate>
            <description><![CDATA[<p>자바에서는 2가지 형태로 값을 저장한다.</p>
<h3 id="기본-자료형">기본 자료형</h3>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/8208b07d-2afd-4fe2-b099-9ec3ed53ecf8/image.png" alt="">
위 사진에서 볼 수 있는 10개의 기본 자료형은 값을 그대로 저장한다. 예를 들어 클래스 필드로 </p>
<pre><code class="language-java">int a = 20;</code></pre>
<p>이라는 값을 갖는다고 생각해보자. 그럼 아래 그림처럼 힙 영역에 <code>a</code>를 저장하고, <code>20</code>이라는 값 자체를 2진수로 바꾸어 저장한다.
<img src="https://velog.velcdn.com/images/cobin_dev/post/56ce5794-9efa-42c0-b26a-ab8886303ef0/image.png" alt=""></p>
<hr>
<h3 id="참조-자료형">참조 자료형</h3>
<pre><code class="language-java">int a = 20;
A b = new A();</code></pre>
<p>이번엔 위와 같은 코드가 작성되었다고 가정해보자. 아래에서 볼 수 있듯 <code>new</code>를 통해 인스턴스 <code>A</code>를 생성한 뒤 <code>A</code>의 주소값을 변수 <code>b</code>에 저장한다.</p>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/0a92318a-dab4-43e4-8eed-7984cf510e11/image.png" alt=""></p>
<p>기본자료형이 아닌 참조 자료형은 이렇게 주소값이 저장됨을 알 수 있다.</p>
<p>그렇다면 래퍼클래스(<code>Integer</code>, <code>Double</code>, <code>Byte</code>, <code>Character</code> 등)는 어떻게 저장될까?</p>
<hr>
<h3 id="래퍼클래스도-참조-자료형이다">래퍼클래스도 참조 자료형이다.</h3>
<p>결론부터 말하자면 참조자료형과 똑같이 저장된다. 기본 자료형 외에는 모두 참조 자료형이므로 위에서 본 참조자료형이 저장되는 방식으로 저장된다.</p>
<pre><code class="language-java">int a = 20; // 기본 자료형
Integer b = new Integer(20); // 래퍼클래스(참조자료형)
</code></pre>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/8fc2be0f-8b21-4a42-a692-c98a7fe21a19/image.png" alt=""></p>
<p>위에 그림에서 볼 수 있듯 <code>int</code>(기본 자료형)로 <code>20</code>을 선언하면 <code>20</code>에 해당하는 값을 <code>a</code>에 2진수로 저장한다.
반면 <code>Integer</code>(래퍼클래스)로 <code>20</code>을 선언하면 <code>20</code>에 해당하는 인스턴스를 생성한 뒤 그 인스턴스의 주소값을 <code>b</code>에 저장한다.</p>
<blockquote>
<p><strong>그럼 <code>int c = (int)b;</code>를 하면 어떻게 될까?</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/149a4056-892e-4cd5-9a2d-7f2ff497c54e/image.png" alt=""></p>
<p>위 그림처럼 힙에 <code>c</code>가 생성되고 <code>c</code>의 값은 주소값이 아닌 <code>b</code>가 가리키는 인스턴스의 값을 읽어서 <code>c</code>에 값으로 저장한다.</p>
<h2 id="참고">참고</h2>
<ul>
<li><a href="https://adjh54.tistory.com/119">https://adjh54.tistory.com/119</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 12904번 A와 B]]></title>
            <link>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-12904%EB%B2%88-A%EC%99%80-B</link>
            <guid>https://velog.io/@cobin_dev/%EB%B0%B1%EC%A4%80-12904%EB%B2%88-A%EC%99%80-B</guid>
            <pubDate>Thu, 18 Jul 2024 00:57:50 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/12904">백준 12904번 A와 B 문제 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/f614500d-2fe8-46c0-b708-b4695c2a873b/image.png" alt=""></p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<p>되게 간단한 문제라서 이게 왜 골드5지? 싶었다. 그런데 함정이 있었다. 바로 <strong>시간복잡도</strong>다.
<code>S -&gt; T</code>로 갈 때 두가지 경우가 있고 입력 조건에 따라서 <code>S</code>가 <code>1</code>이고 <code>T</code>가 <code>1000</code>일 경우 최대 <code>2^1000</code>의 시간복잡도를 가진다. 그래서 이렇게 풀면 당연히 틀린다.</p>
<h4 id="그럼-어떻게-풀어야할까">그럼 어떻게 풀어야할까?</h4>
<p>처음에는 <code>S -&gt; T</code>로 가는 과정 중 중간 종료 조건을 생성해야겠다는 생각을 했다. 그런데 <code>S -&gt; T</code> 과정에서 중간에 유망도를 판단하기는 어려웠다.</p>
<p>그래서 좀 더 고민하다가 결국 생각해냈다.</p>
<blockquote>
<p><strong><code>T -&gt; S</code>로 가는 역발상</strong></p>
</blockquote>
<p><code>T -&gt; S</code>로 가는 과정을 구현하면 된다. 얼핏보면 이것도 <code>2^1000</code>의 시간복잡도 아니야? 라고 할 수 있지만 <code>T -&gt; S</code>로 간다고 생각하면 두가지 경우 중 하나의 경우만 일어난다.</p>
<ol>
<li><p><code>T</code>의 끝자리가 <code>A</code>인 경우 : T의 끝자리에서 <code>A</code>를 뺀다.</p>
</li>
<li><p><code>T</code>의 끝자리가 <code>B</code>인 경우 : T의 끝자리에서 <code>B</code>를 뺀 후 문자열을 뒤집는다.</p>
</li>
</ol>
<p>T의 끝자리는 <code>A</code>또는 <code>B</code> 이므로 최대 시간복잡도는 <code>1000</code>이다. </p>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Stack;
import java.util.StringTokenizer;

public class Main {
    //12904번
    public static void main(String[] args) throws Exception {
        // 입력받기
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        Stack&lt;String&gt; stack = new Stack&lt;&gt;();
        String from = br.readLine();
        String to = br.readLine();
        int fromLength = from.length();
        int result = 0;

        stack.push(to);

        while(true) {
            String now = stack.pop();
            if(now.equals(from)) {
                result = 1;
                break;
            }
            else if(now.length()&gt;fromLength) {
                if(now.charAt(now.length()-1) == &#39;A&#39;) {
                    stack.push(now.substring(0, now.length()-1));
                }
                //리버스
                else {
                    stack.push(reverse(now.substring(0, now.length()-1)));
                }
            }else if(stack.size() ==0) break;
        }
        bw.write(result+&quot;\n&quot;);
        bw.flush();

    }
    static String reverse(String s) {
        String newS =&quot;&quot;;
        for(int i=s.length()-1; i&gt;=0; i--) {
            newS += s.charAt(i);
        }
        return newS;
    }

}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 11000번 강의실 배정]]></title>
            <link>https://velog.io/@cobin_dev/%EA%B0%95%EC%9D%98%EC%8B%A4-%EB%B0%B0%EC%A0%95-11000-%EC%9E%90%EB%B0%94</link>
            <guid>https://velog.io/@cobin_dev/%EA%B0%95%EC%9D%98%EC%8B%A4-%EB%B0%B0%EC%A0%95-11000-%EC%9E%90%EB%B0%94</guid>
            <pubDate>Wed, 17 Jul 2024 01:36:29 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><a href="https://www.acmicpc.net/problem/11000">백준 11000번 강의실 배정 문제 링크</a>
<img src="https://velog.velcdn.com/images/cobin_dev/post/d68d8532-5035-46d9-81d2-b0a953556f5a/image.png" alt=""></p>
<hr>
<h2 id="풀이-방법">풀이 방법</h2>
<p>위 문제를 보고 크게 2가지를 떠올릴 수 있었다. 바로 <strong>정렬</strong>과 <strong>우선순위 큐</strong>이다.</p>
<h3 id="1-입력값들을-배열에-저장">1. 입력값들을 배열에 저장</h3>
<p>먼저 입력값들을 배열에 저장해야한다. </p>
<h3 id="2-배열을-정렬">2. 배열을 정렬</h3>
<p>이후 저장한 배열을 정렬해야한다. 그런데 여기서 중요한 점이 있다. 강의 시작 시각을 기준으로 정렬해야할지, 강의 종료 시각을 기준으로 정렬해야할지다. 사실 나는 직관적으로 강의 종료 시각을 기준으로 정렬해서 틀렸었다...
결론적으로 강의 시작 시각을 기준으로 정렬해야한다.</p>
<blockquote>
<p><strong>강의 시작 시각을 기준으로 정렬해야하는 이유</strong>
우리가 우선순위 큐를 어떻게 사용할지 매커니즘을 그림으로 그려보면 명확해진다.
<br>
<strong>&lt;우선순위 큐를 사용하는 매커니즘&gt;</strong><br>
<strong>1)</strong> 우선순위 큐에 정렬된 첫번째 수업을 삽입<br>
<strong>2)</strong> 정렬된 두번째 수업의 시작시각과 우선순위 큐에서 가장 우선순위가 높은 수업의 종료시각을 비교<br>
<strong>2-1)</strong> 종료시각 &gt; 시작시각 -&gt; 우선순위 큐에 해당 수업을 삽입<br>
<strong>2-2)</strong> 종료시각 &lt;= 시작시각 -&gt; 우선순위 큐 요소를 뺀 뒤 해당 수업을 삽입<br>
<strong>3)</strong> 2번 과정을 모든 요소에 대해 반복
<br>
결국 우리는 <strong>2-1에서 정렬된 배열의 시작시간을 우선순위 큐의 우선 요소와 비교</strong>한다. 따라서 우리는 강의 종료시각이 아닌 강의 시작시간으로 배열을 정렬해야하는 것이다.</p>
</blockquote>
<p>따라서 배열을 강의 시작 시간을 기준으로 오름차순 정렬한다. 또한 만약 시작 시간이 같다면 종료시간으로 오름차순 정렬한다.</p>
<h3 id="3-우선순위-큐-사용">3. 우선순위 큐 사용</h3>
<p>위의 예시에서 우선순위 큐를 어떻게 사용해야하는지는 보았다. 따라서 우리는 우선순위 큐의 우선순위는 종료시각임을 알 수 있다.</p>
<p>따라서 우선순위 큐를 생성한 뒤 우선순위를 정할 때 종료 시각이 작은 값이 우선순위를 갖게한다.</p>
<h3 id="4-우선순위-큐에-남은-요소-개수-출력">4. 우선순위 큐에 남은 요소 개수 출력</h3>
<p>총 필요한 강의실의 개수는 우선순위 큐에 남은 요소 개수이다.
나는 처음에는 이게 이해가 안됐었다.</p>
<blockquote>
<p>실행 중간에 100개의 수업이 동시에 있어서 우선순위 큐의 크기가 100까지 커졌다가 실행이 종료되기 전 수업들이 끝나서 마지막에 큐 크기가 5개면 답이 틀리지 않을까?</p>
</blockquote>
<p>라는 의문이 있었기 때문이다.</p>
<p>하지만 이렇게 생각한다면 우리가 우선순위큐를 어떻게 사용할지 제대로 이해하지 못한 것이다. 우리는 우선순위 큐에서 요소를 뺄 때 종료 시각이 지났다고 무조건 빼는 것이 아니다. 종료 시각 이루에 시작하는 요소가 배열에 있을 때 우선순위 큐에서 요소를 뺀다. 
따라서 모든 수업이 종료될 때 큐에 남아있는 요소들의 개수가 정답이 된다.</p>
<p>만약 이 글을 읽고도 이해가 안된다면 실제 사례를 그림을 그려서 생각해보면 금방 이해할 수 있을 것이다.</p>
<hr>
<h2 id="정답-코드">정답 코드</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.StringTokenizer;

public class Main {
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(br.readLine());

        PriorityQueue&lt;Class&gt; pq = new PriorityQueue&lt;&gt;(
                (o1, o2) -&gt; {
                    // 시작시간 오름차순 정렬, 시작시간 같다면 종료시간 오름차순 정렬
                    if (o1.end == o2.end) {
                        return o1.start - o2.start;
                    } else {
                        return o1.end - o2.end;
                    }
                });
        int N = Integer.parseInt(st.nextToken());
        Class [] arr = new Class[N];
        // 입력 받기
        for(int i=0; i&lt;N; i++) {
            st = new StringTokenizer(br.readLine());
            int start = Integer.parseInt(st.nextToken());
            int end = Integer.parseInt(st.nextToken());
            arr[i] = new Class(start, end);
        }
        // 정렬하기
        Arrays.sort(arr);
        // 첫번째 요소 넣기
        pq.offer(arr[0]);

        for(int i=1; i&lt;N; i++) {
            Class now = pq.peek();
            int nowEnd = now.end;
            if(nowEnd &lt;= arr[i].start) pq.poll();
            pq.offer(arr[i]);
        }
        bw.write(pq.size()+&quot;\n&quot;);
        bw.flush();
    }
    static class Class implements Comparable&lt;Class&gt;{
        int start, end;
        public Class(int start, int end) {
            this.start = start;
            this.end = end;
        }
        public int compareTo(Class c) {
            if(this.start == c.start) return this.end - c.end;
            return this.start - c.start;
        }
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[EC2] NAT 루핑(헤어핀) 오류]]></title>
            <link>https://velog.io/@cobin_dev/EC2-NAT-%EB%A3%A8%ED%95%91-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@cobin_dev/EC2-NAT-%EB%A3%A8%ED%95%91-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Sun, 14 Jul 2024 13:19:22 GMT</pubDate>
            <description><![CDATA[<p>CI/CD를 구축하면서 계속 문제가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/1227423f-87ef-4e3b-9752-0c9797b4f394/image.png" alt=""></p>
<p>사...살려줘....</p>
<hr>
<h1 id="오류-상황">오류 상황</h1>
<h2 id="오류-내용">오류 내용</h2>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/06415c30-0e5c-46e2-8d84-8ac7d4412b93/image.png" alt=""></p>
<p>내 프로젝트는 간단하게 그리면 위 그림처럼 구성되어있다.
설명해보자면 하나의 EC2안에 MySQL 서버를 설치하여 3306포트로 구동 중이고, 도커 컨테이너 안에 스프링 서버가 있다. </p>
<p>그런데 스프링 서버를 실행하면 아래와 같이 DB에 연결할 수 없다는 오류가 계속 발생했다.</p>
<pre><code>Cannot invoke &quot;org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(java.sql.SQLException, String)&quot; because the return value of &quot;org.hibernate.resource.transaction.backend.jdbc.internal.JdbcIsolationDelegate.sqlExceptionHelper()&quot; is null</code></pre><p>*<em>이상한 점은 로컬 PC에서 EC2의 MYSQL에 연결하여 스프링 부트를 돌리는 것은 잘 돌아간다는 것이다 😓
*</em></p>
<h2 id="오류-코드">오류 코드</h2>
<p>이 상황에서 스프링 서버에서 application.yml을 통해 MySQL과 연결을 할 때 public IP 주소를 입력했더니 계속 오류가 발생했다.</p>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://{public IP 주소}:3306/{DB 이름}
    username: {유저이름}
    password: {비밀번호}
    driver-class-name: com.mysql.cj.jdbc.Driver</code></pre>
<hr>
<h1 id="예상-원인">예상 원인</h1>
<h2 id="인바운드-규칙-설정-및-방화벽-설정">인바운드 규칙 설정 및 방화벽 설정</h2>
<p>그런데 저번 <a href="https://velog.io/@cobin_dev/MySQL-Unable-to-connect-to-localhost-%EC%98%A4%EB%A5%98">게시물</a>에서 이미 인바운드 규칙과 방화벽 설정을 통해 MySQL Workbench로 서버에 접속하였다.</p>
<p>그래서 예상원인을 찾기 어려워서 하루동안 고민했다.</p>
<hr>
<h1 id="실제-해결-방법">실제 해결 방법</h1>
<h2 id="public-ip-주소가-아닌-private-ip-주소를-사용하기">public IP 주소가 아닌 private IP 주소를 사용하기</h2>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/9e3a0c66-c039-4992-85ee-5df21dc3007e/image.png" alt=""></p>
<p>AWS 콘솔에 접속하여 사진의 주황색 부분에 있는 프라이빗 ip 주소를 <code>application.yml</code>에 적어주면 된다.</p>
<pre><code class="language-yaml">spring:
  datasource:
    url: jdbc:mysql://{priavte IP 주소}:3306/{DB 이름}
    username: {유저이름}
    password: {비밀번호}
    driver-class-name: com.mysql.cj.jdbc.Driver</code></pre>
<p>의외로 간단한 해결방안이다.</p>
<h3 id="그런데-왜-이렇게-해야할까-왜-public-ip-주소를-사용하면-안될까">그런데 왜 이렇게 해야할까? 왜 public IP 주소를 사용하면 안될까??</h3>
<hr>
<h1 id="nat-루핑헤어핀-문제">NAT 루핑(헤어핀) 문제</h1>
<h2 id="nat란">NAT란</h2>
<p>NAT는 Network Address Translation의 약자로 내부 네트워크의 여러 장치가 하나의 공인 IP 주소를 공유하여 인터넷에 접속할 수 있도록 해주는 기술이다. 일반적으로 라우터에서 실행된다.</p>
<h2 id="nat-루핑헤어핀-문제란">NAT 루핑(헤어핀) 문제란</h2>
<blockquote>
<p><strong>NAT 루핑(헤어핀) 문제는 동일한 네트워크 내의 장치가 자신의 외부 IP 주소를 통해 통신하려고 할 때 발생하는 문제이다.</strong>
헤어핀 문제라고 불리는 이유는 네트워크 트래픽이 내부 네트워크를 떠나 외부로 나갔다가 다시 내부로 돌아오는 경로가 마치 머리핀(Hairpin)처럼 구부러져 있는 모양을 닮았기 때문이다.</p>
</blockquote>
<p>나의 상황으로 예를 들어보자.</p>
<h3 id="주어진-상황">주어진 상황</h3>
<blockquote>
<p>하나의 EC2 인스턴스가 있고, 인스턴스에는 MySQL 서버가 설치되어 있으며 Docker 컨테이너 안에서 Spring Boot 애플리케이션이 실행되고 있다.
인스턴스의 프라이빗 IP 주소는 10.0.0.1이고, 퍼블릭 IP 주소는 203.0.113.1이다.</p>
</blockquote>
<h3 id="통신-시도-상황">통신 시도 상황</h3>
<blockquote>
<p>Docker 컨테이너 안의 Spring Boot 애플리케이션이 MySQL 서버에 접속하려고 할 때, 퍼블릭 IP 주소 203.0.113.1을 사용하여 연결을 시도한다.</p>
</blockquote>
<h3 id="라우팅-과정">라우팅 과정</h3>
<blockquote>
<p><strong>1. Spring Boot 애플리케이션에서 203.0.113.1로 패킷을 보낸다.</strong>
<strong>2. 이 패킷은 NAT 라우터를 통해 외부로 나가야 하지만, 결국에는 동일한 인스턴스 내로 다시 들어오게 된다.</strong>
<strong>3. NAT 라우터는 이 패킷을 처리하는 과정에서 루핑(Loopback)이 발생하고, 이를 처리할 수 없는 네트워크 설정에서 연결이 실패한다.</strong></p>
</blockquote>
<h2 id="루핑헤어핀-문제의-원인">루핑(헤어핀) 문제의 원인</h2>
<h4 id="라우터의-제한">라우터의 제한</h4>
<p>대부분의 NAT 라우터는 내부 네트워크의 트래픽을 외부로 내보내고 다시 내부로 받아들이는 헤어핀 트래픽을 처리하는 기능을 갖추고 있지 않다. 이는 보안 상의 이유이기도 하며, 일반적인 네트워크 사용 패턴에서는 필요 없는 기능이기 때문이다.</p>
<h4 id="불필요한-트래픽-증가">불필요한 트래픽 증가</h4>
<p>동일한 네트워크 내의 통신이 외부 네트워크를 경유하게 되어 네트워크 성능이 저하될 수 있다.</p>
<h4 id="복잡한-네트워크-설정">복잡한 네트워크 설정</h4>
<p>헤어핀 NAT 문제를 해결하기 위해 추가적인 네트워크 설정이 필요할 수 있다.</p>
<h2 id="루칭헤어핀-문제의-해결방법">루칭(헤어핀) 문제의 해결방법</h2>
<h3 id="프라이빗-ip-사용">프라이빗 IP 사용</h3>
<p>내부 네트워크 내에서 통신할 때는 프라이빗 IP 주소를 사용하여 직접 통신하면 된다.</p>
<blockquote>
<p>간단하긴 한데 <em>&#39;Private IP와 Public IP가 뭐가 다르길래 이게 해결방안이지?&#39;</em> 싶을 수 있다.</p>
</blockquote>
<blockquote>
<p>프라이빗 IP를 사용하면 헤어핀 NAT 문제를 해결할 수 있는 이유는 내부 네트워크 내에서 직접 통신할 수 있기 때문이다. <strong>프라이빗 IP를 사용하는 방식은 NAT 라우터를 통하지 않으므로, 트래픽이 외부 네트워크로 나갔다가 다시 내부 네트워크로 돌아올 필요가 없다.</strong> 따라서 루핑 문제가 발생하지 않는 것이다.</p>
</blockquote>
<p>하루동안 삽질했지만, 그래도 생각지도 못한 문제를 해결했고, 네트워크개론에서 배웠던 NAT도 오랜만에 다시 공부할 수 있어서 좋았다.</p>
<h4 id="참고">참고</h4>
<ul>
<li><p><a href="https://en.wikipedia.org/wiki/Network_address_translation">Wikipedia: Network Address Translation</a></p>
</li>
<li><p><a href="https://blog.minny.i234.me/?p=1182">쉽게 이해하는 NAT (공유기) 원리로 NAT 루프백(헤어핀 NAT)을 알아보기</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LOTTO-PILOT 프로젝트 1일차]]></title>
            <link>https://velog.io/@cobin_dev/LOTTO-PILOT-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@cobin_dev/LOTTO-PILOT-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Sun, 14 Jul 2024 11:43:53 GMT</pubDate>
            <description><![CDATA[<p>오늘은 개발 환경 설정과 요구사항 명세서 작성이 주된 목표이다.</p>
<h2 id="개발-환경-설정">개발 환경 설정</h2>
<h3 id="spring-boot">Spring Boot</h3>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/b1bc7f8a-83a2-49b0-9544-43c3bce3c68c/image.png" alt="">
사진처럼 필요한 의존성을 넣어주었다.</p>
<h3 id="github">GitHub</h3>
<h4 id="1-깃허브-레포-생성-및-연동">1. 깃허브 레포 생성 및 연동</h4>
<p><a href="https://github.com/alenjb/lotto-pilot">깃허브 레포 주소</a>와 로컬 개발 폴더를 연동 시켜두었다.</p>
<h4 id="2-자동-ci--cd-구축">2. 자동 CI / CD 구축</h4>
<p>GitHub Actions를 이용하여 구축하였다.
<img src="https://velog.velcdn.com/images/cobin_dev/post/f791a22a-cd0c-46bc-998a-90de0a17aff2/image.png" alt=""></p>
<h4 id="ciyml">CI.yml</h4>
<pre><code class="language-yaml">name: CI

on:
  push:
    branches: [ &quot;main&quot; ]
  pull_request:
    branches: [ &quot;main&quot; ]

jobs:
  build:
    runs-on: ubuntu-22.04
    env:
      working-directory: lotto-pilot

    steps:
      - name: 체크아웃
        uses: actions/checkout@v3

      - name: Set up JDK 21
        uses: actions/setup-java@v3
        with:
          distribution: &#39;temurin&#39;
          java-version: &#39;21&#39;

      - name: application.yml 생성
        run: |
          mkdir ./src/main/resources # resources 폴더 생성
          cd src/main/resources 
          echo &quot;${{ secrets.APPLICATION }}&quot; &gt; ./application.yml

      - name: 빌드
        run: |
          chmod +x gradlew
          ./gradlew build -x test
        shell: bash</code></pre>
<h4 id="cdyml">CD.yml</h4>
<pre><code class="language-yaml">name: CD

on:
  push:
    branches: [ &quot;main&quot; ]

jobs:
  deploy-ci:
    runs-on: ubuntu-22.04
    env:
      working-directory: lotto-pilot

    steps: 
    - uses: actions/checkout@v3

    - name: Set up JDK 21
      uses: actions/setup-java@v3
      with:
        distribution: &#39;temurin&#39;
        java-version: &#39;21&#39;

    - name: application.yaml 생성
      run: | 
          mkdir ./src/main/resources # resources 폴더 생성
          cd src/main/resources
          echo &quot;${{ secrets.APPLICATION }}&quot; &gt; ./application.yml

    - name: 빌드
      run: |
          chmod +x gradlew
          ./gradlew build -x test
      shell: bash

######## 여기까지는 CI.yaml와 동일 #########

    - name: docker build 가능하도록 환경 설정
      uses: docker/setup-buildx-action@v2.9.1

    - name: docker hub에로그인
      uses: docker/login-action@v2.2.0
      with:
        username: ${{ secrets.DOCKERHUB_LOGIN_USERNAME }}
        password: ${{ secrets.DOCKERHUB_LOGIN_ACCESSTOKEN }}

    - name: docker image 빌드 및 푸시
      run: |
        docker build --platform linux/amd64 -t alenjb/lotto-pilot .
        docker push alenjb/lotto-pilot

  deploy-cd:
    needs: deploy-ci
    runs-on: ubuntu-22.04
    steps:
    - name: 도커 컨테이너 제거
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.RELEASE_HOST }}
        username: ${{ secrets.RELEASE_USERNAME }}
        key: ${{ secrets.RELEASE_KEY }}
        script: |
            docker rm -f lotto-pilot-8080 || true

    - name: 도커 컨테이너 실행
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.RELEASE_HOST }}
        username: ${{ secrets.RELEASE_USERNAME }}
        key: ${{ secrets.RELEASE_KEY }}
        script: |
            cd ~
            ./deploy.sh</code></pre>
<h4 id="deploysh">deploy.sh</h4>
<pre><code class="language-bash">#!/bin/bash

nginx_config_path=&quot;/etc/nginx&quot;
all_port=(&quot;8080&quot; &quot;8081&quot;)
available_port=()
server_name=lotto-pilot

docker_ps_output=$(docker ps | grep $server_name)
running_container_name=$(echo &quot;$docker_ps_output&quot; | awk &#39;{print $NF}&#39;)
blue_port=$(echo &quot;$running_container_name&quot; | awk -F&#39;-&#39; &#39;{print $NF}&#39;)
web_health_check_url=/actuator/health

if [ -z &quot;$blue_port&quot; ]; then
    echo &quot;&gt; 실행 중인 서버의 포트: 없음&quot;
else
    echo &quot;&gt; 실행 중인 서버의 포트: $blue_port&quot;
fi

# 실행 가능한 포트 확인 ( all_port 중 blue_port를 제외한 port )
for item in &quot;${all_port[@]}&quot;; do
    if [ &quot;$item&quot; != &quot;$blue_port&quot; ]; then
        available_port+=(&quot;$item&quot;)
    fi
done

# 실행 가능한 포트 없으면 끝내기
if [ ${#available_port[@]} -eq 0 ]; then
    echo &quot;&gt; 실행 가능한 포트가 없습니다.&quot;
    exit 1
fi

green_port=${available_port[0]}

echo &quot;----------------------------------------------------------------------&quot;
# docker image pull
echo &quot;&gt; 도커 이미지 pull 받기&quot;
docker pull alenjb/${server_name}

# green_port로 서버 실행
echo &quot;&gt; ${green_port} 포트로 서버 실행&quot;
echo &quot;&gt; docker run -d --name ${server_name}-${green_port} -p ${green_port}:8080 -e TZ=Asia/Seoul alenjb/${server_name}&quot;
docker run -d --name ${server_name}-${green_port} -p ${green_port}:8080 -e TZ=Asia/Seoul alenjb/${server_name}
echo &quot;----------------------------------------------------------------------&quot;

# green_port 서버 제대로 실행 중인지 확인
sleep 10
for retry_count in {1..10}
do
    echo &quot;&gt; 서버 상태 체크&quot;
    echo &quot;&gt; curl -s http://localhost:${green_port}${web_health_check_url}&quot;
        # http://localhost:{그린포트}{health check 주소} -&gt; nginx
    response=$(curl -s http://localhost:${green_port}${web_health_check_url})
    up_count=$(echo $response | grep &#39;UP&#39; | wc -l)

    if [ $up_count -ge 1 ]
    then
        echo &quot;&gt; 서버 실행 성공&quot;
        break
    else
        echo &quot;&gt; 아직 서버 실행 안됨&quot;
        echo &quot;&gt; 응답 결과: ${response}&quot;
    fi
    if [ $retry_count -eq 10 ]
        then
        echo &quot;&gt; 서버 실행 실패&quot;
        docker rm -f ${server_name}-${green_port}

        exit 1
    fi
    sleep 2
done
echo &quot;----------------------------------------------------------------------&quot;

# nginx switching
echo &quot;&gt; nginx 포트 스위칭&quot;
echo &quot;set \$service_url http://127.0.0.1:${green_port};&quot; | sudo tee ${nginx_config_path}/conf.d/service-url.inc
sudo nginx -s reload

sleep 1

echo &quot;----------------------------------------------------------------------&quot;
# nginx를 통해서 서버 접근 가능한지 확인
response=$(curl -s http://localhost${web_health_check_url})
up_count=$(echo $response | grep &#39;UP&#39; | wc -l)
if [ $up_count -ge 1 ]
then
    echo &quot;&gt; 서버 변경 성공&quot;
else
    echo &quot;&gt; 서버 변경 실패&quot;
    echo &quot;&gt; 서버 응답 결과: ${response}&quot;
    exit 1
fi

# blue_port 서버 있다면 중단
if [ -n &quot;$blue_port&quot; ]; then
    echo &quot;&gt; 기존 ${blue_port}포트 서버 중단&quot;
    echo &quot;&gt; docker rm -f ${server_name}-${blue_port}&quot;
    sudo docker rm -f ${server_name}-${blue_port}
fi</code></pre>
<hr>
<h2 id="요구사항-명세서">요구사항 명세서</h2>
<p>이 프로그램의 핵심 기능은 크게 4가지다.</p>
<blockquote>
<p><strong>1. 정해진 알고리즘에 따라 로또 번호 추출</strong></p>
</blockquote>
<blockquote>
<p><strong>2. 추출된 번호를 통해 자동 로또 구매</strong></p>
</blockquote>
<blockquote>
<p><strong>3. 로또 번호 당첨 내역 조회</strong></p>
</blockquote>
<blockquote>
<p><strong>4. 동행복권 잔액 부족 여부 알림</strong></p>
</blockquote>
<p>따라서 이러한 기능들을 제공하기 위해 아래와 같이 요구사항 명세서를 정의하였다.</p>
<p><img src="https://velog.velcdn.com/images/cobin_dev/post/2e268b95-449e-446e-ad7b-23815c3d533a/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>