去哪儿旅行测试开发笔试编程题

moye Lv6

题 1

题目描述

给出两个整数 a, b,可以对其进行任意次加倍操作。每次操作可以令 a = 2 × ab = 2 × b,目的是使得最终的 |a − b| 最小(a − b 的绝对值最小),输出最小绝对值。

输入描述

每个测试文件均包含多组测试数据。

  • 第一行输入一个整数 T (1 ≤ T ≤ 10^5),代表数据组数。
  • 每组测试数据描述如下:在一行上输入两个整数 a, b (−10^9 ≤ a, b ≤ 10^9),代表初始数字。

输出描述

对于每一组测试数据,在一行上输出一个整数,代表最小绝对值。

思路

算法思路:

  1. 处理特殊情况:

    • 如果 a = b,直接输出 0
    • 如果 a = 0b = 0,输出另一个数的绝对值。
    • 如果 ab 异号,无法通过乘以正数(2 的幂)来改变符号,最小差值为 |a| + |b|
  2. 一般情况:

    • ab 同号且均不为零时,我们可以通过对 ab 进行若干次加倍,使其接近另一个数。
    • 计算 s = log2(|b / a|)
    • k = round(s)(取整到最近的整数)。
    • k-1k+1 的范围内枚举 k,计算对应的差值,更新最小值。

注意事项:

  • 防止溢出: 在进行乘法时,可能会超出 long 的范围,需要在乘法前进行检查。
  • 精度问题: 在计算对数和幂时,使用 double 类型,注意精度误差。
  • 效率优化: 由于 T 很大,需要确保每组测试数据的时间复杂度为 O(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
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
import java.util.Scanner;

public class Main {
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
int T=in.nextInt();
while(T-->0){
long a=in.nextLong();
long b=in.nextLong();
if(a==b){
System.out.println(0);
continue;
}
long minDiff = Long.MAX_VALUE;
if(a==0 || b==0){
minDiff = Math.abs(a==0?b:a);
System.out.println(minDiff);
continue;
}
if((a>0 && b<0) || (a<0 && b>0)){
minDiff = Math.abs(a)+Math.abs(b);
System.out.println(minDiff);
continue;
}
// 调整 a
minDiff = Math.abs(a-b);
double s = Math.log(Math.abs((double)b / a)) / Math.log(2);
int k = (int)Math.round(s);
for(int i = k-1; i <= k+1; i++){
if(i<0 || i>60) continue;
try{
long a2 = multiplyWithCheck(a, 1L << i);
long diff = Math.abs(a2 - b);
if(diff < minDiff) minDiff = diff;
}catch(Exception e){
// 溢出,跳过
}
}
// 调整 b
s = Math.log(Math.abs((double)a / b)) / Math.log(2);
k = (int)Math.round(s);
for(int i = k-1; i <= k+1; i++){
if(i<0 || i>60) continue;
try{
long b2 = multiplyWithCheck(b, 1L << i);
long diff = Math.abs(a - b2);
if(diff < minDiff) minDiff = diff;
}catch(Exception e){
// 溢出,跳过
}
}
System.out.println(minDiff);
}
in.close();
}

// 检查乘法是否溢出
public static long multiplyWithCheck(long x, long y) throws Exception{
if(x > 0 && y > 0 && x > Long.MAX_VALUE / y) throw new Exception("Overflow");
if(x > 0 && y < 0 && y < Long.MIN_VALUE / x) throw new Exception("Overflow");
if(x < 0 && y > 0 && x < Long.MIN_VALUE / y) throw new Exception("Overflow");
if(x < 0 && y < 0 && x < Long.MAX_VALUE / y) throw new Exception("Overflow");
return x * y;
}
}

代码说明:

  1. 特殊情况处理:

    • a = b 时,差值为 0
    • a = 0b = 0 时,差值为另一个数的绝对值。
    • ab 异号时,无法通过乘以正数改变符号,差值为二者绝对值之和。
  2. 一般情况处理:

    • 调整 a
      • 计算 s = log2(|b / a|)
      • k = round(s),在 k-1k+1 的范围内枚举 i
      • 对于每个 i,计算 a2 = a × 2^i,并更新最小差值。
    • 调整 b
      • 类似地,计算 s = log2(|a / b|)
      • k = round(s),在 k-1k+1 的范围内枚举 i
      • 对于每个 i,计算 b2 = b × 2^i,并更新最小差值。
  3. 防止溢出:

    • 在乘法运算前,使用 multiplyWithCheck 方法检查是否会溢出。
    • 如果溢出,则捕获异常并跳过该计算。
  4. 输出结果:

    • 对于每组测试数据,输出计算得到的最小绝对值。

