문제 링크

https://www.acmicpc.net/problem/16234

문제 설명

NxN 크기의 땅이 있고, 땅은 1x1 개의 칸으로 나누어져 있다. 각각의 땅에는 나라가 하나씩 존재하며, r행 c열에 있는 나라에는 A[r][c]명이 살고 있다. 인접한 나라 사이에는 국경선이 존재한다. 모든 나라는 1x1 크기이기 때문에, 모든 국경선은 정사각형 형태이다.

오늘부터 인구 이동이 시작되는 날이다.

인구 이동은 하루 동안 다음과 같이 진행되고, 더 이상 아래 방법에 의해 인구 이동이 없을 때까지 지속된다.

  • 국경선을 공유하는 두 나라의 인구 차이가 L명 이상, R명 이하라면, 두 나라가 공유하는 국경선을 오늘 하루 동안 연다.
  • 위의 조건에 의해 열어야하는 국경선이 모두 열렸다면, 인구 이동을 시작한다.
  • 국경선이 열려있어 인접한 칸만을 이동할 수 있으면, 그 나라를 오늘 하루 동안은 연합이라고 한다.
  • 연합을 이루고 있는 각 칸의 인구수는 (연합의 인구수) / (연합을 이루고 있는 칸의 개수)가 된다. 편의상 소수점은 버린다.
  • 연합을 해체하고, 모든 국경선을 닫는다.

각 나라의 인구수가 주어졌을 때, 인구 이동이 며칠 동안 발생하는지 구하는 프로그램을 작성하시오.

입력

첫째 줄에 N, L, R이 주어진다. (1 ≤ N ≤ 50, 1 ≤ L ≤ R ≤ 100)

둘째 줄부터 N개의 줄에 각 나라의 인구수가 주어진다. r행 c열에 주어지는 정수는 A[r][c]의 값이다. (0 ≤ A[r][c] ≤ 100) 

인구 이동이 발생하는 일수가 2,000번 보다 작거나 같은 입력만 주어진다.

 

문제 풀이

[고려사항]

배열 크기는 최대가 50x50 이고, 인구 이동이 발생하는 일수는 최대 2,000번이다. 

모든 배열을 매번 접근한다고 가정했을때, 최대 50 x 50 x 2000 = 5,000,000 (5백만)번의 접근이 있을 수 있다.

* 시간 제한 2초면 자바는 ×2+1 초라서 5초내에 완료해야한다. 그리고 초당 10^7~10^8회 연산을 수행할 수 있다고 가정했을때, 최대 접근 수가 이보다 적기 때문에 정답을 위해 크게 고려하지는 않아도 될 듯 하다. 물론 아예 생각안해도 된다는건 아니지만.

[알고리즘, 구현방법]

인접 국가를 방문하면서 인구 수 차이 조건에 따라 국경선 개방 여부를 결정할 수 있으므로,  BFS 알고리즘을 사용해서 확인했다.

 

국경선을 공유하는 두 나라의 인구 차이가 L명 이상, R명 이하라면 연합국이 되는데, 연합국은 하루에 여러개가 나올 수 있다.

그래서 연합국을 찾아 인구 이동하는 기준을 하루로 두고, 인접 국가를 탐색하는 방식으로 구현했다.

 

연합국을 찾아 인구 이동하는 함수인 movePeople을 반복해서, movePeople이 성공하는 경우, 인구 이동 일자를 +1 했다.

인구 이동의 성공 여부(availableFlag)는 movePeople 함수내에서 한번이라도 연합국이 만들어지면 true로 세팅하여 반환하였다.

 

인구 이동 함수(movePeople)에서는 인접 국가를 방문하면서 조건에 부합하면 연합국으로 묶어서 (연합국 인구 수 / 연합국 개수)명을 재분배하여 인구 이동을 구현했다. (BFS 알고리즘 사용)

 

상세 사항은 정답 코드의 주석을 따라가면 된다.

