# 016 — Word Break

[অধ্যায়](../README.md) · [সমস্ত অনুশীলন ও উৎস](README.md) · [ধাপে ধাপে শেখা](../learning-guide.md)

<div class="problem-source" markdown="1">

<p class="source-label" id="source">Problem link ও উৎস</p>

- **Original source:** Classic string-segmentation DP exercise
- **Platform:** LeetCode — <https://leetcode.com/problems/word-break/>
- **Topic:** dynamic programming
- **Difficulty:** Medium
- **Pattern:** 1D over string prefixes
- **Basic idea inherited from:** Hashing (folder 05) দ্রুত word-lookup-এর জন্য, আর #2 Climbing Stairs-এর "শেষ ধাপ কোথা থেকে?" prefix-চিন্তা

</div>

## প্রশ্ন ও উদাহরণ {#problem}

### সহজ করে প্রশ্ন {#1-problem-in-simple-words}

একটা string `s` আর একটা শব্দ-তালিকা (dictionary) `wordDict` দেওয়া। প্রশ্ন: `s`-কে কি dictionary-র এক বা একাধিক শব্দে (space দিয়ে) **পুরোপুরি কাটা যায়**? প্রতিটা শব্দ যত খুশি বার ব্যবহার করা যায়। শুধু "হ্যাঁ/না" (boolean) বলতে হবে।

```text
s = "leetcode", wordDict = ["leet", "code"]
"leet" + "code" -> পুরোটা ঢাকা পড়ে -> True

s = "catsandog", wordDict = ["cats","dog","sand","and","cat"]
কোনো বিভাজনই পুরো string ঢাকে না (শেষে "og" আটকে যায়) -> False
```

### Input ও output {#8-example-input-and-output}

```text
Input : "leetcode", ["leet","code"]                      -> True
Input : "applepenapple", ["apple","pen"]                 -> True  (apple+pen+apple)
Input : "catsandog", ["cats","dog","sand","and","cat"]   -> False

Edge case 1: "a", ["a"]      -> True
Edge case 2: "ab", ["a"]     -> False  ("b" ঢাকা পড়ে না)
Edge case 3: "", ["a"]       -> True   (খালি string trivially segmentable)
```

### চেনা উদাহরণে বোঝা {#6-real-life-analogy}

ভাবো একটা space-হীন লম্বা শব্দ, আর তুমি জানা-শব্দের অভিধান দিয়ে সেটায় space বসাতে চাও। বাঁ-থেকে পড়তে পড়তে প্রতিটা position-এ ভাবো: "এই বিন্দু পর্যন্ত কি বৈধভাবে শব্দে কাটা গেছে?" — সেটা সত্যি তখনই, যখন কোনো আগের কাটা-বিন্দু থেকে এই বিন্দু পর্যন্ত অংশটা একটা চেনা শব্দ। একটা চেনা শব্দ পেলেই এই বিন্দু "সবুজ" হয়ে যায়।

## ভিত্তির ধারণা {#idea}

### আগের কোন ধারণাটি লাগবে {#2-which-basic-idea-does-this-inherit-from}

দুটো ভিত্তি। (১) **Hashing (folder 05):** dictionary-কে একটা `set`-এ রাখি, যাতে "এই substring কি একটা শব্দ?" O(1)-এ যাচাই হয়। (২) **#2 Climbing Stairs-এর prefix-চিন্তা:** সেখানে "step `i`-তে পৌঁছানোর শেষ move কোথা থেকে?" জিজ্ঞেস করতাম; এখানে "prefix `s[0:i]`-এর শেষ শব্দটা কোথায় শুরু?" — string-এর index-গুলোর উপর একটা 1D DP।

### ভেতরের যুক্তি {#3-what-is-the-hidden-math-or-logic}

লুকানো logic: prefix `s[0:i]` segmentable হবে তখনই, যখন এমন একটা break-point `j` (`0 ≤ j < i`) আছে যেখানে — (ক) ছোট prefix `s[0:j]` ইতিমধ্যে segmentable, এবং (খ) মাঝের টুকরো `s[j:i]` dictionary-র একটা শব্দ। মানে শেষ শব্দটা `s[j:i]`, তার আগের সবটুকু আগেই বৈধভাবে কাটা। যেকোনো একটা valid `j` পেলেই `s[0:i]` segmentable।

### কী দিয়ে তথ্য রাখব {#4-which-data-structure-is-needed}

