[Python - Dynamic Programming] 11054 가장 긴 바이토닉 부분 수열

2022. 12. 25. 11:07· Algorithm/Dynamic Programming
목차
  1. 📌 문제
  2. 📌 입력
  3. 📌 출력
  4. 📌 문제 풀이
  5. 👨‍🏫 접근
  6. 👨‍🏫 문제 풀이
  7. 📌 총평

📌 문제

수열 S가 어떤 수 Sk를 기준으로 S1 < S2 < ... Sk-1 < Sk > Sk+1 > ... SN-1 > SN을 만족한다면, 그 수열을 바이토닉 수열이라고 한다.

 

예를 들어, {10, 20, 30, 25, 20}과 {10, 20, 30, 40}, {50, 40, 25, 10} 은 바이토닉 수열이지만,  {1, 2, 3, 2, 1, 2, 3, 2, 1}과 {10, 20, 30, 40, 20, 30} 은 바이토닉 수열이 아니다.

 

수열 A가 주어졌을 때, 그 수열의 부분 수열 중 바이토닉 수열이면서 가장 긴 수열의 길이를 구하는 프로그램을 작성하시오.

📌 입력

첫째 줄에 수열 A의 크기 N이 주어지고, 둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (1 ≤ N ≤ 1,000, 1 ≤ Ai ≤ 1,000)

📌 출력

첫째 줄에 수열 A의 부분 수열 중에서 가장 긴 바이토닉 수열의 길이를 출력한다.

📌 문제 풀이

👨‍🏫 접근

부분적으로 증가하는지 체크하기 위해서는 동적 계획법을 사용해주어야 한다. 동적으로 데이터를 갱신하고 사용해 나가면서 시간 복잡도를 크게 줄일 수 있다. 이 경우에도 그러하다.

 

수열을 재귀를 통해서 계속해서 반복하는 것이 아니라, 이전에 계산을 끝마쳤던 부분이 있다면 데이터를 버리지 않고 재활용하는 것입니다. 특히 이 문제는 가장 긴 증가하는 부분 수열 문제와 같은 결의 문제입니다. 가장 긴 증가하는 부분 수열 문제를 풀기 위해서 특정 인덱스를 기준으로 잡고, 처음부터 특정 인덱스 바로 전 인덱스까지 순회하며, 특정 인덱스의 값보다 작을 때, dp 테이블에서 순회한 인덱스 위치의 요소에 1을 더한 것과 특정 인덱스에 위치한 요소 중 더 큰 값을 취해서 업데이트 해줍니다.

 

이렇게 문제를 푸는 이유는 N번 인덱스와 1 ... N - 1 인덱스를 비교해서 n번 인덱스 요소가 k (0 <= k < N) 보다 크다는 것은 부분 수열에서 수열의 길이를 확장시킬 수 있다는 의미이기 때문입니다. 예를 들어 { 1, 3, 2, 4, 5, 3, 6 } 이라는 수열이 있고, 4번 인덱스 값인 5를 기준으로 가장 긴 수열을 찾는다고 가정하자면, { 5 } -> { 1, 5 } -> { 1, 3, 5 } -> { 1, 3, 5 } 혹은 { 1, 2, 5 } -> { 1, 3, 4, 5 } 혹은 { 1, 2, 4, 5 } 이렇게 수를 늘려갈 수 있습니다.

 

이때, 4를 기준으로 가장 긴 증가하는 부분 수열을 구하면 { 1, 3, 4 } 혹은 { 1, 2, 4 }로 두 가지의 수열이 존재합니다. 그래서 이전 값들 중에서 4를 값으로 가지는 3번 인덱스의 dp 테이블 값을 통해서 쉽게 증가하는 수열의 길이를 얻을 수 있습니다.

 

이를 활용하여 가장 긴 바이토닉 부분 수열을 구하면 됩니다. 가장 긴 바이토닉 부분 수열은 특정 인덱스를 기준으로, 왼쪽으로 갈수록 혹은 오른쪽으로, 양 옆으로 갈수록 작아지는 부분 수열 중에서 가장 길이가 긴 수열을 의미합니다. 즉, 가장 긴 증가하는 부분 수열과 가장 긴 감소하는 부분 수열을 서로 결합하여 문제를 풀어야 합니다. 가장 긴 증가하는 부분 수열과 가장 긴 감소하는 부분 수열은 서로 독립적이기 때문에 서로 더해서 문제를 풀 수 있습니다.

 

