접근

이전 20922번과는 달리 k라는 일정한 간격이 정해져 있어서, 상대적으로 투포인터의 느낌이 옅은 것 같은 문제였다. 입력값 저장은 일차원 배열에 하지만 회전 초밥은 시작과 끝이 연결되어 있으므로 left (시작 지점)이 n-1번째까지 오도록 반복하고, right는 n과 같아질 경우 다시 0번째 인덱스로 돌려줬다. 나는 코드에서 d값을 쓰지는 않았다.

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
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());
        int k = Integer.parseInt(st.nextToken());
        int c = Integer.parseInt(st.nextToken());

        int[] arr = new int[n];
        Map<Integer, Integer> map = new HashMap<>();

        for (int i = 0; i < n; i++)
            arr[i] = Integer.parseInt(br.readLine());

        // 처음 k개 초밥 계산
        int cnt = 0;
        for (int i = 0; i < k; i++) {
            map.put(arr[i], map.getOrDefault(arr[i], 0) + 1);
            if (map.get(arr[i]) == 1) cnt++;
        }
        int answer = cnt;

        // 투포인터 적용
        int right = k;
        for (int left = 0; left < n; left++) {
            if(right == n) right = 0;

            map.put(arr[left], map.get(arr[left]) - 1);
            if(map.get(arr[left]) == 0) cnt--;

            map.put(arr[right], map.getOrDefault(arr[right], 0) + 1);
            if(map.get(arr[right]) == 1) cnt++;

            // 쿠폰 번호 확인
            if(map.getOrDefault(c, 0) == 0) answer = Math.max(answer, cnt+1);
            else answer = Math.max(answer, cnt);

            // 포인터 이동
            right++;
        }

        System.out.println(answer);
    }
}

접근

C언어나 C++을 써본 사람이라면 투포인터를 이해하기가 더 쉬울 것이라고 생각한다. 포인터는 말 그대로 어떤 배열을 '가리키는' 화살표를 사용하는 것이다. 그런데 자바에는 포인터가 없으므로, 배열의 특정 위치를 가리키는 값, 즉 인덱스로 보면 된다. (자바에 포인터가 없는 이유도 나중에 정리해보면 좋을 것 같다.)

이 문제에서는 조건을 만족하는 가장 긴 수열의 길이를 찾는 것이 목적이기 때문에, 해당 수열을 찾기 위해 수열의 양 옆을 가리키는 left와 right라는 두 포인터(인덱스값)를 이용하게 된다. 포인터를 한칸씩 이동하며 left ~ right에 해당하는 부분 수열이 문제에서 제시하는 조건에 부합한다면 결과(answer) 값을 갱신하면 된다. 각 숫자의 개수를 카운팅하는 방법은 여러 방법이 있을 것이다. 20만 크기의 배열을 선언하여 바로바로 참조하는 방법도 있겠지만, 나는 굳이 빈 공간을 두기 싫어서 숫자 N을 키로 갖는 Map을 사용했다.

첫 번째 예제에서는 k값이 2이기 때문에, 어떤 숫자의 개수가 2가 되기 전까지는 계속해서 right를 증가시켜 준다. 그래야 left~right 수열의 길이가 최대가 되기 때문이다. 그렇게 한 칸씩 이동하다가 (3, 2, 5, 5, 6, 4, 4, 5) 에서 조건에 위배되는 경우를 마주치게 되었다. 그러면 앞에 나온 동일한 숫자 5가 없을 때까지 left를 이동시켜줘야 하므로, 앞부분의 (3, 2, 5) 를 제외하고(카운팅한 개수에서도 하나씩 없앰) left는 4번째 자리의 5를 가리키게 된다. 이제 K값보다 큰 개수의 숫자가 없기 때문에 다시 조건에 맞는 가장 긴 순열을 찾기 위해 right를 증가시키는 과정을 입력 순열의 마지막 인덱스가 될 때까지 반복하면 된다.

코드

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

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = null;
        int answer = 1; // k의 최솟값은 1임

        // 기본 입력값 받기
        st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken());
        int k = Integer.parseInt(st.nextToken());

        // 입력 수열 저장
        int[] arr = new int[n];
        st = new StringTokenizer(br.readLine());

        for (int i = 0; i < n; i++)
            arr[i] = Integer.parseInt(st.nextToken());

        // 투 포인터 적용
        Map<Integer, Integer> m = new HashMap();
        int left = 0, right = 0; // left~right의 연속된 수열을 위한 인덱스

        while(right < n) { // 입력수열의 마지막까지 확인
            m.put(arr[right], m.getOrDefault(arr[right], 0) + 1);

            while (m.get(arr[right]) > k) {
                m.put(arr[left], m.get(arr[left]) - 1);
                left++;
            }
            
            answer = Math.max(answer, right - left + 1);
            right++;
        }
        System.out.println(answer);
    }

}


오늘 오후 2~5시에 진행됐던 쇼미더코드가 끝났다. 결론은 세 문제 다 풀긴 풀었다! 와아아 👏 A는 부분합 문제, B는 BFS 문제, C는 dp 문제였다. 공개된 문제를 보니 난이도는 골드5, 5, 3이었다. 직접 풀어보고 싶으신 분들은 이쪽 → https://www.acmicpc.net/category/detail/3451

사실 A번부터 풀기 시작했는데 처음에 잘 안풀리길래 그냥 관둘까 하다가 B번이 너무 쉬워보여서 조금만 더 해보자 했다. 다행히 B번은 1트만에 풀었는데 그게 이미 대회가 시작된지 1시간 30분이 지난 뒤여서 좀 아쉬웠다. C번은 처음에 문제를 이해하는 데 시간이 좀 걸렸고, 전체 틀을 금방 잡았는데 사소하게 놓친 부분들이 있어 이것저것 고치다보니 제출횟수가 좀 많아졌다. C번까지 풀고 나니 20분정도가 남았고, A번을 다시 보는데 갑자기 그제서야 방법이 확 생각나서.. 후다닥 풀고 종료 5분 전에 다 제출했다. 아래 코드는 급하게 푼 만큼, 가독성이나 효율성이 안좋을 수 있는 점 참고 바란다.

 