정답 코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int[][] earth;
    static int n, l, r;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine(), " ");
        n = Integer.parseInt(st.nextToken());
        l = Integer.parseInt(st.nextToken());
        r = Integer.parseInt(st.nextToken());

        // 인구수 입력
        earth = new int[n][n];
        for (int i=0; i<n; i++) {
            st = new StringTokenizer(br.readLine(), " ");
            for (int j=0; j<n; j++) {
                earth[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        // 인구 이동
        int day = 0;
        while (true) {
            if (!movePeople()) break; // 인구 이동이 실패한 경우 반복문 종료
            day++; // 인구 이동 성공한 경우 일 수 증가
        }
        System.out.println(day);
    }

    public static boolean movePeople() {
        int[] dx = {0,1,0,-1};
        int[] dy = {1,0,-1,0};
        boolean[][] visited = new boolean[n][n];
        Queue<Pointer> q = new LinkedList<>();
        boolean availableFlag = false;

        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (!visited[i][j]) { // 방문하지 않은 땅인 경우
                    q.offer(new Pointer(i, j)); // BFS Queue에 삽입
                    visited[i][j] = true;

                    // BFS Queue에 저장되어 아래 while문을 타면 하나의 연합국으로 계산
                    List<Pointer> pointers = new ArrayList<>(); // 하나의 연합국 정보를 담을 Pointer 리스트
                    int sum = earth[i][j]; // 연합의 인구수
                    int cnt = 1; // 연합을 이루고 있는 칸의 개수

                    while (!q.isEmpty()) {
                        Pointer nowPointer = q.poll();
                        pointers.add(nowPointer);

                        for (int k = 0; k < 4; k++) { // 4방향 탐색
                            int nx = nowPointer.x + dx[k];
                            int ny = nowPointer.y + dy[k];

                            if (nx >= 0 && nx < n && ny >= 0 && ny < n) { // 범위 체크
                                if (!visited[nx][ny]) { // 방문하지 않은 인접 땅인 경우
                                    int diff = Math.abs(earth[nowPointer.x][nowPointer.y] - earth[nx][ny]); // 현재 땅과 인접땅의 인구 차이 계산
                                    if (diff >= l && diff <= r) { // L명 이상 R명 이하인 경우 국경선 열림
                                        visited[nx][ny] = true; // 인접 땅 방문 표시

                                        // 연합국 처리
                                        sum += earth[nx][ny]; // 연합국 인구 수 누적
                                        cnt++; // 연합국 개수 증가

                                        q.add(new Pointer(nx, ny));
                                        pointers.add(new Pointer(nx, ny)); // 현재 연합국에 추가
                                    }
                                }
                            }
                        }
                    }
                    // 인구 이동을 할 수 없는 경우, 다음 땅에서 인접국 확인
                    if (pointers.size() == 1) continue;

                    // 현 연합국 인구 이동
                    int people = sum / cnt; // 연합에 분배될 인구 수
                    for (Pointer p : pointers) {
                        earth[p.x][p.y] = people;
                    }
                    availableFlag = true; // 인구 이동이 한번이라도 있다면 true 반환
                }
            }
        }
        return availableFlag;
    }

    public static class Pointer{
        int x;
        int y;
        public Pointer(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}

Spring Boot 프로젝트를 하던중에 Security 관련 공부를 하면서 csrf 설정을 해야한다는 것을 보고, csrf가 궁금해져서 알아보게 되었습니다. 그리고 비슷한 개념인 XSS에 대해서도 살짝 맛보도록 하겠습니다.


1. CSRF(Cross-Site Request Forgery: 크로스 사이트 요청 위조)

CSRF는 Cross-Site Request Forgery의 약자로, 번역하면 사이트 간 요청 위조를 의미합니다.

CSRF는 웹사이트의 취약점을 이용하여 사용자의 의지와 무관하게 공격자가 의도한 행위(데이터 삭제, 수정, 등록 등)을 특정 웹사이트에 요청하게 하는 공격입니다.

생성된 요청이 인증된 사용자의 동의를 받았는지 확인할 수 없는 웹 애플리케이션의 CSRF 취약점을 이용하는 것입니다.

  • 사용자가 인증한 세션에서 웹 애플리케이션이 정상적인 요청과 비정상적인 요청을 구분하지 못하는 점을 악용하는 공격 방식

공격자의 요청이 사용자의 요청인 것처럼 속여서 공격하는 방식이기에 '크로스 사이트 요청 위조'라는 명칭이 붙었습니다.

 

CSRF 공격 시나리오

CSRF 공격의 시나리오는 아래와 같습니다.

1️⃣ 사용자가 웹사이트에 접속해서 로그인하여 권한을 인증하고 세션을 생성합니다.

2️⃣ 공격자는 악성 웹사이트로 사용자를 유인합니다. 사용자가 이 링크를 클릭하면 공격자의 악성 웹사이트로 이동하게 됩니다.

3️⃣ 사용자가 악성 웹사이트에 방문하면, 해당 페이지에 포함된 스크립트나 이미지 태그가 사용자가 로그인한 웹사이트에 요청을 자동으로 전송합니다.

  • 예를 들어, 은행 사이트라고 할 때, 공격자가 이미지 태그로 돈을 송금하는 요청을 보낸다고 해봅시다.
< img  src = "http://bank.com/transfer?amount=9999&toAccount=000011112222" />

해당 이미지 태그를 악성 웹사이트에 심어두면 사용자가 접속시 브라우저가 이 이미지를 로드하기 때문에, 해당 은행 웹사이트로 돈 송금 요청이 보내지게 됩니다.

4️⃣ 사용자는 웹사이트에 이미 인증을 한 상태이므로, 해당 요청은 서버에서 사용자의 정상적인 요청으로 판단하고 인증된 요청으로 처리합니다.

5️⃣ 서버는 이 악의적인 요청을 처리하여 사용자가 의도하지 않은, 공격자가 의도한 요청을 수행하게 됩니다.

  • 예시로 보면, 사용자는 의도하지 않은 요청인, '000011112222' 계좌로 9,999원을 송금하게 됩니다.

 

CSRF 공격 사례

CSRF는 데이터 값을 변경하는 요청을 대상으로 합니다.

그러한 요청으로는 제품을 구입하거나 계정 설정, 기록을 삭제하거나 비밀번호를 변경하는 등의 요청이 있습니다. 

 

실제로 2008년에 옥션이 중국인 해커에게 CSRF 공격 방식으로 회원정보가 유출된 사례가 있습니다.

해커는 권한을 가지고 회사 내 작업을 하던 옥션 관리자에게 메일을 보냈고, 관리자는 이 메일을 조회했습니다. 

해커가 보낸 메일에는 태그가 들어간 코드가 포함되어 있었습니다. 

< img  src = "http://auction.com/changeUserAccount?id=admin&password=admin"  너비 = "0"  높이 = "0" >

(이미지 크기가 0이므로 관리자는 이 존재를 알지 못합니다.)

관리자가 메일을 조회하는 순간 이미지 파일을 받아오기 위해 URL이 열리게 되고, 해커가 원하는대로 관리자의 계정이 해커가 설정한 "admin"으로 변경됩니다. 이후 해커는 이 계정으로 서버 관리자 페이지에 접속해서 *백도어 프로그램을 올렸습니다.

*백도어 프로그램: 정상적인 절차를 거치지 않고도 접근을 가능하게 해주는 프로그램

백도어 프로그램을 이용해서 옥션 내부 웹 서버에 접속해 데이터베이스를 해킹했고, 회원 정보를 빼내간 것입니다.

이때 유출된 회원 정보가 1860만명이라고 합니다.

 

이러한 CSRF 공격에 당하게 되면 막대한 피해가 발생할 수 있습니다. 그렇기 때문에 웹 애플리케이션은 CSRF 공격을 방지하기 위해 CSRF 토큰을 통해 해당 요청이 인증된 사용자가 전송한 것이 맞는지를 확인하거나 재인증을 요구하는 등의 방식으로 CSRF 공격을 방지해야합니다.


2. CSRF 공격 방지 - Synchronizer Token Pattern

Spring에서는 CSRF 공격을 방지하기 위한 메커니즘 중 Synchronizer Token Pattern을 가장 우세하고 포괄적인 방법으로 소개하고 있습니다. Synchronizer Token Pattern은 CSRF 공격으로부터 애플리케이션을 보호하기 위해 서버와 클라이언트 간에 고유한 보안 토큰인 CSRF 토큰을 사용하는 방법입니다.

 

🔐 CSRF 토큰

사용자가 인증을 하더라도 CSRF 공격이 가능하기 때문에 보통 JWT만을 이용하여 사용자를 인증한다하였을때에도 공격을 받을 수 있습니다. 그래서 CSRF 토큰을 사용해서 CSRF 공격을 방지합니다.

 

CSRF 토큰이란, 서버에 들어온 요청이 실제 서버에서 허용한 요청이 맞는지를 확인하기 위한 토큰입니다.

CSRF 토큰은 서버에서 생성해서 클라이언트와 공유되는 인증 값입니다. 클라이언트는 서버에 요청시에 서버에서 인증해준 CSRF 토큰을 포함해서 요청해야합니다. CSRF 토큰이 없으면 서버는 요청을 거부하게 됩니다. 

 

CSRF 토큰을 HTML 폼에 담아서 보내는 방식이 일반적으로 사용되는 방식입니다.

 

1️⃣ 클라이언트는 애플리케이션 서버에 HTTP GET 요청을 통해 액세스합니다.

2️⃣ 서버는 CSRF 토큰을 생성하고 HTTP 세션에 저장합니다. 생성된 CSRF 토큰은 HTML 형식의 숨겨진 태그(hidden)를 사용하여 클라이언트와 연결됩니다.

3️⃣ 클라이언트는 HTML 폼의 버튼을 통해 애플리케이션 서버에 요청을 보냅니다. CSRF 토큰은 HTML 폼의 Hidden 필드에 포함되어 있으므로 CSRF 토큰 값이 요청 매개변수로 함께 전송됩니다. 

4️⃣ 서버는 요청 파라미터에 지정된 CSRF 토큰 값과 HTTP POST 메서드를 사용하여 액세스 할 때 HTTP 세션에 유지된 CSRF 토큰 값이 동일한지 확인합니다. 토큰 값이 일치하지 않으면 잘못된 요청으로, 오류가 발생합니다.

 

다시 정리하자면, 아래와 같습니다.

  • 로그인한 사용자가 페이지에 접속시 서버에서는 CSRF 토큰을 생성해서 사용자 세션이 저장합니다.
  • 이후 사용자가 서버에 애플맄이션의 상태를 변경하는 작업을 요청할 때 페이지에 Hidden 으로 숨어있는 CSRF 토큰 값이 서버로 전송됩니다. 
  • 서버에서는 이 CSRF 값이 세션에 저장된 값과 일치하는지 확인하여 해당 요청이 정상적인 요청임을 확인할 수 있습니다. 

CSRF 토큰 유의사항

[CSRF 토큰의 위치]

  • HTML 폼 제출시 hidden 필드로 포함시켜서 전송해야합니다.
  • AJAX 요청을 보낼시에는 CSRF 토큰을 Custom Header 값으로 포함시키거나 JSON payload(데이터)로 전송할 수 있습니다.
  • CSRF 토큰을 쿠키에 포함시켜서 전송하면 안됩니다.
    • CSRF 토큰은 실제 사용자가 요청한 정상적인 작업인지를 검증해야하는데, 쿠키는 브라우저에 의해 자동으로 HTTP 요청에 포함되므로, 정상적인 요청인지 검증할 수 없게 됩니다.
    • '자동 전송'이라는 기능때문에 공격자가 만든 요청이여도 쿠키가 자동으로 포함되어 서버에 전송됩니다. 이로인해 서버는 이 요청이 정상적인 요청이라고 착각하여 요청을 수행할 수도 있습니다.
    • 따라서 CSRF 토큰은 사용자가 직접 제어할 수 있는 방식으로 전송되어야 합니다.
  • CSRF 토큰은 서버 로그나 URL에 노출되어서는 안됩니다.

[HTTP 요청에 따른 요구사항]

  • 애플리케이션의 상태를 변경하는 요청에 대해서만 CSRF를 요청해도 괜찮습니다. (POST, PUT, PATCH, DELETE)
    • 단, 이를 위해서는 상태가 변경되지 않는(=멱등 요청)요청(GET, HEAD, OPTIONS, 및 TRACE)이 오직 읽기 전용(read-only) 작업만 수행하도록 보장해야합니다.
  • HTTP GET(상태가 변경되지 않는 안전한 요청)에는 CSRF 토큰을 포함시키지 않아야합니다.
    • GET 요청은 캐시되거나 URL로 노출될 수 있기 때문에 CSRF 토큰이 유출될 수도 있습니다.
    • 토큰이 유출되면 공격자가 이 토큰을 재사용하여 CSRF 공격을 시도할 수도 있습니다.

3. CSRF vs XSS

CSRF와 XSS는 모두 웹 애플리케이션에서의 공격 유형이지만, 차이점이 있습니다.

먼저 XSS가 무엇인지 간단하게 짚고 넘어가겠습니다.

 

XSS(Cross-Site Scripting)

XSS는 Cross-Site Scripting의 약자로, 번역하면 사이트 간 스크립팅 입니다.

XSS 공격은 공격자가 웹 페이지에 악의적인 스크립트를 삽입하여, 사용자의 브라우저에서 실행시키는 공격입니다. 

이로인해 사용자는 의도하지 않은 동작을 수행하거나, 공격자에게 쿠키, 세션 등의 정보를 탈취 당하게 됩니다.

XSS는 SQL Injection과 함께 웹 상에서 가장 기초적인 취약점 공격 방법 중 하나입니다.

XSS 공격 대응 방안

  • 중요 정보는 쿠키 대신 서버에 저장합니다.
  • 정보를 암호화 합니다.
  • httpOnly 옵션을 설정합니다.
    • document.cookie를 이용해 쿠키에 직접 접근하는 것을 방지
  • URL Encoding이나 문자열을 치환합니다.

CSRF 와 XSS 의 차이

[공격 대상]

  • CSRF 공격 대상은 "Server"이고 XSS 공격 대상은 "Client"입니다.
  • CSRF는 특정 웹 사이트가 사용자의 웹 브라우저를 신뢰하는 상태를 노린 것이고, XSS는 사용자가 특정 웹 사이트를 신뢰하는 점을 노린 것입니다.

[공격 방식]

  • CSRF공격자가 사용자의 브라우저를 속여 의도하지 않은 요청을 특정 웹사이트로 보내도록 유도합니다. 사용자의 권한을 이용해 서버에 대한 악성 공격을을 하게 하는 것입니다. 
  • XSS공격자가 악성 스크립트를 웹페이지에 삽입하여 해당 페이지를 방문하는 사용자의 브라우저에서 스크립트가 실행되게 합니다. 스크립트는 사용자의 브라우저에서 실행되므로 사용자의 권한으로 민감한 데이터를 탈취하거나 임의의 명령을 실행할 수 있습니다.

CSRF 공격

1️⃣ 공격자자 웹 사이트에 자금 이체 요청을 위조합니다.

2️⃣ 공격자는 그 요청을 하이퍼링크(스크립트)에 삽입하여 웹 사이트에 로그인 할 사용자에게 전송합니다.

3️⃣ 사용자가 그 링크를 클릭하면, 사용자도 모르게 웹 사이트에 요청을 전송하게 됩니다.

4️⃣ 웹 사이트의 서버는 로그인 된 사용자의 요청이기 때문에 정상으로 판단하고, 사용자의 계정에서 공격자의 계정으로 자금을 이체합니다.

XSS 공격

1️⃣ 공격자가 스크립트 주입이 가능한 취약점이 있는 웹 사이트를 찾습니다.

2️⃣ 취약점을 찾아 세션 쿠키를 탈취하는 악성 스크립트를 사이트에 삽입합니다.

3️⃣ 사용자가 웹 사이트를 방문할 때마다 스크립트가 작동됩니다.

4️⃣ 작동된 스크립트를 통해 사용자의 세션 쿠키를 탈취합니다.


Reference

 

 

Cross Site Request Forgery (CSRF) :: Spring Security

When should you use CSRF protection? Our recommendation is to use CSRF protection for any request that could be processed by a browser by normal users. If you are creating a service that is used only by non-browser clients, you likely want to disable CSRF

docs.spring.io

 

웹 보안의 기본: CSRF와 XSS 공격 이해하기

CSRF와 XSS 공격의 개념을 이해하고, 이를 방지하기 위한 기본적인 방법들에 대해 알아보는 글입니다.

f-lab.kr

 
[보안] CSRF

여기서는 CSRF 공격이 무엇인지 알아보고, 이것을 예방하기 위해서 사용되는 CSRF Token에 대해서 정리를 할 것입니다.

벨로그.아이오
 

XSS와 CSRF 차이점 및 대응 방안

웹사이트에서 의도치 않은 스크립트를 넣어서 실행시키는 기법을 말합니다.웹 애플리케이션이 사용자로부터 입력 받은 값을 제대로 검사하지 않고 사용할 경우 발생하며, 결과로 사용자는 의

velog.io

문제 링크

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

문제 설명

리코쳇 로봇이라는 보드게임이 있습니다.

이 보드게임은 격자모양 게임판 위에서 말을 움직이는 게임으로, 시작 위치에서 목표 위치까지 최소 몇 번만에 도달할 수 있는지 말하는 게임입니다. 

이 게임에서 말의 움직임은 상, 하, 좌, 우 4방향 중 하나를 선택해서 게임판 위의 장애물이나 맨 끝에 부딪힐때까지 미끄러져 이동하는 것을 한 번의 이동으로 칩니다.

다음은 보드게임판을 나타낸 예시입니다.

...D..R
.D.G...
....D.D
D....D.
..D....

여기서 "." 은 빈 공간을, "R"은 로봇의 처음 위치를, "D"는 장애물의 위치를, "G"는 목표지점을 나타냅니다.

위 예시에서는 "R" 위치에서 아래, 왼쪽, 위, 왼쪽, 아래, 오른쪽, 위 순서로 움직이면 7번 만에 "G" 위치에 멈춰 설 수 있으며, 이것이 최소 움직임 중 하나입니다.

게임판의 상태를 나타내는 문자열 배열 board가 주어졌을 때, 말이 목표위치에 도달하는데 최소 몇 번 이동해야 하는지 return하는 solution 함수를 완성하세요. 만약 목표위치에 도달할 수 없다면 -1을 return 해주세요.

제한사항

  • 3 ≤ board의 길이 ≤ 100
    • 3 ≤ board의 원소의 길이 ≤ 100
    • board의 원소의 길이는 모두 동일합니다.
    • 문자열은 ".", "D", "R", "G"로만 구성되어 있으며 각각 빈 공간, 장애물, 로봇의 처음 위치, 목표 지점을 나타냅니다.
    • "R"과 "G"는 한 번씩 등장합니다.

입출력 예

board result
["...D..R", ".D.G...", "....D.D", "D....D.", "..D...."] 7
[".D.R", "....", ".G..", "...D"] -1

 

문제 풀이

[고려사항]

시작 위치부터 목표 위치까지 모든 경로 중 최소 움직임을 찾아야한다. 

board의 최대 크기는 100 * 100(10,000)이므로, 모든 가능한 경로를 탐색하는 방식으로 구현한다면 시간을 초과한다.

그래서 최단 거리를 찾는 알고리즘으로 BFS를 이용해서 구현한다.

BFS로 구현할 경우에는, 모든 노드를 4방향으로 방문하기 때문에 최악의 경우라도, O(10,000)이 된다.

[알고리즘, 구현방법]

최단 경로를 찾는 문제이므로 BFS를 이용해서 구현했다.

여기서 말의 움직임은 장애물(D)이나 보드의 맨 끝에 부딪힐 때까지 미끄러지는 것이다. 

일반적인 BFS와 다른점은 이동하는 칸의 개수가 한 칸이 아니라, 장애물이나 벽에 부딪힐 때까지 최대한 갈 수 있는 n칸을 이동한다는 것이다.

즉, 상하좌우의 방향 중 한 칸만 움직이는것이 아니라 특정 방향으로 최대한 갈 수 있는 칸을 구해야한다.

그래서 BFS 방문 배열을 boolean이 아니라 int 로 선언해서 해당 지점까지의 이동 횟수를 저장했다.

[풀이 과정]

*자세한건 코드내에 주석을 확인하면서 따라가주세요!

1. 시작 위치 찾기(R)

2. 시작 위치를 Queue에 저장하고 BFS 수행

  • 4방향에 대해 BFS 수행
  • 한 방향에 대해 최대한 갈 수 있는 만큼 이동
  • 벽이나 장애물에 부딪힌 경우, 벽이나 장애물을 부딪히기 전 좌표로 돌아와서, 해당 지점까지의 이동 횟수 저장하고 Queue에 저장하여 해당 지점부터 탐색
  • 현재 지점이 목표 위치(G)라면 이동 횟수 answer에 저장하고 Break;

정답 코드

import java.util.LinkedList;
import java.util.Queue;

public class Solution {
    public int solution(String[] board) {
        int row = board.length;
        int col = board[0].length();
        int[][] cnt = new int[row][col];

        // 1. 시작 위치(R) 찾기
        Queue<Point> q = new LinkedList<>();
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (board[i].charAt(j) == 'R') {
                    q.add(new Point(i, j));
                    cnt[i][j] = 1;
                    break;
                }
            }
        }

        // 2. BFS 탐색
        int[] dx = {1, 0, -1, 0};
        int[] dy = {0, 1, 0, -1};
        int answer = -1; // 목표위치에 도달할 수 없다면 -1을 return
        while (!q.isEmpty()) {
            Point current = q.poll();
            // 현재 지점이 목표 위치인지
            if (board[current.x].charAt(current.y) == 'G') {
                answer = cnt[current.x][current.y] - 1; // 시작 위치를 1로 초기화 했으므로, 이동 횟수는 -1
                break;
            }
            // 4방향에 대해 BFS를 수행
            for (int i = 0; i < 4; i++) {
                int nx = current.x + dx[i];
                int ny = current.y + dy[i];
                // 해당 방향으로 최대한 이동
                while (true) {
                    if ((nx >= 0 && nx < row && ny >= 0 && ny < col) && board[nx].charAt(ny) != 'D') {
                        nx += dx[i];
                        ny += dy[i];
                    } else { // 벽이나 장애물에 부딪힌 경우
                        nx -= dx[i];
                        ny -= dy[i];
                        break;
                    }
                }
                // 최대한 움직인 지점을 방문한 적이 없으면, 해당 지점에서 탐색
                if (cnt[nx][ny] == 0) {
                    q.add(new Point(nx, ny));
                    cnt[nx][ny] = cnt[current.x][current.y] + 1; // 현재 탐색을 시작한 위치에서 이동 횟수 +1
                }
            }
        }
        return answer;
    }

    public class Point {
        int x;
        int y;
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}