একটা **1D boolean array** `dp` (size `n + 1`), `dp[i]` = prefix `s[0:i]` (প্রথম `i` অক্ষর) segmentable কি না। সাথে একটা **hash set** `words` দ্রুত lookup-এর জন্য। Top-down করলে dict (`start` → bool)।

### এটি কেন উপযুক্ত {#5-why-this-data-structure-fits}

State একটামাত্র সংখ্যা: কত-অক্ষর-লম্বা prefix বিবেচনা করছি। তাই prefix-length-কে index ধরা flat boolean array নিখুঁত — `dp[i]` ঠিক `s[0:i]`-এর উত্তর ধরে। dictionary-lookup অসংখ্য বার হয়, তাই list-এর বদলে `set` (hashing) — O(1) membership, যা না হলে প্রতিটা check O(dict size) হতো।

## ধাপে ধাপে সমাধান {#walkthrough}

### প্রথম সরল উপায় {#9-brute-force-thinking}

প্রথম সরল চিন্তা: প্রতিটা শুরু-বিন্দু থেকে recurse করা — প্রথম শব্দটা কতদূর, ঠিক করে বাকি অংশ একইভাবে সমাধান।

```text
can(start):                      # s[start:] কি segmentable?
    if start == n: return True   # পুরোটা শেষ
    for end in start+1 .. n:
        if s[start:end] in words and can(end):
            return True
    return False
answer = can(0)
```

### কোথায় বেশি কাজ হচ্ছে {#10-why-brute-force-is-slow}

Memoization ছাড়া এই recursion **exponential** — প্রতিটা `start` থেকে অনেকগুলো `end` শাখা, আর একই `start` বহু পথ দিয়ে বারবার পৌঁছানো হয় (overlapping subproblems)। যেমন `"aaaa...a"`-তে break-point combination বিস্ফোরিত হয়। লম্বা string-এ সম্পূর্ণ অসম্ভব।

### যে বিষয়টি খেয়াল করতে হবে {#11-key-observation}

মূল observation: **distinct state মাত্র `n + 1`-টা** (প্রতিটা prefix-length, বা প্রতিটা start-index)। প্রতিটার উত্তর একবার গুনে রাখলে exponential কাজ O(n²) (× substring/lookup খরচ) হয়ে যায়। আর `set` lookup O(1) বলে substring-যাচাই সস্তা।

### আরও কম কাজে সমাধান {#12-optimized-intuition}

**State (শব্দে):** `dp[i]` = prefix `s[0:i]` (প্রথম `i` অক্ষর) dictionary-র শব্দে পুরোপুরি কাটা যায় কি না (boolean)। (Top-down-এ: `can(start)` = suffix `s[start:]` segmentable কি না।)

**Transition:**

```text
dp[i] = OR over all j in 0..i-1 of ( dp[j] and s[j:i] in words )
```

কারণ: শেষ শব্দ `s[j:i]` — তার আগের prefix `s[0:j]` আগেই বৈধভাবে কাটা থাকতে হবে (`dp[j]`), আর মাঝের টুকরো শব্দ হতে হবে।

**Base case:** `dp[0] = True` (খালি prefix সবসময় segmentable — কিছুই কাটার নেই)।

**Answer location:** `dp[n]` (পুরো string)।

Memoization (top-down): উপরের `can(start)` recursion, dict যোগ। Tabulation (bottom-up): `i = 1..n` বাঁ-থেকে-ডান, প্রতিটায় সব break-point `j < i` দেখো; একটা valid পেলেই `True` দিয়ে break। dictionary সবসময় `set`-এ রাখো।

### ভাবনার পরিবর্তন {#13-thinking-tweak}

**মোচড়:** "সব সম্ভব বিভাজন বের করব" — এই বিশাল search না ভেবে শুধু **শেষ শব্দ** কোথায় শুরু, তা ঠিক করো; বাকিটা ছোট একই problem। আর performance-এর চাবি hashing: dictionary `list`-এ রাখলে প্রতিটা lookup ধীর হয়ে পুরোটা O(n² × m) হয়ে যায়; `set`-এ রাখলে lookup O(1)। DP কঙ্কাল + সঠিক data structure — দুটো মিলেই efficient সমাধান।

### ছবিতে বোঝা {#7-visual-explanation}