+

1월 30일에 테스트 결과가 이메일로 발송됐다. 제출 횟수나 시간 등을 봤을 때 금손은 절대 아닐거라 생각하고 있었고, 예상대로 은손을 받았다.

 


A번: 신을 모시는 사당

입력된 1의 개수, 2의 개수를 카운팅해서 배열에 넣도록 했다. 1은 음수, 2는 양수로 해서 가장 큰/작은 연속된 수열의 합을 구하는 로직을 적용했다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.StringTokenizer;

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());

        ArrayList<Integer> arr = new ArrayList<>();
        int sum = 0;

        StringTokenizer st = new StringTokenizer(br.readLine());
        for (int i = 0; i < n; i++) {
            int val = Integer.parseInt(st.nextToken());
            if(val == 1){
                if(sum < 0) sum--;
                else{
                    arr.add(sum);
                    sum = -1;
                }
            }
            else{
                if(sum > 0) sum++;
                else{
                    arr.add(sum);
                    sum = 1;
                }
            }
        }
        arr.add(sum);

        int arrSize = arr.size();
        int largeSum = 0, smallSum = 0;
        int largest = 0, smallest = 0;

        for (int i = 0; i < arrSize; i++) {
            for (int j = i; j < arrSize; j++) {
                largeSum += arr.get(j);
                smallSum += arr.get(j);

                largest = Math.max(largest, largeSum);
                smallest = Math.min(smallest, smallSum);
            }
            largeSum = smallSum = 0;
        }

        int answer = Math.max(largest, -smallest);
        System.out.println(answer);
    }
}

 

 

B번: 도넛 행성

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.StringTokenizer;

public class Main {
    static class Pos{
        int x, y;

        Pos(int x, int y){
            this.x = x;
            this.y = y;
        }
    }

    static int N, M;
    static int[] dx = {-1, 0, 1, 0};
    static int[] dy = {0, 1, 0, -1};

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

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

        int[][] star = new int[N][M];
        boolean[][] visited = new boolean[N][M];

        for(int i = 0;i < N;i++){
            st = new StringTokenizer(br.readLine());
            for(int j = 0;j < M;j++){
                star[i][j] = Integer.parseInt(st.nextToken());
                if(star[i][j] == 1) visited[i][j] = true;
            }
        }

        int answer = 0;

        for(int i = 0;i < N;i++) {
            for (int j = 0; j < M; j++) {
                if(visited[i][j]) continue;

                bfs(i, j, visited);
                answer++;
            }
        }

        System.out.println(answer);
    }

    private static void bfs(int i, int j, boolean[][] visited) {
        Queue<Pos> q = new LinkedList();
        q.add(new Pos(i, j));

        while(!q.isEmpty()){
            Pos cur = q.poll();
            if(visited[cur.x][cur.y]) continue;

            visited[cur.x][cur.y] = true;
            for(int d = 0;d < 4;d++){
                int nx = cur.x + dx[d];
                int ny = cur.y + dy[d];

                // set bound
                if(nx >= N) nx -= N;
                else if(nx < 0) nx += N;
                if(ny >= M) ny -= M;
                else if(ny < 0) ny += M;

                if(!visited[nx][ny])
                    q.add(new Pos(nx, ny));
            }
        }
    }

}

 

 

C번: 미팅

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

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

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

        int[][] W = new int[C+1][C+1];
        int[] aChar = new int[N];
        int[] bChar = new int[M];

        // 성격 간의 만족도 저장
        for (int i = 1; i <= C; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 1; j <= C; j++) {
                W[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        // A와 B 학생들의 성격 저장
        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < N; i++)
            aChar[i] = Integer.parseInt(st.nextToken());

        st = new StringTokenizer(br.readLine());
        for (int i = 0; i < M; i++)
            bChar[i] = Integer.parseInt(st.nextToken());

        // dp 초기값 설정
        long[][] dp = new long[N][M];
        long answer = 0;
        dp[0][0] = W[aChar[0]][bChar[0]];

        for (int i = 1; i < N; i++)
            dp[i][0] = Math.max(dp[i-1][0], W[aChar[i]][bChar[0]]);

        for (int i = 1; i < M; i++)
            dp[0][i] = Math.max(dp[0][i-1], W[aChar[0]][bChar[i]]);

        // 최대 만족도 계산
        for (int i = 1; i < N; i++) {
            for (int j = 1; j < M; j++) {
                // 악수를 하는 경우와 안하는 경우 고려
                long val = Math.max(dp[i-1][j], dp[i][j-1]);
                dp[i][j] = Math.max(val, dp[i-1][j-1] + W[aChar[i]][bChar[j]]);
            }
        }

        System.out.println(dp[N-1][M-1]);
    }
}

여러 블로그를 봐도 차이점이 잘 이해되지 않던 중 한 포스팅을 발견했고.. 너무나도 명확히 설명해주셔서 더 정리할게 없지만 제가 보고 싶은 내용만 추려봤습니다. 다이어그램에서는 1:1 / 1:n 관계, 인풋의 위치, 화살표의 방향 등에서 차이점을 확인할 수 있습니다. 


MVC 패턴

특징

  • Input이 컨트롤러로 들어옴
  • 하나의 컨트롤러는 여러 개의 뷰를 참조할 수 있음
  • View와 Model 사이의 의존성이 높음

 

동작

  1. 모든 입력(Input)들은 Controller로 전달됨
  2. Controller는 입력에 해당하는 Model을 업데이트
  3. 업데이트 결과에 따라 View를 선택
  4. Controller는 View를 선택할 뿐, 직접 업데이트를 하지 않음

 


MVP 패턴

