On this page
ข้อ 71 · LC1268 Search Suggestions System (ระบบแนะนำคำค้น) 🟡
แนะนำสินค้าไม่เกิน 3 ชื่อตาม lexicographic order ทุกครั้งที่พิมพ์เพิ่มทีละตัว โดย sort (เรียง) ก่อนแล้วเก็บ suggestions ที่แต่ละ node
โจทย์ (LC1268): กำหนด array of string ชื่อ products และ string ชื่อ searchWord ให้ออกแบบระบบที่แนะนำชื่อสินค้าไม่เกิน 3 ชื่อจาก products หลังจากผู้ใช้พิมพ์ตัวอักษรแต่ละตัวของ searchWord สินค้าที่แนะนำต้องมี common prefix (คำนำหน้าร่วม) กับสิ่งที่พิมพ์มาแล้ว ถ้ามีมากกว่า 3 ชื่อที่ตรงเงื่อนไข ให้เลือก 3 ชื่อที่เรียงตาม lexicographical order (พจนานุกรม) น้อยที่สุด แล้ว return เป็น list of list ของคำแนะนำหลังพิมพ์ตัวอักษรแต่ละตัว
- Input:
- products = ["mobile", "mouse", "moneypot", "monitor", "mousepad"], searchWord = "mouse"
- Output:
- [["mobile","moneypot","monitor"], ["mobile","moneypot","monitor"], ["mouse","mousepad"], ["mouse","mousepad"], ["mouse","mousepad"]]
- Explanation:
- พิมพ์ m, mo, mou ได้ 3 ชื่อแรกตามพจนานุกรมที่ขึ้นต้นด้วยสิ่งที่พิมพ์คือ mobile, moneypot, monitor พอพิมพ์ mous, mouse เหลือแค่ mouse กับ mousepad ที่ยังตรง prefix
- Input:
- products = ["havana"], searchWord = "havana"
- Output:
- [["havana"], ["havana"], ["havana"], ["havana"], ["havana"], ["havana"]]
- Explanation:
- มีสินค้าตัวเดียวในระบบและตรง prefix ทุกตัวอักษรที่พิมพ์ จึงถูกแนะนำซ้ำทุกครั้งจนพิมพ์ครบคำ
- 1 <= products.length <= 1000
- 1 <= products[i].length <= 3000
- ข้อความใน products ไม่ซ้ำกัน
- 1 <= searchWord.length <= 1000
- ผลรวมความยาวของ products ทั้งหมดไม่เกิน 2 × 10^4
แนวทาง — ต้องใช้อะไร & คิดยังไง
ข้อนี้ใช้ trie ผสมกับการ sort (เรียง) ล่วงหน้า idea คือถ้า sort products ก่อนหนึ่งครั้ง แล้วค่อย insert ลง trie คำที่ผ่านแต่ละ node จะมาตาม lexicographic order อยู่แล้ว เราจึงเก็บแค่ 3 ตัวแรกที่แต่ละ node พอ
วิธีตรงไปตรงมาคือทุกครั้งที่พิมพ์ ก็ filter products ทั้งหมดที่ขึ้นต้นด้วย prefix แล้ว sort เอา 3 ตัวแรก ซึ่งทำงานซ้ำ ๆ และช้าเมื่อพิมพ์ยาว ๆ การเก็บ suggestions ไว้ที่แต่ละ node ตั้งแต่ตอนสร้าง trie ทำให้ตอนตอบแค่ traverse (เดินไล่) ตาม prefix แล้วหยิบออกมาได้ทันที
- sort products ก่อนหนึ่งครั้ง
- insert แต่ละคำลง trie ไล่ตัวอักษร ระหว่าง traverse ให้ append (ต่อท้าย) คำนั้นเข้า node.suggestions ถ้ายังเก็บไม่ถึง 3 ตัว
- ตอบ searchWord: traverse ตามตัวอักษรที่พิมพ์ทีละตัวจาก root ถ้ายัง traverse ได้ก็หยิบ node.suggestions ใส่ผลลัพธ์
- ถ้าหลุดเส้นทางเมื่อไร ตั้ง node = None แล้ว append [] ให้ตัวอักษรที่เหลือทั้งหมด
พอพิมพ์ตัวอักษรที่ทำให้หลุดเส้นทางใน trie แล้ว ตัวอักษรที่เหลือหลังจากนั้นต้องแนะนำเป็นลิสต์ว่างทั้งหมด อย่าหยุดเติมผลลัพธ์ ต้องเติม [] ต่อไปให้ครบความยาว searchWord
▶ เฉลยละเอียด (ลองเองก่อนนะ)
class TrieNode:
def __init__(self):
self.children = {}
self.suggestions = [] # สินค้าไม่เกิน 3 ตัวแรก (เรียงแล้ว) ที่ผ่าน node นี้
def suggested_products(products, searchWord):
root = TrieNode()
# sort ก่อน เพื่อให้คำที่ใส่เข้า trie มาตามลำดับพจนานุกรม
for product in sorted(products):
node = root
for ch in product:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
# เก็บได้แค่ 3 ตัวแรกพอ (มาตามลำดับ sort อยู่แล้ว)
if len(node.suggestions) < 3:
node.suggestions.append(product)
result = []
node = root
for ch in searchWord:
# ถ้ายังเดินตาม prefix ได้ ก็หยิบ suggestions ที่ node นั้น
if node and ch in node.children:
node = node.children[ch]
result.append(node.suggestions)
else:
# หลุดเส้นทางแล้ว ที่เหลือไม่มีคำแนะนำ
node = None
result.append([])
return result
print(suggested_products(
["mobile", "mouse", "moneypot", "monitor", "mousepad"], "mouse"))
# [['mobile','moneypot','monitor'], ['mobile','moneypot','monitor'],
# ['mouse','mousepad'], ['mouse','mousepad'], ['mouse','mousepad']][['mobile', 'moneypot', 'monitor'], ['mobile', 'moneypot', 'monitor'], ['mouse', 'mousepad'], ['mouse', 'mousepad'], ['mouse', 'mousepad']]ไอเดียคือ sort products ก่อนหนึ่งครั้ง ทำให้เวลาไล่ insert คำเข้า trie ตามลำดับ ทุก node จะได้รับสินค้าตาม lexicographic order อยู่แล้ว เราจึงเก็บแค่ 3 ตัวแรกที่ผ่าน node นั้นไว้ใน suggestions เมื่อผู้ใช้พิมพ์ prefix มาเรื่อย ๆ ก็แค่ traverse ตามตัวอักษรแล้วหยิบ suggestions ที่ node ปลายทางออกมาได้ทันที ไม่ต้อง search ใหม่ทุกครั้ง
จุดสำคัญคือเมื่อพิมพ์ตัวอักษรที่ทำให้หลุดเส้นทางใน trie (ไม่มีสินค้าไหนขึ้นต้นแบบนั้นแล้ว) ตัวอักษรที่เหลือหลังจากนั้นต้องแนะนำเป็น empty list (ลิสต์ว่าง) ทั้งหมด โค้ดจึงตั้ง node = None แล้ว append [] ต่อไปเรื่อย ๆ
Time O(N log N + total) โดย N คือจำนวนสินค้า มาจากการ sort (N log N) บวกกับการสร้าง trie ตามจำนวนตัวอักษรรวมของทุกคำ ส่วนการตอบ searchWord ใช้ O(ความยาว searchWord) · Space O(total) เก็บตัวอักษรทั้งหมดใน trie (แต่ละ node เก็บ suggestions ไม่เกิน 3)
เมื่อต้องตอบ prefix query ซ้ำ ๆ ให้ preprocess (ประมวลผลล่วงหน้า) — sort + เก็บ suggestions ที่ node แลก memory นิดหน่อยเพื่อให้ตอบแต่ละครั้งเร็วในหนึ่งการ traverse — เป็นแก่นของระบบ autocomplete