# 表格行匹配算法详解：`_match_html_rows_to_paddle_groups`

## 📋 算法概述

这是一个**动态规划（DP）算法**，用于将HTML表格行与OCR检测到的文本分组进行智能匹配。

### 问题场景
- **输入1**: HTML表格行（7行）
  - 第0行：表头信息（部门、柜员、打印日期等）
  - 第1-5行：账户信息行（账号、查询日期、证件信息等）
  - 第6行：表头（Serial Num, Trans Date等）
  - 第7行：数据行（实际交易记录）

- **输入2**: OCR文本分组（48组）
  - 每组包含多个OCR检测框，按Y坐标分组
  - 组0-4：表头信息
  - 组5-47：表头和数据行混合

### 核心挑战
1. **1对多匹配**：一个HTML行可能对应多个OCR组（合并单元格、OCR分割错误）
2. **跳过处理**：某些HTML行可能没有对应的OCR组（OCR漏检）
3. **顺序不一致**：HTML行顺序与OCR组顺序可能不完全一致

---

## 🎯 算法流程

### 阶段1：预处理与验证

#### 1.1 提取文本
```python
# HTML行文本（7行）
html_row_texts = [
    '部门department:01372403999柜员searchteller:ebf0000...',  # 行0
    '账号/卡号account/cardno:6222621010026732125...',         # 行1
    '查询起日querystartingdate:2023-08-12...',                 # 行2
    '查询时间querytime:2024年08月12日...',                     # 行3
    '证件种类idtype:第二代居民身份证...',                       # 行4
    'serialnum序号transdate交易日期...',                       # 行5（表头）
    '12023-08-1213:04:52网上银行卡转入...'                    # 行6（数据行）
]

# OCR组文本（48组）
group_texts = [
    '部门department:01372403999...',  # 组0
    '账号/卡号account/cardno:...',    # 组1
    '查询起日querystartingdate:...',  # 组2
    ...
    '964642s0110305'                  # 组47
]
```

#### 1.2 快速路径：行数相等时的验证
```python
if len(html_rows) == len(grouped_boxes):  # 7 == 48? False
    # 不执行此分支
```

**实际场景**：7行 ≠ 48组，需要DP算法

---

### 阶段2：预计算合并文本缓存

#### 2.1 构建 `merged_cache`
为了支持**一个HTML行匹配多个OCR组**，预计算所有可能的组合：

```python
MAX_MERGE = 4  # 最多合并4个OCR组
merged_cache = {
    (0, 1): '部门department:01372403999...',           # 组0单独
    (0, 2): '部门department:...账号/卡号account...',    # 组0+组1
    (0, 3): '部门department:...查询起日...',            # 组0+组1+组2
    (0, 4): '部门department:...查询时间...',            # 组0+组1+组2+组3
    (1, 1): '账号/卡号account/cardno:...',              # 组1单独
    (1, 2): '账号/卡号...查询起日...',                  # 组1+组2
    ...
    (47, 1): '964642s0110305'                           # 组47单独
}
```

**作用**：避免在DP过程中重复拼接文本，提升性能

---

### 阶段3：动态规划初始化

#### 3.1 DP状态定义
```python
# dp[i][j] = 将HTML前i行匹配到OCR前j组的最大得分
dp = np.full((7, 48), -np.inf)  # 初始化为负无穷

# path[(i, j)] = (prev_j, start_j)
# prev_j: 上一行结束的OCR组索引
# start_j: 当前行开始的OCR组索引
path = {}
```

#### 3.2 初始化第一行（i=0）

**目标**：找到HTML第0行与OCR组的最佳匹配

```python
SEARCH_WINDOW = 15  # 只在前15个组中搜索
MAX_MERGE = 4       # 最多合并4个组

# 遍历所有可能的匹配
for end_j in range(min(48, 15 + 4)):  # end_j: 0..18
    for count in range(1, 5):          # count: 1..4
        start_j = end_j - count + 1    # 计算起始组索引
        
        # 例如：end_j=3, count=2 → start_j=2
        # 表示：HTML行0 匹配 OCR组2+组3
        
        current_text = merged_cache.get((start_j, count), "")
        similarity = calculate_similarity(html_row_texts[0], current_text)
        
        penalty = start_j * 0.1  # 跳过组的惩罚
        score = similarity - penalty
        
        if score > 0.1:  # 只有得分足够高才记录
            dp[0][end_j] = max(dp[0][end_j], score)
            path[(0, end_j)] = (-1, start_j)  # -1表示这是第一行
```

