Skip to content

Commit b180a17

Browse files
committed
Add: Add 2025/11/11
1 parent db8dc50 commit b180a17

File tree

3 files changed

+190
-0
lines changed

3 files changed

+190
-0
lines changed

474-Ones and Zeroes/Note.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# 474. Ones and Zeroes
2+
3+
You are given an array of binary strings `strs` and two integers `m` and `n`.
4+
5+
Return the size of the largest subset of `strs` such that there are at most `m` `0`'s and `n` `1`'s in the subset.
6+
7+
A set `x` is a subset of a set `y` if all elements of `x` are also elements of `y`.
8+
9+
**Constraints:**
10+
11+
- `1 <= strs.length <= 600`
12+
- `1 <= strs[i].length <= 100`
13+
- `strs[i]` consists only of digits `'0'` and `'1'`.
14+
- `1 <= m, n <= 100`
15+
16+
## 基礎思路
17+
18+
本題要求在一組二進位字串中,找出一個最大子集,使得其中所有字串的 `'0'` 總數不超過 `m``'1'` 總數不超過 `n`
19+
換句話說,我們有兩種資源(`0``1` 的數量上限),每個字串都消耗一定數量的這兩種資源,而我們要選出最多的字串,使其總消耗不超出上限。
20+
21+
這是一個**雙維度 0/1 背包問題(Two-dimensional Knapsack)**
22+
在思考解法時,需注意幾個重點:
23+
24+
- 每個字串可以選或不選,不能重複使用;
25+
- 字串同時消耗兩種資源(`0``1`),需以雙維度動態規劃表示;
26+
- 為避免重複計算,需要自下而上反向更新;
27+
- 為應付上限約 100×100 的資源空間,必須使用高效儲存結構(如 TypedArray)。
28+
29+
為了解決這個問題,我們可以採取以下策略:
30+
31+
- **前置統計**:先統計每個字串中的 `0``1` 數量;
32+
- **狀態定義**`dp[i][j]` 表示使用至多 `i``0``j``1` 時,能得到的最大子集大小;
33+
- **轉移關係**:若當前字串的 `(zeros, ones)``(a, b)`,則
34+
35+
$$
36+
dp[i][j] = max(dp[i][j], dp[i - a][j - b] + 1)
37+
$$
38+
39+
- **反向更新**:為確保每個字串僅使用一次,更新時需從大到小迭代;
40+
- **空間優化**:利用一維壓縮並展平成 TypedArray 陣列以減少記憶體與常數開銷。
41+
42+
## 解題步驟
43+
44+
### Step 1:初始化動態規劃表結構
45+
46+
以二維限制 `(m, n)` 建立壓平的一維動態規劃表,`dp[i * (n + 1) + j]` 代表使用至多 `i` 個 0 和 `j` 個 1 時的最大子集數。
47+
48+
```typescript
49+
// 緩存寬度以避免重複計算 (n + 1)
50+
const dynamicTableWidth = n + 1;
51+
52+
// 壓平的 DP 表:dp[zeroBudget * width + oneBudget] = 最大子集大小
53+
const dynamicTable = new Uint16Array((m + 1) * dynamicTableWidth);
54+
```
55+
56+
### Step 2:遍歷所有字串並統計每個字串的 0 與 1 數量
57+
58+
將每個字串視為一個「物品」,計算它的「花費」(使用多少個 0 和 1)。
59+
60+
```typescript
61+
// 遍歷每個二元字串,視為背包中的一個物品
62+
for (let stringIndex = 0; stringIndex < strs.length; stringIndex++) {
63+
const binaryString = strs[stringIndex];
64+
65+
// 統計當前字串中 '1' 的數量
66+
let oneCountInString = 0;
67+
for (let characterIndex = 0; characterIndex < binaryString.length; characterIndex++) {
68+
if (binaryString.charCodeAt(characterIndex) === 49) {
69+
oneCountInString++;
70+
}
71+
}
72+
73+
// 由長度與 1 的數量可得 0 的數量
74+
const zeroCountInString = binaryString.length - oneCountInString;
75+
76+
// 若此字串本身已超過可用資源,直接跳過
77+
if (zeroCountInString > m || oneCountInString > n) {
78+
continue;
79+
}
80+
81+
// ...
82+
}
83+
```
84+
85+
### Step 3:倒序遍歷並更新 DP 狀態(確保每個字串僅被使用一次)
86+
87+
採用 0/1 背包倒序更新策略。對每個字串,從大到小遍歷可用的 0 與 1 預算,
88+
判斷是否加入該字串能使子集大小變大,若能則更新目前最佳值。
89+
90+
```typescript
91+
for (let stringIndex = 0; stringIndex < strs.length; stringIndex++) {
92+
// Step 2:遍歷所有字串並統計每個字串的 0 與 1 數量
93+
94+
// 倒序遍歷 0 與 1 預算,確保每個字串僅被使用一次
95+
for (let zeroBudget = m; zeroBudget >= zeroCountInString; zeroBudget--) {
96+
const currentRowBaseIndex = zeroBudget * dynamicTableWidth;
97+
const previousRowBaseIndex = (zeroBudget - zeroCountInString) * dynamicTableWidth;
98+
99+
for (let oneBudget = n; oneBudget >= oneCountInString; oneBudget--) {
100+
const currentIndex = currentRowBaseIndex + oneBudget;
101+
const previousIndex = previousRowBaseIndex + (oneBudget - oneCountInString);
102+
103+
// 若選取此字串,子集大小為前狀態 + 1
104+
const candidateSubsetSize = dynamicTable[previousIndex] + 1;
105+
106+
// 更新當前最佳值(手動比較以避免 Math.max 開銷)
107+
if (candidateSubsetSize > dynamicTable[currentIndex]) {
108+
dynamicTable[currentIndex] = candidateSubsetSize;
109+
}
110+
}
111+
}
112+
}
113+
```
114+
115+
### Step 4:返回最終結果
116+
117+
最終答案為在資源上限 `(m, n)` 下可達成的最大子集大小。
118+
119+
```typescript
120+
// 結果為 dp[m][n]
121+
return dynamicTable[m * dynamicTableWidth + n];
122+
```
123+
124+
## 時間複雜度
125+
126+
- 對每個字串計算位數總成本為所有字串長度總和:$O(k \times L)$($k=\text{strs.length}$,$L$ 為單字串長度上限;最壞合併為 $O(\sum |str_i|)$)。
127+
- 動態規劃雙重倒序遍歷:每個字串更新 $O(m \times n)$ 狀態。
128+
- 總時間複雜度為 $O(k \times (m \times n + L))$;在常見情況下若 $m,n$ 較大,常簡寫為 $O(k \times m \times n)$。
129+
130+
> $O(k \times (m \times n + L))$
131+
132+
## 空間複雜度
133+
134+
- 壓平的 DP 陣列大小為 $(m + 1) \times (n + 1)$。
135+
- 其餘變數僅為常數空間。
136+
- 總空間複雜度為 $O(m \times n)$。
137+
138+
> $O(m \times n)$