题 2

题目描述

n 个小石子排成了一列,第 i 个石子上写有正整数 a_i,小 M 需要一个个取走它们。每次只能取走当前的第一个石子或最后一个石子。第 i 次取石子获得的分数为:(i * a_j) ⊕ S,其中 a_j 为取走的石子的数字,S 为剩余石子上数字的异或和, 代表按位异或运算。

其中,S 为剩余石子上数字的异或和, 代表按位异或运算。

注: 一个集合 S = {a_1,⋯,a_n} 中所有数字的异或和被定义为 a_1 ⊕ a_2 ⊕⋯⊕ a_n,其中 为按位异或运算,若 S 为空集,则异或和定义为 0

小 M 希望他获得的分数最大,且要求你输出方案。如果有多种方案,任意输出一种即可。

输入描述

第一行一个正整数 n,代表石子数量,保证 1 ≤ n ≤ 10^3

第二行 n 个正整数 a_i,代表石子上的数字,保证 0 ≤ a_i ≤ 2^31

输出描述

第一行输出一个整数,表示能获得的最大分数。

第二行输出 n 个正整数,表示方案,即依次输出每一步操作取走的石子的编号。

示例 1

输入

1
2
5
1 2 3 4 5

输出

1
2
61
1 2 3 4 5

解释

样例输出的方案为:

第一次取走 1,获得的分数为 (1 × 1) ⊕ (2 ⊕ 3 ⊕ 4 ⊕ 5) = 1。

第二次取走 2,获得的分数为 (2 × 2) ⊕ (3 ⊕ 4 ⊕ 5) = 6。

第三次取走 3,获得的分数为 (3 × 3) ⊕ (4 ⊕ 5) = 8。

第四次取走 4,获得的分数为 (4 × 4) ⊕ 5 = 21。

第五次取走 5,获得的分数为 (5 × 5) ⊕ 0 = 25。

总分为 1 + 6 + 8 + 21 + 25 = 61,可以证明这是能达到的最高分数。

示例 2

输入

1
2
10
711 586 770 990 832 203 957 707 110 949

输出

1
2
40979
10 9 8 7 6 5 4 3 2 1

思路

思路分析:

于典型的区间 DP 问题。

主要思路:

  • 状态定义: 定义 dp[l][r] 为在剩余石子从位置 lr 时,能够获得的最大分数。
  • 状态转移: 每次可以选择取左边或右边的石子,根据选择更新 dp[l][r]
  • 关键点:
    • 计算第 k 次操作: 当前是第几次操作,k 的值需要准确计算。
    • 计算剩余石子的异或和 S 使用前缀异或和数组,方便快速计算任意区间的异或和。

实现步骤:

  1. 预处理前缀异或和: 使用一维数组 prefixXor 预处理从第一个石子到第 i 个石子的异或和。
  2. 初始化 DP 数组: 创建二维数组 dp[l][r]choice[l][r],用于存储最大得分和选择方案。
  3. 动态规划递推: 遍历所有可能的区间长度,从短到长,更新 dp[l][r]
    • 计算当前操作数 k k = n - (r - l + 1) + 1
    • 计算取左边石子的得分:
      * 剩余石子的异或和 S_left = prefixXor[r] ^ prefixXor[l]
      * 得分 temp1 = ((k * a[l]) ^ S_left) + dp[l+1][r]
    • 计算取右边石子的得分:
      • 剩余石子的异或和 S_right = prefixXor[r-1] ^ (l > 0 ? prefixXor[l-1] : 0)
        • 得分 temp2 = ((k * a[r]) ^ S_right) + dp[l][r-1]
    • 更新 DP 数组: 选择得分较大的方案,记录得分和选择。
  4. 重构选择方案: 根据 choice[l][r] 数组,逆序重建取石子的方案。

完整代码如下:

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
70
71
72
73
74
import java.util.Scanner;