특징

  • Model + View + Presenter
  • Input이 View로 들어옴
  • Presenter와 View는 1:1 관계임
  • 모델과 뷰가 분리되어 있고 오직 Presenter를 통해서 상태나 변화를 알려줄 수 있음
  • MVC와는 달리 UI(뷰)와 로직(모델)을 분리하고, 서로 간에 상호작용을 하는 다른 객체(Presenter)를 둠으로써 서로의 의존성을 최소화함
  • 장점: Presenter를 통해서만 데이터를 전달받기 때문에 View와 Model의 의존성이 없음
  • 단점: View가 Presenter를 통해 데이터를 주고받기 때문에 매우 의존적임

 

동작

  1. 모든 입력(Input)들은 View로 전달됨
  2. Presenter는 입력에 해당하는 Model을 업데이트
  3. Model 업데이트 결과를 기반으로 View를 업데이트
  4. Presenter는 해당 View를 참조하고 있음
  5. Presenter는 View와 Model 인스턴스를 가지고, Model과 View 사이의 매개체 역할을 함

 


MVVM 패턴

특징

  • Model + View + View Model
  • Input이 View로 들어옴
  • ViewModel과 View는 1:n 관계지만 MVC에서 화살표 방향이 바뀜
  • VM은 뷰를 표현하기 위해 만든 모델로, 뷰를 나타내기 위한 데이터 처리를 하는 부분
  • Command 패턴과 Data Binding 패턴을 이용하여 V-VM 간 의존성을 없앨 수 있음
  • 장점: V-M, V-VM 사이의 의존성이 없음 → 독립적으로 모듈화 개발 가능
  • 단점: ViewModel의 설계가 쉽지 않음

 

동작

  1. 모든 입력(Input)들은 View로 전달됨
  2. ViewModel은 입력에 해당하는 Presentation Logic을 처리하여 View에 데이터를 전달
  3. ViewModel은 View를 참조하지 않기 때문에 독립적임. 따라서 View는 자신이 이용할 ViewModel을 선택해 바인딩하여 업데이트를 받게 됨
  4. Model이 변경되면 해당하는 ViewModel을 이용하는 View가 자동으로 업데이트됨
  5. ViewModel은 View를 나타내 주기 위한 Model이자, View의 Presentation Logic을 처리

 


References

개요

정의 및 특징

  • Model + View + Controller 의 약자
  • 3가지 형태로 역할을 나누어 개발하는 방법론
  • 비즈니스 처리 로직과 사용자 인터페이스 요소를 분리시켜 서로 영향 없이 개발하도록 함
  • 사용자가 입력을 담당하는 View를 통해 요청을 보내면 해당 요청을 Controller가 받고, Controller는 Model을 통해 데이터를 가져오고, 해당 데이터를 바탕으로 출력을 담당하는 View를 제어해서 사용자에게 전달
  • MVC에 기반을 둔 디자인 패턴으로 MVVM, MVP, MVW 등이 있음

 

모델

  • 데이터와 애플리케이션이 무엇을 할 것인지를 정의하는 부분
  • 내부 비즈니스 로직을 처리하기 위한 역할
  • 컨트롤러가 호출하면 DB와 연동하여 사용자의 입출력 데이터를 다루는, 데이터와 연관된 비즈니스 로직을 처리함(데이터 추출, 저장, 삭제, 업데이트)
  • 여러 개의 데이터 변경 작업을 하나의 작업으로 묶은 트랜잭션을 다루는 일도 수행함
더보기
  1. 사용자가 편집하기를 원하는 모든 데이터를 가지고 있어야 함
    - 화면 안에 글자가 표현된다면 박스의 위치, 크기, 글자 포맷 정보 등을 가지고 있어야 함
  2. 뷰나 컨트롤러에 대해서 어떤 정보도 알지 말아야 함
    - 데이터 변경이 일어났을 때 모델에서 직접 UI를 수정할 수 있도록 뷰를 참조하는 내부 속성값을 가지면 안됨
  3. 변경이 일어나면, 변경 통지에 대한 처리방법을 구현해야만 함
    - 모델의 정보가 변경된다면 이벤트를 발생시켜 누군가에게 전달해야 함
    - 누군가가 모델을 변경하도록 요청하는 이벤트를 보냈을 때 이를 수신할 수 있는 처리 방법을 구현해야 함
    - 모델은 재사용가능해야 하며 다른 인터페이스에서도 변하지 않아야 함

 

  • 화면에 무엇인가를 보여주기 위한 역할
  • 사용자에게 보여주는 화면(UI)에 해당하며, 텍스트, 체크박스 항목과 같은 사용자 인터페이스 요소를 나타냄
  • 사용자와 상호작용을 하며 컨트롤러로부터 받은 모델의 결과값을 출력하는 화면을 만듦
더보기
  1. 모델이 가지고 있는 정보를 따로 저장해서는 안됨
    - 화면에 글자를 표시하기 위해 모델이 갖고 있던 정보를 전달받는데, 이 정보를 유지하기 위해 임의의 뷰 내부에 저장하면 안됨
    - 단순히 명령에 따라 정보를 표시하기만 하고, 그 정보들은 저장하지 않아야 함
  2. 모델이나 컨트롤러와 같은 다른 구성 요소들을 몰라야 함
    - 자기 자신을 제외하고는 다른 요소는 참조하거나 어떻게 동작하는지 알아서는 안됨
    - 그냥 데이터를 받으면 화면에 표시해주는 역할만 수행함
  3. 변경이 일어나면 변경 통지에 대한 처리 방법을 구현해야만 함
    - 화면에서 사용자가 내용을 변경하게 되면 이를 모델에게 전달해야 하며, 이 작업을 하기 위해 변경 통지를 구현함
    - 재사용이 가능하게끔 설계를 해야함

 

컨트롤러

  • 모델이 데이터를 어떻게 처리할지를 알려주는 역할
  • 비즈니스 로직을 처리하는 모델과 뷰 사이에 위치하며 둘이 서로 직접적으로 연결되지 않게 함
  • 클라이언트의 요청에 대해 모델과 뷰를 결정하여 전달하는 일종의 조정자 역할
  • 사용자가 데이터를 클릭하고 수정하는 것에 대한 “이벤트”들을 처리(+데이터 전처리)
  • 사용자의 요청 내용을 받아 Model과 View에 업데이트 요청을 보냄
  • 자바의 Spring에서는 DispatcherServlet으로 컨트롤러를 이해해야 함