문제 링크

 

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

문제 설명

[본 문제는 정확성과 효율성 테스트 각각 점수가 있는 문제입니다.]

세로길이가 n 가로길이가 m인 격자 모양의 땅 속에서 석유가 발견되었습니다. 석유는 여러 덩어리로 나누어 묻혀있습니다. 당신이 시추관을 수직으로 단 하나만 뚫을 수 있을 때, 가장 많은 석유를 뽑을 수 있는 시추관의 위치를 찾으려고 합니다. 시추관은 열 하나를 관통하는 형태여야 하며, 열과 열 사이에 시추관을 뚫을 수 없습니다.

(자세한 예시는 문제 링크에서 확인요망)

석유가 묻힌 땅과 석유 덩어리를 나타내는 2차원 정수 배열 land가 매개변수로 주어집니다. 이때 시추관 하나를 설치해 뽑을 수 있는 가장 많은 석유량을 return 하도록 solution 함수를 완성해 주세요.

제한사항

  • 1 ≤ land의 길이 = 땅의 세로길이 = n ≤ 500
    • 1 ≤ land[i]의 길이 = 땅의 가로길이 = m ≤ 500
    • land[i][j]는 i+1행 j+1열 땅의 정보를 나타냅니다.
    • land[i][j]는 0 또는 1입니다.
    • land[i][j]가 0이면 빈 땅을, 1이면 석유가 있는 땅을 의미합니다.
  • 정확성 테스트 케이스 제한사항
    • 1 ≤ land의 길이 = 땅의 세로길이 = n ≤ 100
      • 1 ≤ land[i]의 길이 = 땅의 가로길이 = m ≤ 100
  • 효율성 테스트 케이스 제한사항: 주어진 조건 외 추가 제한사항 없습니다.

입출력 예

land result
[[0, 0, 0, 1, 1, 1, 0, 0], [0, 0, 0, 0, 1, 1, 0, 0], [1, 1, 0, 0, 0, 1, 1, 0], [1, 1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 1, 1]] 9
[[1, 0, 1, 0, 1, 1], [1, 0, 1, 0, 0, 0], [1, 0, 1, 0, 0, 1], [1, 0, 0, 1, 0, 0], [1, 0, 0, 1, 0, 1], [1, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1]] 16

 

