ข้อ 8 · LC334 Increasing Triplet Subsequence 🟡
มีสามตำแหน่ง i < j < k ที่ nums[i] < nums[j] < nums[k] หรือไม่ — ท้าทาย O(n) time / O(1) space
ให้ลิสต์ nums จงตอบว่ามีสามตำแหน่ง i < j < k ที่ทำให้ nums[i] < nums[j] < nums[k] อยู่หรือไม่
สามตัวนี้ไม่ต้องอยู่ติดกันในลิสต์ ขอแค่ตำแหน่งเรียงจากซ้ายไปขวา และค่าเรียงจากน้อยไปมาก
ตอบเป็น true หรือ false เท่านั้น ไม่ต้องบอกว่าสามตัวนั้นคือตัวไหน
- Input:
- nums = [1,2,3,4,5]
- Output:
- true
- Explanation:
- 1 < 2 < 3 ก็นับว่าเจอแล้ว (ไม่ต้องหาชุดที่ดีที่สุดหรือชุดเดียวเท่านั้น แค่หาว่ามีสักชุดหนึ่งที่ใช้ได้ก็พอ)
- Input:
- nums = [5,4,3,2,1]
- Output:
- false
- Explanation:
- ตัวเลขลดลงตลอดทั้งแถว ไม่มีสามตัวไหนเรียงเพิ่มขึ้นได้เลยสักชุด
- 1 <= nums.length <= 5 × 10^5
- -2^31 <= nums[i] <= 2^31 - 1
- ท้าทาย: ทำให้เป็น O(n) time และ O(1) space
ข้อนี้เป็นข้อที่ยากที่สุดในหมวด ไม่ใช่เพราะโค้ดยาว แต่เพราะโค้ดสั้นมากและดูเหมือนผิด ถ้าเขียนแบบวนสามชั้นได้ก่อนก็ถือว่าดีแล้ว แล้วค่อยเปิดใบ้เพื่อลดลง
💡 ใบ้ขั้นที่ 1 — ต้องจำอะไรบ้างจริง ๆ
เดินจากซ้ายไปขวารอบเดียว ถามตัวเองว่าถ้าจะรู้ว่าเจอคำตอบแล้ว เราต้องจำอะไรไว้บ้าง
คำตอบคือจำแค่สองค่า คือค่าที่เล็กที่สุดที่เคยเจอ และค่าที่เล็กเป็นอันดับสองที่อยู่หลังตัวแรก ไม่ต้องจำตำแหน่ง
💡 ใบ้ขั้นที่ 2 — ทำอะไรกับตัวที่กำลังดู
แต่ละตัวที่เดินผ่าน มีสามทางเลือก คือมันเล็กพอจะเป็นตัวที่หนึ่งใหม่ หรือเล็กพอจะเป็นตัวที่สองใหม่ หรือใหญ่กว่าทั้งคู่
ถ้าตกกรณีที่สาม แปลว่าเจอคำตอบแล้ว ตอบ true ได้เลย
💡 ใบ้ขั้นที่ 3 — โครงทั้งหมด
first = second = float("inf") # ยังไม่เจออะไร ตั้งเป็นค่าใหญ่สุดไว้
for x in nums:
if x <= first:
first = x
elif x <= second:
second = x
else:
return ___
return ___จุดที่ต้องระวังคือใช้ <= ไม่ใช่ < เพื่อกันกรณีมีค่าซ้ำกัน เพราะโจทย์ต้องการเพิ่มขึ้นจริง ไม่ใช่เท่ากัน
🧭 กล่องสอน — ไล่ตั้งแต่ศูนย์ทีละขั้น (เปิดเมื่อพร้อม)
ขั้นที่ 1 · โจทย์นี้ขออะไรจริง ๆ
คำสำคัญที่สุดของข้อนี้คือ โจทย์ขอแค่ true หรือ false ไม่ได้ขอว่าสามตัวนั้นคือตัวไหน
ความต่างนี้เปลี่ยนความยากของข้อทั้งข้อ เพราะเมื่อไม่ต้องรายงานว่าใครคือใคร เราจึงไม่ต้องเก็บตำแหน่งหรือชุดคำตอบไว้เลย
อีกคำที่ต้องอ่านให้แตกคือ ไม่ต้องอยู่ติดกัน สามตัวอาจอยู่ห่างกันแค่ไหนก็ได้ ขอแค่ลำดับตำแหน่งและลำดับค่าถูกต้อง
ขั้นที่ 2 · กับดักแรก — เช็คแค่สามตัวที่ติดกัน
วิธีที่คิดออกเร็วที่สุดคือไล่ดูสามตัวติดกันทีละชุด ซึ่งเขียนง่ายและเร็ว แต่ผิด
def wrong(nums):
for i in range(len(nums)-2):
if nums[i] < nums[i+1] < nums[i+2]:
return True
return False
def right(nums):
first = second = float("inf")
for x in nums:
if x <= first: first = x
elif x <= second: second = x
else: return True
return False
for nums in [[1,2,3,4,5], [5,4,3,2,1], [2,1,5,0,4,6], [20,100,10,12,5,13]]:
w, r = wrong(nums), right(nums)
mark = "" if w == r else " <- ต่างกัน!"
print(f"nums={nums} | เช็คติดกัน -> {w} | วิธีถูก -> {r}{mark}")
print()
print("nums=[20,100,10,12,5,13] มีคำตอบคือ 10 < 12 < 13 (ตำแหน่ง 2,3,5) ซึ่งไม่ติดกัน")nums=[1, 2, 3, 4, 5] | เช็คติดกัน -> True | วิธีถูก -> True
nums=[5, 4, 3, 2, 1] | เช็คติดกัน -> False | วิธีถูก -> False
nums=[2, 1, 5, 0, 4, 6] | เช็คติดกัน -> True | วิธีถูก -> True
nums=[20, 100, 10, 12, 5, 13] | เช็คติดกัน -> False | วิธีถูก -> True <- ต่างกัน!
nums=[20,100,10,12,5,13] มีคำตอบคือ 10 < 12 < 13 (ตำแหน่ง 2,3,5) ซึ่งไม่ติดกันสามเคสแรกให้ผลตรงกัน มีแค่เคสสุดท้ายที่ต่าง ซึ่งเป็นเหตุผลที่หลายคนส่งคำตอบผิดไปโดยมั่นใจ
ขั้นที่ 3 · กุญแจของข้อนี้
กุญแจคือถามว่า ถ้าจะรู้ว่ามีสามตัวเรียงเพิ่มขึ้น เราต้องจำอะไรไว้น้อยที่สุด
คำตอบคือจำแค่สองค่า ให้นึกภาพว่าเราเดินไปเรื่อย ๆ ในมือถือกระดาษสองแผ่น แผ่นแรกจดค่าที่เล็กที่สุดที่เคยเจอ แผ่นที่สองจดค่าที่เล็กเป็นอันดับสองที่มาหลังแผ่นแรก
พอเจอตัวที่ใหญ่กว่าทั้งสองแผ่น ก็แปลว่ามีสามตัวเรียงเพิ่มขึ้นครบแล้ว ตอบ true ได้ทันที
ทำไมต้องพยายามให้สองค่านี้เล็กที่สุดเท่าที่จะเป็นได้ เพราะยิ่งเล็กยิ่งเปิดโอกาสให้ตัวข้างหน้าใหญ่กว่ามันได้ง่ายขึ้น
ขั้นที่ 4 · เดินตามโค้ดทีละสเต็ป
def triplet(nums):
first = second = float("inf")
for i, x in enumerate(nums):
if x <= first:
first = x
print(f" i={i} x={x}: เล็กกว่าหรือเท่า first -> first={first} second={second}")
elif x <= second:
second = x
print(f" i={i} x={x}: อยู่ระหว่าง -> first={first} second={second}")
else:
print(f" i={i} x={x}: ใหญ่กว่าทั้งคู่ -> เจอครบสามตัวแล้ว!")
return True
return False
print("nums=[2,1,5,0,4,6] ->", triplet([2,1,5,0,4,6])) i=0 x=2: เล็กกว่าหรือเท่า first -> first=2 second=inf
i=1 x=1: เล็กกว่าหรือเท่า first -> first=1 second=inf
i=2 x=5: อยู่ระหว่าง -> first=1 second=5
i=3 x=0: เล็กกว่าหรือเท่า first -> first=0 second=5
i=4 x=4: อยู่ระหว่าง -> first=0 second=4
i=5 x=6: ใหญ่กว่าทั้งคู่ -> เจอครบสามตัวแล้ว!
nums=[2,1,5,0,4,6] -> Trueขั้นที่ 5 · จุดที่ทุกคนสะดุด และเป็นหัวใจของข้อนี้
ดูบรรทัด i=3 ให้ดี ค่า first ถูกเปลี่ยนจาก 1 เป็น 0 ทั้งที่ second เป็น 5 ซึ่งถูกตั้งไว้ตอน first ยังเป็น 1
ถ้าอ่านตามตัวอักษร ตอนนี้ first คือเลข 0 ที่ตำแหน่ง 3 ส่วน second คือเลข 5 ที่ตำแหน่ง 2 แปลว่า second อยู่ ก่อน first ซึ่งขัดกับกติกาที่เราตั้งไว้
หลายคนถึงตรงนี้จะสรุปว่าโค้ดผิด แต่คำตอบสุดท้ายยังถูกต้องเสมอ และเหตุผลอยู่ที่ตัวคำถามของโจทย์
เพราะโจทย์ถามแค่ว่า มีสามตัวเรียงเพิ่มขึ้นอยู่จริงไหม ไม่ได้ถามว่าสามตัวนั้นคือตัวไหน โค้ดจึงไม่จำเป็นต้องรักษาให้ first กับ second เป็นคู่ที่ถูกต้องตลอดเวลา
ที่ตำแหน่ง 5 มีคำตอบให้เลือกถึงสองชุด คือ (1, 5, 6) จากตำแหน่ง 1, 2, 5 และ (0, 4, 6) จากตำแหน่ง 3, 4, 5 ทั้งสองชุดถูกต้องพร้อมกัน
การอ่านให้แตกว่าโจทย์ขอ ค่า หรือ ตำแหน่ง หรือแค่ ใช่หรือไม่ใช่ เปลี่ยนความยากได้ทั้งข้อ ถ้าข้อนี้เปลี่ยนเป็นขอตำแหน่งของสามตัวนั้นด้วย โค้ดข้างบนใช้ไม่ได้ทันที เพราะมันไม่เคยจำว่าใครคือใคร
ขั้นที่ 6 · ทำไมต้องใช้ <= ไม่ใช่ <
โจทย์ต้องการค่าที่เพิ่มขึ้นจริง ไม่ใช่เท่ากัน ดังนั้นค่าซ้ำต้องไม่นับเป็นการเพิ่ม
การเขียน x <= first ทำให้ค่าที่เท่ากับ first ไปอัปเดต first แทนที่จะเลื่อนไปเป็น second ซึ่งกัน [2, 2, 2] ไม่ให้ตอบ true ผิด ๆ
ถ้าเผลอใช้ < ทั้งสองที่ ลิสต์ที่มีค่าซ้ำกันอาจให้คำตอบผิดได้
🔓 เฉลยเต็ม พร้อมอธิบายทีละบรรทัด (ลองเองก่อนนะ)พับไว้ด้านใน — คลิกเมื่อพร้อมดู
class Solution:
def increasingTriplet(self, nums: List[int]) -> bool:
first = second = float("inf") # ยังไม่เจออะไร ตั้งเป็นค่าใหญ่สุดไว้ก่อน
for x in nums:
if x <= first:
first = x # เจอตัวเล็กสุดใหม่ (<= เพื่อกันค่าซ้ำ)
elif x <= second:
second = x # เล็กเป็นอันดับสอง และอยู่หลัง first
else:
return True # ใหญ่กว่าทั้งคู่ = ครบสามตัวเรียงเพิ่มขึ้น
return Falseอ่านโค้ดทีละส่วน
- float("inf") คือค่าอนันต์ ใช้เป็นค่าเริ่มต้นเพื่อให้ตัวแรกที่เจอเข้าเงื่อนไข x <= first แน่นอน
- ลำดับ if elif else สำคัญมาก ต้องพยายามอัปเดต first ก่อน second ก่อน แล้วจึงสรุปว่าเจอคำตอบ
- ใช้ <= ทั้งสองที่เพื่อกันค่าซ้ำไม่ให้นับเป็นการเพิ่มขึ้น
- return True ทันทีที่เจอ ไม่ต้องกวาดต่อ เพราะโจทย์ขอแค่ว่ามีหรือไม่มี
ต้นทุน
เวลา O(n) กวาดรอบเดียว · หน่วยความจำ O(1) เพราะใช้ตัวแปรแค่สองตัวไม่ว่าลิสต์จะยาวแค่ไหน ซึ่งตอบคำท้าของโจทย์ได้พอดี
เช็คลิสต์ก่อนกดส่ง
- ทดสอบ [20,100,10,12,5,13] ซึ่งคำตอบอยู่ในตำแหน่งที่ไม่ติดกัน ต้องได้ true
- ทดสอบลิสต์ที่ค่าลดลงตลอด เช่น [5,4,3,2,1] ต้องได้ false
- ทดสอบลิสต์ที่มีค่าซ้ำ เช่น [2,2,2] ต้องได้ false
- ทดสอบลิสต์ที่สั้นกว่า 3 ตัว ต้องได้ false และต้องไม่ error
- ตรวจว่าใช้ <= ไม่ใช่ <