πŸͺ„ Programming

[Algorithm] μŠ¬λΌμ΄λ”© μœˆλ„μš° Sliding Window μ•Œκ³ λ¦¬μ¦˜ 톺아보기

ColorFilter 2024. 11. 11. 22:36
λ°˜μ‘ν˜•

μŠ¬λΌμ΄λ”© μœˆλ„μš° μ•Œκ³ λ¦¬μ¦˜ κ°œλ…


μŠ¬λΌμ΄λ”© μœˆλ„μš° μ•Œκ³ λ¦¬μ¦˜(Sliding Window Algorithm)은 λ°°μ—΄κ³Ό 같은 μ„ ν˜• μžλ£Œκ΅¬μ‘°μ—μ„œ μ—°μ†λœ ꡬ간 λ‚΄μ˜ 데이터λ₯Ό 효율적으둜 μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄ μ‚¬μš©ν•˜λŠ” 기법이닀. 특히 λ°°μ—΄ λ‚΄ μ—°μ†λœ μš”μ†Œμ˜ ν•©, μ΅œλŒ“κ°’, μ΅œμ†Ÿκ°’ 등을 계산할 λ•Œ μœ μš©ν•˜λ‹€.

 

μŠ¬λΌμ΄λ”© μœˆλ„μš°λŠ” 이름 κ·ΈλŒ€λ‘œ κ³ μ •/κ°€λ³€ 크기의 λ²”μœ„(μœˆλ„μš°)λ₯Ό 이동(μŠ¬λΌμ΄λ“œ) μ‹œν‚€λ©΄μ„œ ν•„μš”ν•œ 계산을 λ°˜λ³΅ν•˜λŠ” 방식이닀. μ΄λ•Œ 전체 배열을 ν•œ 번만 μˆœνšŒν•˜λ©΄ 되기 λ•Œλ¬Έμ— 쀑볡 연산을 ν”Όν•˜κ³  μ‹œκ°„ λ³΅μž‘λ„λ₯Ό κ°œμ„ ν•  수 μžˆλ‹€. 일반적으둜 μŠ¬λΌμ΄λ”© μœˆλ„μš° 기법을 μ‚¬μš©ν•˜λ©΄ O(n)의 μ‹œκ°„ λ³΅μž‘λ„λ‘œ 문제λ₯Ό ν•΄κ²°ν•  수 μžˆλ‹€.

// μ£Όμ–΄μ§„ λ°°μ—΄μ—μ„œ μ—°μ†λœ k개 μš”μ†Œμ˜ μ΅œλŒ€ 합을 μ°ΎλŠ” ν•¨μˆ˜
function maxSumSubarray(arr, k) {
  let maxSum = 0;
  let windowSum = 0;

  // 초기 μœˆλ„μš°(k개의 μš”μ†Œ) ν•© 계산
  for (let i = 0; i < k; i++) {
    windowSum += arr[i];
  }
  maxSum = windowSum;

  // μŠ¬λΌμ΄λ”© μœˆλ„μš° 이동
  for (let end = k; end < arr.length; end++) {
    const start = end - k;
    // arr[end]κ°€ μƒˆλ‘œ μΆ”κ°€λ˜λŠ” μš”μ†Œ μ΄λ―€λ‘œ arr[end] - arr[start] μˆœμ„œλ‘œ 계산
    windowSum += arr[end] - arr[start];
    maxSum = Math.max(maxSum, windowSum);
  }

  return maxSum;
}

const result = maxSumSubarray([2, 1, 5, 1, 3, 2], 3);
console.log(result); // 9

 

μœ„ μ˜ˆμ‹œλŠ” μŠ¬λΌμ΄λ”© μœˆλ„μš° 기법을 μ‚¬μš©ν•˜μ—¬ λ°°μ—΄ λ‚΄ μ—°μ†λœ k개 μš”μ†Œμ˜ μ΅œλŒ€ 합을 μ°ΎλŠ” ν•¨μˆ˜λ‹€. 예λ₯Ό λ“€μ–΄ 초기 λ°°μ—΄ [2, 1, 5, 1, 3, 2] μ—μ„œ 처음 3개 μš”μ†Œμ˜ 합은 2 + 1 + 5 = 8 이닀. 이후 μœˆλ„μš°λ₯Ό ν•œ μΉΈμ”© μ΄λ™ν•˜λ©΄μ„œ k개의 μš”μ†Œλ₯Ό 더해 λ‚˜κ°„λ‹€.

 

  • (215)132 → 2 + 1 + 5 = 8
  • 2(151)32 → 1 + 5 + 1 = 7
  • 21(513)2 → 5 + 1 + 3 = 9 (μ΅œλŒ“κ°’)
  • 215(132) → 1 + 3 + 2 = 6

 

