携程笔试

携程笔试0313

模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.*;

public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String s = sc.nextLine();
int cnt = 0;
for (int i = 0; i < s.length(); i += cnt) {
System.out.print(s.charAt(i));
cnt++;
}
}
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

游游的数组染色

贪心

将数字按从大到小排序,然后每次将当前值+索引+1就是得分,比较所有得分取最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
// 读取每组数据
for (int t = 0; t < T; t++) {
int n = sc.nextInt();
long[] nums = new long[n];
for (int i = 0; i < n; i++) {
nums[i] = sc.nextLong();
}
// 排序
Arrays.sort(nums);
// 计算得分
long ans = 0;
for (int i = n - 1; i >= 0; i--) {
ans = Math.max(ans, nums[i] + (n - i));
}
System.out.println(ans);
}
}
}

java让数组降序排序还需要将数组转换成Integer型的,非常麻烦。

1
2
3
4
5
6
7
8
9
10
// 初始化一个 int 数组
int[] arr = {10, 5, 3, 8, 12, 1};

// 转换成 Integer 数组以便使用 reverseOrder
Integer[] integerArray = Arrays.stream(arr) // 转换为流
.boxed() // 装箱为 Integer 对象
.toArray(Integer[]::new); // 转换为 Integer 数组

// 使用 Arrays.sort 对 Integer 数组进行降序排序
Arrays.sort(integerArray, Collections.reverseOrder());
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

小牛的数组询问

滑动窗口

  1. 质因子个数计算: 对于数组中的每个数,我们需要计算它的质因子个数。我们可以用一个类似筛法的算法来预处理每个数的质因子个数,这样在处理输入数据时就能迅速得到每个数的质因子个数。
  2. 滑动窗口: 我们需要找到一个长度为 k 的子数组,将它删除,剩下的数组的权值和最大。由于删除子数组后的数组权值和最大化,实际上等价于找到一个长度为 k 的子数组,其质因子个数和最小,然后从总和中减去这个最小值。

具体步骤:

  1. 首先,计算每个数的质因子个数,并存储到一个数组 weight 中。
  2. 计算整个数组的权值和。
  3. 使用滑动窗口来找到长度为 k 的子数组,其权值和最小。
  4. 最后,输出总权值和减去最小子数组权值和的结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.util.*;

public class Main {
public static void main(String[] args) {
// 读取输入
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int k = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = sc.nextInt();
}
// 预处理
int[] primeCnt = calPrimeCnt(10000);

// 权值替换
long total = 0;
int[] weight = new int[n];
for (int i = 0; i < n; i++) {
weight[i] = primeCnt[nums[i]];
total += weight[i];
}

// 滑动窗口
long cur = 0;
for (int i = 0; i < k; i++) {
cur += weight[i];
}
long minWeight = cur;
for (int i = k; i < n; i++) {
cur = cur - weight[i - k] + weight[i];
minWeight = Math.min(minWeight, cur);
}

// 返回结果
System.out.println(total - minWeight);

}

// 预处理筛分
public static int[] calPrimeCnt(int maxNum) {
int[] cnt = new int[maxNum + 1];
for (int i = 2; i <= maxNum; i++) {
// 如果i是质数
if (cnt[i] == 0) {
for (int j = i; j <= maxNum; j += i) {
cnt[j]++;
}
}
}
return cnt;
}
}
  • 时间复杂度:O(n + maxNum * log(maxNum))
  • 空间复杂度:O(n + maxNum)

偶数路径2.0

并查集

核心是计算树中有多少条简单路径的权值为偶数,路径的权值是路径上所有节点权值的最大公约数(GCD)。通过并查集(Union-Find)将所有具有相同特性(即权值为偶数)的节点归为同一组,减少计算复杂度。

  1. 初始化并查集:创建一个并查集 g,每个节点初始时是自己的父节点。
  2. 遍历树的每条边:对每条边 (x, y),判断节点 x 和节点 y 的权值是否都为偶数。如果都为偶数,则将它们合并成一个连通块,因为路径上的 GCD 会是偶数。
  3. 统计每个连通块中的偶数节点数量:使用 find 操作找到每个节点所属的连通块,然后统计每个连通块中偶数节点的数量。
  4. 计算每个连通块的路径数量:对于每个连通块,如果该连通块中有 k 个偶数节点,则该连通块贡献的路径数是 C(k, 2) + k,即所有可能的两节点路径数加上每个节点单独的路径数。
  5. 输出最终结果:输出所有连通块贡献的路径数的和,得到最终的答案。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import java.util.*;

public class Main {
static int[] father; // 将father定义为静态数组,以便在静态方法中访问

// 并查集 find 函数
public static int find(int x) {
if (father[x] == x) return x;
return father[x] = find(father[x]); // 路径压缩
}

// 并查集 union 函数
public static void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
father[rootX] = rootY;
}
}

public static void main(String[] args) {
Scanner sc = new Scanner(System.in);

// 输入节点数量
int n = sc.nextInt();

// 输入节点权值
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = sc.nextInt();
}

// 初始化并查集
father = new int[n + 1]; // 初始化父节点数组
for (int i = 1; i <= n; i++) {
father[i] = i; // 初始时,每个节点的父节点是它自己
}

// 输入树的边并进行并查集合并操作
for (int i = 1; i < n; i++) {
int u = sc.nextInt();
int v = sc.nextInt();
// 结点权值为偶数,合并
if (arr[u - 1] % 2 == 0 && arr[v - 1] % 2 == 0) {
union(u, v);
}
}