문제 예제에서는 {1 5 2 1 4 3 4 5 2 1} 라는 바이토닉 수열이 가장 긴 부분 수열이었습니다. 이를 가장 긴 증가하는 부분 수열, 가장 긴 감소하는 부분 수열로 나누어서 바라보겠습니다. 먼저 가장 긴 증가하는 부분 수열 입장에서는 { 1, 2, 3, 4, 5 }가 가장 긴 증가하는 부분 수열이며, 가장 긴 감소하는 부분 수열 입장에서는 { 5, 4, 3, 2, 1 } 이 가장 긴 감소하는 부분 수열입니다. 하지만, 둘을 이어서 바이토닉 부분 수열로 만들 수는 없습니다. 왜냐하면 가장 긴 증가하는 부분 수열의 top 요소와 가장 긴 감소하는 부분 수열의 bottom 요소는 같은 인덱스를 가지지 않기 때문입니다. 그래서 서로의 top과 bottom이 동일한 경우에만 바이토닉 수열로 인정할 수 있게 됩니다.

 

그러면 이를 dp 테이블로 확인해서 문제를 풀어보겠습니다. 먼저 가장 긴 증가하는 부분 수열부터 분석해보자면 아래와 같습니다.

{1 5 2 1 4 3 4 5 2 1}

1 2 2 1 3 3 4 5 2 1

그리고 가장 긴 감소하는 부분 수열의 dp 테이블은 아래와 같습니다.

1 5 2 1 4 3 3 3 2 1

쉽게 생각해서 각 dp 테이블에서 같은 인덱스의 값들을 모두 더해주고 1을 빼주면 바이토닉 부분 수열의 길이를 얻을 수 있습니다. -1을 하는 이유는 top과 bottom이 동일한 인덱스이기 때문에 중복되는 값을 제거해주기 위함입니다. 그래서 총 길이를 나타낸 테이블은 아래와 같습니다.

1 6 3 1 6 5 6 7 3 1

이렇게 두 번의 연산을 통해서 가장 긴 바이토닉 부분 수열을 찾을 수 있었습니다. 그대로 문제에 접목시켜 풀이해보도록 해봅시다.

 

👨‍🏫 문제 풀이

📄 전체 코드

def traverse_left(arr, n):
  dp = [1] * n
  for i in range(1, n):
    for j in range(i):
      if arr[j] < arr[i]:
        dp[i] = max(dp[i], dp[j] + 1)
    
  return dp

def traverse_right(arr, n):
  dp = [1] * n
  for i in range(n - 1, -1, -1):
    for j in range(n - 1, i, -1):
      if arr[j] < arr[i]:
        dp[i] = max(dp[i], dp[j] + 1)
  
  return dp

n = int(input())
arr = list(map(int, input().split()))

dp_left = traverse_left(arr, n)
dp_right = traverse_right(arr, n)

dp = [a + b for a, b in zip(dp_left, dp_right)]

print(max(dp) - 1)

📄 준비

def traverse_left(arr, n):
  dp = [1] * n
  for i in range(1, n):
    for j in range(i):
      if arr[j] < arr[i]:
        dp[i] = max(dp[i], dp[j] + 1)
    
  return dp

def traverse_right(arr, n):
  dp = [1] * n
  for i in range(n - 1, -1, -1):
    for j in range(n - 1, i, -1):
      if arr[j] < arr[i]:
        dp[i] = max(dp[i], dp[j] + 1)
  
  return dp

가장 긴 증가하는 부분 수열을 위해 특정 인덱스에서 모든 왼쪽 인덱스를 순회하는 함수와 가장 긴 감소하는 부분 수열을 위해 특정 인덱스에서 모든 오른쪽 인덱스를 순회하는 함수 두 개를 정의했습니다.

 

traverse_left는 i 인덱스를 기준으로 0 ... i - 1 인덱스를 순회하면서 자신보다 값이 작은 경우에 dp 테이블에서 자신의 인덱스 값과 비교해 최댓값을 업데이트 해줍니다.

 

traverse_right도 마찬가지로 해주면 됩니다.

📄 풀이

n = int(input())
arr = list(map(int, input().split()))