더보기
  1. 모델이나 뷰에 대해서 알고있어야 함
    - 모델이나 뷰는 서로의 존재를 모르고 변경을 외부로 알리고/수신하는 방법만 갖고 있지만, 컨트롤러가 이를 중재하기 위해서는 모델과 그에 관련된 뷰에 대해서 알고 있어야 함
  2. 모델이나 뷰의 변경을 모니터링해야 함
    - 모델이나 뷰의 변경 통지를 받으면 이를 해석해서 각각의 구성 요소에게 통지해야 함
    - 애플레키에션의 메인 로직을 컨트롤러가 담당하게 됨

 

장점

  • 사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 쉽게 고칠 수 있음
  • 각 구성 요소들을 독립시켜 협업할 때 맡은 부분의 개발에만 집중할 수 있어 효율성을 높여줌
  • 서로 분리되어 각자의 역할에 집중할 수 있도록 개발하여 유지보수성, 애플리케이션의 확장성, 유연성이 증가하고 중복 코드도 줄어듦

 

단점(한계)

Massive ViewController (대규모 MVC 어플리케이션)

  • 모델과 뷰는 서로의 정보를 갖고 있지 않는 독립적인 상태라고 하지만, 모델과 뷰는 컨트롤러를 통해서 소통하기에 의존성이 완전히 분리될 수는 없음
  • 대규모 프로그램의 경우 화면에 복잡한 데이터의 구성이 필요하다면 컨트롤러에 다수의 모델과 뷰가 복잡하게 연결되는 상황이 생길 수 있음
  • 컨트롤러가 불필요하게 커지는 현상이 발생(Massive ViewController)
  • 컨트롤러는 뷰와 라이프 사이클과 강하게 연결되어 있어 분리할 수도 없고, 코드 분석/수정과 테스트가 모두 힘들어짐
  • 복잡하게 엮여있는 모델과 뷰는 여러 Side-Effect를 불러와 프로그램 운영을 힘들게 함

 


MVC 모델의 종류와 방식

MVC 구동 원리

MVC패턴은 Spring프레임워크와 JSP를 사용하는 웹 애플리케이션 개발에 많이 사용되고 있다. 이 때 웹 애플리케이션에서의 MVC의 구동 원리는 다음과 같다.

  1. 웹 브라우저가 웹 서버에 웹 애플리케이션 실행을 요청(MVC 구조가 WAS라고 보면 됨)
  2. 웹 서버는 들어온 요청을 처리할 수 있는 서블릿을 찾아서 요청을 전달(Matching)
  3. 서블릿은 모델 자바 객체의 메서드를 호출
  4. 데이터를 가공하여 값 객체를 생성하거나, JDBC를 사용하여 데이터베이스와의 인터랙션을 통해 값 객체를 생성
  5. 업무 수행을 마친 결과값을 컨트롤러에게 반환
  6. 컨트롤러는 모델로부터 받은 결과값을 View에게 전달
  7. JSP는 전달받은 값을 참조하여 출력할 결과 화면을 만들고 컨트롤러에게 전달
  8. 뷰로부터 받은 화면을 웹 서버에게 전달
  9. 웹 브라우저는 웹 서버로부터 요청한 결과값을 응답받으면 그 값을 화면에 출력

 

모델1

  • JSP에서 출력과 로직을 전부 처리 (컨트롤러와 뷰 영역을 같이 구현하는 방식)
  • 사용자의 요청을 JSP가 전부 처리
  • 빠르고 쉽게 개발할 수 있으나, JSP 파일이 너무 비대해지며 컨트롤러와 뷰가 혼재하므로 향후 유지보수에 어려움

⇒ 백엔드와 프론트엔드의 역할 분담이 모호해져 협업이 쉽지 않기 때문에 실제 서비스에서는 거의 사용하지 않음

 

모델2

  • JSP에서 출력만 처리
  • 사용자의 요청을 서블릿이 받고, 서블릿은 해당 요청으로 뷰로 보여줄지 모델로 보낼지 판단하여 전송
  • HTML 소스와 자바 소스를 분리해 놓았기 때문에 모델1 방식보다 확장시키기 쉽고 유지보수도 쉬움

 


References

개요

프록시란?

  • ‘대리’라는 의미로 무엇인가를 대신 처리함
  • 보안 상의 이유로 서버를 외부에 노출시키지 않기 위해 서버와 클라이언트단 중간에 위치한 서버를 프록시 서버라 함

 

정의

  • 실제 기능을 수행하는 객체(Real Object)를 바로 접근해서 제어하지 않고, 가상의 객체(Proxy Object)를 통해 우회하여 실제 기능에 접근함
  • 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할을 하는 디자인 패턴
  • 클라이언트 쪽에서는 실제 객체를 통해 메서드를 호출하고 반환 값을 받는지, 대리자 객체를 통해 받는지 전혀 모름

 

클래스 다이어그램

  • 클라이언트가 요청을 보냄(RealSubject의 request()메서드 호출)
  • Proxy가 대신 RealSubject의 request()메서드를 호출
  • 그 반환 값을 클라이언트에게 전달
  • Real Object와 Proxy Object는 동일한 인터페이스를 구현
  • Proxy Object는 메소드 수행 시 Real Object의 메소드에 위임함

 

특징

  • 대리자는 실제 서비스와 같은 이름의 메서드를 구현함(이때 인터페이스를 사용)
  • 대리자는 실제 서비스에 대한 참조 변수를 가짐
  • 대리자는 실제 서비스의 메서드 호출 전후에도 별도의 로직 수행 가능
  • 흐름 제어만 할 뿐 결과값을 조작하거나 변경시키면 안됨
  • 일반적으로 프록시는 서비스 객체들의 전체 수명 주기를 관리함
더보기

⇒ 동기적인 처리를 최대한 비동기적으로 처리하기 위함

