Persediaan Temu Duga Pengaturcaraan · Pelajaran

Pemisahan Perkataan dan Pembahagian Rentetan

Gunakan jadual DP 1D untuk menentukan sama ada rentetan boleh dibahagikan kepada perkataan kamus, analisis masa O(n²) dan fahami sebab trie mempercepatkannya.

Pelajaran 3 daripada 413 langkah

Pemisahan Perkataan dan Pembahagian Rentetan ialah pelajaran Persediaan Temu Duga Pengaturcaraan percuma di CoddyKit. Ini ialah pelajaran 3 daripada 4. Anda boleh membaca keseluruhan pelajaran di bawah secara percuma — kemudian berlatih secara praktikal dalam pelayar menggunakan penyunting kod terbina dalam dan tutor kecerdasan buatan 24/7. Pelajaran ini merupakan sebahagian daripada laluan pembelajaran Persediaan Temu Duga Pengaturcaraan, dan kemajuan anda disegerakkan merentas web serta aplikasi CoddyKit. Kursus Persediaan Temu Duga Pengaturcaraan merangkumi sejumlah 4 pelajaran.

Masalah Pemisahan Perkataan

Pemisahan Perkataan (LeetCode 139) meminta anda menentukan: diberikan rentetan s dan kamus perkataan, bolehkah s dibahagikan kepada urutan satu atau lebih perkataan kamus yang dipisahkan oleh ruang? Sebagai contoh, dengan s = 'leetcode' dan wordDict = ['leet', 'code'], jawapannya ialah True kerana 'leet' + 'code' = 'leetcode'. Ini ialah masalah DP satu dimensi yang klasik.

s = 'leetcode'
word_set = {'leet', 'code'}
# Can we split 'leetcode' into words from word_set?
# 'leet' in set → yes, 'code' in set → yes
# So: 'leetcode' = 'leet' + 'code' → True

s2 = 'catsandog'
word_set2 = {'cats', 'dog', 'sand', 'and', 'cat'}
# No matter how we split, last part 'og' not in dict
print('Expected: True, False')

Perumusan dan Keadaan DP

Takrifkan dp[i] sebagai True jika subrentetan s[:i] boleh dibahagikan menggunakan kamus. Kes asasnya ialah dp[0] = True (rentetan kosong sentiasa boleh dibahagikan). Bagi setiap kedudukan i, semak semua kedudukan j < i: jika dp[j] ialah True dan s[j:i] terdapat dalam kamus, maka dp[i] = True. Jawapan akhir ialah dp[len(s)].

