环状图(置换群)
1 概念
-
一个有向图如果有
n
个点,n
条边,并且每个点的入度和出度都为1,这样的图被称为环状图。 -
根据定义可知,环状图一定是由一定数量的环构成的。
-
环状图一般和置换群的联系十分紧密,因为一个置换群对应一个环状图。
2 例题
AcWing 1224. 交换瓶子
问题描述
- 问题链接:AcWing 1224. 交换瓶子
分析
-
给定我们数组
a
,我们可以构造一个环状图,对于每个数据a[i]
,在图中连一条a[i]
指向i
的边。 -
对于本题而言,对于样例1:
3 1 2 5 4
,构造的环状图如下:
-
存在两种情况:
-
(1)当我们交换同一个环中的两个点(对应交换数组中的两个数),会让一个环裂开成两个环;
-
(2)当我们交换不同环中的两个点,会让两个环合并成一个环。
-
-
针对上述两种情况图示如下:
-
我们最终的目标是让整个图中的所有点变为自环。因此只要存在环,我们每次就可以交换环中的两个点,可以让环多一个。
-
因此本题的做法是:统计环的数量,假设为
cnt
,如果点数为n
,则我们还需要操作n-cnt
次得到n
个自环。
代码
#include <iostream>
using namespace std;
const int N = 10010;
int n;
int p[N]; // 每个数指向其对应的下标
bool st[N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
int id;
cin >> id;
p[id] = i;
}
int cnt = 0;
for (int i = 1; i <= n; i++)
if (!st[i]) {
cnt++;
for (int j = i; !st[j]; j = p[j])
st[j] = true;
}
cout << n - cnt << endl;
return 0;
}
AcWing 1553. 用 Swap(0, i) 操作进行排序
问题描述
分析
-
对于每个数
id
,让其指向其下标。这样我们可以得到一个环状图。 -
下列演示了不同操作的影响:
-
我们最终的目标是将所有的点变为自环。
-
通过和
0
交换,可以分为两大类操作:(1)
0
与环内的点交换:① 和0
后面一个点next
交换,会让next
变为自环,有效操作;② 和非0
后面的一个点交换位置,会让0
所在的环变为两个环,操作无意义;(2)
0
和环外的点交换:合成一个环,有效操作。 -
具体操作时,可以先找到
0
所在的环,让0
和后面的一个点交换,让所有该环内的点变为自环;然后找到其他环,首先和0
合并,然后再将合并后的环中的每个点变为自环。
代码
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int p[N]; // 每个数指向其对应的下标
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int id;
scanf("%d", &id);
p[id] = i;
}
int res = 0;
for (int i = 1; i < n; ) {
while (p[0]) swap(p[0], p[p[0]]), res++;
while (i < n && p[i] == i) i++;
if (i < n) swap(p[0], p[i]), res++;
}
printf("%d\n", res);
return 0;
}
AcWing 3775. 数组补全
问题描述
- 问题链接:AcWing 3775. 数组补全
分析
-
使用数组
a
记录输入的数组,使用数组p
记录每个数对应的下标。 -
数组a相当于下标指向数据,根据数组a可以得到一个图。我们的目的是让图中没有自环,例如对于测试用例
5 0 0 2 4
,可以得到下图:
-
可以看到这个图是有缺口的,对于这种情况,我们可以将所有没有归属的点放到环中,即接到链尾
y
的后面。 -
如果得到的图中都可以构成环,则将其余剩余的点单独构成一个环即可。因为题目保证一定有解,因此如果存在这种情况的话,一定不止一个点。
代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 200010;
int n;
int a[N]; // 下标指向数字
int p[N]; // 数字指向下标
bool st[N]; // st[i]表示: i是否已经在某个环中
int main() {
int T;
scanf("%d", &T);
while (T--) {
memset(p, 0, sizeof p);
memset(st, 0, sizeof st);
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
int id;
scanf("%d", &id);
a[i] = id; // i -> id 下标指向数字
p[id] = i; // id -> i 数字指向下标
}
bool flag = false; // 所有缺失的元素是否填补完成
for (int i = 1; i <= n; i++) {
if (st[i] || !a[i]) continue; // i已经在某个环中,或者第i个位置没缺失
st[i] = true;
int x = i, y = i; // 首x,尾y
while (p[x] && !st[p[x]]) {
x = p[x];
st[x] = true;
}
while (a[y] && !st[a[y]]) {
y = a[y];
st[y] = true;
}
if (a[y] == x) continue; // 说明是一个环
if (!flag) { // 不构成一个环,其余缺失元素可以填补到y后面补成一个环
flag = true;
for (int j = 1; j <= n; j++)
if (!a[j] && !p[j]) {
st[j] = true;
a[y] = j;
y = j;
}
}
a[y] = x;
}
if (!flag) { // 之前都是环,不存在右缺口的环,则让剩余的点构成一个环
int x = 0, y = 0; // 首x,尾y
for (int i = 1; i <= n; i++)
if (!a[i]) {
if (!x && !y) x = y = i;
else {
a[y] = i;
y = i;
}
}
a[y] = x;
}
for (int i = 1; i <= n; i++) printf("%d ", a[i]);
puts("");
}
return 0;
}
Leetcode 0765 情侣牵手
问题描述
- 问题链接:Leetcode 0765 情侣牵手
分析
- 本题可以转化为图论问题,每对情侣是图中的一个顶点,边是沙发,例如如果存在
4
对情侣:(1, 1)、(2, 2)、(3, 3)、(4, 4)
,则如果初始坐在一起的顺序是:(1, 2)、(2, 3)、(3, 4)、(4, 1)
,目标状态是每个点是一个自环,如下图:
-
对于这种类型的图称为环状图(上图中的初始状态),所谓环状图是指由一堆环构成的图。环状图有如下两个性质:
(1)交换环内的两个点(对应交换两个人),会多一个环;
(2)交换两个环中的两个点(对应交换两个人),会少一个环。
-
我们发现,每操作一次最多产生一个环,初始的话,如果有
n
个点(n
为情侣对数),cnt
个环,则目标相当于变为n
个环,最少需要操作n-cnt
次。 -
本题相当于要求解环的个数
cnt
,可以使用并查集。
代码
- C++
class Solution {
public:
vector<int> p;
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int minSwapsCouples(vector<int>& row) {
int n = row.size() / 2;
for (int i = 0; i < n; i++) p.push_back(i);
int cnt = n;
for (int i = 0; i < n * 2; i += 2) {
int a = row[i] / 2, b = row[i + 1] / 2; // 沙发上的两个人row[i]、row[i + 1]分别属于情侣的编号
if (find(a) != find(b)) {
p[find(a)] = find(b);
cnt--; // 子图减少一个
}
}
return n - cnt;
}
};
- Java
class Solution {
int[] p;
private int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
public int minSwapsCouples(int[] row) {
int n = row.length / 2;
p = new int[n];
for (int i = 0; i < n; i++) p[i] = i;
int cnt = n;
for (int i = 0; i < n * 2; i += 2) {
int a = row[i] / 2, b = row[i + 1] / 2;
if (find(a) != find(b)) {
p[find(a)] = find(b);
cnt--; // 子图减少一个
}
}
return n - cnt;
}
}
- Python
class Solution:
def minSwapsCouples(self, row: List[int]) -> int:
n = len(row) // 2
p = [i for i in range(n)]
cnt = n
for i in range(0, n * 2, 2):
a = self.find(p, row[i] // 2); b = self.find(p, row[i + 1] // 2)
if a != b:
p[a] = b
cnt -= 1
return n -cnt
def find(self, p, x):
if p[x] != x:
p[x] = self.find(p, p[x])
return p[x]
时空复杂度分析
-
时间复杂度: O ( n ) O(n) O(n),
n
为数组长度。 -
空间复杂度: O ( n ) O(n) O(n)。