การแบ่งพาลินโดรม II
ผสานตารางพาลินโดรมที่คำนวณไว้ล่วงหน้ากับ DP หนึ่งมิติ เพื่อหาจำนวนการตัดต่ำสุดสำหรับแบ่งสตริงเป็นพาลินโดรม
การแบ่งพาลินโดรม II เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
โจทย์: จำนวนครั้งตัดน้อยที่สุดเพื่อแบ่งส่วน
การแบ่งสตริงเป็นพาลินโดรม II มีโจทย์ว่า เมื่อกำหนดสตริง s ให้หาจำนวนครั้งตัดที่น้อยที่สุด เพื่อให้สตริงย่อยทุกส่วนในการแบ่งเป็นพาลินโดรม สำหรับ 'aab' การตัดหนึ่งครั้งทำให้ได้ ['aa', 'b'] ดังนั้นคำตอบคือ 1 ส่วน 'a' มีคำตอบเป็น 0 (เพราะเป็นพาลินโดรมอยู่แล้ว) โจทย์นี้ประกอบด้วย DP สองขั้นตอน: ขั้นแรกคำนวณล่วงหน้าว่าสตริงย่อยใดเป็นพาลินโดรม จากนั้นใช้ DP 1 มิติเพื่อหาจำนวนครั้งตัดที่น้อยที่สุด
ขั้นตอนที่ 1: คำนวณตารางพาลินโดรมล่วงหน้า
ขั้นแรก ให้สร้าง is_pal[i][j] = True หาก s[i..j] เป็นพาลินโดรม โดยใช้ DP แบบช่วง วิธีนี้ใช้เวลา O(n²) และพื้นที่ O(n²) อีกทางเลือกหนึ่งคือการขยายจากจุดกึ่งกลาง ซึ่งเติมค่าลงในตารางเดียวกันโดยใช้เวลา O(n²) เราจำเป็นต้องมีตารางนี้ เพราะ DP การตัดแบบ 1 มิติจะเรียกดู is_pal[i][j] ซ้ำหลายครั้ง การคำนวณล่วงหน้าจึงช่วยหลีกเลี่ยงการตรวจสอบพาลินโดรมซ้ำภายในลูปของ DP การตัด
def build_palindrome_table(s):
n = len(s)
is_pal = [[False]*n for _ in range(n)]
for i in range(n):
is_pal[i][i] = True
for i in range(n-1):
is_pal[i][i+1] = (s[i] == s[i+1])
for length in range(3, n+1):
for i in range(n-length+1):
j = i + length - 1
is_pal[i][j] = (s[i] == s[j]) and is_pal[i+1][j-1]
return is_pal
print(build_palindrome_table('aab'))ขั้นตอนที่ 2: การตั้งค่า DP การตัดแบบ 1 มิติ
กำหนดให้ cuts[i] คือจำนวนครั้งตัดที่น้อยที่สุดสำหรับการแบ่ง s[0..i] หาก s[0..i] เป็นพาลินโดรมในตัวเอง ให้กำหนด cuts[i] = 0 มิฉะนั้นให้ลองทุกจุดแบ่ง: สำหรับแต่ละ j ตั้งแต่ 0 ถึง i-1 หาก s[j+1..i] เป็นพาลินโดรม ก็ให้ cuts[i] = min(cuts[i], cuts[j] + 1) เรากำลังถามว่า หากส่วนสุดท้ายของการแบ่งคือ s[j+1..i] จะเป็นอย่างไร ในกรณีนั้น เราต้องใช้ cuts[j] ครั้งตัดสำหรับคำนำหน้า และตัดเพิ่มอีก 1 ครั้ง
def min_cut(s):
n = len(s)
is_pal = build_palindrome_table(s)
cuts = [float('inf')] * n
for i in range(n):
if is_pal[0][i]:
cuts[i] = 0 # entire prefix is a palindrome
else:
for j in range(i):
if is_pal[j+1][i]:
cuts[i] = min(cuts[i], cuts[j] + 1)
return cuts[n-1]วิธีแก้ปัญหาฉบับเต็มและการไล่ตัวอย่าง
มาลองไล่ดู 'aab' กัน ตารางพาลินโดรมคือ is_pal[0][0]='a'=T, is_pal[1][1]='a'=T, is_pal[2][2]='b'=T, is_pal[0][1]='aa'=T, is_pal[1][2]='ab'=F, is_pal[0][2]='aab'=F ส่วนจำนวนครั้งตัดคือ cuts[0]=0 ('a' เป็นพาลินโดรม), cuts[1]=0 ('aa' เป็นพาลินโดรม), และ cuts[2]: 'aab' ไม่เป็นพาลินโดรม จึงลอง j=1: is_pal[2][2]=T ทำให้ cuts[2] = cuts[1]+1 = 1 คำตอบคือ 1
def build_palindrome_table(s):
n = len(s)
is_pal = [[False]*n for _ in range(n)]
for i in range(n):
is_pal[i][i] = True
for i in range(n-1):
is_pal[i][i+1] = (s[i] == s[i+1])
for length in range(3, n+1):
for i in range(n-length+1):
j = i + length - 1
is_pal[i][j] = (s[i] == s[j]) and is_pal[i+1][j-1]
return is_pal
def min_cut(s):
n = len(s)
is_pal = build_palindrome_table(s)
cuts = [float('inf')] * n
for i in range(n):
if is_pal[0][i]:
cuts[i] = 0
else:
for j in range(i):
if is_pal[j+1][i]:
cuts[i] = min(cuts[i], cuts[j] + 1)
return cuts[n-1]
print(min_cut('aab')) # 1
print(min_cut('ababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababababab'))ความซับซ้อนด้านเวลาและพื้นที่
ขั้นตอนที่ 1 (ตารางพาลินโดรม) ใช้ เวลา O(n²) และพื้นที่ O(n²) ขั้นตอนที่ 2 (DP การตัด) มีลูปภายนอกสำหรับตำแหน่ง n ตำแหน่ง และลูปภายในสำหรับจุดแบ่ง n จุด จึงใช้ เวลา O(n²) เช่นกัน โดยรวมแล้วใช้ เวลา O(n²) และพื้นที่ O(n²) เราสามารถลดพื้นที่สำหรับอาร์เรย์จำนวนครั้งตัดให้เหลือ O(n) ได้ แต่ตารางพาลินโดรมยังคงต้องใช้ O(n²) ผู้สัมภาษณ์คาดหวังคำตอบ O(n²) — วิธีแก้แบบ O(n) โดยใช้อัลกอริทึมมานาเชอร์อยู่นอกขอบเขตเนื้อหาทั่วไป
การขยายจากจุดกึ่งกลางสำหรับตารางพาลินโดรม
แทนที่จะใช้วิธี DP แบบช่วงเพื่อสร้างตารางพาลินโดรม คุณสามารถเติมค่าใน is_pal โดยใช้ การขยายจากจุดกึ่งกลาง สำหรับแต่ละตำแหน่งกึ่งกลาง ให้ขยายออกด้านข้างและทำเครื่องหมายพาลินโดรมทั้งหมดที่พบ วิธีนี้ยังคงใช้เวลา O(n²) และพื้นที่ O(n²) แต่อาจทำงานเร็วกว่าในทางปฏิบัติเนื่องจากพฤติกรรมของแคชที่ดีกว่า ทั้งสองวิธีใช้ตอบในการสัมภาษณ์ได้ถูกต้อง
def build_pal_expand(s):
n = len(s)
is_pal = [[False]*n for _ in range(n)]
def expand(l, r):
while l >= 0 and r < n and s[l] == s[r]:
is_pal[l][r] = True
l -= 1; r += 1
for i in range(n):
expand(i, i) # odd-length centres
expand(i, i+1) # even-length centres
return is_pal
print('Expand-around-centre palindrome table built')การแจกแจงการแบ่งทั้งหมด (ภาคที่ 1)
การแบ่งสตริงเป็นพาลินโดรม I (โจทย์ที่เกี่ยวข้องกัน) มีโจทย์ให้แจกแจงการแบ่งที่ถูกต้องทั้งหมด ซึ่งทุกสตริงย่อยต้องเป็นพาลินโดรม วิธีนี้ใช้ การย้อนกลับ โดยมีตารางพาลินโดรมที่คำนวณไว้ล่วงหน้าเป็นตัวช่วยตัดกิ่ง ต่างจาก DP จำนวนครั้งตัดที่น้อยที่สุดซึ่งใช้การนับ วิธีนี้ต้องแจกแจงคำตอบจำนวนมากในระดับเลขชี้กำลัง และต้องแก้ด้วยแนวทางที่แตกต่างออกไปโดยสิ้นเชิง
def partition_all(s):
n = len(s)
is_pal = build_pal_expand(s)
result = []
def backtrack(start, path):
if start == n:
result.append(path[:])
return
for end in range(start, n):
if is_pal[start][end]:
path.append(s[start:end+1])
backtrack(end+1, path)
path.pop()
backtrack(0, [])
return result
print(partition_all('aab')) # [['a','a','b'], ['aa','b']]การกำหนดค่าเริ่มต้นของจำนวนครั้งตัดเป็น n-1
เคล็ดลับที่ใช้กันบ่อยคือ กำหนดค่าเริ่มต้นให้ cuts[i] = i แทนที่จะใช้ inf เพราะกรณีแย่ที่สุดของ s[0..i] คือการตัดอักขระทุกตัวแยกกัน ทำให้มี i ครั้งตัด วิธีนี้ช่วยให้ไม่ต้องตรวจสอบ inf ในโค้ด เมื่อ is_pal[0][i] เป็นจริง เราจะเขียนทับค่าเป็น 0 การกำหนดค่าเริ่มต้นเช่นนี้ทำให้ขอบเขตบนของจำนวนครั้งตัดชัดเจนขึ้น และทำให้โค้ดง่ายขึ้นเล็กน้อย
def min_cut_clean(s):
n = len(s)
is_pal = build_palindrome_table(s)
cuts = list(range(n)) # cuts[i] = i (worst case)
for i in range(n):
if is_pal[0][i]:
cuts[i] = 0
else:
for j in range(1, i+1):
if is_pal[j][i]:
cuts[i] = min(cuts[i], cuts[j-1] + 1)
return cuts[n-1]ทางเลือก: DP รอบเดียวโดยไม่ใช้ตารางแยก
รูปแบบหนึ่งที่สง่างามคือเติมตารางพาลินโดรมและ DP การตัดไปพร้อมกัน เมื่อใช้ expand เพื่อขยายพาลินโดรมจากแต่ละจุดกึ่งกลาง เราจะอัปเดตอาร์เรย์ cuts ได้ทันที สำหรับพาลินโดรม s[l..r] เราสามารถอัปเดต cuts[r] = min(cuts[r], (cuts[l-1]+1 if l > 0 else 0)) วิธีนี้หลีกเลี่ยงการวนผ่านตาราง O(n²) แยกต่างหาก และอาจเขียนได้สะอาดกว่าในการสัมภาษณ์ที่มีข้อจำกัดด้านเวลา
กรณีขอบเขตที่ควรพิจารณา
กรณีขอบเขตสำคัญสำหรับการแบ่งสตริงเป็นพาลินโดรม II ได้แก่ (1) สตริงที่มีอักขระเดียวให้ผลเป็น 0 ครั้งตัด (2) สตริงที่เป็นพาลินโดรมอยู่แล้วให้ผลเป็น 0 ครั้งตัด (3) สตริงที่มีอักขระแตกต่างกันทั้งหมดต้องใช้ n-1 ครั้งตัด และ (4) สตริงที่มีอักขระเหมือนกันทั้งหมด (เช่น 'aaaa') ต้องใช้ 0 ครั้งตัด เพราะทั้งสตริงเป็นพาลินโดรม อย่าลืมตรวจสอบว่าโซลูชันของคุณจัดการการออกจากลูปก่อนกำหนดเมื่อ is_pal[0][i] = True ได้อย่างถูกต้อง
def build_palindrome_table(s):
n = len(s)
is_pal = [[False]*n for _ in range(n)]
for i in range(n):
is_pal[i][i] = True
for i in range(n-1):
is_pal[i][i+1] = (s[i] == s[i+1])
for length in range(3, n+1):
for i in range(n-length+1):
j = i + length - 1
is_pal[i][j] = (s[i] == s[j]) and is_pal[i+1][j-1]
return is_pal
def min_cut(s):
n = len(s)
is_pal = build_palindrome_table(s)
cuts = list(range(n))
for i in range(n):
if is_pal[0][i]:
cuts[i] = 0
else:
for j in range(1, i+1):
if is_pal[j][i]:
cuts[i] = min(cuts[i], cuts[j-1] + 1)
return cuts[n-1]
print(min_cut('a')) # 0
print(min_cut('aaaa')) # 0
print(min_cut('abc')) # 2เคล็ดลับการสื่อสารในการสัมภาษณ์
เมื่อนำเสนอโจทย์นี้ในการสัมภาษณ์ ให้เริ่มด้วยแนวทางสองขั้นตอน: ขั้นแรกสร้างตารางพาลินโดรม จากนั้นรัน DP 1 มิติบนอาร์เรย์จำนวนครั้งตัด อธิบายสมการเวียนเกิดด้วยคำพูดก่อนเขียนโค้ด กล่าวถึงการที่ตารางพาลินโดรมมีรายการ O(n²) รายการ และแต่ละรายการเติมค่าได้ในเวลา O(1) โดยใช้สมการเวียนเกิดของ DP แบบช่วง ก่อนเขียนวิธีแก้ปัญหาฉบับเต็ม ควรไล่ดูตัวอย่างให้ครบเพื่อแสดงความถูกต้องของวิธีภายใต้แรงกดดัน
ตรวจสอบอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้ว่า การแบ่งสตริงเป็นพาลินโดรม II ใช้ DP สองขั้นตอน — คำนวณตารางพาลินโดรมล่วงหน้า จากนั้นรัน DP การตัดแบบ 1 มิติ, สมการเวียนเกิดของการตัดคือ cuts[i] = min(cuts[j-1] + 1) สำหรับทุก j ที่ s[j..i] เป็นพาลินโดรม และ ความซับซ้อนโดยรวมคือเวลา O(n²) และพื้นที่ O(n²) บทถัดไปเราจะจัดการกับโจทย์ Burst Balloons ซึ่งใช้แนวทาง DP แบบช่วงย้อนกลับที่ชาญฉลาด
เรียนรู้ Coding Interview Prep ด้วย AI tutor — ฟรี
เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป
- คอร์ส
- 90
- บทเรียน
- 360
คำถามที่พบบ่อย
บทเรียน “การแบ่งพาลินโดรม II” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การแบ่งพาลินโดรม II” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การแบ่งพาลินโดรม II”
ผสานตารางพาลินโดรมที่คำนวณไว้ล่วงหน้ากับ DP หนึ่งมิติ เพื่อหาจำนวนการตัดต่ำสุดสำหรับแบ่งสตริงเป็นพาลินโดรม คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “การแบ่งพาลินโดรม II” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- รูปแบบ DP แบบช่วงและลำดับการเติมค่า
- ลำดับย่อยและสตริงย่อยพาลินโดรมที่ยาวที่สุด
- การแบ่งพาลินโดรม II
- ระเบิดลูกโป่ง: DP แบบช่วงย้อนกลับ