ข้อนี้เป็นโจทย์ Dynamic Programming แบบ Sliding Window
Naive Dynamic Programming Solution
แนวคิดพื้นฐานของข้อนี้คือใช้ Dynamic Programming ในการคำนวณ ซึ่งนิยามเป็นค่าใช้จ่ายที่ต่ำที่สุดที่จะทำให้สามารถต่อจากแปลงที่ ไปยังแปลงที่ (รวมถึงสร้างแปลงที่ )
การคำนวน ต้องพิจารณาเพียงแปลง ถึงแปลง และเลือกอันที่มี ต่ำสุด
แนวคิดนี้สามารถเขียนเป็นโค้ด:
min_cost[1] = P[1]; for (int i = 2; i <= N; i++) { min_cost[i] = 1000000001; // inf for(int j = max(i - k, i); j < i; j++) { min_cost[i] = min(min_cost[i], min_cost[j] + P[i]); } }
ผลลัพธ์สำหรับตัวอย่างข้อมูลนำเข้าชุดแรก
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
1 | 4 | 2 | 6 | 2 | 4 | 2 | |
1 | 5 | 3 | 7 | 5 | 7 | 7 | |
1 | |||||||
1 | 5 | ||||||
1 | 5 | 3 | |||||
5 | 3 | 7 | |||||
3 | 7 | 5 | |||||
7 | 5 | 7 |
(อธิบายสัญกรณ์: คือ Array ของค่า สำหรับ ) เช่น คำนวณจาก
สังเกตว่าในโค้ดนี้ต้องคำนวณ สำหรับ ซี่งทุก จะต้องพิจารณาอย่างมาก ค่าเพื่อหาค่าต่ำสุดในช่วง
วิธีนี้จึงมี Time Complexity เป็น ซึ่งมากเกินไปสำหรับ
Full Solution
หากใช้โครงสร้างข้อมูลเพื่อลด Time Complexity ของการหาค่าต่ำสุดใน ค่าที่ผ่านมาให้เหลือ ได้จะทำให้ Time Complexity เหลือ ซึ่งเร็วพอสำหรับ
สังเกตว่าทุกครั้งที่ Query หาค่าต่ำสุดนี้เราจะ Query หาค่าต่ำสุดในช่วง (Window) ที่ขยับไปทางขวาทุกครั้ง
ดังนั้นเราจึงสามารถใช้ Minimum Sliding Window
Sliding Window
Sliding Window ที่จะใช้ในข้อนี้เป็นโครงสร้างข้อมูลที่รองรับ Operation สองแบบ:
- Query(i): หาดัชนี ที่ทำให้ เป็นค่าต่ำสุดในช่วง โดย ที่ถูก Query ต้องไม่ลดลงจาก Operation ที่แล้ว
- Insert(i): เพิ่มดัชนี เข้าไปในช่วงที่พิจารณา โดย ที่ถูก Insert ต้องไม่ลดลงจาก Operation ที่แล้ว ทั้งสอง Operation ต้องใช้เวลา Amortized
แนวคิดของ Sliding Window
Sliding Window จะเก็บดัชนีในช่วงจากน้อยไปมาก และ ที่แต่ละดัชนีจะเรียงจากน้อยไปมากเช่นกัน
แนวคิดหลักของ Sliding Window คือเมื่อเรา Insert(i) ใหม่ที่มีค่า ต่ำกว่า สำหรับ เราจะไม่ต้องพิจารณา อีกเลยเพราะ จะอยู่ในช่วงที่พิจารณานานกว่า (เพราะช่วงขยับไปทางขวาเสมอ) และ เป็นตัวเลือกที่ดีกว่า เสมอ
ดังนั้นหากใส่ค่า ที่ ที่มีค่าต่ำกว่าค่าท้ายสุดก่อนใส่ สามารถเอาค่าท้ายสุดใน Sliding Window ออกจนเหลือเพียงค่า ที่ ไม่มากกว่า และค่อยใส่ เข้าไปเป็นค่าท้ายสุดใหม่
สังเกตว่าเนื่องจาก ของดัชนีใน Sliding Window เรียงกันก่อนหน้าที่จะทำการ Insert(i) ครั้งนี้ จะยังเรียงจากน้อยไปมากเช่นเดิมหลังการ Insert
สำหรับการ Query เนื่องจากค่า ของดัชนีใน Sliding Window เรียงกันจากน้อยไปมาก เราต้องพิจารณาเพียงดัชนีแรกที่เหลืออยู่ใน Sliding Window ที่ไม่น้อยกว่า (เพื่อให้อยู่ในช่วง ถึง )
หากดัชนีแรกไม่อยู่ในช่วงแล้วสามารถเอาออกจาก Sliding Window ได้ เนื่องจากดัชนีดังกล่าวจะไม่อยู่ในช่วงสำหรับ Query ต่อๆ ไปอีกเลย (เพราะช่วง Query ขยับไปทางขวาเสมอ) จึงสามารถเอาดัชนีแรกออกจนดัชนีแรกไม่น้อยกว่า
หลังการ Query ค่า ของดัชนีใน Sliding Window จะเรียงจากน้อยไปมากเช่นเดิม เช่นเดียวกับการ Insert
Deque
สังเกตว่าการ Query / Insert ใช้เพียง 5 Operation รองรับ คือ 1) เอาค่าหน้าสุดออก 2) เอาค่าท้ายสุดออก 3) ใส่ค่าใหม่เป็นค่าสุดท้าย 4) หาค่าหน้าสุด 5) หาค่าท้ายสุด จึงสามารถ Implement ด้วย Deque (Double-ended Queue) ซึ่่งรองรับทั้ง 5 Operation
หากใช้ภาษา C++ จะสามารถใช้ std::deque
ที่มีอยู่ใน Library <deque>
ของ Standard Template Library
เราต้องการใช้ 5 Operation ของ std::deque
คือ
- pop_front(): เอาค่าหน้าสุดออก
- pop_back(): เอาค่าท้ายสุดออก
- push_back(value): ใส่ค่า value เข้าไปเป็นค่าหลังสุด
- front(): return ค่าหน้าสุด
- back(): return ค่าท้ายสุด
ทั้ง 5 Operation ใช้เวลา Amortized
Implementation
Query(i)
การ Query ต้องเอาดัชนีแรกออกจนดัชนีแรกไม่ต่ำกว่า และดัชนีแรกใน Sliding Window จะเป็นดัชนี ที่ค่า ต่ำสุดในช่วง
while (sliding_window.front() < i - k) sliding_window.pop_front(); // sliding_window.front() <- ค่าต่ำสุด
Insert(i)
การ Insert ต้องเอาดัชนีท้ายสุดออกจน ของดัชนีท้ายสุดไม่มากกว่า แล้วจึงใส่ดัชนี เข้าไปในตำแหน่งสุดท้าย
while (!sliding_window.empty() && min_cost[sliding_window.back()] > min_cost[i]) sliding_window.pop_back(); sliding_window.push_back(i);
สังเกตว่าในการ Query / Insert สำหรับดัชนีใดๆ เมื่อโดน push_back หนึ่งครั้งจะสามารถโดน pop_front / pop_back ได้อย่างมากหนึ่งครั้ง ทั้งสอง Operation จึงใช้เวลา Amortized
Code
deque < int > sliding_window; min_cost[1] = P[1]; sliding_window.push_back(1); for (int i = 2; i <= N; i++) { while (sliding_window.front() < i - k) sliding_window.pop_front(); min_cost[i] = P[i] + min_cost[sliding_window.front()]; while (!sliding_window.empty() && min_cost[sliding_window.back()] > min_cost[i]) sliding_window.pop_back(); sliding_window.push_back(i); }
คำอธิบายโค้ด:
- เราต้องเลือกแปลงหมายเลข 1 เสมอ จึงตั้ง และใส่ดัชนี 1 เข้าไปใน Sliding Window
- สำหรับแปลงที่ 2 ถึง เราจะ:
- Query(i) ใน Sliding Window เพื่อค่าต่ำสุดในช่วง ถึง
- ตั้ง ( คือค่าต่ำสุดในช่วง)
- Insert(i) เข้าไปใน Sliding Window สำหรับการ Query ต่อๆ ไป
- คำตอบคือ
การ Query / Insert ใช้เวลา Amortized และถูกเรียก รอบ Algorithm นี้จึงใช้เวลารวม
Time Complexity:
ผลลัพธ์สำหรับตัวอย่างข้อมูลนำเข้าชุดแรก เมื่อแสดงเพียงค่า ของดัชนีที่เหลืออยู่ใน Sliding Window ในแต่ละ Query
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
1 | 4 | 2 | 6 | 2 | 4 | 2 | |
1 | 5 | 3 | 7 | 5 | 7 | 7 | |
Query(i=2) | 1 | ||||||
Query(i=3) | 1 | 5 | |||||
Query(i=4) | 1 | 3 | |||||
Query(i=5) | 3 | 7 | |||||
Query(i=6) | 3 | 5 | |||||
Query(i=7) | 5 | 7 |