**示例计算过程**：

| end_j | count | start_j | 匹配的OCR组 | 相似度 | 惩罚 | 得分 | 是否记录 |
|-------|-------|---------|-------------|--------|------|------|----------|
| 0 | 1 | 0 | 组0 | 0.95 | 0.0 | 0.95 | ✅ |
| 1 | 1 | 1 | 组1 | 0.15 | 0.1 | 0.05 | ❌ |
| 1 | 2 | 0 | 组0+组1 | 0.98 | 0.0 | 0.98 | ✅ |
| 2 | 3 | 0 | 组0+组1+组2 | 0.92 | 0.0 | 0.92 | ✅ |
| ... | ... | ... | ... | ... | ... | ... | ... |

**结果**：假设 `dp[0][2] = 0.98` 是最优，表示HTML行0匹配OCR组0+组1+组2

---

### 阶段4：DP状态转移（核心）

#### 4.1 处理第i行（i=1..6）

**状态转移方程**：
```
dp[i][end_j] = max {
    dp[i-1][prev_j] + match_score(i, start_j..end_j) - gap_penalty
}
```

**具体步骤**：

##### Step 1: 获取上一行的有效状态
```python
# 找到所有 dp[i-1][j] > -inf 的位置
valid_prev_indices = [j for j in range(48) if dp[i-1][j] > -np.inf]

# 例如处理第1行时：
# valid_prev_indices = [2]  # 因为dp[0][2] = 0.98
```

##### Step 2: 剪枝优化
```python
if len(valid_prev_indices) > 30:
    # 只保留得分最高的前30个
    valid_prev_indices.sort(key=lambda j: dp[i-1][j], reverse=True)
    valid_prev_indices = valid_prev_indices[:30]
```

##### Step 3: 允许跳过HTML行
```python
# 如果当前HTML行没有匹配，可以跳过（继承上一行状态）
for prev_j in valid_prev_indices:
    score_skip = dp[i-1][prev_j] - 0.3  # SKIP_HTML_PENALTY
    if score_skip > dp[i][prev_j]:
        dp[i][prev_j] = score_skip
        path[(i, prev_j)] = (prev_j, prev_j + 1)  # 空范围，表示跳过
```

##### Step 4: 正常匹配逻辑
```python
for prev_j in valid_prev_indices:  # 例如：prev_j = 2
    prev_score = dp[i-1][prev_j]    # 例如：0.98
    
    max_gap = min(15, 48 - prev_j - 1)  # 最多向前搜索15个组
    
    for gap in range(max_gap):  # gap: 0..14
        start_j = prev_j + 1 + gap  # 例如：gap=0 → start_j=3
        
        for count in range(1, 5):  # count: 1..4
            end_j = start_j + count - 1  # 例如：count=2 → end_j=4
            
            # 获取合并文本
            current_text = merged_cache.get((start_j, count), "")
            
            # 长度预筛选（避免明显不匹配的情况）
            if len(html_text) > 10 and len(current_text) < len(html_text) * 0.2:
                continue  # 跳过
            
            # 计算相似度
            similarity = calculate_similarity(html_text, current_text)
            
            # 计算惩罚
            gap_penalty = gap * 0.1  # 跳过组的惩罚
            len_penalty = ...         # 长度不匹配的惩罚
            
            current_score = similarity - gap_penalty - len_penalty
            
            if current_score > 0.1:  # 只有正收益才转移
                total_score = prev_score + current_score
                
                if total_score > dp[i][end_j]:
                    dp[i][end_j] = total_score
                    path[(i, end_j)] = (prev_j, start_j)
```

**示例：处理HTML行1（账号信息行）**

假设 `dp[0][2] = 0.98`（上一行匹配到组2）

| gap | start_j | count | end_j | 匹配的OCR组 | 相似度 | 总得分 | 是否更新 |
|-----|---------|-------|-------|-------------|--------|--------|----------|
| 0 | 3 | 1 | 3 | 组3 | 0.85 | 0.98+0.85=1.83 | ✅ |
| 0 | 3 | 2 | 4 | 组3+组4 | 0.92 | 0.98+0.92=1.90 | ✅ |
| 1 | 4 | 1 | 4 | 组4 | 0.20 | 0.98+0.20=1.18 | ✅ |
| ... | ... | ... | ... | ... | ... | ... | ... |

最终：`dp[1][4] = 1.90`，`path[(1, 4)] = (2, 3)`

---

### 阶段5：回溯找最优路径