public class Main {
static int n;
static long[] a;
static long[][] dp;
static int[][] choice;
static long[] prefixXor;

public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
a = new long[n];
for (int i = 0; i < n; i++) {
a[i] = scanner.nextLong();
}
dp = new long[n][n];
choice = new int[n][n];
prefixXor = new long[n];

// 预处理前缀异或和
prefixXor[0] = a[0];
for (int i = 1; i < n; i++) {
prefixXor[i] = prefixXor[i - 1] ^ a[i];
}

// 动态规划
for (int len = 1; len <= n; len++) {
for (int l = 0; l <= n - len; l++) {
int r = l + len - 1;
int k = n - len + 1; // 当前操作数

if (l == r) {
// 只有一个石子
dp[l][r] = (k * a[l]) ^ 0;
choice[l][r] = 0; // 无所谓,只有一个选择
} else {
// 取左边
long S_left = prefixXor[r] ^ prefixXor[l];
long temp1 = ((k * a[l]) ^ S_left) + dp[l + 1][r];

// 取右边
long S_right = prefixXor[r - 1] ^ (l > 0 ? prefixXor[l - 1] : 0);
long temp2 = ((k * a[r]) ^ S_right) + dp[l][r - 1];

if (temp1 > temp2) {
dp[l][r] = temp1;
choice[l][r] = 0; // 取左边
} else {
dp[l][r] = temp2;
choice[l][r] = 1; // 取右边
}
}
}
}

// 输出结果
System.out.println(dp[0][n - 1]);

// 重构方案
int l = 0, r = n - 1;
StringBuilder sb = new StringBuilder();
while (l <= r) {
if (choice[l][r] == 0) {
sb.append((l + 1) + " ");
l++;
} else {
sb.append((r + 1) + " ");
r--;
}
}
System.out.println(sb.toString().trim());
}
}

注意事项:

  • 关于 k 的计算: 在剩余石子从位置 lr 时,已经取走的石子数量为 n - (r - l + 1),因此当前的操作数 k = n - (r - l + 1) + 1
  • 关于异或和的计算: 使用前缀异或和数组,可以在 O(1) 时间内计算任意区间的异或和。
    • 计算区间 [l, r] 的异或和: prefixXor[r] ^ (l > 0 ? prefixXor[l - 1] : 0)
  • 运算符优先级: 在计算中,确保使用括号来明确运算顺序,避免因优先级导致的错误。

题 3

题目描述

n 个城市之间通过 m 条航线联结,其中第 i 条航线双向连接着城市 u_i 和城市 v_i,票价为 w_i。同时也有 k 条铁路线,第 j 条铁路线双向连接城市 a_j 和城市 b_j,票价为 c_j

小 N 要从城市 s 出发旅行,且中途必须途径城市 r 作为中转站。由于时间匆忙,小 N 使用某个出行 APP 进行了一番搜索,发现直飞票非常昂贵,于是他考虑采用 APP 提供的转机/转火车的方案。然而,还有一点特别的是,小 N 不喜欢坐火车,他只接受最多坐 1 次火车的方案。

请你为小 N 规划一条中转路线,使得他的总开销最小。

输入描述

第一行输入六个整数 n, m, k, s, t, r,其中:

  • n (1 ≤ n ≤ 10^5):表示城市的数量。

  • m (1 ≤ m ≤ 2×10^5):表示航线的数量。

  • k (0 ≤ k ≤ 2×10^5):表示铁路线的数量。

  • s, t, r (1 ≤ s, t, r ≤ n):表示小 N 的起点城市、终点城市和中转城市。

  • 接下来 m 行,每行三个整数 u_i, v_i, w_i,表示第 i 条航线连接城市 u_iv_i,票价为 w_i

  • 然后 k 行,每行三个整数 a_j, b_j, c_j,表示第 j 条铁路线连接城市 a_jb_j,票价为 c_j

输出描述

在一行上输出一个整数,代表答案。

示例

输入

1
2
3
4
5
6
7
8
9
4 5 3 2 1 3
1 3 5
1 4 10
1 2 1
2 4 3
3 4 8
1 3 1
1 2 4
3 4 1

输出

1
7

说明

  • 此时 s = 2, t = 1, r = 3。
  • 从 2 到 3 通过 2 → 1 的飞机(开销为 1)和 1 → 3 的火车(开销为 1)实现;
    从 3 到 1 通过坐一次飞机(开销为 5)实现。
  • 总开销为 1 + 1 + 5 = 7。

思路

使用带状态的 Dijkstra 算法,需要考虑以下两个条件:

  • 是否已经使用过一次铁路线 (usedRailwayFlag)。

  • 是否已经经过了中转城市 r (passedRFlag)。

  • 将每个节点的状态表示为 (cost, city, usedRailwayFlag, passedRFlag)。

  • 由于每个标志位有 2 个可能的值,所以每个城市的状态总数为 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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import java.util.*;