dp_left = traverse_left(arr, n)
dp_right = traverse_right(arr, n)

dp = [a + b for a, b in zip(dp_left, dp_right)]

print(max(dp) - 1)

문제의 각 입력들을 받아주고, 가장 긴 증가하는 부분 수열 dp 테이블과 가장 긴 감소하는 부분 수열 dp 테이블을 구해줍니다. 그 후에 최종적인 dp 테이블을 생성해주고, zip함수로 각 인덱스를 모두 순회하면서 더해서 새로운 바이토닉 부분 수열 dp 테이블을 완성합니다.

 

이때, 각 수열의 길이는 한 인덱스가 중복되어 있기 때문에 1을 빼주어야 합니다.

📌 총평

SW 소프트웨어 마에스트로가 슬슬 모집을 시작하는 시기이기 때문에 다시 코테를 공부하고 있습니다. 오랜만에 풀어서 감이 오지 않았는데 다행히 무난한 문제여서 금방 풀었습니다. DP는 점화식을 잘 세워야 하는데 아직 이 부분이 부족한 듯 싶어 아쉽습니다.

저작자표시 비영리 (새창열림)

'Algorithm > Dynamic Programming' 카테고리의 다른 글

[Python - Dynamic Programming] 2225 합분해  (0) 2022.09.27
[Python - Dynamic Programming] 2482 색상환  (1) 2022.09.25
[Python - Dynamic Programming] 11048 이동하기  (0) 2022.09.25
[Python - Dynamic Programming] 11660 구간 합 구하기 5  (1) 2022.09.23
[Python - Dynamic Programming] 9657 돌 게임 3  (1) 2022.09.23
  1. 📌 문제
  2. 📌 입력
  3. 📌 출력
  4. 📌 문제 풀이
  5. 👨‍🏫 접근
  6. 👨‍🏫 문제 풀이
  7. 📌 총평
'Algorithm/Dynamic Programming' 카테고리의 다른 글
  • [Python - Dynamic Programming] 2225 합분해
  • [Python - Dynamic Programming] 2482 색상환
  • [Python - Dynamic Programming] 11048 이동하기
  • [Python - Dynamic Programming] 11660 구간 합 구하기 5
턴태
턴태
import { Dream } from "future";
턴태
턴태의 밑바닥부터 시작하는 de-vlog
턴태
전체
오늘
어제
  • ROOT (187)
    • Node.js (37)
      • ES6 (1)
      • TypeScript (3)
      • Express.js (16)
      • NestJS (16)
      • JS (24)
    • 프론트엔드 (29)
      • CS (5)
    • 백엔드 (1)
      • 검색 (2)
      • Database (1)
    • 기타 툴 (1)
      • git (1)
    • 데브옵스 & 인프라 (19)
      • Kubernetes (15)
      • Docker (2)
      • Monitoring (1)
      • IaC (1)
    • Algorithm (90)
      • Implementation & simulation (5)
      • Math (4)
      • Brute Force (1)
      • String (0)
      • Graph (5)
      • Recursion & Backtracking (19)
      • Divide & Conquer (2)
      • Dynamic Programming (18)
      • Greedy (13)
      • Priority Queue (2)
      • Binary Search (6)
      • Data Structure (7)
      • Shortest Path (5)
      • Minimum Spanning Tree (1)
      • Sorting (1)
      • Prefix Sum (1)
    • Linux (1)
      • Ubuntu (1)
    • Diary (5)
      • Algorithm (1)
      • Conference (1)
      • Retrospective (3)
    • Book (0)
      • Self-Development (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • TypeScript
  • 백준
  • python
  • 자바스크립트
  • Express
  • 토이프로젝트
  • 노드
  • Toy Project
  • 파이썬
  • baekjoon
  • N과 M
  • 다이나믹 프로그래밍
  • 백트래킹
  • 네스트
  • dynamic programming
  • 디프만
  • 인프런X디프만
  • 오먹
  • 타입스크립트
  • 쿠버네티스
  • 함수형 프로그래밍
  • Omuk
  • backtracking
  • 익스프레스
  • Kubernetes
  • k8s
  • 인프런
  • GREEDY
  • node.js
  • nestjs

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.1
턴태
[Python - Dynamic Programming] 11054 가장 긴 바이토닉 부분 수열
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.