Notes & software courses · Free to learn
Aph's Blog

ข้อ 8 · LC334 Increasing Triplet Subsequence 🟡

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

มีสามตำแหน่ง 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 เท่านั้น ไม่ต้องบอกว่าสามตัวนั้นคือตัวไหน

Example 1
Input:
nums = [1,2,3,4,5]
Output:
true
Explanation:
1 < 2 < 3 ก็นับว่าเจอแล้ว (ไม่ต้องหาชุดที่ดีที่สุดหรือชุดเดียวเท่านั้น แค่หาว่ามีสักชุดหนึ่งที่ใช้ได้ก็พอ)
Example 2
Input:
nums = [5,4,3,2,1]
Output:
false
Explanation:
ตัวเลขลดลงตลอดทั้งแถว ไม่มีสามตัวไหนเรียงเพิ่มขึ้นได้เลยสักชุด
Constraints (ข้อจำกัด)
  • 1 <= nums.length <= 5 × 10^5
  • -2^31 <= nums[i] <= 2^31 - 1
  • ท้าทาย: ทำให้เป็น O(n) time และ O(1) space
⏸ ลองเองก่อน 20 นาที

ข้อนี้เป็นข้อที่ยากที่สุดในหมวด ไม่ใช่เพราะโค้ดยาว แต่เพราะโค้ดสั้นมากและดูเหมือนผิด ถ้าเขียนแบบวนสามชั้นได้ก่อนก็ถือว่าดีแล้ว แล้วค่อยเปิดใบ้เพื่อลดลง

💡 ใบ้ขั้นที่ 1 — ต้องจำอะไรบ้างจริง ๆ

เดินจากซ้ายไปขวารอบเดียว ถามตัวเองว่าถ้าจะรู้ว่าเจอคำตอบแล้ว เราต้องจำอะไรไว้บ้าง

คำตอบคือจำแค่สองค่า คือค่าที่เล็กที่สุดที่เคยเจอ และค่าที่เล็กเป็นอันดับสองที่อยู่หลังตัวแรก ไม่ต้องจำตำแหน่ง

💡 ใบ้ขั้นที่ 2 — ทำอะไรกับตัวที่กำลังดู

แต่ละตัวที่เดินผ่าน มีสามทางเลือก คือมันเล็กพอจะเป็นตัวที่หนึ่งใหม่ หรือเล็กพอจะเป็นตัวที่สองใหม่ หรือใหญ่กว่าทั้งคู่

ถ้าตกกรณีที่สาม แปลว่าเจอคำตอบแล้ว ตอบ true ได้เลย

💡 ใบ้ขั้นที่ 3 — โครงทั้งหมด
python
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 · กับดักแรก — เช็คแค่สามตัวที่ติดกัน

วิธีที่คิดออกเร็วที่สุดคือไล่ดูสามตัวติดกันทีละชุด ซึ่งเขียนง่ายและเร็ว แต่ผิด

หลักฐานว่าเช็คแค่ตัวติดกันแล้วพลาดคำตอบpython
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) ซึ่งไม่ติดกัน")
Output
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 · เดินตามโค้ดทีละสเต็ป

ไล่ทีละตัว ดูว่า first กับ second เปลี่ยนยังไงpython
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]))
Output
  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 ผิด ๆ

ถ้าเผลอใช้ < ทั้งสองที่ ลิสต์ที่มีค่าซ้ำกันอาจให้คำตอบผิดได้

🔓 เฉลยเต็ม พร้อมอธิบายทีละบรรทัด (ลองเองก่อนนะ)พับไว้ด้านใน — คลิกเมื่อพร้อมดู
คำตอบpython
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

อ่านโค้ดทีละส่วน

  1. float("inf") คือค่าอนันต์ ใช้เป็นค่าเริ่มต้นเพื่อให้ตัวแรกที่เจอเข้าเงื่อนไข x <= first แน่นอน
  2. ลำดับ if elif else สำคัญมาก ต้องพยายามอัปเดต first ก่อน second ก่อน แล้วจึงสรุปว่าเจอคำตอบ
  3. ใช้ <= ทั้งสองที่เพื่อกันค่าซ้ำไม่ให้นับเป็นการเพิ่มขึ้น
  4. 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
  • ตรวจว่าใช้ <= ไม่ใช่ <