ข้อ 2 · LC1071 Greatest Common Divisor of Strings 🟡
หาสตริง x ที่ยาวที่สุด ซึ่งหาร (divide) ได้ทั้ง str1 และ str2 ตามนิยามที่โจทย์กำหนด ถ้าไม่มีให้คืนสตริงว่าง
โจทย์ตั้งนิยามขึ้นมาเองก่อน: เราจะบอกว่าสตริง t "หาร" สตริง s ได้ ก็ต่อเมื่อเอา t มาต่อกันซ้ำ ๆ ตั้งแต่ 1 ครั้งขึ้นไป แล้วได้ s พอดี ไม่ขาดไม่เกิน
ตัวอย่างของนิยามนี้: AB หาร ABABAB ได้ เพราะ AB ต่อกัน 3 ครั้งได้ ABABAB เป๊ะ · แต่ AB ไม่หาร ABA เพราะต่อกี่ครั้งก็ได้ความยาวเป็นเลขคู่เสมอ ไม่มีทางได้ความยาว 3
โจทย์ให้ str1 กับ str2 มา แล้วขอสตริง x ที่ ยาวที่สุด ที่หารได้ทั้ง str1 และ str2 ถ้าไม่มีเลยให้คืนสตริงว่าง
- Input:
- str1 = "ABCABC", str2 = "ABC"
- Output:
- "ABC"
- Explanation:
- ABC ต่อกัน 2 ครั้งได้ ABCABC และต่อกัน 1 ครั้งได้ ABC จึงหารได้ทั้งคู่
และไม่มีบล็อกไหนยาวกว่านี้ที่ทำได้ เพราะบล็อกต้องไม่ยาวเกิน str2 ซึ่งยาวแค่ 3
- Input:
- str1 = "ABABAB", str2 = "ABAB"
- Output:
- "AB"
- Explanation:
- ลองบล็อก ABAB ดูก่อน: มันหาร ABAB ได้ แต่หาร ABABAB ไม่ได้ เพราะ 6 หารด้วย 4 ไม่ลงตัว
จึงต้องถอยลงมาที่ AB ซึ่งต่อกัน 3 ครั้งได้ str1 และต่อกัน 2 ครั้งได้ str2 — สังเกตว่าคำตอบสั้นกว่าทั้งสองตัว
- Input:
- str1 = "LEET", str2 = "CODE"
- Output:
- ""
- Explanation:
- ตัวอักษรตัวแรกก็คนละตัวแล้ว (L กับ C) บล็อกร่วมจึงต้องเริ่มด้วยทั้ง L และ C พร้อมกัน ซึ่งเป็นไปไม่ได้ ต้องคืน string ว่าง
- Input:
- str1 = "ABABABAB", str2 = "ABAB"
- Output:
- "ABAB"
- Explanation:
- คู่นี้ต่างจากตัวอย่างที่ 2 ตรงที่ 8 หารด้วย 4 ลงตัว ABAB จึงหาร str1 ได้จริง (ต่อกัน 2 ครั้ง)
คำตอบจึงเป็น ABAB ไม่ใช่ AB เพราะโจทย์ขอตัวที่ยาวที่สุด — เทียบสองตัวอย่างนี้คู่กันจะเห็นว่าความยาวคือหัวใจ
- 1 <= str1.length, str2.length <= 1000
- str1 และ str2 เป็นตัวอักษรอังกฤษพิมพ์ใหญ่
ข้อนี้ยากขึ้นจากข้อ 1 ชัดเจน ถ้าคิดไม่ออกว่าจะเริ่มจากไหน ให้ลองเขียนวิธีที่ตรงไปตรงมาที่สุดก่อน คือไล่ลองบล็อกทุกความยาว วิธีนั้นผ่านได้จริงกับ n เท่านี้ แล้วค่อยเปิดใบ้เพื่อดูวิธีที่สั้นกว่า
💡 ใบ้ขั้นที่ 1 — ตัดตัวเลือกให้แคบลงก่อน
ถ้าบล็อก x หาร str1 ได้ ความยาวของ x ต้องหารความยาวของ str1 ลงตัว และต้องหารความยาวของ str2 ลงตัวด้วย
แปลว่าความยาวที่เป็นไปได้ของคำตอบมีไม่กี่ค่า และค่าที่ยาวที่สุดในนั้นคือค่าเดียวที่เราต้องลอง
💡 ใบ้ขั้นที่ 2 — ตัวเลขนั้นเรียกว่าอะไร
ตัวเลขที่หารทั้งสองความยาวลงตัวและใหญ่ที่สุด คือ ห.ร.ม. ของสองความยาวนั้น
Python มีให้ใช้แล้วคือ math.gcd(len(str1), len(str2)) ดังนั้นคำตอบมีตัวเลือกเดียวคือ str1 ตัดหัวมาตามความยาวนี้ เหลือแค่ต้องเช็คว่ามันหารได้จริงทั้งสองฝั่งไหม
💡 ใบ้ขั้นที่ 3 — วิธีเช็คแบบบรรทัดเดียว
การเช็คว่าหารได้จริงไหม เขียนตรง ๆ ก็ได้ (เอาบล็อกคูณจำนวนครั้งแล้วเทียบ) และแบบนั้นถูกต้องสมบูรณ์
แต่มีทางลัดที่สั้นกว่า: ถ้าสองสตริงสร้างจากบล็อกเดียวกันจริง แล้ว str1 + str2 จะเท่ากับ str2 + str1 ถ้าไม่เท่าก็ตอบสตริงว่างได้เลย
🧭 กล่องสอน — ไล่ตั้งแต่ศูนย์ทีละขั้น (เปิดเมื่อพร้อม)
ขั้นที่ 1 · โจทย์นี้ขออะไรจริง ๆ
คำว่าหารในข้อนี้ไม่ใช่การหารเลข แต่เป็นนิยามที่โจทย์ตั้งขึ้นเอง ให้นึกภาพเป็นตัวต่อเลโก้ชิ้นเดียวกัน
ถ้า str1 คือแถวที่ต่อจากเลโก้ชิ้น AB จำนวน 3 ชิ้น และ str2 คือแถวที่ต่อจากชิ้น AB จำนวน 2 ชิ้น แปลว่าทั้งสองแถวใช้เลโก้ชิ้นเดียวกัน
โจทย์ถามหาเลโก้ชิ้นที่ ใหญ่ที่สุด ที่ต่อเป็นทั้งสองแถวได้ ไม่ใช่ชิ้นเล็กสุด
ขั้นที่ 2 · ความเชื่อผิดสองข้อที่ต้องเคลียร์ก่อน
ความเชื่อผิดข้อแรกคือ คำตอบต้องเป็นสตริงที่สั้นกว่าเสมอ ลองดูว่าจริงไหม
import math
def wrong(a, b):
return b if len(b) <= len(a) else a
def right(a, b):
if a + b != b + a:
return ""
return a[:math.gcd(len(a), len(b))]
for a, b in [("ABCABC","ABC"), ("ABABAB","ABAB"), ("LEET","CODE")]:
print(f"str1={a!r} str2={b!r} | เดาว่าตัวสั้นกว่า -> {wrong(a,b)!r} | คำตอบจริง -> {right(a,b)!r}")str1='ABCABC' str2='ABC' | เดาว่าตัวสั้นกว่า -> 'ABC' | คำตอบจริง -> 'ABC'
str1='ABABAB' str2='ABAB' | เดาว่าตัวสั้นกว่า -> 'ABAB' | คำตอบจริง -> 'AB'
str1='LEET' str2='CODE' | เดาว่าตัวสั้นกว่า -> 'CODE' | คำตอบจริง -> ''บรรทัดแรกถูกโดยบังเอิญ แต่บรรทัดที่สองผิด เพราะ ABAB หาร ABABAB ไม่ได้ (6 หารด้วย 4 ไม่ลงตัว) ต้องถอยลงมาเป็น AB
และบรรทัดที่สามผิดที่สุด เพราะสองสตริงนี้ไม่มีบล็อกร่วมกันเลย คำตอบต้องเป็นสตริงว่าง
ความเชื่อผิดข้อที่สองคือ ยิ่งสั้นยิ่งปลอดภัย ซึ่งก็ไม่ใช่ เพราะโจทย์ขอตัวที่ยาวที่สุด ตอบสั้นเกินไปก็ผิด
ขั้นที่ 3 · วิธีตรงไปตรงมาที่ใช้ได้จริง
จากความยาวข้างบน เราได้เบาะแสว่าความยาวของบล็อกต้องหารความยาวของทั้งสองสตริงลงตัว
ความยาวที่หารทั้งคู่ลงตัวและใหญ่ที่สุด คือ ห.ร.ม. ของสองความยาว ดังนั้นเราไม่ต้องไล่ลองทุกความยาวเลย มีตัวเลือกเดียว
import math
for a, b in [("ABCABC","ABC"), ("ABABAB","ABAB"), ("ABABABAB","ABAB"), ("LEET","CODE")]:
g = math.gcd(len(a), len(b))
print(f"str1={a!r}({len(a)}) str2={b!r}({len(b)}) -> gcd={g} -> ตัวเลือกคำตอบ = str1[:{g}] = {a[:g]!r}")str1='ABCABC'(6) str2='ABC'(3) -> gcd=3 -> ตัวเลือกคำตอบ = str1[:3] = 'ABC'
str1='ABABAB'(6) str2='ABAB'(4) -> gcd=2 -> ตัวเลือกคำตอบ = str1[:2] = 'AB'
str1='ABABABAB'(8) str2='ABAB'(4) -> gcd=4 -> ตัวเลือกคำตอบ = str1[:4] = 'ABAB'
str1='LEET'(4) str2='CODE'(4) -> gcd=4 -> ตัวเลือกคำตอบ = str1[:4] = 'LEET'บรรทัดสุดท้ายเตือนเราว่า ห.ร.ม. บอกได้แค่ความยาวที่เป็นไปได้ ไม่ได้รับประกันว่าบล็อกนั้นใช้ได้จริง จึงยังต้องเช็คอีกชั้น
import math
def divides(block, s):
return len(s) % len(block) == 0 and block * (len(s) // len(block)) == s
for a, b in [("ABCABC","ABC"), ("ABABAB","ABAB"), ("ABABABAB","ABAB"), ("LEET","CODE")]:
g = math.gcd(len(a), len(b))
cand = a[:g]
print(f"str1={a!r} str2={b!r} | ลอง {cand!r}: หาร str1 ได้? {divides(cand,a)} หาร str2 ได้? {divides(cand,b)}")str1='ABCABC' str2='ABC' | ลอง 'ABC': หาร str1 ได้? True หาร str2 ได้? True
str1='ABABAB' str2='ABAB' | ลอง 'AB': หาร str1 ได้? True หาร str2 ได้? True
str1='ABABABAB' str2='ABAB' | ลอง 'ABAB': หาร str1 ได้? True หาร str2 ได้? True
str1='LEET' str2='CODE' | ลอง 'LEET': หาร str1 ได้? True หาร str2 ได้? Falseเอา ห.ร.ม. หาความยาว ตัดหัว str1 มาเป็นบล็อก แล้วเช็คด้วย divides ทั้งสองฝั่ง ถ้าผ่านทั้งคู่ก็ตอบบล็อกนั้น ถ้าไม่ผ่านก็ตอบสตริงว่าง วิธีนี้ถูกต้องสมบูรณ์และเข้าใจง่ายที่สุด ถ้าพอใจแล้วข้ามขั้นที่ 4 ไปดูเฉลยได้เลย
ขั้นที่ 4 · ทางลัดที่สั้นกว่า และเหตุผลที่มันใช้ได้
มีวิธีเช็คที่สั้นกว่า divides คือเช็คว่า str1 + str2 เท่ากับ str2 + str1 ไหม
ทิศทางที่เข้าใจง่ายก่อน: ถ้าสองสตริงสร้างจากบล็อกเดียวกันจริง ต่อสลับข้างย่อมได้ผลเหมือนกัน เพราะทั้งสองฝั่งก็แค่บล็อกเดิมมาเรียงต่อกันจำนวนรวมเท่ากัน
ทิศทางกลับกันคือส่วนที่ลึกกว่า: ถ้า str1 + str2 เท่ากับ str2 + str1 แล้วรับประกันได้ว่ามีบล็อกร่วมอยู่จริง ข้อนี้เป็นทฤษฎีบทที่พิสูจน์แล้วในทฤษฎีสตริง เราขอไม่พิสูจน์ที่นี่
ถ้าเป็นการสัมภาษณ์ แนะนำเขียนแบบ divides เพราะอธิบายได้ทุกบรรทัดด้วยตัวเอง แล้วบอกกรรมการว่ารู้ทางลัดด้วย · การเขียนทางลัดโดยอธิบายไม่ได้ว่าทำไมมันถูก มักถูกถามต่อจนตอบไม่ได้
🔓 เฉลยเต็ม ทั้งสองแบบ (ลองเองก่อนนะ)พับไว้ด้านใน — คลิกเมื่อพร้อมดู
แบบที่ 1 · เช็คตรง ๆ (แนะนำสำหรับสัมภาษณ์)
import math
class Solution:
def gcdOfStrings(self, str1: str, str2: str) -> str:
def divides(block: str, s: str) -> bool:
# ความยาวต้องหารลงตัว และต่อบล็อกซ้ำแล้วต้องได้ s เป๊ะ
return len(s) % len(block) == 0 and block * (len(s) // len(block)) == s
g = math.gcd(len(str1), len(str2)) # ความยาวที่เป็นไปได้มีค่าเดียว
cand = str1[:g] # ตัดหัวมาเป็นบล็อกที่จะลอง
if divides(cand, str1) and divides(cand, str2):
return cand
return ""แบบที่ 2 · ทางลัด (สั้นกว่า)
import math
class Solution:
def gcdOfStrings(self, str1: str, str2: str) -> str:
# ถ้าต่อสลับข้างแล้วไม่เท่ากัน แปลว่าไม่มีบล็อกร่วมเลย
if str1 + str2 != str2 + str1:
return ""
return str1[:math.gcd(len(str1), len(str2))]อ่านโค้ดทีละส่วน
- math.gcd หาความยาวที่หารทั้งสองสตริงลงตัวและใหญ่ที่สุด ซึ่งเป็นความยาวเดียวที่คำตอบเป็นไปได้
- str1[:g] ตัดหัวมาเป็นบล็อกผู้ท้าชิง ตัดจาก str1 หรือ str2 ก็ได้ผลเดียวกันถ้ามีคำตอบจริง
- แบบที่ 1 ตรวจด้วยการต่อบล็อกซ้ำแล้วเทียบกับต้นฉบับตรง ๆ อ่านแล้วเชื่อได้ทันที
- แบบที่ 2 ตรวจด้วยการต่อสลับข้าง สั้นกว่ามากแต่ต้องอ้างทฤษฎีบทเพื่ออธิบายความถูกต้อง
ต้นทุน
ทั้งสองแบบเป็น O(n + m) เพราะการต่อสตริงและการเทียบสตริงทำงานตามความยาวรวม · หน่วยความจำ O(n + m) จากสตริงที่สร้างขึ้นระหว่างเทียบ
เช็คลิสต์ก่อนกดส่ง
- ทดสอบเคสที่ไม่มีคำตอบ เช่น LEET กับ CODE ต้องได้สตริงว่าง
- ทดสอบคู่ที่ความยาวหารกันไม่ลงตัว เช่น ABABAB กับ ABAB ต้องได้ AB ไม่ใช่ ABAB
- ทดสอบคู่ที่หารลงตัว เช่น ABABABAB กับ ABAB ต้องได้ ABAB ไม่ใช่ AB
- ระวังตอบสตริงที่สั้นกว่าโดยไม่ตรวจ เพราะโจทย์ขอตัวที่ยาวที่สุด