2023春招算法题汇总(1)
Author:baiyucraft
BLog: baiyucraft’s Home
0304 美团 春招
第一题:平滑值
题目
小红定义一个数组的平滑值为:任意两个相邻元素的差的绝对值的最大值。例如:数组 [2,4,3,3]
的平滑值为 |2-4|=2
。
小红希望你构造一个长度为 n
的排列,满足排列的平滑值等于 k
。你能帮帮她吗?排列是指:长度为n的数组,1到n每个正整数都恰好出现 1
次。
输入描述
第一行输入两个正整数,分别表示n和k。其中, 1 ≤ k < n ≤ 1 0 5 1 \le k \lt n \le 10^5 1≤k<n≤105
输出描述
输出 n
个数字,用空格隔开。输入 4 2
输出 1 3 2 4
代码
脑筋急转弯
const { readLineNum } = require('../utils/read');
const [n, k] = readLineNum();
const arr1 = Array.from({ length: k }, (_, i) => i + 1);
const arr2 = Array.from({ length: n - k - 1 }, (_, i) => i + k + 2);
const arr = [k + 1, ...arr1, ...arr2];
console.log(arr.join(' '));
第二题:p的倍数
题目
小红拿到了一个正整数 n
,她可以进行若干次操作,每次操作将选择一个数位,使其加 1
或者减 1
。
不过有两条限制:
-
每个数位最多只能操作一次。
-
如果选择的是
9
,则无法进行加1
操作。如果选择的是0
,则无法进行减1
操作。
小红希望最终 n
成为 p
的倍数,你能帮小红输出操作结束后的整数 n
吗?
输入描述
两个正整数 n
和 p
。
1
≤
n
,
p
≤
1
0
13
1 \le n, p \le 10^{13}
1≤n,p≤1013
输出描述
如果误解,请输出 -1
。假设有多解的时候,输出任意解即可。(如果操作包含前导零,请将前导零一起输出)
代码
枚举
const { readLineNum } = require('../utils/read');
const [n, p] = readLineNum();
const arr = [];
let tmpN = n;
while (tmpN > 0) {
arr.push(tmpN % 10);
tmpN = Math.floor(tmpN / 10);
}
arr.reverse();
let res = -1;
const dfs = (cur, digits) => {
if (res !== -1 || digits === arr.length) return;
if (cur % p === 0) {
res = cur;
return;
}
dfs(cur, digits + 1);
if (arr[digits] !== 0) {
dfs(cur - 10 ** digits, digits + 1);
}
if (arr[digits] !== 9) {
dfs(cur + 10 ** digits, digits + 1);
}
};
dfs(n, 0);
console.log(res);
0307 携程 春招
第一题:游游的字符串
题目
游游拿到一个长度为 n
的字符串,她每次操作会选择一个区间 [l, r]
,将第 l
个字母到第 r
个字母各重复一次,插入到该字母的后面。
例如,对于字符串 “abcd”
,若选择区间 [2, 3]
进行操作,字符串将变成 “abbccd”
。
游游将进行 q
次操作。她想知道,q
次操作结束后,最终字符串是什么样子?
输入描述
第一行输入两个正整数 n
和 q
,分别代表字符串的长度和操作次数。
第二行输入一个仅有小写英文字母组成的字符串,代表初始的字符串。
接下来 q
行,每行输入两个正整数 l, r
,代表操作的区间。
限制:
1
≤
n
≤
1000
1 \le n \le 1000
1≤n≤1000,
1
≤
q
≤
10
1 \le q \le 10
1≤q≤10,
1
≤
l
≤
r
≤
1
0
6
1 \le l \le r \le 10^6
1≤l≤r≤106,保证每次操作时,r
不大于当前的字符串长度。
示例
// 输入
6 2
abcdef
2 4
3 6
// 输出
abbbccccdddef
代码
const { readlines } = require('../utils/read');
const data = readlines();
const [, str, ...queries] = data;
const arr = Array.from(str);
while (queries.length) {
const [l, r] = queries.shift();
const mid = arr.slice(l - 1, r).reduce((pre, val) => [...pre, val, val], []);
arr.splice(l - 1, r - l + 1, ...mid);
}
console.log(arr.join(''));
第二题:游游出游
题目
游游准备开车出游,她的车非常特殊,油越多则最高速度越快,即最高速度和油量是成正比的。另外,行驶过程中油是不会消耗的。
已知游游的车初始的最高速度为 v0
,当游游花费了 t
时间加油时,车的最高速度会变成 v0 + t * x
。
游游开车的总里程为 y
,假设游游始终以最高速度行驶(即忽略加速时间),游游想知道,自己最少花费多少时间可以完成出游?
输入描述
三个整数 v0
, x
, y
, 用空格隔开。
0
≤
v
0
≤
1
0
9
0 \le v_0 \le 10^9
0≤v0≤109,
1
≤
x
,
y
≤
1
0
9
1 \le x,y \le 10^9
1≤x,y≤109
输出描述
一个浮点数,代表最终花费的总时间。如果你的答案和标准答案的相对误差不超过 1 0 − 6 10^-6 10−6,则认为答案正确。
示例
// 输入
0 1 2
// 输出
2.8284271
代码
数学做法,设加油时间为 t t t,则总时间为 f ( t ) = y v o + x t + t f(t) = \dfrac{y}{v_o + xt} + t f(t)=vo+xty+t,此时对 f ( t ) f(t) f(t) 求导,可得 f ′ ( t ) = − x y ( v o + x t ) 2 + 1 f'(t) = -\dfrac{xy}{(v_o + xt)^2} + 1 f′(t)=−(vo+xt)2xy+1,此时令 f ′ ( t ) = 0 f'(t) = 0 f′(t)=0,可得 x y = ( v o + x t ) 2 xy = (v_o + xt)^2 xy=(vo+xt)2 ,即 x y − v o x \dfrac{\sqrt{xy} - v_o}{x} xxy−vo。
const { readLineNum } = require('../utils/read');
const [v0, x, y] = readLineNum();
const t = (Math.sqrt(x * y) - v0) / x;
const ft = y / (v0 + x * t) + t;
console.log(ft);
第三题:游游购物
题目
游游正在逛超市,有 n
个商品摆成一排,第 i
个商品的价格为 ai
,,游游对它的喜爱度为 bi
。所有商品的价格都是偶数。
超市开展了一个活动,当游游花费原价买了一件商品时,她可以用半价买下一件右边相邻的商品(也可以用原价购买,这样该商品右边的商品就有一次享受半价的机会)。但如果游游半价购买了一件商品,那么下一件右边相邻的商品只能原价购买。
换言之,如果游游想要半价买某一件商品,必须先用原价买下它相邻的左边的那个商品。
游游初初始的钱为 x
,她想要买的商品的喜爱度总和尽可能大,但总价格不能超过 x
。你能帮帮她计算最大的喜爱度总和吗?
输入描述
第一行输入两个正整数 n
和 x
,分别代表商品的数量,以及游游初始的金额数。
第二行输入 n
个正整数 ai
,分别代表每个商品的价格。
第三行输入 n
个正整数 bi
,分别代表每个商品可以给游游带来的喜爱度。
1 ≤ n , x , a i ≤ 1000 1 \le n, x, a_i \le 1000 1≤n,x,ai≤1000, 1 ≤ b i ≤ 1 0 9 1 \le b_i \le 10^9 1≤bi≤109, 保证所有的ai都是偶数。
输出描述
一个整数,代表最终喜爱度之和的最大值。
示例
//输入
4 7
2 2 6 2
3 4 5 1
// 输出
12 // 第一个使用原价卖,第二个物品使用原价买,第三个物品使用半价买,不买第四个物品,这样是最优的。
代码
递归
const { readlinesNum } = require('../utils/read');
const [[n, x], a, b] = readlinesNum();
// 递归, 第i个商品,剩余的钱,是否可以半价
const dfs = (index, money, half) => {
if (index === n) return 0;
// 如果钱不够半价购买,那么就不能购买
if (money < a[index] / 2) return dfs(index + 1, money, false);
// 如果half为false,说明上一个商品半价购买,这个商品只能原价购买
if (!half && money < a[index]) return dfs(index + 1, money, false);
return Math.max(
// 如果half为true,说明上一个商品原价购买,这个商品可以半价购买
half ? dfs(index + 1, money - a[index] / 2, false) + b[index] : 0,
dfs(index + 1, money - a[index], true) + b[index],
dfs(index + 1, money, false)
);
};
console.log(dfs(0, x, false));
记忆化搜索优化,空间换时间
// 定义一个二维数组,用于保存已经计算过的结果,n*(x+1)*2
const memo = Array.from({ length: n }, () => Array.from({ length: x + 1 }, () => Array(2).fill(-1)));
const dfs = (index, money, half) => {
if (index === n || money < 0) return 0;
// 如果已经计算过,则直接返回保存的结果
if (memo[index][money][half] !== -1) return memo[index][money][half];
let ans = 0;
if (money < a[index] / 2) {
ans = dfs(index + 1, money, 0);
} else if (!half && money < a[index]) {
ans = dfs(index + 1, money, 0);
} else {
ans = Math.max(
half ? dfs(index + 1, money - a[index] / 2, 0) + b[index] : 0,
dfs(index + 1, money - a[index], 1) + b[index],
dfs(index + 1, money, 0)
);
}
// 将计算结果保存到memo数组中
memo[index][money][half] = ans;
return ans;
};
dfs(0, x, 0);
console.log(memo[0][x][0]);
0311 美团 春招实习
第一题:小美的字符串
题目
小美有一个由数字字符组成的字符串。现在她想对这个字符串进行一些修改。具体地,她可以将这个字符串中任意位置字符修改为任意的数字字符。她想知道,至少进行多少次修改,可以使修改后的字符串不包含两个连续相同的字符?
例如,对于字符串 ”111222333”
,她可以进行 3
次修改将其变为 ”121212313"
。
输入描述
一行, 一个字符串 s
,保证 s
只包含数字字符。
1 ≤ ∣ s ∣ ≤ 100000 1 \le |s| \le 100000 1≤∣s∣≤100000
输出描述
一行,一个整数,表示修改的最少次数。
示例
// 输入 -> 输出
111222333 -> 3
11551111 -> 4
代码
const { readline } = require('../utils/read');
let s = readline();
if (s.length < 2) console.log(0);
s += 'e';
let res = 0;
let i = 1; let cur = s[0]; let count = 1;
while (i < s.length) {
if (s[i] !== cur) {
res += count >> 1;
cur = s[i];
count = 1;
} else {
++count;
}
++i;
}
console.log(res);
第二题:流星
题目
小美是一位天文爱好者,她收集了接下来一段时间中所有会划过她所在的观测地上空的流星信息。具体地,她收集了 n
个流星在她所在观测地上空的出现时刻和消失时刻。对于一个流星,若其的出现时刻为 s
,消失时刻为 t
, 那么小美在时间段 [s, t]
都能够观测到它。对于一个时刻,观测地上空出现的流星数量越多,则小美认为该时刻越好。小美希望能够选择一个最佳的时刻进行观测和摄影,使她能观测到最多数量的流星。现在小美想知道,在这个最佳时刻,她最多能观测到多少个流星以及一共有多少个最佳时刻可供她选择。
输入描述
第一行是一个正整数 n
,表示流星的数量。
第二行是 n
个用空格隔开的正整数,第 i
个数 si
表示第 i
个流星的出现时间。
第三行是 n
个用空格隔开的正整数,第 i
个数 ti
表示第 i
个流星的消失时间。
1 ≤ n ≤ 100000 1 \le n \le 100000 1≤n≤100000, 1 ≤ s i ≤ t i ≤ 1 0 9 1 \le si \le ti \le 10^9 1≤si≤ti≤109
输出描述
输出一行用空格隔开的两个数x和y,其中x表示小美能观测到的最多的流行数,y表示可供她选择的最佳时刻数量。
示例
// 输入
3
2 1 5
6 3 7
// 输出
2 4
代码
遍历时间
const { readlinesNum } = require('../utils/read');
const [n, s, t] = readlinesNum();
// 1 代表当前区间 +1,-1 代表当前区间 -1
const points = [
...s.map(val => [val, 1]),
...t.map(val => [val + 1, -1])
];
points.sort((a, b) => a[0] - b[0]);
// 遍历时间,否则计算不了没在points中的时间点
let i = 0; let maxMeteors = 0; let currentMeteors = 0; let bestTimeCount = 0;
for (let time = points[0][0]; time <= points[points.length - 1][0]; ++time) {
while (i < points.length && time === points[i][0]) {
currentMeteors += points[i][1];
++i;
}
if (currentMeteors === maxMeteors) {
++bestTimeCount;
} else if (currentMeteors > maxMeteors) {
maxMeteors = currentMeteors;
bestTimeCount = 1;
}
}
console.log(maxMeteors, bestTimeCount);
线段树
const { readlinesNum } = require('../utils/read');
const [n, s, t] = readlinesNum();
class SegmentTreeNode {
constructor(start, end) {
this.start = start;
this.end = end;
this.left = null;
this.right = null;
this.val = { count: 0, num: 0 };
}
}
class SegmentTree {
constructor(appearanceTimes, disappearanceTimes) {
this.start = Math.min(...appearanceTimes);
this.end = Math.max(...disappearanceTimes);
this.root = new SegmentTreeNode(this.start, this.end);
for (let i = 0; i < appearanceTimes.length; ++i) {
this.updateNode(this.root, appearanceTimes[i], disappearanceTimes[i]);
}
}
// 更新节点, start, end 为需要更新的区间,delta 为更新的值
updateNode(node, start, end) {
// 如果是叶子节点,更新叶子节点的值
if (node.start === node.end) {
node.val.count += 1;
node.val.num = 1;
return;
}
// 区间划分为[node.left, mid] [mid + 1, node.right]
const mid = (node.start + node.end) >> 1;
// 如果更新区间在左子树
if (node.start <= end && start <= mid) {
if (!node.left) node.left = new SegmentTreeNode(node.start, mid);
this.updateNode(node.left, start, end);
}
// 如果更新区间在右子树
if (mid + 1 <= end && start <= node.end) {
if (!node.right) node.right = new SegmentTreeNode(mid + 1, node.end);
this.updateNode(node.right, start, end);
}
}
_query(node) {
// 叶子节点
if (!node.left && !node.right) return node.val;
const leftVal = this._query(node.left);
const rightVal = this._query(node.right);
node.val.count = Math.max(leftVal.count, rightVal.count);
if (leftVal.count === node.val.count) {
node.val.num += leftVal.num;
}
if (rightVal.count === node.val.count) {
node.val.num += rightVal.num;
}
return node.val;
}
query() {
return this._query(this.root);
}
}
const segmentTree = new SegmentTree(s, t);
segmentTree.query();
console.log(Object.values(segmentTree.root.val).join(' '));
第三题:最佳规划
题目
小团在一个 n*m
的网格地图上探索。网格地图上第 i
行第 j
列的格子用坐标 (i,j)
简记。初始时,小团的位置在地图的左上角。即坐标 (1,1)
,地图上的每一个格子上都有一定的金币,特别地,小团位于的初始位置(1,1)上的金币为0。小团在进行探索移动时,可以选择向右移动一格(即从 (x, y)
到达 (x, y+1)
)或向下移动一格(即从 (x, y)
到达 (x+1,y)
)。地图上的每个格子都有一个颜色,红色或蓝色。如果小团一次移动前后的两个格子颜色不同,那么他需要支付k
个金币才能够完成这一次移动;如果移动前后的两个格子颜色相同,则不需要支付金币。小团可以在任意格子选择结束探索。
现在给你网格地图上每个格子的颜色与金币数量,假设小团初始时的金币数量为0,请你帮助小团计算出最优规划,使他能获得最多的金币,输出能获得的最多金币数量即可。
注意:要求保证小团任意时刻金币数量不小于零。
输入描述
第一行是三个用空格隔开的整数 n
,m
和 k
,表示网格地图的行数为 n
,列数为 m
,在不同颜色的两个格子间移动需要支付 k
个金币。
接下来 n
行,每行是一个长度为 m
的字符串,字符串仅包含字符 'R'
或 'B'
。第 i
行字符串的第 j
个字符表示地图上第 i
行第 j
列的格子颜色。如果字符为 'R'
,则表示格子颜色为红色,为 'B'
表示格子颜色为蓝色。
接下来是一个 n
行 m
列的非负整数矩阵,第 i
行第 j
列的数字表示地图上第 i
行第 j
列的格子上的金币数量。保证所有数据中数字大小都是介于 [0,10]
的整数。
1 ≤ n , m ≤ 200 1 \le n, m \le 200 1≤n,m≤200, 1 ≤ k ≤ 5 1 \le k \le 5 1≤k≤5
输出描述
一个正整数,表示能获得最多金币的数量。
示例
// 输入
3 3 2
BBB
RBR
RBR
0 2 3
2 4 1
3 2 2
// 输出
8
代码
动态规划
const { readlines } = require('../utils/read');
const [[n, m, k], ...lines] = readlines();
const colorMap = lines.slice(0, n);
const goldMap = lines.slice(n, n + n).map(line => line.map(Number));
console.log(n, m, k, colorMap, goldMap);
// 初始化 dp 数组,dp[i][j] 表示走到 (i, j) 位置时的最大金币数,初始化为 -1 表示未走过
const dp = Array.from({ length: n }, () => Array.from({ length: m }, () => -1));
dp[0][0] = goldMap[0][0];
// 初始化第一列
for (let i = 1; i < n; i++) {
const curColor = colorMap[i][0];
const upColor = colorMap[i - 1][0];
// 如果上边的颜色和当前颜色不同且走到上边的金币数小于 k,那么可以从上边走过来
if (upColor === curColor || dp[i - 1][0] >= k) {
dp[i][0] = dp[i - 1][0] + goldMap[i][0] - (upColor === curColor ? 0 : k);
}
}
// 初始化第一行
for (let j = 1; j < m; j++) {
const curColor = colorMap[0][j];
const leftColor = colorMap[0][j - 1];
// 如果左边的颜色和当前颜色不同且走到左边的金币数小于 k,那么可以从左边走过来
if (leftColor === curColor || dp[0][j - 1] >= k) {
dp[0][j] = dp[0][j - 1] + goldMap[0][j] - (leftColor === curColor ? 0 : k);
}
}
let maxDp = 0;
for (let i = 1; i < n; i++) {
for (let j = 1; j < m; j++) {
const curColor = colorMap[i][j];
const leftColor = colorMap[i][j - 1];
const upColor = colorMap[i - 1][j];
let left2Cur = 0; let up2Cur = 0;
// 保证左边走过不为 -1,如果左边的颜色和当前颜色不同且走到左边的金币数小于 k,那么可以从左边走过来
if (dp[i][j - 1] !== -1 && (leftColor === curColor || dp[i][j - 1] >= k)) {
left2Cur = dp[i][j - 1] + goldMap[i][j] - (leftColor === curColor ? 0 : k);
}
// 保证上边走过不为 -1,如果上边的颜色和当前颜色不同且走到上边的金币数小于 k,那么可以从上边走过来
if (dp[i - 1][j] !== -1 && (upColor === curColor || dp[i - 1][j] >= k)) {
up2Cur = dp[i - 1][j] + goldMap[i][j] - (upColor === curColor ? 0 : k);
}
dp[i][j] = Math.max(left2Cur, up2Cur);
maxDp = Math.max(maxDp, dp[i][j]);
}
}
console.log(dp);
console.log(maxDp);
第四题:坦克大战
题目
小D和小W最近在玩坦克大战,双方操控自己的坦克在 16*16
的方格图上战斗,小D的坦克初始位置在地图的左上角,朝向为右,其坐标 (0, 0)
,小W的坦克初始位置在地图右下角,朝向为左,坐标为 (15, 15)
。坦克不能移动到地图外,坦克会占领自己所在的格子,己方坦克不可以进入对方占领过的格子。每一个回合双方必须对自己的坦克下达以下五种指令中的一种:
- 移动指令
U
:回合结束后,使已方坦克朝向为上,若上方的格子未被对方占领,则向当前朝向移动一个单位(横坐标-1
),否则保持不动; - 移动指令
D
:回合结束后,使己方坦克朝向为下,若下方的格子未被对方占领,则向当前朝向移动一个单位(横坐标+1
),否则保持不动; - 移动指令
L
:回合结束后,使己方坦克朝向为左,若左侧的格子未被对方占领,则向当前朝向移动一个单位(纵坐标-1
),否则保持不动; - 移动指令
R
:回合结束后,使己方坦克朝向为右,若右侧的格子未被对方占领,则向当前朝向移动一个单位(纵坐标+1
),否则保特不动; - 开火指令
F
:已方坦克在当前回合立即向当前朝向开火;
己方坦克开火后,当前回合己方坦克的正前方若有对方的坦克,对方的坦克将被摧毁,游戏结束,己方获得胜利;
若双方的坦克在同一回合被摧毁,游戏结束,判定为平局;若双方的坦克在同一回合内进入到同一个未被占领的格子,则双方的坦克发生碰撞,游戏结束,判定为平局;当游戏进行到第256个回合后,游戏结束,若双方坦克均未被摧毁,则占领格子数多的一方获得胜利,若双方占领的格子数一样多,判定为平局。注意,若一方开火,另一方移动,则认为是先开火,后移动。
现在小D和小W各自给出一串长度为256的指令字符串,请你帮助他们计算出游戏将在多少个回合后结束,以及游戏的结果。
示例
// 输入
DDDDDDDRF
UUUUUUUUU
// 输出
9 D
代码
模拟
const { readlines } = require('../utils/read');
const [dInstru, wInstru] = readlines();
// 创建初始方格图 16*16,初始为 -1,0 表示被 d 占据,1 表示被 w 占据
const grid = Array.from({ length: 16 }, () => Array.from({ length: 16 }, () => -1));
grid[0][0] = 1; grid[15][15] = -1;
// 位置
const curPos = [[0, 0], [15, 15]];
// 方向
const ds = [[0, 1], [0, -1]];
// 占领数量
const cnts = [1, 1];
// 定义移动
const dirs = { 'U': [-1, 0], 'D': [1, 0], 'L': [0, -1], 'R': [0, 1] };
// 移动函数,p 表示玩家,0 表示 d,1 表示 w
const move = (instru, p) => {
// 移动指令
const dir = dirs[instru];
// 当前位置
let [i, j] = curPos[p];
// 下一步位置
[i, j] = [i + dir[0], j + dir[1]];
// 保存方向
ds[p] = dir;
if (grid[i][j] === -1 || grid[i][j ] === p) {
curPos[p][0] += dir[0];
curPos[p][1] += dir[1];
}
if (grid[i][j] === -1) {
grid[i][j] = p;
cnts[p]++;
}
};
// 定义 p1 向 p2 开火
const fire = (p1, p2) => {
// 向上面开火,p1 朝向上方且 p2 在 p1 的正上面
if (ds[p1][0] === -1 && curPos[p2][1] === curPos[p1][1] && curPos[p2][0] < curPos[p1][0]) return true;
// 向下面开火,p1 朝向下方且 p2 在 p1 的正下面
if (ds[p1][0] === 1 && curPos[p2][1] === curPos[p1][1] && curPos[p2][0] > curPos[p1][0]) return true;
// 向左面开火,p1 朝向左方且 p2 在 p1 的正左面
if (ds[p1][1] === -1 && curPos[p2][0] === curPos[p1][0] && curPos[p2][1] < curPos[p1][1]) return true;
// 向右面开火,p1 朝向右方且 p2 在 p1 的正右面
if (ds[p1][1] === 1 && curPos[p2][0] === curPos[p1][0] && curPos[p2][1] > curPos[p1][1]) return true;
return false;
};
const main = () => {
for (let i = 0; i < dInstru.length; i++) {
const di = dInstru[i]; const wi = wInstru[i];
if (di === 'F' && wi === 'F' && fire(0, 1) && fire(1, 0)) {
console.log(i + 1, '平局');
return;
}
if (di === 'F' && fire(0, 1)) {
console.log(i, 'D 胜');
return;
}
if (wi === 'F' && fire(1, 0)) {
console.log(i, 'W 胜');
return;
}
if (di !== 'F') move(di, 0);
if (wi !== 'F') move(wi, 1);
if (curPos[0][0] === curPos[1][0] && curPos[0][1] === curPos[1][1]) {
console.log(i, '平局');
return;
}
}
if (cnts[0] === cnts[1]) {
console.log(256, '平局');
return;
}
if (cnts[0] > cnts[1]) {
console.log(256, 'D 胜');
return;
}
console.log(256, 'W 胜');
};
main();