ν•œνŽΈ 맀번 μœˆλ„μš°μ— μžˆλŠ” λͺ¨λ“  k개 μš”μ†Œλ₯Ό λ°˜λ³΅ν•΄μ„œ 더할 ν•„μš” 없이, 이전 μœˆλ„μš°μ—μ„œ κ³„μ‚°ν•œ 값에, μƒˆλ‘œ μΆ”κ°€ν•  μš”μ†Œμ™€ μ œκ±°ν•  μš”μ†Œλ₯Ό λ°˜μ˜ν•˜λ©΄ 합계λ₯Ό 더 효율적으둜 μ—…λ°μ΄νŠΈν•  수 μžˆλ‹€. 이전 μœˆλ„μš° 계산값 + (μΆ”κ°€ν•˜λŠ” μš”μ†Œ - λΉ μ§€λŠ” μš”μ†Œ)

 

μ‹œκ°μ μœΌλ‘œ ν‘œν˜„ν•΄ 보면 μ•„λž˜μ™€ κ°™λ‹€.

μŠ¬λΌμ΄λ”© μœˆλ„μš° κΈ°λ²•μœΌλ‘œ λ°°μ—΄λ‚΄ μ—°μ†λœ k개 μš”μ†Œμ˜ μ΅œλŒ€ 합을 μ°ΎλŠ” κ³Όμ •

 

μŠ¬λΌμ΄λ”© μœˆλ„μš° μ•Œκ³ λ¦¬μ¦˜ ν™œμš©


ν”„λ‘œκ·Έλž˜λ¨ΈμŠ€ 레벨 2 - 할인행사 문제(131127)도 μŠ¬λΌμ΄λ”© μœˆλ„μš° 기법을 ν™œμš©ν•  수 μžˆλ‹€. μ£Όμ–΄μ§„ 할인 λͺ©λ‘(discount)μ—μ„œ 10일 연속 ꡬ간에 ν¬ν•¨λ˜μ–΄ μžˆλŠ” μ œν’ˆ/μˆ˜λŸ‰κ³Ό, κ΅¬λ§€ν•˜κ³  싢은 μ œν’ˆ/μˆ˜λŸ‰μ΄ μ •ν™•νžˆ μΌμΉ˜ν•˜λŠ” 경우의 총개수λ₯Ό κ΅¬ν•˜λŠ” λ¬Έμ œλ‹€.

 

  • want: ꡬ맀 μ œν’ˆ λͺ©λ‘ (λ°°μ—΄)
  • number: ꡬ맀 μ œν’ˆ μˆ˜λŸ‰ (λ°°μ—΄, want λ°°μ—΄ μΈλ±μŠ€μ™€ λ§€ν•‘)
  • discount : 할인 μ œν’ˆ λͺ©λ‘ (λ°°μ—΄, 각 μΈλ±μŠ€λŠ” λ‚ μ§œλ₯Ό 가리킴)

 

문제λ₯Ό ν•΄κ²°ν•˜λ €λ©΄ discount 배열을 0번 μΈλ±μŠ€λΆ€ν„° μˆœνšŒν•˜λ©΄μ„œ, ν•΄λ‹Ή μœ„μΉ˜μ—μ„œ μ‹œμž‘ν•˜λŠ” 10일 κ΅¬κ°„μ˜ 할인 μ œν’ˆ/μˆ˜λŸ‰μ΄, ꡬ맀 μ œν’ˆ/μˆ˜λŸ‰κ³Ό μΌμΉ˜ν•˜λŠ”μ§€ μ—¬λΆ€λ₯Ό ν™•μΈν•˜λ©΄ λœλ‹€.

 

