XYZ 마트에서는 10일 동안 회원 할인이 적용되며 하루에 하나의 할인 품목만 구매할 수 있다.
정현이는 자신이 원하는 상품과 수량이 연속된 10일 동안의 할인 목록과 정확히 일치하는 시작 날짜에 회원가입하려 한다.
주어지는 정보
want : 원하는 상품 목록number : 각 상품의 필요한 수량discount : 날짜별 할인 상품목표
제약 조건
discount 최대 길이 : 100,000want 최대 길이 : 10number 합 : 10처음에는 다음과 같은 방식으로 접근하였다.
want에 있는 각 상품에 대해discount에서 해당 상품이 등장하는 날짜 인덱스 목록을 저장이 접근 방식에는 몇 가지 문제가 있었다.
문제의 핵심은 다음과 같은 연속 구간 검사이다.
discount[s] ~ discount[s+9]
하지만 상품 등장 날짜 기준으로 접근하면
예를 들어
banana 3
apple 2
같은 경우 단순히 등장 여부가 아니라
10일 구간 안에서 정확한 개수
를 만족해야 한다.
날짜 리스트 방식은 개수 관리가 매우 복잡해지는 문제가 있었다.
discount 최대 길이는
100000
하지만 검사 대상은 10일 구간이다.
따라서
100000 * 10 = 1,000,000
정도의 연산이면 충분하다.
즉 복잡한 구조 없이도 해결 가능하다.
이 문제는 슬라이딩 윈도우 + 빈도 맵 패턴으로 해결할 수 있다.
핵심 아이디어
연속된 10일 구간을 하나의 윈도우로 유지
윈도우를 한 칸씩 이동시키면서
빠지는 상품 제거
새로 들어오는 상품 추가
하여 현재 구간의 상품 개수를 관리한다.
need[상품] = 필요한 개수
cur[상품] = 현재 할인 구간에 등장한 개수
discount[0] ~ discount[9]
구간의 상품 개수를 cur에 기록
다음 날짜로 이동할 때
discount[i-10] 제거
discount[i] 추가
모든 want 상품에 대해
cur[item] == need[item]
이면 조건 만족
윈도우 이동
O(N)
검사
O(want)
최종
O(N * want)
최대 연산량
100000 * 10 = 1,000,000
충분히 빠르게 해결 가능하다.
#include <string>
#include <vector>
#include <unordered_map>
using namespace std;
int solution(vector<string> want, vector<int> number, vector<string> discount) {
int answer = 0;
unordered_map<string, int> need;
for (int i = 0; i < want.size(); i++) {
need[want[i]] = number[i];
}
unordered_map<string, int> cur;
auto add = [&](const string& item, int delta) {
int next = (cur.count(item) ? cur[item] : 0) + delta;
if (next == 0) cur.erase(item);
else cur[item] = next;
};
auto isMatch = [&]() {
for (int i = 0; i < want.size(); i++) {
int have = cur.count(want[i]) ? cur[want[i]] : 0;
if (have != need[want[i]]) return false;
}
return true;
};
if (discount.size() < 10) return 0;
for (int i = 0; i < 10; i++) {
add(discount[i], 1);
}
if (isMatch()) answer++;
for (int i = 10; i < discount.size(); i++) {
add(discount[i - 10], -1);
add(discount[i], 1);
if (isMatch()) answer++;
}
return answer;
}
다음과 같은 키워드가 등장하면 슬라이딩 윈도우를 먼저 고려하는 것이 좋다.
연속 구간
k일
부분 배열
고정 길이 구간
고정 길이 구간 문제에서는
을 얻을 수 있다.
몬스터 웨이브를 모두 처치했을 때 Night 페이즈가 끝나기를 기다리지 않고 즉시 보상 페이즈(Dawn)로 이동하도록 기능을 추가하였다.
구현 방식
하지만 테스트 과정에서 보상 페이즈가 표시되지 않고 바로 Day 페이즈로 넘어가는 문제가 발생하였다.
결과 패널이나 보상 UI가 나타나기 전에 페이즈가 넘어가 보상 페이즈가 스킵된 것처럼 보이는 현상이 발생하였다.
DayNightCycle의 구조를 확인한 결과 각 페이즈는 다음과 같은 방식으로 동작하고 있었다.
EnterX()
→ OnXStarted.Broadcast()
→ SetTimer()로 다음 페이즈 예약
예시
EnterDawn()
{
OnDawnStarted.Broadcast();
SetTimer(... EnterDay ...);
}
즉 Dawn에 진입하면 자동으로 Day로 넘어가는 타이머가 예약되는 구조였다.
문제 발생 흐름
ForceToDawn() 호출EnterDawn() 실행결과적으로 보상 UI가 표시되기 전에 Day 페이즈로 넘어가면서 보상 페이즈가 스킵된 것처럼 보였다.
기존 DayNightCycle의 자동 진행 구조를 유지하는 방식으로 해결하였다.
수정 내용
void APotatoGameMode::HandleRoundFinished(int32 Round)
{
if (Round != CurrentDay) return;
if (DayNightSystem)
{
DayNightSystem->ForceToDawn(true);
}
}
시스템 흐름이 다음과 같이 개선되었다.
몬스터 전멸
→ 웨이브 종료 Delegate
→ GameMode 수신
→ Dawn 페이즈 강제 진입
→ DawnDuration 동안 보상 UI 표시
→ 이후 자동으로 Day 페이즈 진행
이를 통해
라는 세 가지 목표를 모두 만족할 수 있었다.