def word_break(s, word_dict):
    word_set = set(word_dict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True  # empty string
    
    for i in range(1, n + 1):
        for j in range(i):
            # If s[:j] is segmentable AND s[j:i] is a word
            if dp[j] and s[j:i] in word_set:
                dp[i] = True
                break  # no need to check other j values
    return dp[n]

print(word_break('leetcode', ['leet', 'code']))        # True
print(word_break('catsandog', ['cats','dog','sand','and','cat']))  # False

Menjejak Jadual DP

Bagi s = 'leetcode' dan dict {'leet', 'code'}: dp[0]=T. Pada i=4: j=0, dp[0]=T dan s[0:4]='leet' terdapat dalam dict → dp[4]=T. Pada i=8: j=4, dp[4]=T dan s[4:8]='code' terdapat dalam dict → dp[8]=T. Semua kedudukan lain yang tidak berakhir dengan perkataan kekal False. Jawapan dp[8]=True mengesahkan bahawa rentetan itu boleh dibahagikan.

def word_break_trace(s, word_dict):
    word_set = set(word_dict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(1, n + 1):
        for j in range(i):
            if dp[j] and s[j:i] in word_set:
                dp[i] = True
                print(f'dp[{i}]=True via s[{j}:{i}]={repr(s[j:i])}')
                break
    print('dp table:', dp)
    return dp[n]

word_break_trace('leetcode', ['leet', 'code'])

Analisis Kerumitan Masa

DP naif berjalan dalam masa O(n²): n lelaran luar didarab dengan sehingga n lelaran dalam. Walau bagaimanapun, penghirisan s[j:i] juga memerlukan kos O(n), menjadikan kerumitan sebenar O(n³) dalam Python. Satu pengoptimuman ialah melakukan lelaran pada perkataan dalam kamus dan menyemak sama ada setiap perkataan berakhir pada kedudukan i, yang memberikan O(n × W × L), dengan W ialah saiz kamus dan L ialah panjang purata perkataan. Bagi kebanyakan data masukan temu duga, O(n²) atau O(n³) boleh diterima.

# Slightly faster: iterate over words rather than all j positions
def word_break_v2(s, word_dict):
    word_set = set(word_dict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(1, n + 1):
        for word in word_set:
            wl = len(word)
            # Does 'word' end exactly at position i?
            if i >= wl and dp[i - wl] and s[i - wl:i] == word:
                dp[i] = True
                break
    return dp[n]

print(word_break_v2('applepenapple', ['apple', 'pen']))  # True

Alternatif Rekursi dengan Memoisasi

Masalah yang sama boleh diselesaikan secara atas ke bawah dengan memoization. Takrifkan fungsi rekursif can_break(start) yang mengembalikan True jika s[start:] boleh dibahagikan. Cuba setiap perkataan sebagai awalan s[start:] dan lakukan rekursi pada bakinya. Simpan hasil untuk mengelakkan penerokaan semula indeks mula yang sama berkali-kali. Ini setara dengan DP bawah ke atas, tetapi dalam amalan boleh menjadi lebih pantas jika banyak kedudukan dipangkas lebih awal.

from functools import lru_cache

def word_break_memo(s, word_dict):
    word_set = set(word_dict)
    
    @lru_cache(maxsize=None)
    def can_break(start):
        if start == len(s): return True
        for end in range(start + 1, len(s) + 1):
            if s[start:end] in word_set and can_break(end):
                return True
        return False
    
    return can_break(0)

print(word_break_memo('leetcode', ['leet', 'code']))  # True
print(word_break_memo('catsandog', ['cats','dog','sand','and','cat']))  # False

Memulangkan Semua Pembahagian Sah

Pemisahan Perkataan II (LeetCode 140) meminta semua pembahagian yang mungkin. Pendekatannya ialah undur balik dengan memoisasi: lakukan rekursi dari setiap kedudukan dan, apabila sesuatu perkataan sepadan, lakukan rekursi pada bakinya. Simpan semua hasil separa sebagai senarai rentetan. Untuk mengelakkan TLE, memoisasikan senarai ayat yang mungkin daripada setiap indeks mula. Bilangan ayat boleh menjadi eksponen dalam kes terburuk, tetapi memoization menghapuskan pengiraan berlebihan.

from functools import lru_cache

def word_break_ii(s, word_dict):
    word_set = set(word_dict)
    
    @lru_cache(maxsize=None)
    def break_from(start):
        if start == len(s): return ['']
        results = []
        for end in range(start + 1, len(s) + 1):
            word = s[start:end]
            if word in word_set:
                for rest in break_from(end):
                    results.append(word if not rest else word + ' ' + rest)
        return results
    
    return break_from(0)

print(word_break_ii('catsanddog', ['cat','cats','and','sand','dog']))
# ['cat sand dog', 'cats and dog']

Pengoptimuman Pokok Awalan

Apabila kamus besar atau perkataan panjang, pemeriksaan s[j:i] in word_set bagi semua j menjadi perlahan disebabkan pencincangan rentetan Python. Pokok awalan membolehkan anda menelusuri pokok itu aksara demi aksara sambil memangkas laluan yang mustahil lebih awal. Daripada menyemak semua O(n) kedudukan permulaan, anda hanya mengikuti laluan yang wujud dalam pokok. Ini mengurangkan masa jalan dengan ketara dalam amalan apabila hanya sedikit awalan membawa kepada perkataan yang sah.

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False

def build_trie(words):
    root = TrieNode()
    for word in words:
        node = root
        for ch in word:
            node = node.children.setdefault(ch, TrieNode())
        node.is_end = True
    return root

def word_break_trie(s, word_dict):
    root = build_trie(word_dict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(n):
        if not dp[i]: continue
        node = root
        for j in range(i, n):
            ch = s[j]
            if ch not in node.children: break
            node = node.children[ch]
            if node.is_end:
                dp[j + 1] = True
    return dp[n]

print(word_break_trie('leetcode', ['leet', 'code']))  # True

Kes Tepi dan Kekangan

Kes tepi yang penting: (1) Rentetan kosong: pulangkan nilai benar (rentetan kosong boleh dibahagikan secara jelas). (2) Perkataan tiada dalam kamus: dp tidak pernah menetapkan kedudukan yang sepadan kepada True, lalu mengembalikan nilai palsu dengan betul. (3) Perkataan bertindih: contohnya, 'a' dan 'aa' dalam kamus dengan s='aaa' — DP mengendalikannya secara semula jadi dengan menyemak semua nilai j. (4) Aksara berulang: s='aaaaab' dengan dict=['a','aa','aaa'] — terdapat laluan eksponen, tetapi memoization mengehadkannya kepada O(n²).

def word_break(s, word_dict):
    word_set = set(word_dict)
    dp = [False] * (len(s) + 1)
    dp[0] = True
    for i in range(1, len(s) + 1):
        for j in range(i):
            if dp[j] and s[j:i] in word_set:
                dp[i] = True
                break
    return dp[len(s)]

# Edge cases
print(word_break('', ['hello']))          # True (empty string)
print(word_break('a', ['b']))             # False
print(word_break('aaa', ['a', 'aa']))     # True (many ways)

Pengitlakan Pembahagian Rentetan

Pemisahan Perkataan boleh diperluas kepada sebarang masalah pembahagian rentetan: bolehkah rentetan s dibahagikan mengikut peraturan tertentu? Gantikan carian kamus dengan sebarang semakan O(1) atau O(L). Contohnya: bolehkah s dibahagikan kepada palindrom? Gunakan jadual palindrom yang telah dikira terlebih dahulu dan bukannya set perkataan. Struktur DP adalah sama — hanya semakan kesahan yang berubah.

def palindrome_partition_possible(s):
    '''Can s be partitioned into palindromes? (Always yes — single chars are palindromes)'''
    n = len(s)
    # Precompute palindrome table
    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]
    # DP similar to word break
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(1, n + 1):
        for j in range(i):
            if dp[j] and is_pal[j][i-1]:
                dp[i] = True
                break
    return dp[n]

print(palindrome_partition_possible('aab'))  # True (a,a,b or aa,b)

Pendekatan DP berbanding BFS

Pemisahan Perkataan juga boleh dirumuskan sebagai masalah laluan terpendek BFS: setiap kedudukan dalam rentetan ialah nod, dan terdapat sisi dari j ke i jika s[j:i] terdapat dalam kamus. BFS dari nod 0 menanyakan sama ada nod n boleh dicapai. BFS memberikan kerumitan O(n² × L) yang sama, tetapi mungkin lebih mudah difahami jika anda memodelkannya sebagai masalah graf semasa temu duga.

from collections import deque

def word_break_bfs(s, word_dict):
    word_set = set(word_dict)
    n = len(s)
    visited = set()
    queue = deque([0])
    while queue:
        start = queue.popleft()
        if start == n: return True
        for end in range(start + 1, n + 1):
            if end not in visited and s[start:end] in word_set:
                visited.add(end)
                queue.append(end)
    return False

print(word_break_bfs('leetcode', ['leet', 'code']))    # True
print(word_break_bfs('catsandog', ['cats','dog','and','sand','cat']))  # False

Strategi Komunikasi Temu Duga

Dalam temu duga, terangkan proses pemikiran ini: (1) Perhatikan bahawa pilihan pada setiap kedudukan bergantung pada perkara yang boleh dicapai sebelum itu — ini menandakan DP. (2) Takrifkan keadaan: dp[i] = bolehkah kita membahagikan s[:i]? (3) Nyatakan hubungan rekursi dan kes asas sebelum menulis kod. (4) Tulis penyelesaian O(n²) terlebih dahulu, kemudian sebutkan pengoptimuman pokok awalan sebagai susulan. (5) Bincangkan kes tepi: rentetan kosong, satu aksara, dan perkataan yang tiada dalam kamus.

# Clean final solution to present in interview
def word_break(s, word_dict):
    '''O(n^2 * L) time, O(n + W) space where W = total word length in dict'''
    word_set = set(word_dict)   # O(W) space
    n = len(s)
    dp = [False] * (n + 1)     # O(n) space
    dp[0] = True
    for i in range(1, n + 1):
        for j in range(i):     # try all split points
            if dp[j] and s[j:i] in word_set:
                dp[i] = True
                break
    return dp[n]

# Time: O(n^2 * L) - n^2 pairs, each dict lookup is O(L)
# Space: O(n) for dp array, O(W) for word_set
print(word_break('applepenapple', ['apple', 'pen']))  # True

Semakan Ringkas

Uji pemahaman anda tentang konsep Struktur Data & Algoritma — Persediaan Temu Duga Pengekodan daripada pelajaran ini.

Imbas Kembali Pelajaran

Dalam pelajaran ini anda telah mempelajari: dp[i] mewakili sama ada s[:i] boleh dibahagikan kepada perkataan kamus, hubungan rekursi O(n²) menyemak semua titik pemisah j apabila dp[j]=True dan s[j:i] terdapat dalam set perkataan, dan pokok awalan boleh mempercepatkan gelung dalaman dengan memangkas awalan yang tidak wujud lebih awal. Seterusnya, kita akan meneroka Penyahkodan Cara dan Pengiraan Laluan, satu lagi corak DP satu dimensi yang menyerupai Fibonacci.

Percuma untuk bermula

Pelajari Persediaan Temu Duga Pengaturcaraan dengan tutor kecerdasan buatan — percuma

Tulis dan jalankan kod sebenar dalam pelayar anda, dapatkan bantuan segera daripada tutor kecerdasan buatan yang tersedia 24/7, dan sambung semula dari tempat anda berhenti di web atau dalam aplikasi.

Kursus
90
Pelajaran
360

Soalan Lazim

Adakah pelajaran “Pemisahan Perkataan dan Pembahagian Rentetan” percuma?

Ya — teks penuh “Pemisahan Perkataan dan Pembahagian Rentetan” boleh dibaca secara percuma di web ini. Untuk berlatih secara interaktif menggunakan penyunting kod terbina dalam dan tutor kecerdasan buatan 24/7, serta membuka kunci baki kursus Persediaan Temu Duga Pengaturcaraan, tingkat taraf kepada CoddyKit PRO. Kursus Persediaan Temu Duga Pengaturcaraan merangkumi sejumlah 4 pelajaran.

Apakah yang akan saya pelajari dalam “Pemisahan Perkataan dan Pembahagian Rentetan”?

Gunakan jadual DP 1D untuk menentukan sama ada rentetan boleh dibahagikan kepada perkataan kamus, analisis masa O(n²) dan fahami sebab trie mempercepatkannya. Anda berlatih Persediaan Temu Duga Pengaturcaraan menggunakan kod praktikal yang dijalankan terus dalam pelayar, manakala tutor kecerdasan buatan 24/7 menjawab soalan anda semasa anda mengikuti pelajaran.

Adakah saya memerlukan pengalaman untuk memulakan Persediaan Temu Duga Pengaturcaraan?

Tiada pengalaman terdahulu diperlukan. Pembelajaran Persediaan Temu Duga Pengaturcaraan di CoddyKit disusun untuk pelajar daripada peringkat pemula hingga lanjutan, jadi anda boleh bermula di sini atau dari awal dan belajar mengikut kadar anda sendiri. Ini ialah pelajaran 3 daripada 4.

Berapa lamakah pelajaran “Pemisahan Perkataan dan Pembahagian Rentetan” diambil?

Kebanyakan pelajaran CoddyKit mengambil masa kira-kira 5–10 minit. Setiap pelajaran ringkas dan interaktif, jadi anda boleh membuat kemajuan secara berterusan dan menyambung tepat dari tempat anda berhenti di web atau aplikasi.

Bolehkah saya menulis dan menjalankan kod dalam pelajaran Persediaan Temu Duga Pengaturcaraan ini?

Ya. Setiap pelajaran Persediaan Temu Duga Pengaturcaraan menyertakan penyunting kod terbina dalam, jadi anda boleh menulis dan menjalankan kod sebenar terus dalam pelayar serta menerima maklum balas kecerdasan buatan serta-merta — tanpa memerlukan persediaan setempat.

Semua pelajaran dalam kursus ini

  1. House Robber: Pengulangan Ambil atau Langkau
  2. Sub­tatasusunan Maksimum dan Sub­tatasusunan Hasil Darab Maksimum
  3. Pemisahan Perkataan dan Pembahagian Rentetan
  4. Menyahkod Cara dan Mengira Laluan
← Kembali ke Persediaan Temu Duga Pengaturcaraan