회전 초밥

조소복·2022년 10월 11일
0
post-custom-banner

문제

회전 초밥 음식점에는 회전하는 벨트 위에 여러 가지 종류의 초밥이 접시에 담겨 놓여 있고, 손님은 이 중에서 자기가 좋아하는 초밥을 골라서 먹는다. 초밥의 종류를 번호로 표현할 때, 다음 그림은 회전 초밥 음식점의 벨트 상태의 예를 보여주고 있다. 벨트 위에는 같은 종류의 초밥이 둘 이상 있을 수 있다.

새로 문을 연 회전 초밥 음식점이 불경기로 영업이 어려워서, 다음과 같이 두 가지 행사를 통해서 매상을 올리고자 한다.

  1. 원래 회전 초밥은 손님이 마음대로 초밥을 고르고, 먹은 초밥만큼 식대를 계산하지만, 벨트의 임의의 한 위치부터 k개의 접시를 연속해서 먹을 경우 할인된 정액 가격으로 제공한다.
  2. 각 고객에게 초밥의 종류 하나가 쓰인 쿠폰을 발행하고, 1번 행사에 참가할 경우 이 쿠폰에 적혀진 종류의 초밥 하나를 추가로 무료로 제공한다. 만약 이 번호에 적혀진 초밥이 현재 벨트 위에 없을 경우, 요리사가 새로 만들어 손님에게 제공한다.

위 할인 행사에 참여하여 가능한 한 다양한 종류의 초밥을 먹으려고 한다. 위 그림의 예를 가지고 생각해보자. k=4이고, 30번 초밥을 쿠폰으로 받았다고 가정하자. 쿠폰을 고려하지 않으면 4가지 다른 초밥을 먹을 수 있는 경우는 (9, 7, 30, 2), (30, 2, 7, 9), (2, 7, 9, 25) 세 가지 경우가 있는데, 30번 초밥을 추가로 쿠폰으로 먹을 수 있으므로 (2, 7, 9, 25)를 고르면 5가지 종류의 초밥을 먹을 수 있다.

회전 초밥 음식점의 벨트 상태, 메뉴에 있는 초밥의 가짓수, 연속해서 먹는 접시의 개수, 쿠폰 번호가 주어졌을 때, 손님이 먹을 수 있는 초밥 가짓수의 최댓값을 구하는 프로그램을 작성하시오.

입력

첫 번째 줄에는 회전 초밥 벨트에 놓인 접시의 수 N, 초밥의 가짓수 d, 연속해서 먹는 접시의 수 k, 쿠폰 번호 c가 각각 하나의 빈 칸을 사이에 두고 주어진다. 단, 2 ≤ N ≤ 3,000,000, 2 ≤ d ≤ 3,000, 2 ≤ k ≤ 3,000 (k ≤ N), 1 ≤ c ≤ d이다. 두 번째 줄부터 N개의 줄에는 벨트의 한 위치부터 시작하여 회전 방향을 따라갈 때 초밥의 종류를 나타내는 1 이상 d 이하의 정수가 각 줄마다 하나씩 주어진다.

출력

주어진 회전 초밥 벨트에서 먹을 수 있는 초밥의 가짓수의 최댓값을 하나의 정수로 출력한다.

예제 입력 1

8 30 4 30
7
9
7
30
2
7
9
25

예제 출력 1

5

예제 입력 2

8 50 4 7
2
7
9
25
7
9
7
30

예제 출력 2

4

문제 풀이

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[] dish=new int[N+k];
        int[] visited=new int[d+1];
        for(int i=0;i<N;i++){
            //초밥 종류
            dish[i]=Integer.parseInt(br.readLine());
        }

        ArrayList<Integer> list=new ArrayList<>();
        int result=1;

        visited[c]++;
        for(int i=0;i<k;i++){
            list.add(dish[i]);
            if(visited[dish[i]]==0) result++;
            visited[dish[i]]++;
        }

        for(int i=N;i<k+N;i++){
            dish[i]=dish[i-N];
        }

        int cnt=result;

        for(int right=k;right<N+k;right++){
            int tmp=list.remove(0);
            visited[tmp]--;
            if(visited[tmp]==0) cnt--;

            if(visited[dish[right]]==0) cnt++;
            visited[dish[right]]++;
            list.add(dish[right]);

            result=Math.max(result,cnt);
        }

        System.out.println(result);
    }
}