문제 풀이

[고려사항]

n, m은 최대 500으로, 땅을 최대 500 * 500이다. 최대 250,000(25만)개의 좌표를 매번 순회한다는 등의 계산으로는 시간이 초과할 수 있기에 최대한 효율적인 방식으로 풀어야한다.

[알고리즘, 구현방법]

이 문제는 석유 덩어리를 시추관이 통과하면 석유량에 해당 석유 덩어리의 크기가 누적되고, 그 중 가장 많은 양의 석유를 반환해야한다. 그렇기때문에 일단 1)석유 덩어리를 구하고 2)시추관을 열을 기준으로 땅에 꽂았을때 열 별로 나오는 석유량을 계산 해야한다.

 

1)석유 덩어리 구하기 : BFS

석유가 있는 땅(land[i][j] = 1)에서 석유 덩어리들을 식별하고, 각 덩어리를 객체로 만들어 리스트에 저장했다. 이 덩어리들을 모두 식별해야만 석유 시추 시 정확한 석유량을 계산할 수 있기때문이다. 

[BFS(너비 우선 탐색)를 이용한 석유 덩어리 탐색]

석유 덩어리는 BFS 를 이용해서 탐색했다. 

먼저, land를 끝까지 순회하면서, 석유인(=1) 좌표를 발견하면 새로운 석유 덩어리의 시작이라고 판단한다.

BFS를 이용해서 이 시작이 되는 좌표와 연결된 모든 1을 탐색하고, 이를 하나의석유 덩어리로 묶어서 리스트에 저장했다 .

이렇게 해서 모든 좌표를 순회하면, land 배열에 존재하는 모든 석유 덩어리들을 식별할 수 있다.

 

2)가장 많은 석유를 뽑을 수 있는 시추관 찾기

처음에는 land를 다시 순회하면서 석유가 있는 땅(=1)일 때 분기 처리를 통해 석유량을 계산하는 방법을 생각했다. 그런데 이 방법은 이미 한 번 석유 덩어리를 찾아 순회한 이후, 또다시 땅 전체를 순회해야 하므로 비효율적이다. 땅 전체를 다시 순회하는 것은 시간과 자원의 낭비로 이어지기 때문이다. 

땅 전체를 다시 순회하는 대신, 이미 식별하고 저장해둔 석유 덩어리 정보를 활용하여 석유량을 계산하는 방법으로 계산했다.

이전에 저장해둔 석유 덩어리 객체들에 포함된 좌표들을 활용해서, 각 석유 덩어리의 좌표 중에서 열 값을 기준으로 시추관 별로 뽑을 수 있는 석유량을 계산했다.

이 방법을 통해서 전체 땅을 다시 순회하지 않고, 이미 탐색한 석유 덩어리 좌표만 순회하여서 석유량을 계산할 수 있게된다. 어차피 구해야하는 것은 특정 열에 속한 석유 덩어리 정보이기 때문에, 이를 직접 활용하는 것이 효율적이다. 

 

[풀이 과정]

1. 석유 덩어리 구하기 (BFS)

  • 땅 배열(land)을 전체 탐색
  • 방문한적 없는 좌표이고(좌표 방문 여부 배열 (visited[i][j] = false), 석유가 있는 땅(land[i][j] = 1)이라면 주변 탐색 시작
    • Queue에서 좌표를 하나씩 꺼내면서 상하좌우로 인접한 좌표를 확인하여 방문한적 없고 석유가 있는 땅이면 Queue에 추가
    • 이때 석유 덩어리에 포함되는 좌표들이 Queue에 추가되고, 이 좌표들은 동시에 해당 석유 덩어리의 좌표 리스트(pointList)에 추가
  • BFS 탐색이 끝나면, 해당 석유 덩어리의 좌표 리스트인 pointList를 석유 덩어리 객체를 담은 리스트(oils)에 저장

2. 가장 많은 석유를 뽑을 수 있는 시추관 찾기

  • 저장된 석유 덩어리 리스트(oils)를 순회하며 각 석유 덩어리의 좌표를 확인
  • 좌표의 열 값을 기준으로 해당 열에서 시추할 수 있는 석유량 계산
    • 열에 대한 시추관 정보(Drill) 객체를 생성해서 시추할 수 있는 석유량을 누적
    • 특정 열에 있는 석유 덩어리가 아직 누적되지 않았다면, 즉, 아직 해당 열에 해당 석유 덩어리를 계산한 적 없다면, 해당 석유 덩어리의 크기(좌표 리스트의 크기)를 누적합에 더하고, 해당 석유 덩어리를 방문처리한다.

3. 열마다 시추할 수 있는 석유량을 저장한 리스트(drils)를 순회하여 최댓값 찾아서 반환

 

정답 코드

import java.util.*;
public class Solution { 
	public int solution(int[][] land) {
        int n = land.length; // 세로 길이
        int m = land[0].length; // 가로 길이

        boolean[][] visited = new boolean[n][m];
        int[] dx = {1, 0, -1, 0};
        int[] dy = {0, 1, 0, -1};

        // 석유 덩어리 구하기
        Queue<Point> queue = new LinkedList<>();
        List<Oil> oils = new ArrayList<>();
        // BFS 탐색
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (!visited[i][j] && land[i][j] == 1) { // 방문한적 없고, 석유가 있는 땅인 경우
                    visited[i][j] = true;

                    List<Point> pointList = new ArrayList<>(); // 현재 석유 덩어리의 좌표 리스트
                    pointList.add(new Point(i,j));
                    queue.offer(new Point(i,j));

                    while (!queue.isEmpty()) {
                        Point point = queue.poll();

                        for (int k = 0; k < 4; k++) {
                            int nx = point.x + dx[k];
                            int ny = point.y + dy[k];

                            if(nx >= 0 && nx < n && ny >= 0 && ny < m) {
                                if (!visited[nx][ny] && land[nx][ny] == 1) {
                                    visited[nx][ny] = true;
                                    queue.offer(new Point(nx, ny));
                                    pointList.add(new Point(nx, ny));
                                }
                            }
                        }
                    }
                    oils.add(new Oil(pointList)); // 석유 덩어리 리스트에 현재 석유 덩어리 정보 저장
                }
            }
        }

        // 가장 많은 석유를 뽑을 수 있는 시추관 찾기
        Drill[] drills = new Drill[m]; // 열 개수(=가로 길이)만큼 할당
        int oilCnt = oils.size();
        // 석유 덩어리 정보를 저장한 List를 순회하면서 해당 포인트의 열을 기준으로 석유 덩어리 사이즈 계산
        for (int o = 0; o < oilCnt; o++) {
            for (Point point : oils.get(o).pointList) {
                int y = point.y;
                // 아직 생성한적 없다면 석유 덩어리 개수를 매개변수로 넣어서 생성
                if (drills[y] == null) {
                    drills[y] = new Drill(oilCnt);
                }

                if (!drills[y].visitedOil[o]) { // 현재 석유 덩어리가 해당 좌표 열에 아직 누적합 되지 않은 경우
                    drills[y].visitedOil[o] = true; // 방문한 석유 덩어리니까 true 할당
                    drills[y].addOilSum(oils.get(o).pointList.size()); // 현재 석유 덩어리의 크기 누적
                }
            }
        }

        // Max 석유량 찾기
        int max = 0;
        for (Drill drill: drills) {
            // 석유가 없는 열이라면 drill이 null 일 수도 있으므로 null 검증
            if (drill != null && drill.oilSum > max) max = drill.oilSum;
        }
        return max;
    }

    public class Point {
        int x;
        int y;
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    public class Oil {
        List<Point> pointList;

        public Oil(List<Point> pointList) {
            this.pointList = pointList;
        }
    }

    public class Drill {
        boolean[] visitedOil;
        int oilSum = 0;

        public Drill(int oilCnt) {
            this.visitedOil = new boolean[oilCnt];
        }
        public void addOilSum(int oilSize) {
            this.oilSum += oilSize;
        }
    }
}

 

문제 링크

https://www.acmicpc.net/problem/2493

문제 설명

KOI 통신연구소는 레이저를 이용한 새로운 비밀 통신 시스템 개발을 위한 실험을 하고 있다. 실험을 위하여 일직선 위에 N개의 높이가 서로 다른 탑을 수평 직선의 왼쪽부터 오른쪽 방향으로 차례로 세우고, 각 탑의 꼭대기에 레이저 송신기를 설치하였다. 모든 탑의 레이저 송신기는 레이저 신호를 지표면과 평행하게 수평 직선의 왼쪽 방향으로 발사하고, 탑의 기둥 모두에는 레이저 신호를 수신하는 장치가 설치되어 있다. 하나의 탑에서 발사된 레이저 신호는 가장 먼저 만나는 단 하나의 탑에서만 수신이 가능하다.

예를 들어 높이가 6, 9, 5, 7, 4인 다섯 개의 탑이 수평 직선에 일렬로 서 있고, 모든 탑에서는 주어진 탑 순서의 반대 방향(왼쪽 방향)으로 동시에 레이저 신호를 발사한다고 하자. 그러면, 높이가 4인 다섯 번째 탑에서 발사한 레이저 신호는 높이가 7인 네 번째 탑이 수신을 하고, 높이가 7인 네 번째 탑의 신호는 높이가 9인 두 번째 탑이, 높이가 5인 세 번째 탑의 신호도 높이가 9인 두 번째 탑이 수신을 한다. 높이가 9인 두 번째 탑과 높이가 6인 첫 번째 탑이 보낸 레이저 신호는 어떤 탑에서도 수신을 하지 못한다.

