P1053 [NOIP 2005 提高组] 篝火晚会
题目描述
佳佳刚进高中,在军训的时候,由于佳佳吃苦耐劳,很快得到了教官的赏识,成为了“小教官”。在军训结束的那天晚上,佳佳被命令组织同学们进行篝火晚会。一共有 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
总结
本题通过排列的循环分解,巧妙地计算了最小交换次数。关键在于:
理解排列的循环结构
掌握位置映射的建立方法
实现高效的循环检测算法
这种基于循环分解的方法不仅适用于本题,也是解决许多排列相关问题的通用思路。