474-Ones and Zeroes/answer.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
function findMaxForm(strs: string[], m: number, n: number): number {
2+
// Cache width once to avoid repeated (n + 1) computation
3+
const dynamicTableWidth = n + 1;
4+
5+
// Flattened DP table: dp[zeroBudget * width + oneBudget] = best subset size
6+
const dynamicTable = new Uint16Array((m + 1) * dynamicTableWidth);
7+
8+
// Iterate over each binary string and treat it as an item in the knapsack
9+
for (let stringIndex = 0; stringIndex < strs.length; stringIndex++) {
10+
const binaryString = strs[stringIndex];
11+
12+
// Count the number of ones in the string
13+
let oneCountInString = 0;
14+
for (let characterIndex = 0; characterIndex < binaryString.length; characterIndex++) {
15+
if (binaryString.charCodeAt(characterIndex) === 49) {
16+
oneCountInString++;
17+
}
18+
}
19+
// Count of the zeros is total length minus count of ones
20+
const zeroCountInString = binaryString.length - oneCountInString;
21+
22+
// Skip items that alone exceed the budgets
23+
if (zeroCountInString > m || oneCountInString > n) {
24+
continue;
25+
}
26+
27+
// Reverse iterate zero and one budgets to enforce 0/1 usage
28+
for (let zeroBudget = m; zeroBudget >= zeroCountInString; zeroBudget--) {
29+
const currentRowBaseIndex = zeroBudget * dynamicTableWidth;
30+
const previousRowBaseIndex = (zeroBudget - zeroCountInString) * dynamicTableWidth;
31+
32+
for (let oneBudget = n; oneBudget >= oneCountInString; oneBudget--) {
33+
const currentIndex = currentRowBaseIndex + oneBudget;
34+
const previousIndex = previousRowBaseIndex + (oneBudget - oneCountInString);
35+
36+
// Candidate if we include this string
37+
const candidateSubsetSize = (dynamicTable[previousIndex] + 1);
38+
39+
// Manual max avoids Math.max overhead in the innermost loop
40+
if (candidateSubsetSize > dynamicTable[currentIndex]) {
41+
dynamicTable[currentIndex] = candidateSubsetSize;
42+
}
43+
}
44+
}
45+
}
46+
47+
// Answer is the best value achievable with budgets (m, n)
48+
return dynamicTable[m * dynamicTableWidth + n];
49+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function findMaxForm(strs: string[], m: number, n: number): number {
2+
3+
}

0 commit comments

Comments
 (0)