#### 5.1 找到最优终点
```python
best_end_j = -1
max_score = -np.inf

# 从最后一行往前找
for i in range(6, -1, -1):  # i: 6, 5, 4, ..., 0
    for j in range(48):
        if dp[i][j] > max_score:
            max_score = dp[i][j]
            best_end_j = j
            best_last_row = i

# 例如：dp[6][47] = 5.2 是最大值
# → best_end_j = 47, best_last_row = 6
```

#### 5.2 回溯构建映射
```python
mapping = {}
curr_i = 6  # best_last_row
curr_j = 47  # best_end_j

while curr_i >= 0:
    if (curr_i, curr_j) in path:
        prev_j, start_j = path[(6, 47)]  # 例如：(45, 46)
        
        # 如果 start_j <= curr_j，说明消耗了OCR组
        if start_j <= curr_j:  # 46 <= 47? True
            indices = list(range(start_j, curr_j + 1))  # [46, 47]
            mapping[6] = [46, 47]  # HTML行6匹配OCR组46+47
        
        curr_j = prev_j  # 45
        curr_i -= 1      # 5
    else:
        break

# 继续回溯...
# mapping[5] = [40, 41, 42, 43, 44, 45]
# mapping[4] = [38, 39]
# ...
# mapping[0] = [0, 1, 2]
```

**最终映射结果**：
```python
mapping = {
    0: [0, 1, 2],           # HTML行0 → OCR组0+1+2
    1: [3, 4],               # HTML行1 → OCR组3+4
    2: [5, 6],               # HTML行2 → OCR组5+6
    3: [7, 8, 9],            # HTML行3 → OCR组7+8+9
    4: [10, 11],             # HTML行4 → OCR组10+11
    5: [12, 13, ..., 39],   # HTML行5 → OCR组12..39（表头）
    6: [40, 41, ..., 47]    # HTML行6 → OCR组40..47（数据行）
}
```

---

### 阶段6：后处理

#### 6.1 处理未匹配的OCR组（Orphans）
```python
used_groups = {0, 1, 2, ..., 47}  # 已使用的组
unused_groups = []  # 如果没有未使用的组，跳过

# 如果有未使用的组，找到最近的已匹配组，合并过去
```

#### 6.2 按Y坐标排序
```python
for row_idx in mapping:
    if mapping[row_idx]:
        # 按OCR组的Y坐标排序，确保顺序正确
        mapping[row_idx].sort(key=lambda idx: grouped_boxes[idx]['y_center'])
```

---

## 📊 算法复杂度

- **时间复杂度**：O(n_html × n_paddle × SEARCH_WINDOW × MAX_MERGE)
  - 本例：7 × 48 × 15 × 4 ≈ 20,160 次计算
- **空间复杂度**：O(n_html × n_paddle)
  - 本例：7 × 48 = 336 个DP状态

---

## 🎨 可视化示例

### DP状态矩阵（简化版）

```
        OCR组索引 →
HTML  0  1  2  3  4  5  6  7  ...  47
行↓
0    [0.98] -  -  -  -  -  -  ...  -
1    -  -  -  [1.90] -  -  -  ...  -
2    -  -  -  -  -  [2.85] -  ...  -
3    -  -  -  -  -  -  [3.80] ...  -
4    -  -  -  -  -  -  -  [4.75] ...  -
5    -  -  -  -  -  -  -  -  ...  [5.20]
6    -  -  -  -  -  -  -  -  ...  [5.50] ← 最优终点
```

### 匹配路径（箭头表示）

```
HTML行0 → OCR组[0,1,2]
    ↓
HTML行1 → OCR组[3,4]
    ↓
HTML行2 → OCR组[5,6]
    ↓
...
    ↓
HTML行6 → OCR组[40..47]
```

---

## 🔑 关键优化点

1. **预计算合并文本**：避免重复拼接，提升性能
2. **剪枝优化**：只保留前30个最优状态，防止组合爆炸
3. **允许跳过HTML行**：提高鲁棒性，处理OCR漏检
4. **长度预筛选**：快速排除明显不匹配的情况
5. **多组合并**：支持一个HTML行匹配多个OCR组（MAX_MERGE=4）

---

## 📝 总结

这个算法通过**动态规划**实现了：
- ✅ 智能匹配HTML行与OCR组
- ✅ 支持1对多匹配（合并单元格）
- ✅ 处理OCR漏检（跳过HTML行）
- ✅ 处理顺序不一致（通过gap搜索）
- ✅ 高效计算（预计算+剪枝）

最终得到每个HTML行对应的OCR组索引列表，用于后续的单元格匹配。