탑들의 개수 N과 탑들의 높이가 주어질 때, 각각의 탑에서 발사한 레이저 신호를 어느 탑에서 수신하는지를 알아내는 프로그램을 작성하라.

입력

첫째 줄에 탑의 수를 나타내는 정수 N이 주어진다. N은 1 이상 500,000 이하이다. 둘째 줄에는 N개의 탑들의 높이가 직선상에 놓인 순서대로 하나의 빈칸을 사이에 두고 주어진다. 탑들의 높이는 1 이상 100,000,000 이하의 정수이다.

출력

첫째 줄에 주어진 탑들의 순서대로 각각의 탑들에서 발사한 레이저 신호를 수신한 탑들의 번호를 하나의 빈칸을 사이에 두고 출력한다. 만약 레이저 신호를 수신하는 탑이 존재하지 않으면 0을 출력한다.

 

 

문제 풀이

[고려사항]

탑의 개수(N)는 최대 500,000 (50만)개 이고 시간 제한이 1.5초이므로 전체 탐색을 하면 자신을 제외한 모든 건물을 탐색해야하므로 O(N(N-1)) = O(N^2) 가 발생해서 시간 초과하여 실패할 것이다. 

[알고리즘, 구현방법]

이 문제는 한 방향만 고려하면 되고, 가장 먼저 만나는 건물 번호를 구하면 되므로, Stack을 이용해서 Stack의 최상단 값이 현재 탐색중인 건물의 높이보다 높은 값이 나올 때까지 값을 제거하면서 계산하면 된다.

 

입력된 탑이 6 9 5 7 4 일때, 레이저는 왼쪽 방향으로 쏘기때문에 입력되는 값의 반대 방향으로 본인보다 높은 건물을 찾아야한다.

그리고 가장 먼저 만나는 && 나보다 높은 건물 번호를 구하면 되기 때문에 별도의 고려사항이 추가되지 않는다.

즉, 입력되는 건물을  Stack에 담으면서 현재 Stack의 최상단 값이 높다면 정답으로 할당하고, 낮다면 Stack의 최상단 값을 높은 건물이 나올때까지 pop하면 된다. 

[풀이 과정]

1. 건물 높이 입력값을 받으면서,

  • Stack 에 건물이 있다면
    • Stack의 최상단에 있는 높이 값이 현재 건물 높이보다 높을때까지 stack.pop()
    • 현재 건물보다 높은 건물을 만나면 해당 건물 번호를 정답으로 할당
  • 없다면, Stack에 건물 정보(건물 번호, 건물 높이) 저장

3. 정답 출력

정답 코드

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());

        StringTokenizer st = new StringTokenizer(br.readLine(), " ");
        Deque<Building> stack = new ArrayDeque<>(); // 건물 정보를 담을 Stack
        int[] answers = new int[n];
        for (int i=0; i<n; i++) {
            int height = Integer.parseInt(st.nextToken()); // 현재 건물 높이

            while (!stack.isEmpty()) {
                Building building = stack.peek();
                // Stack의 최상단에 있는 높이 값이 현재 견물보다 높을때까지 stack.pop()
                if (building.height <= height) {
                    stack.pop();
                } else { // 현재 건물보다 높은 건물을 만나면 해당 건물 번호 정답으로 할당
                    answers[i] = building.num;
                    break;
                }
            }
            stack.push(new Building(i+1, height));
        }

        StringBuilder sb = new StringBuilder();
        for (int answer: answers) {
            sb.append(answer);
            sb.append(" ");
        }
        System.out.println(sb);
    }
    static class Building {
        int num;
        int height;

        public Building(int num, int height) {
            this.num = num;
            this.height = height;
        }
    }
}

문제 링크

https://www.acmicpc.net/problem/15989

문제 설명

정수 4를 1, 2, 3의 합으로 나타내는 방법은 총 4가지가 있다. 합을 나타낼 때는 수를 1개 이상 사용해야 한다. 합을 이루고 있는 수의 순서만 다른 것은 같은 것으로 친다.

  • 1+1+1+1
  • 2+1+1 (1+1+2, 1+2+1)
  • 2+2
  • 1+3 (3+1)

정수 n이 주어졌을 때, n을 1, 2, 3의 합으로 나타내는 방법의 수를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 테스트 케이스의 개수 T가 주어진다. 각 테스트 케이스는 한 줄로 이루어져 있고, 정수 n이 주어진다. n은 양수이며 10,000보다 작거나 같다.

출력

각 테스트 케이스마다, n을 1, 2, 3의 합으로 나타내는 방법의 수를 출력한다.

 

문제 풀이

[고려사항]

숫자 합은 중복이 없는 조합으로 구해야한다.

그리고 주어지는 테스트 케이스 개수는 최대 10,000개이므로 매 테스트마다 모든 수에 대한 계산을 하면 시간초과가 발생한다. 

[알고리즘, 구현방법]

이미 한 계산에 대한 값을 꺼내 쓰는 문제이기 때문에 DP 로 구현했다.

dp[i-1]은 dp[i]의 1을 포함한 숫자 합의 경우의 수이다.

dp[2]는 1+1, 2의 경우로 2가지의 합의 조합이 나온다.

dp[3]은 1+1+1, 1+2, 3 의 합의 조합이 나오는데, 이때 dp[2]의 (1+1)에 +1, 2에 +1을 한 두가지의 경우의 수가 무조건 포함된다

 

따라서 dp[i]는 dp[i-1]의 합의 조합에 +1을 한 경우와 2,3의 조합으로만 이루어진 합의 조합의 수를 합한 것이 된다.

dp[i] = dp[i-1] + (2,3으로만 이루어진 합의 조합)

 

2,3으로만 이루어진 합의 조합을 찾는 방법은 아래와 같다.

n을 2로 나눈 몫만큼 반복하면서, 2의 개수를 순차적으로 줄여, 2,3의 합으로 n을 만들 수 있다면 카운팅하면 된다.

예를 들어, n = 10 일때, 10을 2로 나눈 몫은 5이다. 2의 개수를 5개, 4개, 3개, 2개, 1개로 줄이면서 3과의 합의 조합이 되는지 확인한다.

  • 2가 5개: 10 - (2 * 5) = 0이므로 합이 된다. (O)
  • 2가 4개: 10 - (2 * 4) = 2 | 2 < 3이므로 합이 되지 않는다. (X)
  • 2가 3개: 10 - (2 * 3) = 4 | 4 > 3 | 4 % 3 != 0 이므로 합이 되지 않는다. (X) 
    • → 10에서 2를 3개 합한 6을 뺀 값인 4는 3보다 크지만 3으로 나누어떨어지지 않기 때문
  • 2가 2개: 10 -(2 * 2) = 6 | 6 > 3 | 6 % 3 = 0 이므로 합이 된다. (O)
  • 2가 1개: 10 -(2 * 1) = 8 | 8 > 3 | 8 % 3 != 0 이므로 합이 되지 않는다. (X)
  • 2가 0개: 10 -(2 * 0) = 10 | 10 > 3 | 10 % 3 != 0 이므로 합이 되지 않는다. (X)

 

정리하면,

n을 2로 나눈 몫부터 0까지, 2의 개수를 순차적으로 줄이면서 n과 2의 개수만큼의 합을 뺀 값이 0이거나, 3보다 크고 3으로 나누어떨어지는 경우에만 합이 되는 경우로 쳐서 dp[n]에 카운팅하면 된다.

[풀이 과정]

1. dp[] 정수 배열은 n의 최댓값 + 1로 초기화, dp[1] = 1로 할당

2. 테스트 케이스만큼 반복

3. 2부터 n까지 반복하면서 dp 값 계산

  • 이전에 이미 계산된 값은 넘어감
  • 2로 나눈 몫에서 0까지 반복하면서 2의 개수를 줄여가며 3과 합의 조합이 이루어지는지 확인
    • 현재 dp[i] 라 했을때, i가 2의 배수인 경우, 카운팅
    • i 에서 2의 개수의 합을 뺀 값이 3보다 크고(AND) 3으로 나누어 떨어지는 경우, 카운팅

4. dp[n] 출력

정답 코드

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int t = Integer.parseInt(br.readLine());

        int[] test = new int[t];
        for (int i = 0; i < t; i++) {
            test[i] = Integer.parseInt(br.readLine());
        }

        int[] dp = new int[10001]; // n 은 최대 10,000
        dp[1] = 1;
        for (int num : test) { // Test case 만큼 반복
            for (int i = 2; i <= num; i++) {
                if (dp[i] > 0) continue; // 이미 계산된 값은 넘어감
                int count = 0;
                int div = i / 2;
                for (int j = div; j >= 0; j--) { // 2로 나눈 몫만큼 반복하면서 2의 개수를 순차적으로 줄임
                    int diff = i - (j * 2);
                    // i가 2의 배수인 경우(diff == 0)
                    // 2를 i개 만큼 더한 후 남은 수를 3으로 나누어서, 나누어 떨어지면 3을 채워서 합으로 만들 수 있는 경우
                    if (diff == 0 || (diff >= 3 && diff % 3 == 0)) count++;
                }
                dp[i] = dp[i-1] + count; // dp[i] = dp[i-1] + (2,3으로만 이루어진 합)
            }
            System.out.println(dp[num]);
        }
    }
}

문제 링크

https://www.acmicpc.net/problem/20922

문제 설명

홍대병에 걸린 도현이는 겹치는 것을 매우 싫어한다. 특히 수열에서 같은 원소가 여러 개 들어 있는 수열을 싫어한다. 도현이를 위해 같은 원소가 𝐾개 이하로 들어 있는 최장 연속 부분 수열의 길이를 구하려고 한다.

 이하의 양의 정수로 이루어진 길이가 𝑁인 수열이 주어진다.  이 수열에서 같은 정수를 𝐾개 이하로 포함한 최장 연속 부분 수열의 길이를 구하는 프로그램을 작성해보자.