`dp[i]` = `s[0:i]` segmentable কি না (T/F)। `s = "leetcode"`, words = {leet, code}:

```text
index :  0   1   2   3   4   5   6   7   8
s     :      l   e   e   t   c   o   d   e
dp    :  T   F   F   F   T   F   F   F   T

dp[i] = OR over j<i of ( dp[j] AND s[j:i] in words )

dp[0] = True                      (base: খালি prefix)
dp[4]: j=0 -> dp[0]=T এবং s[0:4]="leet" in words -> True
dp[8]: j=4 -> dp[4]=T এবং s[4:8]="code" in words -> True   <- answer

(dp[1..3], dp[5..7] কোনো valid break পায় না -> False)
```

প্রতিটা position-এ সব আগের break-point দেখি; একটাও "আগে-সবুজ + মাঝের-টুকরো-শব্দ" হলে এই position সবুজ। উত্তর `dp[n]`।

### ছোট input-এ ধাপে ধাপে {#14-step-by-step-dry-run}

`s = "cars"`, words = {car, ca, rs}, tabulation:

1. `dp = [T, F, F, F, F]` (dp[0]=T)
2. i=1: j=0 → dp[0]=T, s[0:1]="c" in words? না → dp[1]=F
3. i=2: j=0 → s[0:2]="ca" in words? হ্যাঁ, dp[0]=T → dp[2]=T
4. i=3: j=0 → "car"? হ্যাঁ, dp[0]=T → dp[3]=T (j=2 দেখার দরকার নেই)
5. i=4: j=0 → "cars"? না; j=2 → dp[2]=T, s[2:4]="rs" in words? হ্যাঁ → dp[4]=T
6. return `dp[4] = True`

হাতে মিলিয়ে: "ca" + "rs" = "cars" ঢেকে যায়। ✔

## কোড ও ব্যাখ্যা {#code}

### Python {#15-python-solution}

```python
# ---- way 1: tabulation (bottom-up), set = hashing ----
def word_break_tab(s, word_dict):
    words = set(word_dict)                    # O(1) lookup
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True                              # খালি prefix
    for i in range(1, n + 1):
        for j in range(i):
            if dp[j] and s[j:i] in words:     # আগে-সবুজ + মাঝের টুকরো শব্দ
                dp[i] = True
                break
    return dp[n]

# ---- way 2: memoization (top-down) ----
def word_break_memo(s, word_dict):
    words = set(word_dict)
    n = len(s)
    memo = {}
    def can(start):                           # s[start:] segmentable?
        if start == n:
            return True
        if start in memo:
            return memo[start]
        for end in range(start + 1, n + 1):
            if s[start:end] in words and can(end):
                memo[start] = True
                return True
        memo[start] = False
        return False
    return can(0)

# ---- tests ----
cases = [
    ("leetcode", ["leet", "code"], True),
    ("applepenapple", ["apple", "pen"], True),
    ("catsandog", ["cats", "dog", "sand", "and", "cat"], False),
    ("a", ["a"], True),
    ("ab", ["a"], False),
    ("aaaaaaa", ["aaaa", "aaa"], True),
    ("", ["a"], True),                        # খালি string segmentable
    ("cars", ["car", "ca", "rs"], True),
]
for s, wd, want in cases:
    assert word_break_tab(s, wd) == want, (s, word_break_tab(s, wd))
    assert word_break_memo(s, wd) == want, (s, word_break_memo(s, wd))

print("সব test pass!")
```

### কোডের প্রতিটি অংশ {#16-line-by-line-code-explanation}

- `word_break_tab`: dictionary-কে `set`-এ রাখে (O(1) lookup); `dp[0]=True`; প্রতিটা prefix-শেষ `i`-এর জন্য সব break-point `j` দেখে — `dp[j]` সত্যি ও `s[j:i]` শব্দ হলে `dp[i]=True` করে `break`; answer `dp[n]`।
- `word_break_memo`: `can(start)` suffix `s[start:]` segmentable কি না; প্রথম শব্দ `s[start:end]` শব্দ হলে আর বাকিটা segmentable হলে True; `memo` repeated কাজ এড়ায়।
- দুই function-ই `set`-এর উপর নির্ভর করে — এটাই lookup সস্তা রাখে।

### Output মিলিয়ে দেখা {#17-output-walkthrough}

