Table Recovery

基于频率分析的网格重构算法

1. 题目理解

我们得到了一个 $N \times N$ 的网格。

核心线索:这个网格是由一个长度为 $2N-1$ 的序列生成的。每一行不仅代表序列的一部分,而且行与行之间存在大量的重叠

目标:
通过分析数字在网格中出现的规律,反推出原始序列的相对顺序,并输出字典序最小的方案。

2. 核心观察 (Key Observation)

思考:如果我们把一个长序列切成 $N$ 个长度为 $N$ 的片段,每个数字会出现几次?

假设序列为:$A, B, C, D, E$ ($N=3$)

  • 第1行: [A, B, C]
  • 第2行: [B, C, D]
  • 第3行: [C, D, E]

统计出现次数:

  • A: 出现 1 次 (只在第1行)
  • B: 出现 2 次 (第1, 2行)
  • C: 出现 3 次 (第1, 2, 3行) -> 中心峰值
  • D: 出现 2 次
  • E: 出现 1 次
结论:频率 = 距离边缘的距离
数字出现的次数完全取决于它在原始序列中的位置。越靠近中间,出现次数越多;越靠近两端,出现次数越少。

3. 算法可视化

Freq: 1 Edge Freq: 2 Freq: N=3 Center Freq: 2 Freq: 1 Edge Original Sequence Position

出现1次的数字位于两端,出现N次的数字位于正中间。

4. 实例推演

输入矩阵:
3 4 2
5 2 3
6 3 5
统计频率 (cnt):
4: 1次 $\to$ প্রান্তে (Pos 1 或 5)
6: 1次 $\to$ প্রান্তে (Pos 5 或 1)
2: 2次 $\to$ 邻近 (Pos 2 或 4)
5: 2次 $\to$ 邻近 (Pos 4 或 2)
3: 3次 $\to$ 中间 (Pos 3)
构建序列:
可能的序列为:$4 \to 2 \to 3 \to 5 \to 6$
或者反转:$6 \to 5 \to 3 \to 2 \to 4$

5. 字典序策略

我们得到了两个可能的原始序列(正序或逆序)。

题目要求输出转换后的网格,我们需要选择字典序最小的那个。


1. 找到开头元素(Freq=1)和结尾元素(Freq=1)。

2. 将所有数字映射为它们在序列中的位置($1 \dots 2N-1$)。

3. 检查网格第一个元素的值。如果它对应的位置很大(比如 $> N+1$),说明我们可能“看反了”,需要将整个映射关系翻转。

6. 代码实现:输入与统计


#include <bits/stdc++.h>
using namespace std;

int n;
int a[1001][1001];
int cnt[2001]; // 统计频率
int perm[2001]; // 映射表:数字 -> 最终位置值

int main() {
    scanf("%d",&n);
    // 读入并统计频率
    for (int i=0; i<n; i++) 
        for (int j=0; j<n; j++) {
            scanf("%d",&a[i][j]);
            cnt[a[i][j]]++;
        }
            
            

6. 代码实现:寻找端点


    // 特殊情况处理
    if (n == 1) { printf("2\n"); return 0; }

    // 找到只出现一次的两个数(首和尾)
    int first = 0, last = 0;
    for (int i=2; i<=2*n; i++) {
        if (cnt[i] == 1) {
            if (first == 0) first = i;
            else last = i;
        }
    }
    
    // 找到这两个数所在的行,用于确定方向参照
    int firstline, lastline;
    for (int i=0; i<n; i++) {
        for (int j=0; j<n; j++) {
            if (a[i][j] == first) firstline = i;
            if (a[i][j] == last) lastline = i;
        }
    }
            

6. 代码实现:构建映射

这里利用 firstline 和 lastline 巧妙地填充了整个映射表。


    // 核心逻辑:填充映射表
    // firstline 包含前半部分,lastline 包含后半部分
    for (int j=0; j n+1 || 
       (perm[a[0][0]] == n+1 && perm[a[0][1]] > n+1)) {
        for (int i = 2; i <= 2*n; i++)
            perm[i] = 2*n + 2 - perm[i]; // 反转映射值
    }
            

6. 代码实现:输出结果


    // 输出转换后的网格
    for (int i=0; i<n; i++) {
        for (int j=0; j<n; j++) {
            printf("%d%s", perm[a[i][j]], j==n-1?"":" ");
        }
        printf("\n");
    }

    return 0;
}
            

7. 总结

  • 观察性质: 利用滑动窗口的重叠特性。
  • 频率即坐标: 出现次数直接对应序列中的位置深度。
  • 时间复杂度: $O(N^2)$ 用于读取和输出网格,逻辑处理仅需 $O(N)$。
  • 技巧: 无论是正推还是反推,先构造一个,再根据字典序决定是否翻转。