On this page
- ทำไมต้องมีท่านี้ — ดู brute force ก่อน
- สองแบบที่ต้องแยกให้ออก: Fixed vs Variable
- แบบที่ 1 — Fixed-size (ขนาดคงที่)
- แบบที่ 2 — Variable-size (ขนาดยืดหยุ่น)
- สัญญาณในโจทย์ — รู้ได้ยังไงว่าต้องใช้ sliding window
- ลองไล่ variable window ของจริง
- Window state — อะไรที่ต้องถือไว้ในมือ
- ต่างจาก Two Pointers ตรงไหน
- กับดักที่เจอบ่อย
- สี่ข้อในหมวดนี้ เรียงยังไง
Sliding Window — พื้นฐาน & แนวคิด
เทคนิคเลื่อนช่วงต่อเนื่อง (subarray / substring) ด้วย left-right แล้วอัปเดตค่าแบบทบเดิม — จาก brute force O(n²) เหลือ O(n)
Sliding Window (หน้าต่างเลื่อน) คือเทคนิคหาคำตอบของ "ช่วงต่อเนื่อง" ใน array หรือ string เช่น ผลรวมมากสุดของช่วงยาว k, substring ยาวสุดที่ไม่มีตัวซ้ำ, หรือช่วงที่มี 0 ไม่เกิน k ตัว หลักการคือเก็บ window เดียวไว้แล้วเลื่อนขอบทีละก้าว อัปเดตแค่ตัวที่เข้ากับตัวที่ออก แทนที่จะคำนวณทั้งช่วงใหม่ทุกครั้ง
ขอบ window คุมด้วยตัวแปรสองตัว left กับ right — คล้าย two pointers แต่เดินไปทางเดียวกันเสมอ และสิ่งที่สนใจคือทุกอย่างระหว่างสองขอบ ไม่ใช่แค่ค่าปลายสองข้าง
ทำไมต้องมีท่านี้ — ดู brute force ก่อน
ตัวอย่าง: หาผลรวมมากสุดของช่วงยาว k = 3 ใน nums = [2, 1, 5, 1, 3, 2] — brute force คือลองทุกช่วงยาว 3 แล้วบวกใหม่ทุกครั้ง
nums = [2, 1, 5, 1, 3, 2]
k = 3
best = float("-inf")
adds = 0 # นับว่าบวกเลขกี่ครั้ง
for i in range(len(nums) - k + 1): # ทุกจุดเริ่มต้นที่เป็นไปได้
total = 0
for j in range(i, i + k): # บวก k ตัวใหม่ทุกครั้ง
total += nums[j]
adds += 1
best = max(best, total)
print(f"ช่วง [{i}:{i+k}] = {nums[i:i+k]} รวม = {total}")
print("best =", best, "| บวกไปทั้งหมด", adds, "ครั้ง")ช่วง [0:3] = [2, 1, 5] รวม = 8
ช่วง [1:4] = [1, 5, 1] รวม = 7
ช่วง [2:5] = [5, 1, 3] รวม = 9
ช่วง [3:6] = [1, 3, 2] รวม = 6
best = 9 | บวกไปทั้งหมด 12 ครั้งช่วงถัดไปซ้อนกับช่วงก่อนเกือบหมด ต่างแค่ตัวซ้ายหลุด ตัวขวาเข้าใหม่ แต่ brute force บวกทั้งก้อนใหม่ทุกครั้ง เมื่อ n และ k ใหญ่ (เช่น n = 10⁵, k = 10⁴) จะกลายเป็น O(nk) ที่รันไม่ทัน
nums = [2, 1, 5, 1, 3, 2]
k = 3
window = sum(nums[:k]) # สร้างหน้าต่างแรกครั้งเดียว
best = window
ops = k # นับการบวก/ลบ
print(f"เริ่ม window = {nums[:k]} รวม = {window}")
for i in range(k, len(nums)):
entering = nums[i] # ตัวใหม่เข้าทางขวา
leaving = nums[i - k] # ตัวเก่าหลุดออกทางซ้าย
window += entering - leaving
ops += 2
best = max(best, window)
print(f"เลื่อน เข้า {entering}, ออก {leaving} -> รวม = {window}")
print("best =", best, "| บวก/ลบไปทั้งหมด", ops, "ครั้ง")เริ่ม window = [2, 1, 5] รวม = 8
เลื่อน เข้า 1, ออก 2 -> รวม = 7
เลื่อน เข้า 3, ออก 1 -> รวม = 9
เลื่อน เข้า 2, ออก 5 -> รวม = 6
best = 9 | บวก/ลบไปทั้งหมด 9 ครั้งได้คำตอบเดียวกัน (best = 9 จาก [5, 1, 3]) แต่งานลดลง และสำคัญกว่านั้นคืองานโตตาม n ไม่ใช่ n×k — นี่คือเหตุผลที่เหลือ O(n)