`cases`-এ আটটা input — LeetCode-এর তিন classic (`leetcode`, `applepenapple` True; `catsandog` False), single-char True/False, repeat-word `aaaaaaa` True, খালি string edge (`""` → True), আর একটা ছোট `cars` True। প্রতিটার জন্য দুই function-ই মিলতে হবে। সব pass হলে শেষে print: `সব test pass!`।

### JavaScript ও test cases {#25-javascript-solution-with-test-cases}

prefix-এর উপর 1D boolean DP + `Set` lookup; `dp[i] = OR_j (dp[j] && s.slice(j,i) in words)`।

```javascript
const assert = (cond, msg = "") => { if (!cond) throw new Error("FAIL " + msg); };
// dp[i] = prefix s[0:i] segmentable কি না
function wordBreak(s, wordDict) {
  const words = new Set(wordDict);            // O(1) lookup
  const n = s.length;
  const dp = new Array(n + 1).fill(false);
  dp[0] = true;                               // খালি prefix
  for (let i = 1; i <= n; i++) {
    for (let j = 0; j < i; j++) {
      if (dp[j] && words.has(s.slice(j, i))) {  // আগে-সবুজ + মাঝের টুকরো শব্দ
        dp[i] = true;
        break;
      }
    }
  }
  return dp[n];
}
// ---- test cases (example + edge) ----
const cases = [
  ["leetcode", ["leet", "code"], true],
  ["applepenapple", ["apple", "pen"], true],
  ["catsandog", ["cats", "dog", "sand", "and", "cat"], false],
  ["a", ["a"], true],
  ["ab", ["a"], false],                       // "b" ঢাকা পড়ে না
  ["aaaaaaa", ["aaaa", "aaa"], true],
  ["", ["a"], true],                          // খালি string segmentable
  ["cars", ["car", "ca", "rs"], true],
];
for (const [s, wd, want] of cases) {
  assert(wordBreak(s, wd) === want, "break " + JSON.stringify(s));
}
console.log("সব JS test pass!");
```
> **JS টীকা:** dictionary-কে `Set`-এ রাখো — `array.includes` O(n), `Set.has` O(1); list-এ রাখলে পুরোটা অনেক ধীর। substring পেতে `s.slice(j, i)` (`i` exclusive) — Python-এর `s[j:i]`-এর সমতুল্য; index alignment নিখুঁত হতে হবে (`dp[j]` আগের অংশ, `s.slice(j,i)` শেষ শব্দ)। `dp[0] = true` base ভুলে গেলে সব false ভুল উত্তর।

### TypeScript ও test cases {#26-typescript-solution-with-test-cases}

```typescript
function wordBreak(s: string, wordDict: string[]): boolean {
  const words: Set<string> = new Set(wordDict);
  const n: number = s.length;
  const dp: boolean[] = new Array(n + 1).fill(false);
  dp[0] = true;
  for (let i = 1; i <= n; i++) {
    for (let j = 0; j < i; j++) {
      if (dp[j] && words.has(s.slice(j, i))) {
        dp[i] = true;
        break;
      }
    }
  }
  return dp[n];
}
function expect<T>(actual: T, exp: T, msg = ""): void {
  if (actual !== exp) throw new Error(`FAIL ${msg}: got ${actual}`);
}
const cases: Array<[string, string[], boolean]> = [
  ["leetcode", ["leet", "code"], true],
  ["applepenapple", ["apple", "pen"], true],
  ["catsandog", ["cats", "dog", "sand", "and", "cat"], false],
  ["a", ["a"], true],
  ["ab", ["a"], false],
  ["", ["a"], true],
  ["cars", ["car", "ca", "rs"], true],
];
for (const [s, wd, want] of cases) expect(wordBreak(s, wd), want, "break");
console.log("সব TS test pass!");
```
> **TS টীকা:** `Set<string>` typing দিয়ে dictionary যে string-এর set তা locked, ভুল-type membership-check আটকায়; `dp: boolean[]` segmentability-flag-এর type স্পষ্ট করে। `cases: Array<[string, string[], boolean]>` tuple-type প্রতিটা case-এর `[s, dict, expected]` গঠন নিশ্চিত করে।

## খরচ ও সাধারণ ভুল {#cost}

### সময় {#18-time-complexity}

দুই version-ই **O(n²)** breakpoint জোড়া, প্রতিটায় substring তৈরি + `set` lookup (substring slicing O(length) ধরলে worst-case O(n³); set lookup নিজে O(1) average)। (Naive recursion ছিল exponential।)