μ΄λ•Œ 10일 κ΅¬κ°„μ˜ λͺ¨λ“  μ œν’ˆ μˆ˜λŸ‰μ„ 맀번 계산할 ν•„μš” 없이, 이전 κ΅¬κ°„μ—μ„œ κ³„μ‚°ν•œ μœˆλ„μš° 정보에 μΆ”κ°€/μ œκ±°λ˜λŠ” μ œν’ˆμ˜ μˆ˜λŸ‰λ§Œ μ‘°μ ˆν•˜λŠ” λ°©μ‹μœΌλ‘œ λΆˆν•„μš”ν•œ 연산을 쀄일 수 μžˆλ‹€.

const WINDOW_SIZE = 10;

function solution(want, number, discount) {
  const wantMap = want.reduce((map, item, i) => map.set(item, number[i]), new Map());
  const windowMap = new Map();
  let count = 0;

  // 초기 μœˆλ„μš° μ„€μ • (첫 10일)
  // chicken => 1, apple => 3, banana => 2, rice => 2, pork => 2
  for (let i = 0; i < WINDOW_SIZE; i++) {
    windowMap.set(discount[i], (windowMap.get(discount[i]) ?? 0) + 1);
  }

  // ν˜„μž¬ μœˆλ„μš°κ°€ 쑰건을 λ§Œμ‘±ν•˜λŠ”μ§€ 확인
  const checkIfValid = () => {
    for (const [item, qty] of wantMap.entries()) {
      if ((windowMap.get(item) ?? 0) < qty) return false;
    }
    return true;
  };

  // 첫 10일 체크
  if (checkIfValid()) count++;

  // μŠ¬λΌμ΄λ”© μœˆλ„μš°λ‘œ 탐색
  for (let i = WINDOW_SIZE; i < discount.length; i++) {
    const oldItem = discount[i - WINDOW_SIZE]; // 이전 ν•­λͺ© 제거
    const newItem = discount[i]; // μƒˆλ‘œ μΆ”κ°€λœ ν•­λͺ© μΆ”κ°€

    // μœˆλ„μš° μ—…λ°μ΄νŠΈ
    // i10 = chicken => 1, apple => 3, banana => 2, rice => 2, pork => 2
    // i11 = chicken => 0, apple => 3, banana => 2, rice => 2, pork => 2, pot => 1
    // i12 = chicken => 0, apple => 2, banana => 3, rice => 2, pork => 2, pot => 1
    // ...
    windowMap.set(oldItem, windowMap.get(oldItem) - 1);
    windowMap.set(newItem, (windowMap.get(newItem) ?? 0) + 1);

    // 쑰건 만쑱 μ—¬λΆ€ 체크
    if (checkIfValid()) count++;
  }

  return count;
}
더보기
const want = ["banana", "apple", "rice", "pork", "pot"];
const number = [3, 2, 2, 2, 1];
const discount = [
  "chicken",
  "apple",
  "apple",
  "banana",
  "rice",
  "apple",
  "pork",
  "banana",
  "pork",
  "rice",
  "pot",
  "banana",
  "apple",
  "banana",
];

const result = solution(want, number, discount);
console.log(result); // 3

 

  1. ꡬ맀해야 ν•˜λŠ” μ œν’ˆ λͺ©λ‘/μˆ˜λŸ‰μ„ λ§€ν•‘ν•œ Map 생성 (wantMap)
  2. 초기 μœˆλ„μš° Map 생성 (windowMap)
  3. μŠ¬λΌμ΄λ”© μœˆλ„μš°λ‘œ discount λ°°μ—΄ 탐색 및 windowMap μ—…λ°μ΄νŠΈ
  4. μ—…λ°μ΄νŠΈν•œ windowMapκ³Ό wantMap 비ꡐ

 

μœ„ 과정을 μ‹œκ°μ μœΌλ‘œ ν‘œν˜„ν•΄ 보면 μ•„λž˜μ™€ κ°™λ‹€.

μŠ¬λΌμ΄λ”© μœˆλ„μš° κΈ°λ²•μœΌλ‘œ discount 배열을 νƒμƒ‰ν•˜λ©΄μ„œ μœˆλ„μš°λ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” κ³Όμ •

 


κΈ€ μˆ˜μ •μ‚¬ν•­μ€ λ…Έμ…˜ νŽ˜μ΄μ§€μ— κ°€μž₯ λΉ λ₯΄κ²Œ λ°˜μ˜λ©λ‹ˆλ‹€. 링크λ₯Ό μ°Έκ³ ν•΄ μ£Όμ„Έμš”
λ°˜μ‘ν˜•