슬라이딩 윈도우를 사용해야하는 문제이다.

k개의 접시들을 하나의 윈도우라고 생각하고 한칸씩 이동하여 전체 초밥들 중 연속된 k개의 초밥들을 선택한다고 생각하면 된다.

이때, 초밥들은 처음과 끝이 연결되어있기 때문에 배열을 조금 변형시켜야 한다.


초밥 입력받기

int[] dish=new int[N+k];
int[] visited=new int[d+1];
for(int i=0;i<N;i++){
    //초밥 종류
    dish[i]=Integer.parseInt(br.readLine());
}

ArrayList<Integer> list=new ArrayList<>();
int result=1;

visited[c]++;
for(int i=0;i<k;i++){
    list.add(dish[i]);
    if(visited[dish[i]]==0) result++;
    visited[dish[i]]++;
}

for(int i=N;i<k+N;i++){
    dish[i]=dish[i-N];
}

N개의 접시가 주어졌지만 N까지 반복을 하게 되면 접시의 끝부분과 처음부분이 이어지는 경우는 제외가 된다.

때문에 dish 배열은 N+k 길이로 만들어준다.

그리고 N ~ N+k 까지의 인덱스에는 0 ~ k 인덱스의 값을 넣어준다.

list 배열은 해당 윈도우에 들어있는 초밥들을 체크해주기 위해 만든 Arraylist로 윈도우가 이동할때마다 list의 0 인덱스값을 빼주고 들어오는 값은 뒤에 추가해준다.

visited라는 int 배열을 만들어 방문했는지 체크해준다.

우선 쿠폰값은 무조건 추가해준다고 생각하고 해당 쿠폰 값의 인덱스에 +1 해준다.

처음 k까지의 값은 우선 list에 넣어주고 visited에 값도 체크해준다.

이때, 이미 이전에 먹은 종류의 초밥이라면 cnt값을 추가하지 않고 처음 먹는 종류의 초밥이라면 cnt++해준다.

-> 해당 윈도우 내에서 처음 먹는 초밥인지 체크하기 위해 visited의 값이 1이상인지 확인한다.


최대 초밥 종류 구하기

int cnt=result;

for(int right=k;right<N+k;right++){
    int tmp=list.remove(0);
    visited[tmp]--;
    if(visited[tmp]==0) cnt--;
    
    if(visited[dish[right]]==0) cnt++;
    visited[dish[right]]++;
    list.add(dish[right]);
    
    result=Math.max(result,cnt);
}

처음 k-1까지의 값은 이미 방문처리했기 때문에 k인덱스부터 슬라이딩 윈도우를 수행한다.

슬라이딩 윈도우는 k개까지의 윈도우 길이만 확인하는 것으로 현재 0~k-1까지의 값이 방문되었다면 다음 칸으로 이동했을때 0 인덱스의 값은 빠지고 k 인덱스의 값이 추가되어 값을 비교하는것이다.

따라서 반복문도 k부터 N+k까지 반복하게 되고

0 인덱스의 값을 빼고 방문처리 배열에서도 값을 빼준다.

이때, dish 배열의 0 인덱스가 아니라 윈도우에 들어있는 0 인덱스 값을 빼주어야하기 때문에 list배열에 있는 0 인덱스를 빼준다.

윈도우에서 빠져나온 인덱스의 방문처리 배열값이 0이라면 cnt값도 빼준다.

반대로 들어오는 초밥이 한번도 방문하지 않은 종류라면 cnt값을 추가해주고

list에도 해당 초밥의 값을 넣어준다.

이동할때마다 resultcnt의 최대값을 갱신해준다.

profile
개발을 꾸준히 해보자
post-custom-banner

0개의 댓글