### অতিরিক্ত জায়গা {#19-space-complexity}

- Tabulation: **O(n)** — `dp` array (+ dictionary set)।
- Memoization: **O(n)** — dict + recursion stack (+ set)।

### যে ভুলগুলো এড়াতে হবে {#20-common-mistakes}

- dictionary-কে `list`-এ রেখে lookup করা — তখন প্রতিটা membership-check O(dict size), পুরোটা অনেক ধীর; `set`-এ রাখো।
- `dp[0] = True` না বসানো — কোনো prefix-ই সবুজ হয় না, সবসময় False ভুল উত্তর।
- substring সীমা গুলিয়ে ফেলা — শেষ শব্দ `s[j:i]` (`j` থেকে `i`, `i` বাদ), আর `dp[j]` আগের অংশ; এই index alignment নিখুঁত হতে হবে।

## শেখা ও পরের অনুশীলন {#practice}

### ভিত্তির সঙ্গে সম্পর্ক {#21-which-basic-problem-this-inherits-from}

ভিত্তি: Hashing (folder 05) দ্রুত lookup-এর জন্য + #2 Climbing Stairs-এর prefix/last-step চিন্তা, string-এর index-এ তোলা। [../state-transition-thinking.md](../state-transition-thinking.md)-এর "Problem 1 — Climbing Stairs"-এর "শেষ move কোথা থেকে?" lens এখানে "শেষ শব্দ কোথা থেকে?"; [../patterns.md](../patterns.md)-এর "1D over string prefixes" family-ও এখানে।

### পরের অনুশীলন {#22-similar-harder-problems}

- Word Break II (শুধু হ্যাঁ/না নয়, সব বিভাজন বের করো) — <https://leetcode.com/problems/word-break-ii/>
- Concatenated Words (dictionary-র শব্দ অন্য শব্দ জুড়ে বানানো) — <https://leetcode.com/problems/concatenated-words/>
- Palindrome Partitioning (একই prefix-DP কঙ্কাল, condition palindrome) — <https://leetcode.com/problems/palindrome-partitioning/>

### কৌশলটি মনে রাখুন {#23-pattern-learned}

**1D over string prefixes:** state `dp[i]` = prefix `s[0:i]`-এর কোনো property (এখানে segmentable), transition সব break-point `j < i`-এর উপর `dp[j] and (s[j:i] valid)`, base `dp[0]=True`, answer `dp[n]`। DP কঙ্কালের পাশে সঠিক data structure (hash set) — দুটো একসাথে efficiency দেয়।

### নিজেকে যাচাই করুন {#24-final-summary}

ভবিষ্যতের আমাকে: "Word Break = prefix-এর উপর 1D boolean DP + hash set lookup।" State `dp[i]` = `s[0:i]` segmentable, transition `dp[i] = OR_j (dp[j] and s[j:i] in words)`, base `dp[0]=True`, উত্তর `dp[n]`। মনে রেখো: dictionary সবসময় `set`-এ; "শেষ শব্দ কোথা থেকে শুরু?" — এই last-decision lens-ই চাবি।

### কোথায় কাজে লাগে {#27-real-world-use}

- **Tokenization / word segmentation:** space-হীন বা compound text (যেমন চীনা/জাপানি, বা URL slug) জানা-শব্দে ভাঙা যায় কিনা।
- **Spell-check / input validation:** একটা string বৈধ শব্দ-অংশে বিভাজ্য কিনা যাচাই (autocomplete dictionary দিয়ে)।
- **DNA / sequence parsing:** sequence-কে জানা motif/segment-এ সম্পূর্ণ ভাগ করা সম্ভব কিনা।
- **Command / DSL parsing:** input-কে জানা token-এ segmentable কিনা পরীক্ষা (lexing feasibility)।
- **Compound word detection:** একটা শব্দ ছোট জানা-শব্দ জুড়ে তৈরি কিনা।

মূল pattern: prefix-এর উপর 1D DP — "শেষ শব্দ কোথা থেকে শুরু?" last-decision lens; dictionary সবসময় hash `Set`-এ।

---

> **মনে রাখার নিয়ম (legal):** সব নিজের ভাষায় লেখা; problem statement copy করা হয়নি; official link শুধু attribution-এর জন্য; "asked by Google/Amazon" দাবি নেই — এটা একটা common interview pattern।
