题目描述
佳佳刚进高中,在军训的时候,由于佳佳吃苦耐劳,很快得到了教官的赏识,成为了“小教官”。在军训结束的那天晚上,佳佳被命令组织同学们进行篝火晚会。一共有 n 个同学,编号从 1 到 n。一开始,同学们按照 1,2,⋯,n 的顺序坐成一圈,而实际上每个人都有两个最希望相邻的同学。如何下命令调整同学的次序,形成新的一个圈,使之符合同学们的意愿,成为摆在佳佳面前的一大难题。
佳佳可向同学们下达命令,每一个命令的形式如下:
(b1,b2,...bm−1,bm)
这里 m 的值是由佳佳决定的,每次命令 m 的值都可以不同。这个命令的作用是移动编号是 b1,b2,⋯,bm 的这 m 个同学的位置。要求 b1 换到 b2 的位置上,b2 换到 b3 的位置上,……,要求 bm 换到 b1 的位置上。执行每个命令都需要一些代价。我们假定如果一个命令要移动 m 个人的位置,那么这个命令的代价就是 m。我们需要佳佳用最少的总代价实现同学们的意愿,你能帮助佳佳吗?
输入格式
第一行是一个整数 n,表示一共有 n 个同学。
其后 n 行每行包括 2 个不同的正整数,以一个空格隔开,分别表示编号是 1 的同学最希望相邻的两个同学的编号,编号是 2 的同学最希望相邻的两个同学的编号,……,编号是n的同学最希望相邻的两个同学的编号。
输出格式
一个整数,为最小的总代价。如果无论怎么调整都不能符合每个同学的愿望,则输出 −1。
输入输出样例
输入 #1复制
4 3 4 4 3 1 2 1 2
输出 #1复制
2
说明/提示
- 对于 30% 的数据,满足 n≤1000;
- 对于 100% 的数据,满足 3≤n≤50000。
【题目来源】
NOIP 2005 提高组第三题
题目解析
题目描述了一个班级同学围坐在篝火周围的情景。初始时,同学们按1~n的顺序顺时针围坐。现在给出两个目标排列方式,要求通过最少的交换操作(每次可以交换任意两个人的位置)将初始排列转换为其中一个目标排列。我们需要计算这个最小的交换次数。
算法分析
关键思路
这道题的核心在于理解排列变换的最小交换次数计算。对于排列变换问题,最小交换次数等于排列长度减去排列中循环的个数。这是因为:
-
每个长度为k的循环需要k-1次交换来解开
-
总交换次数 = Σ(每个循环的长度-1) = n - 循环个数
解题步骤
-
构建目标排列:读取并存储两个目标排列
-
检查特殊情况:判断两个目标排列是否互为逆序
-
计算循环个数:对于每个目标排列,计算初始排列变换到该排列的循环个数
-
求最小交换次数:根据循环个数计算最小交换次数,取两个目标排列中的较小值
代码实现
cpp
#include <iostream>
#include <cstring>
using namespace std;
const int MAXN = 50005; // 最大人数
int n; // 总人数
int a[MAXN], b[MAXN]; // 两个目标排列
int pos[MAXN]; // 位置映射数组
bool vis[MAXN]; // 访问标记数组
// 读取并构建目标排列
void read(int arr[]) {
for (int i = 1; i <= n; ++i) {
cin >> arr[i];
}
}
// 计算到目标排列的最小交换次数
int calc(int target[]) {
memset(vis, false, sizeof(vis)); // 初始化访问标记
memset(pos, 0, sizeof(pos)); // 初始化位置映射
// 建立目标排列的位置映射
for (int i = 1; i <= n; ++i) {
pos[target[i]] = i;
}
int cycle = 0; // 循环计数器
for (int i = 1; i <= n; ++i) {
if (!vis[i]) {
cycle++; // 发现新循环
int j = i;
// 标记整个循环
while (!vis[j]) {
vis[j] = true;
j = pos[j];
}
}
}
return n - cycle; // 最小交换次数
}
int main() {
cin >> n;
read(a); // 读取第一个目标排列
read(b); // 读取第二个目标排列
// 检查两个排列是否互为逆序
bool is_rev = true;
for (int i = 1; i <= n; ++i) {
if (a[i] != b[n - i + 1]) {
is_rev = false;
break;
}
}
// 计算两个目标排列的最小交换次数
int ans1 = calc(a);
int ans2 = calc(b);
// 输出较小值
cout << min(ans1, ans2) << endl;
return 0;
}
代码解析
数据结构
-
a[MAXN]和b[MAXN]:存储两个目标排列 -
pos[MAXN]:建立从同学编号到其在目标排列中位置的映射 -
vis[MAXN]:标记同学是否已被访问,用于循环检测
核心函数
-
read()函数:-
功能:读取并存储目标排列
-
参数:目标排列数组
-
实现:简单循环读取
-
-
calc()函数:-
功能:计算到目标排列的最小交换次数
-
参数:目标排列数组
-
实现步骤:
a. 初始化访问标记和位置映射
b. 建立目标排列的位置映射
c. 检测循环个数
d. 返回n-cycle作为最小交换次数
-
主函数逻辑
-
读取输入数据
-
检查特殊排列情况(逆序)
-
分别计算两个目标排列的最小交换次数
-
输出较小值
复杂度分析
-
时间复杂度:O(n)
-
读取输入:O(n)
-
逆序检查:O(n)
-
循环检测:O(n)
-
总体线性复杂度
-
-
空间复杂度:O(n)
-
使用了多个大小为n的数组
-
空间使用与输入规模成正比
-
优化与注意事项
-
逆序检查优化:
-
两个互为逆序的排列具有相同的最小交换次数
-
检测到这种情况可以节省一次calc计算
-
-
边界条件处理:
-
n=1时直接返回0
-
确保数组访问不越界
-
-
循环检测技巧:
-
使用vis数组避免重复计算
-
通过位置映射快速找到下一个元素
-
数学原理深入
排列与循环分解
任何排列都可以分解为若干不相交的循环。例如:
排列(2,4,1,3)可以分解为两个循环:(1→2→4→3→1)和(5→5)
交换次数计算
将一个排列转换为另一个排列的最小交换次数等于n-c,其中:
-
n是排列长度
-
c是循环个数
这是因为:
-
恒等排列有n个循环(每个元素自成一个循环)
-
每次交换最多能将循环个数增加1
-
因此需要至少n-c次交换
实例分析
假设n=4,初始排列为(1,2,3,4)
目标排列1:(2,1,4,3)
-
循环分解:(1→2→1), (3→4→3)
-
循环个数:2
-
最小交换次数:4-2=2
目标排列2:(4,3,2,1)
-
循环分解:(1→4→1), (2→3→2)
-
循环个数:2
-
最小交换次数:4-2=2
总结
本题通过排列的循环分解,巧妙地计算了最小交换次数。关键在于:
-
理解排列的循环结构
-
掌握位置映射的建立方法
-
实现高效的循环检测算法
这种基于循环分解的方法不仅适用于本题,也是解决许多排列相关问题的通用思路。

1486

被折叠的 条评论
为什么被折叠?