// 统计每个连通块中的偶数节点数量
long[] cnt = new long[n + 1];
long answer = 0; // 使用answer变量来存储最终结果
for (int i = 1; i <= n; i++) {
if (arr[i - 1] % 2 == 0) {
int root = find(i);
cnt[root]++;
}
}

// 计算每个连通块的路径数量
for (int i = 1; i <= n; i++) {
if (cnt[i] > 0) {
answer += cnt[i] * (cnt[i] - 1) / 2 + cnt[i]; // C(k, 2) + k
}
}

// 输出结果
System.out.println(answer);
}
}
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

总结

携程的题目也不难,除了最后一道题不太会写。

并查集(Disjoint Set Union, DSU)

并查集是一种数据结构,用来处理一些不交集(disjoint sets)的合并及查询问题。并查集的基本操作包括 查找(Find)和 合并(Union)。它的主要应用场景是处理一些元素的合并和查询,例如 连接问题连通性问题 等。

主要操作:

  1. **find(x)**:查找元素 x 所在的集合的代表元素(根节点),即查找一个元素属于哪个集合。
  2. **union(x, y)**:将两个集合合并成一个集合,即将元素 xy 所在的两个集合合并。
  3. **connected(x, y)**:判断两个元素是否在同一个集合中(即是否连通)。

关键技巧:

  1. 路径压缩(Path Compression):在执行 find 操作时,将路径上的所有节点都直接连接到根节点,以加速后续的查找操作。
  2. 按秩合并(Union by Rank/Size):在执行 union 操作时,总是将较小的树合并到较大的树下,以保持树的平衡,减少树的高度,从而减少查找的时间复杂度。

这两个技巧可以大大提高并查集的效率,确保其操作时间接近常数时间。

并查集的应用场景

并查集主要用于 动态连通性问题,也就是说,当我们面对一组元素,我们需要处理这些元素间的合并和查询操作时,适合用并查集。以下是一些典型问题:

  1. 连通性问题:判断图中两个节点是否连通。

    • 比如在一个图中,判断两个点是否在同一个连通分量中。
  2. 图的连通分量数量:找出一个无向图中的连通分量数量。

    • 比如给定一个无向图,计算图中的连通块数目。
  3. 动态连通性问题:在一个动态变化的图中,处理边的增删操作,快速判断两个节点是否连通。

  4. 网络问题:在某些情况下,多个计算机之间的网络连接问题也可以通过并查集解决,判断哪些计算机在同一个网络中。

  5. 集合合并问题:在一些集合合并的应用中,使用并查集可以帮助我们高效地处理合并和查询操作。

并查集代码模板(包括路径压缩和按秩合并)

以下是一个典型的并查集模板,包含路径压缩和按秩合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class UnionFind {
// father[i] 表示节点 i 的父节点
// rank[i] 用于按秩合并
private int[] father;
private int[] rank;

// 初始化并查集
public UnionFind(int n) {
father = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
father[i] = i; // 初始时,每个节点的父节点是自己
rank[i] = 0; // 初始时,树的深度是 0
}
}

// 查找操作,带路径压缩
public int find(int x) {
if (father[x] != x) {
// 路径压缩:将路径上的所有节点都指向根节点
father[x] = find(father[x]);
}
return father[x];
}

// 合并操作,按秩合并
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);

if (rootX != rootY) {
// 按秩合并:将树矮的根节点连接到树高的根节点
if (rank[rootX] > rank[rootY]) {
father[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
father[rootX] = rootY;
} else {
father[rootY] = rootX;
rank[rootX]++; // 如果深度相同,选择一个作为根节点,并增加它的秩
}
}
}

// 判断两个元素是否在同一集合中
public boolean connected(int x, int y) {
return find(x) == find(y);
}
}

代码说明:

  1. 初始化:我们通过一个 father 数组表示每个节点的父节点,rank 数组表示树的深度(用于按秩合并)。每个节点最初的父节点是它自己,秩是 0。
  2. **find(x)**:查找操作,使用路径压缩优化。每次递归查找时,将路径上的节点直接连接到根节点,以加速未来的查找。
  3. **union(x, y)**:合并操作,使用按秩合并。通过比较两个集合的秩,将较小的树合并到较大的树下,从而保持树的平衡,减少树的深度。
  4. **connected(x, y)**:判断 xy 是否在同一个集合中,只需要比较它们的根节点是否相同。

复杂度分析:

  • 时间复杂度:由于路径压缩和按秩合并的使用,findunion 操作的时间复杂度接近 **O(α(n))**,其中 α(n) 是阿克曼函数的反函数,它增长非常慢,接近常数时间。因此,通常可以认为这两个操作是常数时间。
  • 空间复杂度:并查集的空间复杂度是 **O(n)**,主要用于存储 fatherrank 数组。

总结:

  • 并查集 是一种非常高效的数据结构,尤其适合解决 动态连通性 问题,常用于图算法、集合合并等问题。
  • 通过 路径压缩按秩合并 技巧,可以使得 findunion 操作接近常数时间。
  • 应用场景 包括:图的连通性问题、集合合并问题、网络连通问题等。

通过这种方法,很多与连通性相关的问题都可以高效地解决。


携程笔试
http://example.com/2025/03/18/posts/xiecheng/
作者
Xuan Yang
发布于
2025年3月18日
许可协议