Hidden Permutation隐藏排列
通信题:矩阵构造与排列还原
一、题目描述
这是一道通信题(Communication Problem),你的程序会在两个独立进程中运行两次。
First Run
给定一个 1 ~ n 的排列 p = (p1, p2, ..., pn)。
输出一个 n × n 矩阵 A,要求:
- 每一行都是
1 ~ n的一个排列 - 任意两行互不相同
Second Run
评测机随机打乱 A 的行顺序(每行内部元素顺序不变),得到矩阵 B。
给定 B,要求还原出第一次运行时的原始排列 p。
两次运行之间不保留任何进程状态,B 是唯一传递的信息。
对应题目:Hidden Permutation
二、思路分析
核心问题
second 阶段拿到的矩阵 B 是 A 的行被打乱后的结果。
关键:行顺序丢失了,但每行的内容完好。我们要利用矩阵的结构,从 B 中还原出 p。
first 阶段的构造
构造一个 n × n 矩阵 A,要求每行是排列,且两两不同。
代码采用的方式:
- 第 1 行:直接填入
p - 第 2 行:把
p反转(但保持第一个元素不变) - 第 3 ~ n 行:把
p循环右移i - 2位
具体构造:
第 1 行:p[1], p[2], ..., p[n]
第 2 行:p[1], p[n], p[n-1], ..., p[2]
第 3 行:p[n], p[1], p[2], ..., p[n-1] (右移 1 位)
第 4 行:p[n-1], p[n], p[1], ..., p[n-2] (右移 2 位)
...
second 阶段的还原
观察第 1 列(a[i][1]):
- 第 1 行第 1 列 =
p[1] - 第 2 行第 1 列 =
p[1] - 第 3 行第 1 列 =
p[n] - 第 4 行第 1 列 =
p[n-1] - ……
发现规律:第 1 行和第 2 行的第 1 列都是 p[1],其余行的第 1 列各不相同。
所以在 B 的第 1 列中:
- 有一个值会出现两次 → 这个值就是
p[1] - 其余值各出现一次
利用这个信息:
- 找到第 1 列出现两次的值
t,它等于p[1] - 找到第 1 列只出现一次的那一行
tt(这行就是原来的第 3 ~ n 行之一) - 在
tt行中找到t的位置cnt - 从
cnt开始循环输出tt行的元素,就还原出p
为什么从 cnt 开始循环输出能还原 p?
因为第 3 ~ n 行都是 p 的循环移位。在 tt 行中找到 p[1] 的位置 cnt,说明这一行的 cnt 位置就是 p[1]。从 cnt 开始读这一行,相当于把循环移位“逆回去”,得到原始顺序。
三、AC 代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2005;
int a[N][N], n;
int arr[N];
void output()
{
for (int i = 1; i <= n; i++)
{
for (int k = 1; k <= n; k++)
{
if (k != 1) cout << ' ';
cout << a[i][k];
}
cout << endl;
}
}
int main()
{
string op;
cin >> op;
if (op == "first")
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[1][i];
// 第 2 行:保持第一个元素,其余反转
a[2][1] = a[1][1];
for (int i = 2; i <= n; i++)
a[2][i] = a[1][n - i + 2];
// 第 3 ~ n 行:第 1 行循环右移 i - 2 位
for (int i = 3; i <= n; i++)
for (int k = 1; k <= n; k++)
a[i][k] = a[1][(k + i - 2 - 1) % n + 1];
output();
}
else if (op == "second")
{
cin >> n;
int t;
// 统计第 1 列每个值的出现次数
for (int i = 1; i <= n; i++)
{
for (int k = 1; k <= n; k++)
{
cin >> a[i][k];
if (k == 1)
{
arr[a[i][k]]++;
if (arr[a[i][k]] == 2) t = a[i][k];
}
}
}
// 找到第 1 列只出现一次的那一行
int tt = 1;
while (arr[a[tt][1]] != 1) tt++;
// 在 tt 行中找到 t 的位置
int cnt;
for (int i = 1; i <= n; i++)
{
if (a[tt][i] == t)
{
cnt = i;
break;
}
}
// 从 cnt 开始循环输出,还原 p
for (int i = 1; i <= n; i++)
{
if (i != 1) cout << ' ';
cout << a[tt][(cnt + i - 1 - 1) % n + 1];
}
cout << endl;
}
return 0;
}
四、代码说明
1. first 阶段的构造
a[2][1] = a[1][1];
for (int i = 2; i <= n; i++)
a[2][i] = a[1][n - i + 2];
第 2 行:第一个元素保持 p[1],其余元素反转。这样第 2 行的第 1 列和第 1 行的第 1 列都是 p[1]。
a[i][k] = a[1][(k + i - 2 - 1) % n + 1];
第 3 ~ n 行:第 1 行循环右移 i - 2 位。用取模实现循环。
2. second 阶段的还原
arr[a[i][k]]++;
if (arr[a[i][k]] == 2) t = a[i][k];
遍历第 1 列,统计每个值出现次数。出现两次的是 p[1]。
int tt = 1;
while (arr[a[tt][1]] != 1) tt++;
找到第 1 列只出现一次的那一行,这行是原来的第 3 ~ n 行之一。
for (int i = 1; i <= n; i++)
if (a[tt][i] == t) { cnt = i; break; }
在这一行中找到 p[1] 的位置 cnt。
cout << a[tt][(cnt + i - 1 - 1) % n + 1];
从 cnt 开始循环输出这一行,就还原出了原始排列 p。
五、反思:
比赛的时候没有理解题目本身的含义,本质上还是刷题少了,之前没怎么碰到通信,
题目虽然是说代码运行两次其实是只有一个代码,这个通信也就是来构造情况让矩阵和编码相互转化,理解题目之后自行构造方案即可
六、总结
| 要点 | 内容 |
|---|---|
| 题型 | 通信题(Communication Problem),两次独立运行 |
| first 任务 | 构造 n × n 矩阵,第 2 行反转,其余行循环右移 |
| second 任务 | 利用第 1 列出现两次的值定位 p[1],循环输出还原 |
| 核心技巧 | 让 p[1] 在矩阵中出现两次,其余位置唯一,作为还原的锚点 |

浙公网安备 33010602011771号