public class Main {
static class Edge {
int to;
long weight;
boolean isRailway;

Edge(int to, long weight, boolean isRailway) {
this.to = to;
this.weight = weight;
this.isRailway = isRailway;
}
}

static class State implements Comparable<State> {
int city;
int usedRailwayFlag;
int passedRFlag;
long cost;

State(int city, int usedRailwayFlag, int passedRFlag, long cost) {
this.city = city;
this.usedRailwayFlag = usedRailwayFlag;
this.passedRFlag = passedRFlag;
this.cost = cost;
}

@Override
public int compareTo(State other) {
return Long.compare(this.cost, other.cost);
}
}

static final long INF = Long.MAX_VALUE / 2;

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

int n = sc.nextInt();
int m = sc.nextInt();
int k = sc.nextInt();
int s = sc.nextInt() - 1;
int t = sc.nextInt() - 1;
int r = sc.nextInt() - 1;

List<Edge>[] adj = new ArrayList[n];
for (int i = 0; i < n; i++) adj[i] = new ArrayList<>();

// 读取航线信息
for (int i = 0; i < m; i++) {
int u = sc.nextInt() - 1;
int v = sc.nextInt() - 1;
long w = sc.nextLong();
adj[u].add(new Edge(v, w, false));
adj[v].add(new Edge(u, w, false));
}

// 读取铁路线信息
for (int i = 0; i < k; i++) {
int u = sc.nextInt() - 1;
int v = sc.nextInt() - 1;
long c = sc.nextLong();
adj[u].add(new Edge(v, c, true));
adj[v].add(new Edge(u, c, true));
}

long[][][] dist = new long[n][2][2];
for (int i = 0; i < n; i++) {
Arrays.fill(dist[i][0], INF);
Arrays.fill(dist[i][1], INF);
}

int initialPassedRFlag = (s == r) ? 1 : 0;
dist[s][0][initialPassedRFlag] = 0;

PriorityQueue<State> pq = new PriorityQueue<>();
pq.add(new State(s, 0, initialPassedRFlag, 0));

while (!pq.isEmpty()) {
State cur = pq.poll();

if (cur.city == t && cur.passedRFlag == 1) {
System.out.println(cur.cost);
return;
}

if (dist[cur.city][cur.usedRailwayFlag][cur.passedRFlag] < cur.cost)
continue;

for (Edge e : adj[cur.city]) {
int nextCity = e.to;
int nextUsedRailwayFlag = cur.usedRailwayFlag;
int nextPassedRFlag = cur.passedRFlag;
long nextCost = cur.cost + e.weight;

if (nextCity == r) nextPassedRFlag = 1;

if (e.isRailway) {
if (cur.usedRailwayFlag == 1) continue;
nextUsedRailwayFlag = 1;
}

if (dist[nextCity][nextUsedRailwayFlag][nextPassedRFlag] > nextCost) {
dist[nextCity][nextUsedRailwayFlag][nextPassedRFlag] = nextCost;
pq.add(new State(nextCity, nextUsedRailwayFlag, nextPassedRFlag, nextCost));
}
}
}

// 如果无法找到满足条件的路径,输出 -1
System.out.println(-1);
}
}

解释:

  • 邻接表构建:使用邻接表来表示图结构,每条边标记为飞机航线或铁路线。
  • 状态表示:在 Dijkstra 算法中,每个状态包括当前城市、是否使用过铁路线、是否经过中转城市 r、当前总花费。
  • 优先队列:使用优先队列以保证每次扩展的都是当前花费最小的状态。

状态转移:当遍历到一条边时,检查该边是否为铁路线以及是否已经使用过铁路线。更新 passedRFlag,如果到达了城市 r。如果条件满足且新的花费更小,则更新状态并加入队列。
终止条件:当到达目的城市 t 且已经经过中转城市 r 时,输出当前的总花费。

注意事项:
如果起点城市 s 就是中转城市 r,则初始状态的 passedRFlag 应设为 1。
如果无法找到符合条件的路径,则输出 -1。

  • 标题: 去哪儿旅行测试开发笔试编程题
  • 作者: moye
  • 创建于 : 2024-10-10 08:51:40
  • 更新于 : 2025-12-12 18:22:53
  • 链接: https://www.kanes.top/2024/10/10/去哪儿旅行测试开发笔试编程题/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论