입력

첫째 줄에 정수 𝑁(1≤𝑁≤200,000)과 𝐾(1≤𝐾≤100)가 주어진다.

둘째 줄에는 𝑎1,𝑎2,...𝑎𝑛이 주어진다 (1≤𝑎𝑖≤100,000)

출력

조건을 만족하는 최장 연속 부분 수열의 길이를 출력한다.

 

문제 풀이

[고려사항]

정수 N은 최대 200,000(20만)이라서 수열의 모든 정수를 돌면서 확인하면 시간초과가 될 것이다.

단순 반복해서 계산하면 안되고, 알고리즘을 사용해야한다.

[알고리즘, 구현방법]

주어진 수열에서 같은 정수를 K개 이하를 포함하는 최장 연속 부분 수열을 찾아야하므로, 두개의 포인터를 두고 조건을 확인해서 구해야한다.

즉, 투 포인터로 푸는 문제이다.

정수를 카운팅하는 배열을 두고 수열을 투 포인터(start, end)를 이용해서 확인한다.

수열의 현재 정수의 개수가 K개를 초과하면 start를 오른쪽으로 한칸씩 옮기면서 부분 수열을 확인한다.

의사코드(pseudocode)로 작성하면 아래와 같다.

while (end 포인터가 수열 원소의 개수(N)보다 작은 경우) 
    if (end 포인터가 가리키는 정수의 개수가 K개 초과하면)
        최장 거리 갱신
        while (현재 정수의 개수가 K개 이하가 될 때까지)
            start 포인터가 가리키는 정수 개수 1 감소(=부분 수열의 범위를 한칸 옆으로 이동)
            부분 수열 길이 1 감소
            start 포인터 1 증가
    else
        현재 정수(=end 포인터가 가리키는) 개수 1 증가
        부분 수열 길이 1 증가

최장 거리 갱신

[풀이 과정]

1. 입력 값 세팅 및 수열 정수의 개수 배열 생성(int[] cnt)

2. end 포인터가 n 이상이 될때까지 반복

  • 현재 수열의 숫자가 연속으로 K개 초과한 경우,
    • 최장 거리 갱신
    • 현재 숫자가 K개 이하가 될 때까지
      • start 포인터 이동(=부분 수열의 시작 부분 이동)
      • 수열 길이 1 감소 & start 포인터가 가리키는 정수의 개수 1 감소(부분 수열의 범위가 옆으로 한칸 이동하기 때문)
  • K개 초과하지 않은 경우
    • 현재 숫자 개수 1 증가
    • 부분 수열 길이 1 증가

3. 마지막 갱신된 부분 수열의 길이를 고려하여 최장 거리 갱신 후 정답 출력

정답 코드

public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    StringTokenizer st = new StringTokenizer(br.readLine(), " ");

    int n = Integer.parseInt(st.nextToken());
    int k = Integer.parseInt(st.nextToken());

    st = new StringTokenizer(br.readLine(), " ");
    int[] sequence = new int[n];
    for (int i=0; i<n; i++) {
        sequence[i] = Integer.parseInt(st.nextToken());
    }

    // 숫자 개수 배열
    int[] cnt = new int[100001]; // 숫자는 최대 100_000
    int start = 0;
    int end = 0;
    int answer = 0;
    int max = 0;
    while (end < n) {
        if (cnt[sequence[end]] + 1 > k) { // 하나의 숫자가 연속 k개 초과
            if (max < answer) max = answer; // 최장 거리 갱신
            // while (sequence[start] != sequence[end]) { // 1개가 늘어서 연속 k개 초과했으므로, start가 end 숫자랑 같아질때까지 이동시키기
            while (cnt[sequence[end]] + 1 > k) { // 현재 숫자가 k개 이하가 될 때까지 start 이동
                cnt[sequence[start]]--; // start 숫자 제외
                answer--; // 수열 길이 -1
                start++; // start pointer +1
            }
        } else { // 초과 하지 않는 경우, 1) 현재 숫자 카운팅 & 2) 숫자 연속 길이 증가
            cnt[sequence[end++]]++;
            answer++;
        }
    }
    if (max < answer) max = answer; // 마지막 갱신된 길이를 고려하여 최종 max 값 갱신
    System.out.println(max);
}

문제 링크

https://www.acmicpc.net/problem/1446

문제 설명

매일 아침, 세준이는 학교에 가기 위해서 차를 타고 D킬로미터 길이의 고속도로를 지난다. 이 고속도로는 심각하게 커브가 많아서 정말 운전하기도 힘들다. 어느 날, 세준이는 이 고속도로에 지름길이 존재한다는 것을 알게 되었다. 모든 지름길은 일방통행이고, 고속도로를 역주행할 수는 없다.

세준이가 운전해야 하는 거리의 최솟값을 출력하시오.

입력

첫째 줄에 지름길의 개수 N과 고속도로의 길이 D가 주어진다. N은 12 이하인 양의 정수이고, D는 10,000보다 작거나 같은 자연수이다. 다음 N개의 줄에 지름길의 시작 위치, 도착 위치, 지름길의 길이가 주어진다. 모든 위치와 길이는 10,000보다 작거나 같은 음이 아닌 정수이다. 지름길의 시작 위치는 도착 위치보다 작다.

출력

세준이가 운전해야하는 거리의 최솟값을 출력하시오.

 

문제 풀이

[고려사항]

지름길로 가는 거리와 아닌 거리의 합을 구하면서, 모든 조합 중 가장 짧은 거리를 찾아야한다.

여기서 지름길은 원래 거리보다 짧은 거리여야하고, 지름길 여러개가 범위가 겹치면 더 짧은 거리의 지름길을 선택해야한다.

이런 식으로 지름길을 선택하고 최단 거리를 고려해서 구현하려고하면 구현이 복잡해져서 예외처리를 제대로 하지 못할 수 있다.

그래서 이 문제는 DP(Dynamic Programming)로 풀어야 한다.

[알고리즘, 구현방법]

DP로 모든 거리(0 ~ D)의 지점을 방문해서 해당 거리에서의 최단 거리를 저장한다.

즉, dp[i] = i meter 까지의 최단 거리를 의미한다.

 

그리고 지름길의 도착 위치에서는 지름길 시작 위치 ~ 도착 위치까지의 거리와 이전 위치 + 1 중에 최솟값을 할당해야한다.

즉, 현재 지점까지의 최단 거리 = min(이전 지점까지의 최단 거리 + 1, 지름길 최단 거리(=지름길 시작 지점까지의 최단 거리 + 지름길 거리)로 계산해야한다.

 

지름길의 시작 위치를 s, 지름길 길이가 distance라고 한다면, (i = 현재 거리 = 지름길의 도착 위치)

dp[i] = MIN(dp[i-1] + 1, dp[s] + distance) 가 된다.

 

[지름길 정보 저장 방식]

지름길에 대한 정보를 가지고 있다가 0 ~ D거리를 방문하면서 지름길의 도착 위치에 도달했는지를 확인할 수 있어야한다.

dp[i]를 할당할때, 지름길의 도착 위치라면 지름길 거리를 반영해서 최솟값을 계산해야하기 때문이다.

그래서 지름길 정보를 Map<(도착 위치), (지름길 정보)>로 저장했다. Key를 도착 위치로 해두면 dp[i] 값을 할당할 때 현재 위치(i)가 Key값으로 포함되어있는지를 확인하여 지름길 여부를 판단할 수 있기 때문이다.

 

그리고 지름길의 도착 위치가 2개 이상인 경우도 있기 때문에, 도착 위치(Key)에 매핑되는 지름길 정보를 List로 저장했다.

Map<Integer, List<Shortcut>> shortcutInfo = new HashMap<>();

[풀이 과정]

1. 지름길 정보 저장

  • 도착 위치가 고속도로 길이를 초과하면 저장 안함
  • 끝 지점 - 시작 지점이 지름길 거리보다 짧으면 저장 안함 (즉, 지름길 거리가 짧은 경우에만 저장)
  • 지름길 도착 위치가 2개 이상인 경우, 저장된 리스트에 지름길 정보 추가
  • 지름길 도착 위치에 대해 저장된 지름길 정보가 없다면, 새로 리스트에 지름길 정보 추가

2. dp 배열에 각 위치별 최단 거리를 저장

  • 현재 위치에 지름길 정보가 있다면 (=도착 위치 정보가 있다면 =Map의 containsKey가 true라면), 지름길 정보 계산
    • 도착 위치까지의 지름길 정보가 여러개인 경우가 있기때문에 해당 위치에서의 지름길 리스트를 반복하면서 최단 거리 갱신
      • 지름길 거리를 포함한 현재 위치에서의 거리 = 시작 지점까지의 최단 거리 + 해당 지름길 거리
  • 현재 위치에서의 최단 거리 할당
    • 현재 지점까지의 최단 거리 = min(이전 지점까지의 최단 거리 + 1, 지름길 최단 거리(=지름길 시작 지점까지의 최단거리 + 지름길 거리)

3. dp[D] 출력

 

정답 코드

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine(), " ");

        int N = Integer.parseInt(st.nextToken());
        int D = Integer.parseInt(st.nextToken());

        Map<Integer, List<Shortcut>> shortcutInfo = new HashMap<>();
        for (int i = 0; i < N; i++) {
            st = new StringTokenizer(br.readLine(), " ");
            int s = Integer.parseInt(st.nextToken());
            int e = Integer.parseInt(st.nextToken());
            int d = Integer.parseInt(st.nextToken());

            // 고속도로 길이를 초과하면 저장 안함
            if (e > D) continue;
            // 끝 지점 - 시작 지점이 지름길 거리보다 짧으면 저장 안함 (즉, 지름길 거리가 짧은 경우에만 저장)
            if (e - s <= d) continue;

            // 지름길 정보 세팅
            Shortcut sc = new Shortcut(s, e, d);
            List<Shortcut> list = new ArrayList<>();
            // 이미 도착 지점 지름길 정보가 있으면, 저장된 리스트 정보에 추가해야됨
            if (shortcutInfo.containsKey(e)) list = shortcutInfo.get(e);

            list.add(sc);
            shortcutInfo.put(e, list);
        }

        int[] dp = new int[D + 1];
        dp[1] = 1;
        for (int i = 1; i <= D; i++) {
            int min = D;
            if (shortcutInfo.containsKey(i)) { // 지름길 루트가 있는 경우
                int val = 0;
                for (Shortcut s: shortcutInfo.get(i)) { // 도착 지점까지의 지름길이 여러개인 경우를 위한 반복문
                    val = dp[s.start] + s.distance; // 시작 지점까지의 최단 거리 + 해당 지름길 거리
                    if (val < min) min = val; // 도착 지점까지의 지름길 중 최솟값 갱신
                }
            }
            // 현재 지점까지의 최단 거리 = min(이전 지점까지의 최단 거리 + 1, 지름길 최단 거리(=지름길 시작 지점까지의 최단 거리 + 지름길 거리)
            dp[i] = Math.min(dp[i-1] + 1, min); // 해당 지점까지의 최단 거리 계산
        }
        System.out.println(dp[D]);
    }

    public static class Shortcut {
        int start;
        int end;
        int distance;

        public Shortcut(int start, int end, int distance) {
            this.start = start;
            this.end = end;
            this.distance = distance;
        }
    }
}