1. 프록시 객체를 사용하지 않는 경우

  • 많은 양의 리소스를 필요로 하는 상황에서 DB 쿼리가 많이 느려질 수 있음
  • 지연 초기화를 위한 코드 작성이 필요하는데, 이를 모든 클래스마다 직접 넣을 경우 엄청 많은 코드 중복이 발생함

2. 프록시 객체를 사용하는 경우

  • 프록시 객체가 요청을 먼저 받은 뒤 흐름을 제어하여 DB에 쿼리를 날릴 수 있음
  • 서비스를 유연하게 제공 가능

 

장점

  • 참조를 통한 메모리 공간 확보
    • 해당 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정할 수 있음
    • 사이즈가 큰 객체가 로딩되기 전에도 프록시를 통해서 참조할 수 있음
    • 객체의 기능이 필요한 시점까지 객체의 생성을 미룰 수 있음
    • 클라이언트들의 활성 상태 여부를 확인하여, 비어있을 경우 해당 서비스 객체를 닫고 그에 해당하는 시스템 자원을 확보할 수 있음
  • 흐름 제어
    • 클라이언트의 모든 요청을 받음 → 서비스를 유연하게 제공
    • 실제 메소드가 호출되기 이전에 전처리 등을 구현 객체의 변경 없이 추가할 수 있음
  • 캐시 사용 가능
    • 프록시가 내부 캐시를 통해 데이터가 캐시에 존재하지 않는 경우에만 주체 클래스에서 작업이 실행되도록 할 수 있음
  • 인터페이스를 두기 때문에 개발 코드에서 구현체에 영향을 받지 않아 코드 변경이 최소화
  • 실제 객체의 메소드를 숨기고 인터페이스를 통해 노출시킬 수 있어 방패(보안) 역할
  • 사용자 입장에서는 프록시 객체나 실제 객체나 사용법은 유사하므로 사용성이 좋음

 

활용

  • 기존의 작업 수행하며 부가적인 작업(초기화 지연, 로깅, 액세스 제어, 캐싱 등)을 수행하기 좋음
  • 비용이 많이 드는 연산(DB 쿼리, 대용량 텍스트 파일 등)을 실제로 필요한 시점에 수행할 수 있음
  • 페이지를 로드할 때 이미지는 텍스트보다 용량이 커서 로딩 시간이 오래 걸릴 수 있기 때문에, 프록시의 흐름 제어를 통해 현재까지 완료된 내용들을 우선적으로 보여주도록 함

 

단점

  • 객체를 생성할 때 한 단계를 거치게 되므로 빈번한 객체 생성은 성능을 저하시킬 수 있음
  • 객체 생성을 위해 스레드가 생성, 동기화가 구현되어야 하는 경우 성능을 저하시킬 수 있음
  • 로직이 난해해져 가독성이 떨어짐

 


프록시 패턴의 종류

가상 프록시와 보호 프록시의 사용 예시는 아래 예제 코드의 시나리오를 참고하면 도움이 됩니다.
해당 예제는 이 블로그를 참고했습니다. 

 

가상 프록시(Virtual Proxy)

  • 생성하기 힘든 자원에 대한 접근을 제어
  • 꼭 필요로 하는 시점까지 객체의 생성을 연기하고, 해당 객체가 생성된 것처럼 동작하도록 만들고 싶을 때 사용
  • 프록시 클래스에서 작은 단위의 작업을 처리하고 리소스가 많이 요구되는 작업들이 필요할 경우만 주체 클래스를 사용하도록 구현
  • 무거운 서비스 객체가 항상 가동되며 시스템 자원을 낭비하는 초기화 지연 경우에 사용

 

보호 프록시(Protection Proxy)

  • 접근 권한이 필요한 자원에 대한 접근을 제어
  • 특정 클라이언트에게만 서비스 객체를 사용할 수 있도록 하는 경우에 사용
  • 프록시 클래스에서 클라이언트가 주체 클래스에 대한 접근을 허용할지 말지 결정하도록 함
  • 예를 들어 운영 체제의 중요한 부분에 악의적인 응용 프로그램들이 접근하지 못하도록 할 수 있음

 

원격 프록시(Remote Proxy)

  • 원격 객체에 대한 접근을 제어
  • 서비스 객체가 원격 서버에 존재하는 경우, 네트워크를 통해 클라이언트의 요청을 전달하여, 네트워크와의 작업의 모든 복잡한 세부 사항을 처리함
  • 서로 다른 주소 공간에 있는 객체에 대해 마치 같은 주소 공간에 있는 것처럼 동작하게 하는 패턴
  • Ex: Google Docs

 

기타

  • 로깅 프록시(요청들의 로깅)
    • 서비스 객체에 대한 요청들의 기록을 유지하고 싶은 경우
    • 프록시는 각 요청을 서비스에 전달하기 전에 로깅할 수 있음
  • 캐싱 프록시(요청 결과들의 캐싱)
    • 요청 결과들을 캐싱하고, 이 캐시들의 수명 주기를 관리할 때(특히 결과들이 상당히 큰 경우)에 사용
    • 항상 같은 결과를 생성하는 반복 요청들에 대해 캐싱을 구현
    • 요청들의 매개변수들을 캐시 키로 사용할 수 있음

 


예제 코드

일반

public interface Subject {
    void request();
}
public class RealSubject implements Subject {
 
    @Override
    public void request() {
        System.out.println("Hello World!");
    }
}
public class Proxy implements Subject {
 
    private Subject subject;
 
    @Override
    public void request() {
        // 객체의 초기화를 지연시켜 실제로 사용될 때 생성함으로써 메모리 절약 가능
        if (this.subject == null) {
            this.subject = new RealSubject();
        }
 
        subject.request(); // 프록시가 RealSubject()의 메소드를 호출
    }
}
public class Main {
    public static void main(String[] args) {
        // RealSubject 클래스의 메소드를 호출하는것이 아닌 Proxy 클래스의 메소드를 호출
        Subject subject = new Proxy();
        subject.request(); // 내부적으로 RealSubject의 메소드를 호출
    }
}

 