แต่ละ index เข้า window ผ่าน right ครั้งเดียว และออกผ่าน left ไม่เกินครั้งเดียว รวมไม่เกิน 2n ก้าว → O(n) เสมอ ถ้า left ถอยหลังหรือคำนวณทั้งช่วงใหม่ทุกครั้ง แปลว่ายังไม่ใช่ sliding window จริง
สองแบบที่ต้องแยกให้ออก: Fixed vs Variable
ทุกโจทย์ sliding window ตกอยู่ในหนึ่งในสองแบบ การตั้งชื่อให้ถูกตั้งแต่แรกคือครึ่งหนึ่งของการแก้
แบบที่ 1 — Fixed-size (ขนาดคงที่)
โจทย์บอกความยาว k มาชัดเจน window ไม่เปลี่ยนขนาด แค่เลื่อนไปทีละ 1: บวกตัวเข้าขวา ลบตัวออกซ้าย ใช้เมื่อเห็น "ของช่วงยาว k", "substring of length k", "ทุก window ขนาด k"
def fixed_window(arr, k):
window = sum(arr[:k]) # สร้างหน้าต่างแรก
best = window
for right in range(k, len(arr)):
window += arr[right] # ตัวใหม่เข้า
window -= arr[right - k] # ตัวเก่าออก
best = max(best, window)
return bestแบบที่ 2 — Variable-size (ขนาดยืดหยุ่น)
ไม่มีความยาวตายตัว: ขยายขวา (expand) รับของเข้าเรื่อย ๆ พอ window invalid ค่อยหดซ้าย (shrink) จนถูกต้องแล้วบันทึกคำตอบ ใช้เมื่อเห็น "ยาวที่สุด / สั้นที่สุด ที่ยัง…", "at most k", "ไม่มีตัวซ้ำ"
def variable_window(arr):
left = 0
state = 0 # ผลรวม / จำนวน / อะไรก็ตามที่ track
best = 0
for right in range(len(arr)):
# 1) Expand: รับ arr[right] เข้าหน้าต่าง
state += arr[right]
# 2) Shrink: หดซ้ายจนกว่าหน้าต่างจะถูกต้อง
while is_invalid(state): # <- แทนที่ด้วยเงื่อนไขของโจทย์
state -= arr[left]
left += 1
# 3) Update: ตรงนี้ [left..right] valid แล้ว
best = max(best, right - left + 1)
return best
"ยาวที่สุด": อัปเดต best หลัง shrink (window valid แล้ว) · "สั้นที่สุด": อัปเดต best ก่อนหด ตอนยัง valid อยู่ — สลับจังหวะนี้คำตอบจะเพี้ยนแบบเงียบ ๆ
สัญญาณในโจทย์ — รู้ได้ยังไงว่าต้องใช้ sliding window
ต้องมีสองอย่างพร้อมกัน: (1) คำตอบเป็นช่วงต่อเนื่อง (contiguous — ติดกัน ไม่กระโดดข้ามแบบ subsequence) และ (2) ต้องการค่าดีที่สุดหรือนับภายใต้เงื่อนไข (ยาวสุด / สั้นสุด / ผลรวมมากสุด / ไม่เกิน k ตัว)
| สัญญาณในโจทย์ | ใช้แบบไหน | state ที่ track |
|---|---|---|
| ช่วงยาว k ชัดเจน ("of length k", "size k") | Fixed | ผลรวม / จำนวนสระ / freq map ของขนาด k |
| ยาวที่สุดที่ยังตรงเงื่อนไข | Variable (maximize) | count / set / freq map |
| สั้นที่สุดที่ยังตรงเงื่อนไข | Variable (minimize) | ผลรวม / freq ที่ยังขาด |
| at most k … (ไม่เกิน k ตัว / k ชนิด) | Variable | ตัวนับของสิ่งที่จำกัด |
| ไม่มีตัวซ้ำใน substring | Variable | set หรือ last-seen index |
| สัญญาณนี้ | ไม่ใช่ sliding window — ใช้อะไรแทน |
|---|---|
| subsequence (ไม่ต้องติดกัน) เช่น Is Subsequence | Two Pointers แบบ same-direction |
| ผลรวมช่วงใดก็ได้ที่มีเลขติดลบ (max subarray ทั่วไป) | Kadane / DP — ไม่ใช่ window |
| subarray sum = K ที่มีเลขติดลบได้ | Prefix Sum + Hash Map |
| หาคู่ค่าสองตัว ไม่สนของระหว่างกลาง | Two Pointers / Hash Map |
ลองไล่ variable window ของจริง
ตัวอย่างคลาสสิก: หา substring ยาวสุดที่ไม่มีตัวซ้ำ (LC3) — invariant คือ "ห้ามมีตัวซ้ำใน window" พอตัวซ้ำเข้าทางขวา ต้องหดซ้ายจนตัวเก่าหลุดออก
s = "abcabcbb"
seen = set()
left = 0
best = 0
for right, ch in enumerate(s):
while ch in seen: # ซ้ำแล้ว → หดซ้าย
seen.remove(s[left])
left += 1
seen.add(ch) # รับตัวใหม่เข้า
best = max(best, right - left + 1)
print(f"right={right} ch={ch!r} window={s[left:right+1]!r} best={best}")
print("คำตอบ:", best)right=0 ch='a' window='a' best=1
right=1 ch='b' window='ab' best=2
right=2 ch='c' window='abc' best=3
right=3 ch='a' window='bca' best=3
right=4 ch='b' window='cab' best=3
right=5 ch='c' window='abc' best=3
right=6 ch='b' window='cb' best=3
right=7 ch='b' window='b' best=3
คำตอบ: 3ตอน right=3 ตัว 'a' ซ้ำ → while เอา 'a' ตัวแรกออก (left ไปที่ 1) window เหลือ "bca" ยาว 3 เท่าเดิม จากนั้นทุกครั้งที่ซ้ำ left ก็ไล่ตาม ความยาวดีสุดค้างที่ 3 ("abc")
Window state — อะไรที่ต้องถือไว้ในมือ
Window ไม่ใช่แค่ left กับ right — ต้องมี state ที่อัปเดตได้เร็วตอนของเข้า/ออก เลือกแบบที่ง่ายสุดที่บอกได้ว่า window ยัง valid ไหมใน O(1)
- ตัวเลขเดียว: ผลรวม, จำนวน 0, จำนวนสระ — ใช้กับ fixed window หรือเงื่อนไขตัวเลข
- set: จำว่ามีตัวไหนอยู่ใน window — ใช้กับ "ห้ามซ้ำ"
- freq map / Counter: นับความถี่ต่อตัวหรือต่อค่า — ใช้กับ "at most k distinct", anagram, ครอบคลุมตัวอักษรของ t
- deque: เก็บตัวแข่ง max/min ใน window — ใช้ตอนถามค่ามากสุด/น้อยสุดในทุก window ยาว k (นอก LC75 หมวดนี้ แต่เจอบ่อยตอนสัมภาษณ์)
ขยับ left/right แล้วลืมอัปเดต state ให้ตรงกัน — เช่น left += 1 แต่ลืมลบ count ของตัวที่หลุดออก window ในโค้ดกับ state จริงจะไม่ตรงกัน คำตอบเพี้ยนโดยไม่มี error
ต่างจาก Two Pointers ตรงไหน
| Two Pointers | Sliding Window | |
|---|---|---|
| สนใจอะไร | ค่าที่ปลายสองข้าง (หรือ slow/fast) | ทั้งช่วงระหว่าง left..right |
| ทิศทาง | เข้าหากัน / คนละความเร็ว / คนละแถว | ทั้งคู่ไปทางเดียวกัน (ซ้าย→ขวา) |
| คำถามหลัก | มีคู่ไหม / กรอง in-place / รวมสองแถว | ช่วงต่อเนื่องที่ดีที่สุดภายใต้เงื่อนไข |
| ต้องเรียงก่อนไหม | แบบ opposite ends ต้องเรียง | ไม่ต้อง — ลำดับเดิมมีความหมาย |
พูดอย่างสั้น: sliding window คือ two pointers แบบ same-direction ที่ถือ "ของทั้งช่วง" เป็น state ไว้ด้วย ถ้าโจทย์พูดถึง contiguous / subarray / substring + optimize → นึก window ก่อน
กับดักที่เจอบ่อย
- ความยาว window ผิด: ช่วง inclusive [left..right] ยาว = right - left + 1 ไม่ใช่ right - left
- เริ่ม best = 0 ทั้งที่คำตอบอาจติดลบ — initialize ด้วยหน้าต่างแรก หรือ float("-inf")
- fixed window ลืมสร้างหน้าต่างแรกก่อน loop หรือลบตัวออกผิดตำแหน่ง (ต้อง right - k ไม่ใช่ left)
- variable window อัปเดต best ตอนยัง invalid — ต้อง shrink ให้จบก่อน
- เลขติดลบ + เงื่อนไขผลรวม: อย่าสมมติว่าขยายแล้วผลรวมต้องโต ถ้าเป็น max subarray ทั่วไปให้ไป Kadane
- left ถอยหลังได้ พัง amortized O(n) ทันที — left เดินหน้าอย่างเดียว
สี่ข้อในหมวดนี้ เรียงยังไง
| ข้อ | โจทย์ | แบบ | state |
|---|---|---|---|
| 14 | LC643 Maximum Average Subarray I | Fixed k | ผลรวม → หาร k ตอนท้าย |
| 15 | LC1456 Max Vowels in Substring of Length K | Fixed k | นับสระใน window |
| 16 | LC1004 Max Consecutive Ones III | Variable | นับ 0 (พลิกได้ ≤ k) |
| 17 | LC1493 Longest Subarray of 1's After Deleting One | Variable | นับ 0 (ลบได้ 1 ตัว = k=1) |
สองข้อแรกซ้อม fixed ให้มือขึ้น สองข้อหลังเป็น variable แบบ "at most k ตัวที่ผิดกติกา" — ข้อ 17 คือข้อ 16 ที่ k ล็อกเป็น 1 ถ้าเข้าใจข้อ 16 แล้ว ข้อ 17 จะรู้เรื่องทันที
เริ่มจากข้อ 14 Maximum Average Subarray I — fixed window ตัวแรก กดถัดไปได้เลย