题目链接
题目描述
一种新的排序方法规定,如果任意两个数 具有大于 1 的公因数(即
),则它们可以交换位置。
给定一个数列,判断能否通过这种交换方法将其排为升序。
思路分析
这是一个典型的连通性问题,可以用并查集 (Disjoint Set Union, DSU) 来解决。
核心思想
“可以交换”定义了一种关系。如果数字 可以和
交换,
可以和
交换,那么
就属于同一个“连通块”或“等价类”。在这个块内的所有数字,都可以通过一系列交换,被移动到块内数字占据的任何一个初始位置上。
因此,问题可以转化为:对于原数组中的每一个数 ,它需要移动到的目标位置上的数(即排序后数组的
),是否和它在同一个连通块内? 如果对所有的
这个条件都成立,那么数组就可以被成功排序,否则就不能。
构建连通块
两个数可以交换的条件是它们的最大公因数 (GCD) 大于 1,这等价于它们共享至少一个大于 1 的质因数。
基于这个性质,我们可以构建连通关系:
- 一个数
和它的所有质因数
都是连通的。
- 如果数
和数
都拥有共同的质因数
,那么
和
连通,
和
也连通。根据并查集的传递性,
和
也属于同一个连通块。
这启发我们使用并查集来维护这些连通关系。我们可以将每个数字和它的质因数看作并查集中的节点。对于数组中的每一个数 ,我们找到它的所有质因数
,然后将
与这些质因数在并查集中合并(
unite(x, p_1)
, unite(x, p_2)
, ...)。
算法步骤
-
预处理:为了快速分解质因数,我们可以使用线性筛(Sieve of Eratosthenes)预处理出
到
范围内所有数的最小质因数(Smallest Prime Factor, SPF)。这一步的时间复杂度大约是
。
-
构建并查集:
- 对于输入的数组
中的每一个数
(
):
- 利用预处理好的 SPF 数组,找到
的所有不同质因数
。
- 将
和它的每一个质因数
在并查集中进行合并。
- 对于输入的数组
-
处理特殊值 0:
- 数字
无法与任何数交换,因为它没有大于 1 的质因数。
- 数字
比较特殊,
。所以只要
,
就可以和
交换。这意味着如果数组中存在
,它将把所有大于
的数字所在的连通块全部合并成一个大的连通块。
- 数字
-
检验:
- 将原数组
复制并排序,得到目标数组
。
- 遍历数组,比较
和
。如果对于任意的
,
find(a[i]) != find(b[i])
,即它们不属于同一个连通块,那么排序无法完成,输出No
。 - 如果遍历完成,所有对应元素都在同一个连通块内,则输出
Yes
。
- 将原数组
这个算法结合了数论和数据结构,能够高效地解决本题。
代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAX_VAL = 1000001;
int spf[MAX_VAL];
int parent[MAX_VAL];
void sieve() {
iota(spf, spf + MAX_VAL, 0);
for (int i = 2; i * i < MAX_VAL; ++i) {
if (spf[i] == i) { // i is prime
for (int j = i * i; j < MAX_VAL; j += i) {
if (spf[j] == j) {
spf[j] = i;
}
}
}
}
}
int find_set(int v) {
if (v == parent[v])
return v;
return parent[v] = find_set(parent[v]);
}
void unite_sets(int a, int b) {
a = find_set(a);
b = find_set(b);
if (a != b)
parent[b] = a;
}
void solve() {
int n;
cin >> n;
vector<int> a(n);
vector<int> b(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
b[i] = a[i];
}
sort(b.begin(), b.end());
iota(parent, parent + MAX_VAL, 0);
for (int x : a) {
if (x <= 1) continue;
int temp = x;
while (temp > 1) {
int p = spf[temp];
unite_sets(x, p);
while (temp % p == 0) {
temp /= p;
}
}
}
bool possible = true;
for (int i = 0; i < n; ++i) {
if (find_set(a[i]) != find_set(b[i])) {
possible = false;
break;
}
}
if (possible) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
}
int main() {
sieve();
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
import java.util.Scanner;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class Main {
private static final int MAX_VAL = 1000001;
private static int[] spf = new int[MAX_VAL];
private static int[] parent = new int[MAX_VAL];
private static void sieve() {
for (int i = 0; i < MAX_VAL; i++) {
spf[i] = i;
}
for (int i = 2; i * i < MAX_VAL; i++) {
if (spf[i] == i) { // i is prime
for (int j = i * i; j < MAX_VAL; j += i) {
if (spf[j] == j) {
spf[j] = i;
}
}
}
}
}
private static int findSet(int v) {
if (v == parent[v]) {
return v;
}
return parent[v] = findSet(parent[v]);
}
private static void uniteSets(int a, int b) {
a = findSet(a);
b = findSet(b);
if (a != b) {
parent[b] = a;
}
}
private static void solve(Scanner sc) {
int n = sc.nextInt();
int[] a = new int[n];
int[] b = new int[n];
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
b[i] = a[i];
}
Arrays.sort(b);
for (int i = 0; i < MAX_VAL; i++) {
parent[i] = i;
}
for (int x : a) {
if (x <= 1) continue;
int temp = x;
while (temp > 1) {
int p = spf[temp];
uniteSets(x, p);
while (temp % p == 0) {
temp /= p;
}
}
}
boolean possible = true;
for (int i = 0; i < n; i++) {
if (findSet(a[i]) != findSet(b[i])) {
possible = false;
break;
}
}
if (possible) {
System.out.println("Yes");
} else {
System.out.println("No");
}
}
public static void main(String[] args) {
sieve();
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();
while (t-- > 0) {
solve(sc);
}
}
}
import sys
# It's better to use fast I/O in competitive programming
# but following user preference for standard input()
# def input(): return sys.stdin.readline().rstrip()
MAX_VAL = 1000001
spf = list(range(MAX_VAL))
parent = []
def sieve():
global spf
i = 2
while i * i < MAX_VAL:
if spf[i] == i: # i is prime
for j in range(i * i, MAX_VAL, i):
if spf[j] == j:
spf[j] = i
i += 1
def find_set(v):
if v == parent[v]:
return v
parent[v] = find_set(parent[v])
return parent[v]
def unite_sets(a, b):
a = find_set(a)
b = find_set(b)
if a != b:
parent[b] = a
def solve():
global parent
n = int(input())
a = list(map(int, input().split()))
b = sorted(a)
parent = list(range(MAX_VAL))
for x in a:
if x <= 1:
continue
temp = x
while temp > 1:
p = spf[temp]
unite_sets(x, p)
while temp % p == 0:
temp //= p
possible = True
for i in range(n):
if find_set(a[i]) != find_set(b[i]):
possible = False
break
if possible:
print("Yes")
else:
print("No")
def main():
sieve()
t = int(input())
for _ in range(t):
solve()
if __name__ == "__main__":
main()
算法及复杂度
-
算法:线性筛 + 并查集
-
时间复杂度:
。
- 线性筛预处理所有数的最小质因数,时间复杂度为
。
- 对于每组测试数据,遍历数组
构建并查集。对每个数
进行质因数分解,复杂度为
。总共为
。
- 最后的检验步骤,并查集的
find
操作近乎,总共为
。
是测试数据组数,
是数组长度。
- 线性筛预处理所有数的最小质因数,时间复杂度为
-
空间复杂度:
。
- 需要一个数组
spf
来存储最小质因数,以及一个数组parent
来维护并查集,大小都与数值上限相关。
- 需要一个数组