가상 프록시

콘솔 프로그램으로 20개씩 난독화된 전자 서류의 본문을 복호화해서 보여주는 프로그램 작성한다고 해보자. 아래처럼 텍스트 파일을 읽는 인터페이스가 있다. 메서드가 하나밖에 없는 아주 간단한 인터페이스로, 앞으로 이 인터페이스를 구현하는 클래스는 반드시 fetch() 메서드를 구현해야 한다.

 

interface TextFile {
    String fetch();
}

실제 업무를 수행하기에 앞서 협업 조직에 SecretTextFile 이라는 클래스를 인수 받았다. 이 클래스는 난독화되어 있는 텍스트 파일을 복호화해서 평문으로 바꿔주는 클래스로, 협업 조직에서 라이브러리로 제공한 클래스라고 가정하자. 즉, 나는 이 클래스를 수정할 권한이 없다.

 

class SecretTextFile implements TextFile {
	private String plainText;

	public SecretTextFile(String fileName) {
		// 특별한 복호화 기법을 이용해 데이터를 복원해서 내용을 반환
		this.plainText = SecretFileHolder.decodeByFileName(fileName);
	}

	@Override
	public String fetch() {
		return plainText;
	}
}

이 클래스로 콘솔 프로그램을 구성했으나, 실행 후 첫 결과가 나오기까지 6초라는 시간이 걸렸다. 이유를 확인해보니 SecretTextFile 클래스에서 사용 중인 SecretFileHolder.decodeByFileName() 메서드의 수행 속도가 0.3초였던 것이다. 그리고 목록에 20개의 파일 내용을 노출해야 하는 상태였기 때문에 문제가 된 것이다. 화면을 구성할 때 이 파일들을 전부 객체로 만들다 보니 6초 정도의 로딩 시간을 갖게 된 것이다. 이제 프록시 클래스를 구현하자.

 

class ProxyTextFile implements TextFile {
    private String fileName;
    private TextFile textFile;

    public ProxyTextFile(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public String fetch() {
        if (textFile == null) {
            textFile = new SecretTextFile(fileName);
        }
				// 프록시 객체를 사용하는 경우를 확인하기 위해 [proxy] 문구를 넣음
        return "[proxy] " + textFile.fetch();
    }
}

프록시 패턴을 적용해 필요할 때만 파일 복호화를 하도록 수정하기로 했다. ProxyTextFile 클래스는 객체를 생성할 때에는 별다른 동작을 하지 않지만, 실제로 데이터를 가져와야 할 때는 실제 객체인 SecretTextFile 객체를 만들어내고 기능을 위임한다. 이제 프로그램 코드를 수정하자.

 

void main() {
	List<TextFile> textFileList = new ArrayList<>();

	textFileList.addAll(TextFileProvider.getSecretTextFile(0, 3));
	textFileList.addAll(TextFileProvider.getProxyTextFile(3, 20));

	textFileList.stream().map(TextFile::fetch).forEach(System.out::println);
}

다음의 코드는 처음 3개의 파일만 실제 객체를 사용하고 나머지는 프록시 객체를 사용해 프로그램에서 첫 결과가 나오는 것을 1초 내로 만들도록 한다. textFileList를 사용하는 입장에서는 별다른 조치없이 그대로 사용하면 된다. 콘솔에서 textFileList를 순회하면서 노출한다고 하면 처음 세개는 이미 로딩이 되어 있는 상태이므로 바로 노출하고 그다음 아이템부터는 차근차근 노출할 것이다.

따라서 ProxyTextFile 같은 프록시 클래스를 만들고 기존 SecretTextFile 클래스 대신 사용한것만으로도 초기 객체 생성 시간이 대폭 감소했다. 정말 필요한 시점에만 텍스트 복호화를 하게 된 것이다. 이렇게 초기 비용이 많이 드는 연산이 포함된 객체의 경우 가상 프록시를 사용했을 때 효과를 볼 수 있다.

 

보호 프록시

인사팀에서 인사정보에 대한 데이터 접근을 직책 단위로 세분화 하려고 한다. 기존에는 오로지 인사팀에서만 사용했던 부분이었으나 최근 인사정보를 직책별로 공개해줘야 하는 경우가 생겼다. 따라서 직책에 따라서 조직원의 인사정보 접근을 제어하는 업무를 수행해야 한다. 기존에 인사팀만 사용하던 코드는 아래와 같다.

 

// 직책 등급(차례대로 조직원, 조직장, 부사장)
enum GRADE {
    Staff, Manager, VicePresident
}

// 구성원
interface Employee {
    String getName(); // 구성원의 이름
    GRADE getGrade(); // 구성원의 직책
    String getInformation(Employee viewer); // 구성원의 인사정보(매개변수는 조회자)
}

// 일반 구성원
class NormalEmployee implements Employee {
    private String name;
    private GRADE grade;

    public NormalEmployee(String name, GRADE grade) {
        this.name = name;
        this.grade = grade;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public GRADE getGrade() {
        return grade;
    }

    // 기본적으로 자신의 인사정보는 누구나 열람할 수 있도록 되어있습니다.
    @Override
    public String getInformation(Employee viewer) {
        return "Display " + getGrade().name() + " '" + getName() + "' personnel information.";
    }
}

NormalEmployee 클래스는 단순히 Employee 인터페이스를 구현하기만 한 것으로, getInformation() 메서드는 조직원이 누구든 자신의 인사정보를 열람할 수 있도록 작성이 되어있다. 지금 이 상태로만 놔둔다면 누구든지 Employee 객체에서 getInformation() 메서드를 호출하면 누가 조회하든 정보를 보여줄 것이다. 이제부터 보호 프록시 클래스를 구성해보자. 이 클래스는 조회자의 직책을 확인하고 예외를 던지거나 인사 정보를 노출할 수 있도록 실제 객체에게 위임한다. 보호 프록시에서 메서드 호출시 조회자에게 권한이 없으면 NotAuthorizedException 예외를 던지도록 한다.

 

// 인사정보가 보호된 구성원(인사 정보 열람 권한 없으면 예외 발생)
class ProtectedEmployee implements Employee {
    private Employee employee;