문제 링크

https://www.acmicpc.net/problem/17615

문제 설명

빨간색 볼과 파란색 볼이 <그림 1>에서 보인 것처럼 일직선상에 섞여 놓여 있을 때, 볼을 옮겨서 같은 색 볼끼리 인접하게 놓이도록 하려고 한다. 볼을 옮기는 규칙은 다음과 같다.

  1. 바로 옆에 다른 색깔의 볼이 있으면 그 볼을 모두 뛰어 넘어 옮길 수 있다. 즉, 빨간색 볼은 옆에 있는 파란색 볼 무더기를 한 번에 뛰어 넘어 옮길 수 있다. 유사하게, 파란색 볼은 옆에 있는 빨간색 볼 무더기를 한 번에 뛰어 넘어 옮길 수 있다.
  2. 옮길 수 있는 볼의 색깔은 한 가지이다. 즉, 빨간색 볼을 처음에 옮겼으면 다음에도 빨간색 볼만 옮길 수 있다. 유사하게, 파란색 볼을 처음에 옮겼으면 다음에도 파란색 볼만 옮길 수 있다.

예를 들어, 처음에 볼이 <그림 1>에서 보인 것처럼 있을 때, 빨간 볼을 <그림 2>에서 보인 것처럼 옮긴 후, <그림 3>에서 보인 것처럼 옮긴다면 두 번 만에 같은 색끼리 모을 수 있다.

반면에 파란색 볼을 선택하여 에서 보인 것처럼 옮기면(화살표에 있는 수는 옮기는 순서를 나타낸다) 네 번을 옮겨야 같은 색의 볼끼리 모을 수 있다.

일직선상에 놓여 있는 볼에 관한 정보가 주어질 때, 규칙에 따라 볼을 이동하여 같은 색끼리 모으되 최소 이동횟수를 찾는 프로그램을 작성하시오.

입력

첫 번째 줄에는 볼의 총 개수 N이 주어진다. (1 ≤ N ≤ 500,000) 다음 줄에는 볼의 색깔을 나타내는 문자 R(빨간색 볼) 또는 B(파란색 볼)가 공백 없이 주어진다. 문자열에는 R 또는 B 중 한 종류만 주어질 수도 있으며, 이 경우 답은 0이 된다.

출력

최소 이동횟수를 출력한다.

입출력 예

입력 출력
9
RBBBRBRRR
2
8
BBRBBBBR
1

 

문제 풀이

[고려사항]

볼의 총 개수인 N은 최대 500,000(50만)이고, 시간 제한은 1초(추가 시간 없음)이므로, O(N^2)이 나오지 않게 고려하여서 구현해야한다.

[알고리즘, 구현방법]

이 문제는 *그리디 알고리즘과 구현으로 해결해야한다.

*그리디 알고리즘(Greedy Algorithm): 최적해를 구하는 알고리즘으로, 각 단계에서 최적이라고 생각되는 것을 선택해나가는 방식으로 진행하여 최종적인 해답을 찾는 알고리즘이다.

 

빨간공(R), 파란공(B)을 각각 왼쪽, 오른쪽으로 모으는 경우에 대해 모두 확인해야한다. (총 4번) 

옮기는 횟수는 옮기려는 방향의 가장 끝 인덱스부터, 현재 확인하려하는 볼의 연속된 개수를 해당 색상의 총 볼의 개수에서 빼주면 된다. 

  • 빨간 공(R)을 왼쪽/오른쪽으로 옮기는 횟수: 빨간 공의 총 개수 - 왼쪽/오른쪽에 연속으로 있는 빨간 공의 개수
  • 파란 공(B)을 왼쪽/오른쪽으로 옮기는 횟수: 파란 공의 총 개수 - 왼쪽/오른쪽에 연속으로 있는 파란 공의 개수

문제의 예시로 예를 들어보면,

빨간 공(R)을 왼쪽으로 옮기는 경우, 가장 왼쪽에 있는 빨간 공(R) 하나를 제외한 4개의 공을 옮겨야하고, 옮기는 횟수는 4회이다.

파란 공(B)을 왼쪽으로 옮기는 경우, 가장 왼쪽에 있는 파란 공이 없으므로 총 4개의 파란 공을 옮겨야하고, 횟수는 4회이다.

오른쪽 방향에서는 오른쪽 끝에 있는 각 공의 수를 확인해서 계산하면 된다.

[풀이 과정]

1. 입력을 받아서 각 공의 총 개수를 저장 | 공의 위치를 배열로 저장

2. 한 종류의 공만 있는 경우, 답은 0

3. 왼쪽 끝으로 빨간 공과 파란 공을 옮기는 경우를 계산

  • 왼쪽 끝부터 빨간 공, 파란 공이 연속적으로 있을때까지만 공의 개수(cnt) 카운팅
  • 옮기는 횟수(moved) 계산 (moved = total - cnt)하고 횟수 최솟값 갱신

4. 오른쪽 끝으로 빨간 공과 파란 공을 옮기는 경우를 계산

  • 왼쪽 끝으로 모은 경우와 방향만 다름

5. 횟수 최솟값(min) 출력

 

정답 코드

import java.io.*;
public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

    int N = Integer.parseInt(br.readLine());
    String ball = br.readLine();

    char[] balls = new char[N];
    int[] total = {0, 0}; // 빨간 공의 수, 파란 공의 수
    for(int i=0; i<N; i++) {
        char c = ball.charAt(i);
        balls[i] = c;
        // 색상별 공의 개수 카운팅
        if (c == 'R') total[0]++;
        else total[1]++;
    }
    // 한 종류의 공만 있는 경우
    if (total[0] == N || total[1] == N) {
        System.out.println(0);
        return;
    }

    int min = N+1;
    char[] color = {'R', 'B'};
    // 왼쪽 끝으로 모으는 경우
    for (int b = 0; b < 2; b++) {
        int idx = 0;
        int cnt = 0;
        // 공 순서에서 옮기는 공이 연속으로 존재할때까지만 왼쪽 끝에 있는 공의 수 카운팅
        while (balls[idx++] == color[b]) {
            cnt++;
        }
        int moved = total[b] - cnt; // 옮긴 횟수 계산 (총 공의 개수 - 왼쪽 끝에 있는 공의 수)
        if (moved < min) min = moved; // 최솟값 갱신
    }
    // 오른쪽 끝으로 모으는 경우
    for (int b = 0; b < 2; b++) {
        int idx = N - 1;
        int cnt = 0;
        // 공 순서에서 옮기는 공이 연속으로 존재할때까지만 오른쪽 끝에 있는 공의 수 카운팅
        while(balls[idx--] == color[b]) {
            cnt++;
        }
        int moved = total[b] - cnt; // 옮긴 횟수 계산 (총 공의 개수 - 왼쪽 끝에 있는 공의 수)
        if (moved < min) min = moved; // 최솟값 갱신
    }

    System.out.println(min);
}

1. 모노톤 스택(Monotone stack) 이란?

스택(Stack)은 한쪽 끝에서만 데이터를 넣고 뺄 수 있는 후입선출(LIFO:Last In, First Out) 형태의 자료구조 입니다.
즉, 가장 먼저 들어온 데이터가 가장 마지막에 나가고, 마지막에 들어온 데이터가 가장 먼저 나가는 구조입니다.

 

  • 모노톤 스택(Monotone stack)은 특정한 순서를 유지하면서 데이터를 관리하는 스택 자료구조 입니다.
    • Monotone은 증가하거나 감소하는 등의 일정한 경향이 보임을 의미합니다.
  • 모노톤 스택(Monotone stack)은 증가하거나 감소하는 일정한 경향을 가진 스택입니다.
  • 모노톤 스택(Monotone stack)은 특정 문제에 대해 시간 복잡도를 O(N)으로 줄여주는 매우 효율적인 해결책을 제공합니다.
    • 다음/이전 데이터의 최댓값/최솟값을(Nearest Greater/Smaller Element) 찾는 문제에 유용하게 쓰입니다.

 

2. 모노톤 스택(Monotone stack)의 종류 및 구현