    public ProtectedEmployee(Employee employee) {
        this.employee = employee;
    }

    @Override
    public String getInformation(Employee viewer) {
        // 본인 인사정보 조회
        if (this.employee.getGrade() == viewer.getGrade() && this.employee.getName().equals(viewer.getName())) {
            return this.employee.getInformation(viewer);
        }

        switch (viewer.getGrade()) {
            case VicePresident:
            	// 부사장은 조직장, 조직원들을 볼 수 있다.
                if (this.employee.getGrade() == GRADE.Manager || this.employee.getGrade() == GRADE.Staff) {
                    return this.employee.getInformation(viewer);
                }
            case Manager:
                if (this.employee.getGrade() == GRADE.Staff) { // 조직장은 조직원들을 볼 수 있다.
                    return this.employee.getInformation(viewer);
                }
            case Staff:
            default:
                throw new NotAuthorizedException(); // 조직원들은 다른 사람의 인사정보를 볼 수 없다.
        }
    }

    @Override
    public String getName() {
        return employee.getName();
    }

    @Override
    public GRADE getGrade() {
        return employee.getGrade();
    }
}

class NotAuthorizedException extends RuntimeException {
    private static final long serialVersionUID = -1714144282967712658L;
}

References

해당 포스팅에 사용된 예제는 이 블로그 를 참고했습니다 

개요

등장 배경

  • 객체 지향 디자인 패턴의 기본 원칙(OCP; Open Closed Principle)
    • “확장에 있어서는 열려 있어야 하며, 수정에 있어서는 닫혀있어야 한다”
    • 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있어야 함
    수정이 일어날 가능성이 큰 부분과 그렇지 않은 부분을 분리하는 것이 좋음
  • 객체는 속성과 함수가 변경, 추가될 가능성이 높음 → 객체의 생성을 담당하는 코드는 변경될 가능성이 높음
  • 객체의 생성을 담당하는 클래스를 한 곳에서 관리하여 클라이언트 코드와의 결합도를 줄이기 위해 사용
    • 결합도(의존도)란 한 클래스의 변경 사항이 다른 클래스에 얼마나 영향을 주는가를 의미
    • 클라이언트 코드란 분리시킨 객체 생성 코드를 호출하는 쪽을 말함

 

정의 및 특징

  • 객체를 생성하는 작업을 (팩토리) 클래스에 따로 모아두는 것을 의미
  • 상속 관계에서 부모 클래스가 중요한 뼈대를 결정하고, 자식 클래스가 객체 생성에 관한 구체적인 내용을 결정함
  • 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가짐
  • 크게 2가지 종류를 가짐
    • 팩토리 메소드 패턴: 객체를 생성하기 위한 인터페이스를 정의할 때, 어떤 클래스의 인스턴스를 만들지 서브 클래스에서 결정
    • 추상 팩토리 패턴: 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 인터페이스를 이용하여 생성할 수 있음
      (ex: 핸드폰=기종+OS+…)
    • 추상 팩토리 패턴에 팩토리 메소드 패턴이 포함될 수 있음

 

장점

  • 서로 간의 결합도를 낮추고 확장을 쉽게 함 → 유지보수성 향상
  • 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 더 많은 유연성을 가짐
  • 사용자가 사용할 때는 정의된 인터페이스에 정의된 추상 메소드만 사용하면 됨
  • 단일 책임 원칙을 따름. 프로그램의 코드에서 생성자 코드를 분리함으로써 코드를 더욱 간결하게 만들 수 있음
  • 개방 폐쇄 원칙을 따름. 기존 client의 코드를 파괴하지 않고 새로운 타입을 추가할 수 있음

 

활용

  • 어떤 클래스가 자신이 생성해야 하는 객체의 클래스를 예측할 수 없는 경우
  • 생성할 객체를 기술하는 책임을 자신의 서브 클래스가 지정했으면 할 경우
  • java.util 패키지에 있는 Calendar, ResourceBundle, NumberFormat 등의 클래스에 정의된 getInstance() 메소드에서 팩토리 패턴을 사용
  • Boolean, Integer, Long 등 Wrapper 클래스에 정의된 valueOf() 메소드에서 팩토리 패턴을 사용

 

단점

  • 패턴을 구현할 많은 서브 클래스를 도입함으로써 코드가 복잡해질 수 있음

 

종합

  • 팩토리 패턴에 사용되는 슈퍼 클래스는 인터페이스나 추상 클래스, 혹은 그냥 평범한 자바 클래스여도 상관 없음
  • 팩토리 클래스를 Singleton으로 구현해도 되고, 서브 클래스를 리턴하는 static 메소드로 구현해도 됨
  • 팩토리 메소드는 입력된 파라미터에 따라 다른 서브 클래스의 인스턴스를 생성하고 리턴함

팩토리 메소드 패턴

정의 및 특징

  • 객체를 생성하기 위한 인터페이스는 미리 정의하되, 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정하도록 함
  • 즉, 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것
  • 여러 개의 서브 클래스를 가진 슈퍼 클래스가 있을 때, 인풋에 따라 하나의 자식 클래스의 인스턴스를 리턴해주는 방식

 

예제 코드

1. 프로토콜 생성

protocol Shape {
    func draw()
}

2. 클래스 생성

class Rectangle : Shape {
    func draw() {
        print("Inside Rectangle::draw() method")
    }
}
class Square : Shape {
    func draw() {
        print("Inside Square::draw() method")
    }
}
class Circle : Shape {
    func draw() {
        print("Inside Circle::draw() method")
    }
}

3. 팩토리 생성

class ShapeFactory {
    public func getShape(shapeType : String) -> Shape? {
        if shapeType == nil {
            return nil
        }
        if shapeType == "CIRCLE" {
            return Circle()
        }
        else if shapeType == "RECTANGLE" {
            return Rectangle()
        }
        else if shapeType == "SQUARE"{
            return Square()
        }
        return nil
    }
}

4. 데모

let shapeFactory = ShapeFactory()

let shape1 = shapeFactory.getShape(shapeType: "CIRCLE")
shape1?.draw()

let shape2 = shapeFactory.getShape(shapeType: "RECTANGLE")
shape2?.draw()

let shape3 = shapeFactory.getShape(shapeType: "SQUARE")
shape3?.draw()

5. 출력 결과

Inside Circle::draw() method
Inside Rectangle::draw() method
Inside Square::draw() method

추상 팩토리 패턴

정의 및 특징