스택은 LIFO(Last In, First Out) 자료 구조이기 때문에 모노톤 스택의 최상위 요소를 제거하면서 단조 증가/감소 순서를 유지합니다. 새로운 요소는 항상 최상위에 추가됩니다.

1) 단조 증가 스택(Monotone Increasing Stack)

  • 스택의 요소들이 오름차순으로 정렬되는 스택
    • 오름차순을 유지한다는 것은 스택의 아래쪽부터 위쪽으로 올라갈수록 값이 커진다는 의미
    • 스택의 최상위에서부터 시작하여 아래로 내려갈수록 값이 작아짐
10 (top)
5
2
1
  • 스택의 맨 위에 있는 요소가 항상 그 아래에 있는 요소보다 크거나 같은 경우를 유지하는 스택
    • 스택에 새로운 요소를 추가할 때, 그 요소가 스택의 최상위 요소보다 작거나 같을 때까지 기존 요소를 제거
    • 이때, 단조성을 유지해야합니다. (스택에 있는 요소들이 항상 특정 순서(오름차순)를 유지)
    • 중복 값 허용 여부에 따라 값이 같은 경우에 대한 제거 여부를 정하면 됩니다.
Deque<Integer> stack = new ArrayDeque<>();
for (int i=0; i<arr.length; i++) {
    // 스택의 최상위 요소가 현재 값보다 크거나 같으면 pop();
    while (!stack.isEmpty() && stack.peekFirst() >= arr[i]) {
    	stack.removeFirst();
    }
    // 스택의 최상위 요소보다 작을때까지 기존 요소를 제거하고, 현재 요소 스택에 추가
    stack.offerFirst(arr[i]);
}

2) 단조 감소 스택(Monotone Decreasing Stack)

  • 스택의 요소들이 내림차순으로 정렬되는 스택
    • 내림차순을 유지한다는 것은 스택의 아래쪽부터 위쪽으로 올라갈수록 값이 작아진다는 의미
    • 스택의 최상위에서부터 시작하여 아래로 갈수록 값이 커짐
1 (top)
2
5
10
  • 스택의 맨 위에 있는 요소가 항상 그 아래에 있는 요소보다 작거나 같은 경우를 유지하는 스택
    • 스택에 새로운 요소를 추가할 때, 그 요소가 스택의 최상위 요소보다 크거나 같을 때까지 기존 요소를 제거
    • 이때, 단조성을 유지해야합니다. (스택에 있는 요소들이 특정 순서(내림차순)를 유지)
    • 중복 값 허용 여부에 따라 값이 같은 경우에 대한 제거 여부를 정하면 됩니다.
Deque<Integer> stack = new ArrayDeque<>();
for (int i=0; i<arr.length; i++) {
    // 스택의 최상위 요소가 현재 값보다 작거나 같으면 pop();
    while (!stack.isEmpty() && stack.peekFirst() <= arr[i]) {
    	stack.removeFirst();
    }
    // 스택의 최상위 요소보다 클 때까지 기존 요소를 제거하고, 현재 요소 스택에 추가
    stack.offerFirst(arr[i]);
}

 

 

3. 모노톤 스택(Monotone stack) 구현 - 예시로 확인

개인적으로 모노톤 스택의 종류에 대해 이해하는데에 조금 시간이 걸렸습니다. 단조 증가 스택과 단조 감소 스택의 정렬 순서가 어디부터 시작하는지에 대한 정확한 이해를 못하고 무작정 이해하려고 하다보니 헷갈렸습니다. 

그래서 마지막으로 배열 예시를 정리하면서 아직은 조금 헷갈리는 부분을 정리해보겠습니다.

public static void main(String[] args) {
    int[][] testArrays = {
        {1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
        {10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
        {5, 5, 4, 4, 3, 3, 2, 2, 1, 1},
        {3, 6, 1, 9, 4, 7, 2, 8, 5, 10}
    };

    for (int[] arr : testArrays) {
        System.out.println("Monotonic Stack for array: " + Arrays.toString(arr));
        incresing(arr);
        decresing(arr);
        System.out.println();
    }
}

테스트 배열을 위와 같이 4가지 정도로 생성해두고 단조 증가 스택과 단조 감소 스택을 출력해보았습니다.

*각 스택은 2번 목차의 코드를 수행해서 생성

 

위 코드에 대한 출력 결과는 아래와 같습니다.

Monotonic Stack for array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Monotonic Increasing Stack: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 
Monotonic Decreasing Stack: 10, 

Monotonic Stack for array: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
Monotonic Increasing Stack: 1, 
Monotonic Decreasing Stack: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 

Monotonic Stack for array: [5, 5, 4, 4, 3, 3, 2, 2, 1, 1]
Monotonic Increasing Stack: 1, 
Monotonic Decreasing Stack: 1, 2, 3, 4, 5, 

Monotonic Stack for array: [3, 6, 1, 9, 4, 7, 2, 8, 5, 10]
Monotonic Increasing Stack: 10, 5, 2, 1, 
Monotonic Decreasing Stack: 10,

 

단조 증가 스택은 요소들이 오름차순(스택의 아래부터 최상위까지)으로 정렬되어 있는 스택

단조 감소 스택은 요소들이 내림차순(스택의 아래부터 최상위까지)으로 정렬되어 있는 스택

인 점을 머리속에 꼭 담아두고 출력 결과를 확인해보면, 제대로 출력되는 것을 알 수 있습니다.

 

코드 구현시 중복 값은 허용하지 않았기 때문에 3번 예시처럼 중복 값이 없는 것을 확인할 수 있습니다.

만약 중복을 허용한다면 아래와 같은 결과가 출력됩니다.

Monotonic Stack for array: [5, 5, 4, 4, 3, 3, 2, 2, 1, 1]
Monotonic Increasing Stack: 1, 1, 
Monotonic Decreasing Stack: 1, 1, 2, 2, 3, 3, 4, 4, 5, 5,

 

단조 증가 스택 함수는 아래와 같을때 마지막 예시인 {3, 6, 1, 9, 4, 7, 2, 8, 5, 10} 에 대한 단조 증가 스택 생성 과정을 구체적으로 보겠습니다.

public static void incresing(int[] arr) {
    Deque<Integer> stack = new ArrayDeque<>();
    for (int i=0; i<arr.length; i++) {
        // 스택의 최상위 요소가 현재 값보다 크거나 같으면 pop();
        while (!stack.isEmpty() && stack.peekFirst() >= arr[i]) {
        	stack.removeFirst();
        }
        // 스택의 최상위 요소보다 작을때까지 기존 요소를 제거하고, 현재 요소 스택에 추가
        stack.offerFirst(arr[i]);
    }
}

 

  • 배열: {3, 6, 1, 9, 4, 7, 2, 8, 5, 10}
배열 요소 설명 스택
첫번째 요소: 3 스택이 비어있으므로 요소 3 추가  [3]
두번째 요소: 6 스택의 맨 위 요소(3)가 6보다 작으므로 6을 스택에 추가 [6, 3]
세번째 요소: 1 스택의 맨 위 요소(6)가 1보다 크므로, 6을 스택에서 제거 [3]
스택의 맨 위 요소(3)가 1보다 크므로, 3을 스택에서 제거 [ ]
스택이 비었으므로 요소 1 추가 [1]
네번째 요소: 9 스택의 맨 위 요소(1)가 9보다 작으므로, 9를 스택에 추가 [9, 1]
다섯번째 요소: 4 스택의 맨 위 요소(9)가 4보다 크므로, 9를 스택에서 제거 [1]
스택의 맨 위 요소(1)가 4보다 작으므로, 4를 스택에 추가 [4, 1]
여섯번째 요소: 7 스택의 맨 위 요소(4)가 7보다 작으므로, 7을 스택에 추가 [7, 4, 1]
일곱번째 요소: 2 스택의 맨 위 요소(7)가 2보다 크므로, 7을 스택에서 제거 [4, 1]
스택의 맨 위 요소(4)가 2보다 크므로, 4를 스택에서 제거 [1]
스택의 맨 위 요소(1)가 2보다 작으므로, 2를 스택에 추가 [2, 1]
여덟번째 요소: 8 스택의 맨 위 요소(2)가 8보다 작으므로, 8을 스택에 추가 [8, 2, 1]
아홉번째 요소: 5 스택의 맨 위 요소(8)가 5보다 크므로, 8을 스택에서 제거 [2, 1]
스택의 맨 위 요소(2)가 5보다 작으므로, 5를 스택에 추가 [5, 2, 1]
열번째 요소: 10 스택의 맨 위 요소(5)가 10보다 작으므로, 10을 스택에 추가 [10, 5, 2, 1]

 

최종적으로 스택을 출력하면 [10, 5, 2, 1] 이 출력됩니다.

스택의 맨 위 요소가 현재 배열 요소보다 크거나 같으면 스택에서 제거하고, 현재 요소를 스택에 추가하면서 단조성을 유지할 수 있는 것입니다.

 

4. 모노톤 스택(Monotone stack) 관련 문제

 

[백준] 탑 보기 - 자바(Java)

문제 링크https://www.acmicpc.net/problem/22866문제 설명일직선으로 다양한 높이의 건물이 총 N개가 존재한다. 각 건물 옥상에서 양 옆에 존재하는 건물의 옆을 몇 개 볼 수 있는지 궁금해졌다. i번째 건

coding-vvon.tistory.com

 

 

참고 링크

 

모노톤 스택, 큐,덱에 관하여.

서론. 스택, 큐, 덱 문제들을 푸는 동안 많은 알고리즘들이 해당 알고리즘을 사용하는 것을 보았다 하나같이 해당 형식을 따르고 있는 것을 보고 뭔가 정형화된 알고리즘이 있나? 생각이 들었는

velog.io

 

+ Recent posts