  • 팩토리 메소드 패턴과 유사하나, 팩토리를 만드는 상위 팩토리(super-factory) 클래스가 존재
  • 연관된 서브 클래스를 그룹화할 수 있고 그 그룹을 자유롭게 교체할 수 있는 패턴
  • 인터페이스를 이용하여 구체적인 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체들의 조합을 생성함

 

예제 코드

1. 클래스 생성

class RoundedRectangle : Shape {
    func draw() {
        print("Inside RoundedRectangle::draw() method")
    }
}
class RoundedSquare : Shape {
    func draw() {
        print("Inside RoundedSquare::draw() method")
    }
}

2. 슈퍼 팩토리 생성

protocol AbstractFactory {
    func getShape(shapeType : String) -> Shape?
}

3. 하위 팩토리 생성

class ShapeFactory : AbstractFactory {
    public func getShape(shapeType : String) -> Shape? {
        if shapeType == "RECTANGLE" {
            return Rectangle()
        } else if shapeType == "SQUARE"{
            return Square()
        }
        return nil
    }
}
class RoundedShapeFactory : AbstractFactory {
    public func getShape(shapeType : String) -> Shape? {
        if shapeType == "RECTANGLE" {
            return RoundedRectangle()
        } else if shapeType == "SQUARE"{
            return RoundedSquare()
        }
        return nil
    }
}

4. 팩토리를 생성하는 Producer 생성

class FactoryProducer {
    static func getFactory(rounded : Bool) -> AbstractFactory {
        if rounded {
            return RoundedShapeFactory()
        }else {
            return ShapeFactory()
        }
    }
}

5. 데모

let shapeFactory1 = FactoryProducer.getFactory(rounded: false)

let shape1 = shapeFactory1.getShape(shapeType: "RECTANGLE")
shape1?.draw()

let shape2 = shapeFactory1.getShape(shapeType: "SQUARE")
shape2?.draw()


let shapeFactory2 = FactoryProducer.getFactory(rounded: true)

let shape3 = shapeFactory2.getShape(shapeType: "RECTANGLE")
shape3?.draw()

let shape4 = shapeFactory2.getShape(shapeType: "SQUARE")
shape4?.draw()

6. 출력 결과

Inside Rectangle::draw() method
Inside Square::draw() method
Inside RoundedRectangle::draw() method
Inside RoundedSquare::draw() method

 


References

접근

우선 예제를 보고 문제에서 구하고자 하는 바이토닉 수열은 연속되지 않아도 된다는 점을 알았다. 바이토닉 수열은 계속해서 증가하다가 어느 시점을 기점으로 계속해서 감소하는 수열인데, 그러면 배열 하나로 구하기보다는 증가하는 수열의 길이를 담는 배열과 감소하는 수열의 길이를 담는 배열 2개를 계산해서 그 합을 비교하면 될 것 같다. 감소하는 수열은 결국 증가하는 수열을 반대 방향에서 계산한 것과 동일하므로, 부호만 수정해주면 된다. 즉 11053번의 응용 문제이다.

 

그치만 여기까지 생각해놓고 LIS 알고리즘이 생각 안나는 나는,,, 처음에는 스택 두개로 어떻게 해보려고 삽질하다가 이게 맞나 싶어서 결국 솔루션을 찾아봤다. 해보지는 않았는데 스택 방법도 논리적으로는 문제가 없다고 생각한다.. 아마도? 간략하게만 적어보면 스택 2개를 두고, 하나에는 (현재) 가장 긴 증가하는 수열을 저장 / 나머지 하나는 (앞으로) 가장 길 수도 있는 수열을 저장하는 것이었다. 그래서 만약 {7, 8, 9, 1, 2, 3, 4, 5} 가 있다면 뒤쪽의 5숫자가 가장 길기 때문에, 작은 수가 나온다면 임시 후보로 스택에 저장해두다가 그 길이가 더 길어지면 스위치하는 방식을 생각해봤다. 

 

코드

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

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());
        int answer = 0;

        int[] arr = new int[n];
        int[] increase = new int[n];
        int[] decrease = new int[n];

        StringTokenizer st = new StringTokenizer(br.readLine());
        for(int i = 0;i < n;i++)
            arr[i] = Integer.parseInt(st.nextToken());

        // 최장 증가 수열(LIS) 구하기
        for(int i = 0;i < n;i++){
            increase[i] = 1;
            for(int j = 0;j < i;j++){
                if(arr[i] > arr[j])
                    increase[i] = Math.max(increase[i], increase[j] + 1);
            }
        }

        // 최장 감소 수열(LDS) 구하기
        for(int i = n-1;i >= 0;i--){
            decrease[i] = 1;
            for(int j = n-1;j >= i;j--){
                if(arr[i] > arr[j])
                    decrease[i] = Math.max(decrease[i], decrease[j]+1);
            }
        }

        for(int i = 0;i < n;i++)
            answer = Math.max(answer, increase[i] + decrease[i]);

        System.out.println(answer - 1); // 가운데 중복되는 숫자 제외
    }
}

'Algorithm' 카테고리의 다른 글

[백준] 2531: 회전 초밥 JAVA  (0) 2023.01.15
[백준] 20922: 겹치는 건 싫어 JAVA  (0) 2023.01.15
[백준] 12865: 평범한 배낭 JAVA  (0) 2023.01.09
[백준] 1932: 정수 삼각형 JAVA  (0) 2023.01.09
[백준] 1912: 연속합 JAVA  (0) 2023.01.08

+ Recent posts